1#[cfg(feature = "rotation-optimizations")]
9mod active_aura;
10mod bindings;
11mod families;
12
13use std::ops::Range;
14
15use wowlab_engine_domain::{
16 dbc::{
17 MechanicMask, ModifierPropertyKind, ResolvedGameDataSemanticExt as _,
18 SpellAuraInterruptFlags,
19 },
20 rotation::AuraSlotHandle,
21};
22use wowlab_engine_gamedata::ResolvedGameData;
23use wowlab_types::sim::{ActorId, AuraKey, PetIdx};
24
25use self::{
26 bindings::{ActionSpellModifierBindings, RelevantEffectList},
27 families::{family_indexes, single_family_index},
28};
29use super::BufferBindings;
30use crate::state::{
31 AuraData, BuffEffect, EffectConsumer, EffectFamily, LocalAuraIdx, MAX_AURA_BUFF_EFFECTS,
32 PassiveDriverEffect,
33};
34
35#[rustfmt::skip]
36#[cfg(feature = "rotation-optimizations")]
37pub(crate) use active_aura::{ActiveAuraBreakPlans, ActiveAuraEffectPlan};
38
39const _: () = assert!(
40 MAX_AURA_BUFF_EFFECTS <= u16::BITS as usize,
41 "aura effect positions must fit in each u16 consumer mask"
42);
43const AURA_ORDER_LOCAL_SHIFT: u32 = 48;
44const AURA_ORDER_BINDING_SHIFT: u32 = 8;
45
46#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
48pub(crate) struct AuraEffectMasks {
49 general_totals: u16,
50 dynamic_totals: u16,
51 spell_modifiers: u16,
52 costs: u16,
53 timing: u16,
54 targeting: u16,
55 gates: u16,
56 recharge_charges: u16,
57 stats: u16,
58 speed_projection: u16,
59 snapshot_totals: u16,
60 player_scaling: u16,
61}
62
63#[derive(Clone, Copy, Debug)]
64pub(crate) struct PlannedAuraEffects {
65 pub(crate) key: AuraKey,
66 pub(crate) handle: AuraSlotHandle,
67 pub(crate) local: LocalAuraIdx,
68 pub(crate) static_totals_mask: u16,
69 pub(crate) dynamic_totals_mask: u16,
70 pub(crate) dispel_type: i32,
71 pub(crate) dispel_resistance_flat: f64,
72 pub(crate) dispel_resistance_multiplier: f64,
73 pub(crate) mechanic_mask: MechanicMask,
74 pub(crate) granted_aura_state: Option<crate::state::AuraState>,
75}
76
77#[derive(Clone, Copy, Debug)]
78pub(crate) struct PlannedAuraEffect {
79 pub(crate) key: AuraKey,
80 pub(crate) handle: AuraSlotHandle,
81 pub(crate) local: LocalAuraIdx,
82 pub(crate) position: u8,
83 pub(crate) order: u64,
84 families: EffectFamily,
85}
86
87#[derive(Debug)]
88struct ActorRanges<T> {
89 entries: Box<[T]>,
90 player: Range<usize>,
91 external: Range<usize>,
92 pets: Box<[Range<usize>]>,
93 enemies: Box<[Range<usize>]>,
94}
95
96impl<T> ActorRanges<T> {
97 fn for_actor(&self, actor: ActorId) -> &[T] {
98 let range = match actor {
99 ActorId::Player => Some(self.player.clone()),
100 ActorId::External => Some(self.external.clone()),
101 ActorId::Pet(pet) => self.pets.get(pet.as_usize()).cloned(),
102 ActorId::Enemy(enemy) => self.enemies.get(enemy.as_usize()).cloned(),
103 };
104
105 range.map_or(&[], |range| {
106 &self.entries[range]
108 })
109 }
110
111 fn maximum_actor_len(&self) -> usize {
112 std::iter::once(&self.player)
113 .chain(std::iter::once(&self.external))
114 .chain(self.pets.iter())
115 .chain(self.enemies.iter())
116 .map(Range::len)
117 .max()
118 .unwrap_or(0)
119 }
120}
121
122impl<T> Default for ActorRanges<T> {
123 fn default() -> Self {
124 Self {
125 entries: Box::default(),
126 player: 0..0,
127 external: 0..0,
128 pets: Box::default(),
129 enemies: Box::default(),
130 }
131 }
132}
133
134type ActorEffectRanges = ActorRanges<PlannedAuraEffects>;
135type ActorExactEffectRanges = ActorRanges<PlannedAuraEffect>;
136
137#[derive(Debug, Default)]
138struct ActorEffectBindings {
139 all: ActorEffectRanges,
140 static_totals: ActorEffectRanges,
141 dynamic_totals: ActorEffectRanges,
142 by_family: Box<[ActorExactEffectRanges]>,
143 action_break: ActorEffectRanges,
144 periodic_damage_break: ActorEffectRanges,
145 direct_damage_break: ActorEffectRanges,
146 aura_state: ActorEffectRanges,
147 shapeshift: ActorEffectRanges,
148 dispellable: ActorEffectRanges,
149 mechanic: ActorEffectRanges,
150}
151
152impl AuraEffectMasks {
153 pub(crate) const fn totals_relevant(self) -> bool {
154 self.general_totals != 0
155 }
156
157 pub(crate) const fn snapshot_totals_relevant(self) -> bool {
158 self.snapshot_totals != 0
159 }
160
161 pub(crate) const fn player_scaling_relevant(self) -> bool {
162 self.player_scaling != 0
163 }
164
165 #[cfg(test)]
166 pub(super) const fn mask(self, class: EffectConsumerClass) -> u16 {
167 match class {
168 EffectConsumerClass::GeneralTotals => self.general_totals,
169 EffectConsumerClass::DynamicTotals => self.dynamic_totals,
170 EffectConsumerClass::SpellModifiers => self.spell_modifiers,
171 EffectConsumerClass::Costs => self.costs,
172 EffectConsumerClass::Timing => self.timing,
173 EffectConsumerClass::Targeting => self.targeting,
174 EffectConsumerClass::Gates => self.gates,
175 EffectConsumerClass::RechargeCharges => self.recharge_charges,
176 EffectConsumerClass::Stats => self.stats,
177 EffectConsumerClass::SpeedProjection => self.speed_projection,
178 }
179 }
180
181 fn insert(&mut self, position: usize, consumers: EffectConsumer) {
182 let bit = 1_u16 << position;
183
184 if consumers.contains(EffectConsumer::GENERAL_TOTALS) {
185 self.general_totals |= bit;
186 }
187
188 if consumers.contains(EffectConsumer::DYNAMIC_TOTALS) {
189 self.dynamic_totals |= bit;
190 }
191
192 if consumers.contains(EffectConsumer::SPELL_MODIFIERS) {
193 self.spell_modifiers |= bit;
194 }
195
196 if consumers.contains(EffectConsumer::COSTS) {
197 self.costs |= bit;
198 }
199
200 if consumers.contains(EffectConsumer::TIMING) {
201 self.timing |= bit;
202 }
203
204 if consumers.contains(EffectConsumer::TARGETING) {
205 self.targeting |= bit;
206 }
207
208 if consumers.contains(EffectConsumer::GATES) {
209 self.gates |= bit;
210 }
211
212 if consumers.contains(EffectConsumer::RECHARGE_CHARGES) {
213 self.recharge_charges |= bit;
214 }
215
216 if consumers.contains(EffectConsumer::STATS) {
217 self.stats |= bit;
218 }
219
220 if consumers.contains(EffectConsumer::SPEED_PROJECTION) {
221 self.speed_projection |= bit;
222 }
223
224 if consumers.contains(EffectConsumer::SNAPSHOT_TOTALS) {
225 self.snapshot_totals |= bit;
226 }
227 }
228
229 fn insert_player_scaling(&mut self, position: usize, effect: &BuffEffect) {
230 if matches!(
231 effect,
232 BuffEffect::Haste(..)
233 | BuffEffect::HasteMult(..)
234 | BuffEffect::HasteMultFromEffect { .. }
235 | BuffEffect::SpellCastSpeed(..)
236 | BuffEffect::AttackSpeed(..)
237 | BuffEffect::Crit(..)
238 | BuffEffect::CritFromEffect { .. }
239 | BuffEffect::CritActive(..)
240 ) {
241 self.player_scaling |= 1_u16 << position;
242 }
243 }
244
245 const fn mask_for_build(self, class: EffectConsumerClass) -> u16 {
246 match class {
247 EffectConsumerClass::GeneralTotals => self.general_totals,
248 EffectConsumerClass::DynamicTotals => self.dynamic_totals,
249 EffectConsumerClass::SpellModifiers => self.spell_modifiers,
250 EffectConsumerClass::Costs => self.costs,
251 EffectConsumerClass::Timing => self.timing,
252 EffectConsumerClass::Targeting => self.targeting,
253 EffectConsumerClass::Gates => self.gates,
254 EffectConsumerClass::RechargeCharges => self.recharge_charges,
255 EffectConsumerClass::Stats => self.stats,
256 EffectConsumerClass::SpeedProjection => self.speed_projection,
257 }
258 }
259}
260
261const EFFECT_CONSUMER_CLASS_COUNT: usize = 10;
262const EFFECT_FAMILY_COUNT: usize = 85;
263const MODIFIER_PROPERTY_SLOT_COUNT: usize = ModifierPropertyKind::MaxTargets as usize + 1;
264const FIXED_ACTOR_BUCKET_COUNT: usize = 2;
265const PLAYER_BUCKET: usize = 0;
266const EXTERNAL_BUCKET: usize = 1;
267const PET_BUCKETS_START: usize = FIXED_ACTOR_BUCKET_COUNT;
268
269#[derive(Clone, Copy, Debug, Eq, PartialEq)]
270#[repr(u8)]
271pub(crate) enum EffectConsumerClass {
272 GeneralTotals,
273 DynamicTotals,
274 SpellModifiers,
275 Costs,
276 Timing,
277 Targeting,
278 Gates,
279 RechargeCharges,
280 Stats,
281 SpeedProjection,
282}
283
284impl EffectConsumerClass {
285 const ALL: [Self; EFFECT_CONSUMER_CLASS_COUNT] = [
286 Self::GeneralTotals,
287 Self::DynamicTotals,
288 Self::SpellModifiers,
289 Self::Costs,
290 Self::Timing,
291 Self::Targeting,
292 Self::Gates,
293 Self::RechargeCharges,
294 Self::Stats,
295 Self::SpeedProjection,
296 ];
297
298 const fn consumer(self) -> EffectConsumer {
299 match self {
300 Self::GeneralTotals => EffectConsumer::GENERAL_TOTALS,
301 Self::DynamicTotals => EffectConsumer::DYNAMIC_TOTALS,
302 Self::SpellModifiers => EffectConsumer::SPELL_MODIFIERS,
303 Self::Costs => EffectConsumer::COSTS,
304 Self::Timing => EffectConsumer::TIMING,
305 Self::Targeting => EffectConsumer::TARGETING,
306 Self::Gates => EffectConsumer::GATES,
307 Self::RechargeCharges => EffectConsumer::RECHARGE_CHARGES,
308 Self::Stats => EffectConsumer::STATS,
309 Self::SpeedProjection => EffectConsumer::SPEED_PROJECTION,
310 }
311 }
312
313 const fn index(self) -> usize {
314 self as usize
315 }
316}
317
318#[derive(Debug)]
319pub(crate) struct EffectPlan {
320 global_capabilities: EffectFamily,
321 aura_masks: Box<[AuraEffectMasks]>,
322 #[cfg(feature = "rotation-optimizations")]
323 static_total_auras: ActiveAuraEffectPlan,
324 #[cfg(feature = "rotation-optimizations")]
325 dynamic_total_auras: ActiveAuraEffectPlan,
326 #[cfg(feature = "rotation-optimizations")]
327 break_auras: ActiveAuraBreakPlans,
328 #[cfg(feature = "rotation-optimizations")]
329 family_auras: Box<[ActiveAuraEffectPlan]>,
330 relevant: [RelevantEffectList; EFFECT_CONSUMER_CLASS_COUNT],
331 passive_by_family: Box<[Box<[usize]>]>,
332 owner_duplicates: Box<[Box<[AuraSlotHandle]>]>,
333 actor_bindings: ActorEffectBindings,
334 action_spell_modifiers: ActionSpellModifierBindings,
335}
336
337impl EffectPlan {
338 pub(crate) fn compile(auras: &[AuraData], passive_drivers: &[PassiveDriverEffect]) -> Self {
340 let mut global_capabilities = EffectFamily::empty();
341 let mut aura_masks = Vec::with_capacity(auras.len());
342 #[cfg(feature = "rotation-optimizations")]
343 let mut family_effect_masks = vec![vec![0_u16; auras.len()]; EFFECT_FAMILY_COUNT];
344 let mut relevant_auras: [Vec<LocalAuraIdx>; EFFECT_CONSUMER_CLASS_COUNT] =
345 std::array::from_fn(|_| Vec::new());
346 let mut relevant_passives: [Vec<usize>; EFFECT_CONSUMER_CLASS_COUNT] =
347 std::array::from_fn(|_| Vec::new());
348 let mut passive_by_family = vec![Vec::new(); EFFECT_FAMILY_COUNT];
349
350 for (raw_local, aura) in auras.iter().enumerate() {
351 let local = LocalAuraIdx::new(
352 u8::try_from(raw_local).expect("finalized aura count is validated below 256"),
353 );
354 let mut masks = AuraEffectMasks::default();
355
356 for (position, effect) in aura.effects.iter().enumerate() {
357 let Some(effect) = effect else {
358 continue;
359 };
360 let classification = effect.classification();
361
362 global_capabilities |= classification.families();
363 masks.insert(position, classification.consumers());
364 masks.insert_player_scaling(position, effect);
365
366 #[cfg(feature = "rotation-optimizations")]
367 for family_index in family_indexes(classification.families()) {
368 family_effect_masks[family_index][raw_local] |= 1_u16 << position;
369 }
370 }
371
372 for class in EffectConsumerClass::ALL {
373 if masks.mask_for_build(class) != 0 {
374 relevant_auras[class.index()].push(local);
376 }
377 }
378
379 aura_masks.push(masks);
380 }
381
382 for (index, driver) in passive_drivers.iter().enumerate() {
383 let classification = driver.effect.classification();
384
385 global_capabilities |= classification.families();
386
387 for class in EffectConsumerClass::ALL {
388 if classification.consumers().contains(class.consumer()) {
389 relevant_passives[class.index()].push(index);
391 }
392 }
393
394 for family_index in family_indexes(classification.families()) {
395 passive_by_family[family_index].push(index);
397 }
398 }
399
400 let relevant = std::array::from_fn(|index| {
401 let aura_locals = std::mem::take(&mut relevant_auras[index]).into_boxed_slice();
403 let passive_driver_indexes = {
404 std::mem::take(&mut relevant_passives[index]).into_boxed_slice()
406 };
407
408 RelevantEffectList {
409 aura_locals,
410 passive_driver_indexes,
411 }
412 });
413
414 #[cfg(feature = "rotation-optimizations")]
415 let static_total_auras = ActiveAuraEffectPlan::from_masks(
416 aura_masks
417 .iter()
418 .map(|masks| masks.general_totals & !masks.dynamic_totals)
419 .collect(),
420 );
421 #[cfg(feature = "rotation-optimizations")]
422 let dynamic_total_auras = ActiveAuraEffectPlan::from_masks(
423 aura_masks
424 .iter()
425 .map(|masks| masks.dynamic_totals)
426 .collect(),
427 );
428 #[cfg(feature = "rotation-optimizations")]
429 let break_auras = ActiveAuraBreakPlans::compile(auras);
430
431 Self {
432 global_capabilities,
433 aura_masks: aura_masks.into_boxed_slice(),
434 #[cfg(feature = "rotation-optimizations")]
435 static_total_auras,
436 #[cfg(feature = "rotation-optimizations")]
437 dynamic_total_auras,
438 #[cfg(feature = "rotation-optimizations")]
439 break_auras,
440 #[cfg(feature = "rotation-optimizations")]
441 family_auras: family_effect_masks
442 .into_iter()
443 .map(ActiveAuraEffectPlan::from_masks)
444 .collect(),
445 relevant,
446 passive_by_family: passive_by_family
447 .into_iter()
448 .map(Vec::into_boxed_slice)
449 .collect(),
450 owner_duplicates: (0..auras.len())
451 .map(|_| Vec::new().into_boxed_slice())
452 .collect(),
453 actor_bindings: ActorEffectBindings {
454 by_family: (0..EFFECT_FAMILY_COUNT)
455 .map(|_| ActorExactEffectRanges::default())
456 .collect(),
457 ..ActorEffectBindings::default()
458 },
459 action_spell_modifiers: ActionSpellModifierBindings::default(),
460 }
461 }
462
463 pub(crate) fn bind_aura_slots(
465 &mut self,
466 bindings: &BufferBindings,
467 auras: &[AuraData],
468 data: &ResolvedGameData,
469 enemy_count: usize,
470 ) {
471 let bucket_count = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy_count;
472 let mut buckets = vec![Vec::new(); bucket_count];
473 let mut family_buckets = vec![vec![Vec::new(); bucket_count]; EFFECT_FAMILY_COUNT];
474
475 for (raw_local, aura) in auras.iter().enumerate() {
476 let local = LocalAuraIdx::new(
477 u8::try_from(raw_local).expect("finalized aura count is validated below 256"),
478 );
479 let masks = self.aura(local);
480
481 for binding in bindings.aura_bindings() {
482 if binding.local != local {
483 continue;
484 }
485
486 let key = binding.key;
487 let handle = binding.handle;
488 let spell = wowlab_types::sim::SpellIdx::from_raw(aura.aura_id);
489 let props = data.spell_props_for_damage_plan(spell);
490
491 let Some(bucket) = actor_bucket(key.affected(), enemy_count) else {
492 continue;
493 };
494
495 buckets[bucket].push(PlannedAuraEffects {
498 key,
499 handle,
500 local,
501 static_totals_mask: masks.general_totals & !masks.dynamic_totals,
502 dynamic_totals_mask: masks.dynamic_totals,
503 dispel_type: props.dispel_type,
504 dispel_resistance_flat: props.dispel_resistance_flat,
505 dispel_resistance_multiplier: props.dispel_resistance_multiplier,
506 mechanic_mask: data.all_effect_mechanic_mask(spell),
507 granted_aura_state: aura.granted_aura_state,
508 });
509
510 for (position, effect) in aura.effects.iter().enumerate() {
511 let Some(effect) = effect else {
512 continue;
513 };
514 let effect_families = effect.classification().families();
515 let planned = PlannedAuraEffect {
516 key,
517 handle,
518 local,
519 position: u8::try_from(position)
520 .expect("fixed aura effect positions fit in u8"),
521 order: (u64::from(local.raw()) << AURA_ORDER_LOCAL_SHIFT)
522 | (u64::try_from(binding.index().as_usize())
523 .expect("binding ordinal fits in u64")
524 << AURA_ORDER_BINDING_SHIFT)
525 | u64::try_from(position).expect("effect position fits in u64"),
526 families: effect_families,
527 };
528
529 for family_index in family_indexes(effect_families) {
530 family_buckets[family_index][bucket].push(planned);
532 }
533 }
534 }
535 }
536
537 let static_totals = buckets
538 .iter()
539 .map(|bucket| {
540 bucket
541 .iter()
542 .copied()
543 .filter(|entry| entry.static_totals_mask != 0)
544 .collect()
545 })
546 .collect();
547 let dynamic_totals = buckets
548 .iter()
549 .map(|bucket| {
550 bucket
551 .iter()
552 .copied()
553 .filter(|entry| entry.dynamic_totals_mask != 0)
554 .collect()
555 })
556 .collect();
557 let select = |predicate: fn(&PlannedAuraEffects, &AuraData) -> bool| {
558 buckets
559 .iter()
560 .map(|bucket| {
561 bucket
562 .iter()
563 .copied()
564 .filter(|entry| predicate(entry, finalized_aura(auras, entry.local)))
565 .collect::<Vec<_>>()
566 })
567 .collect::<Vec<_>>()
568 };
569 let action_break = select(|_, aura| {
570 aura.interrupt_flags
571 .contains(SpellAuraInterruptFlags::ACTION)
572 });
573 let periodic_damage_break = select(|_, aura| {
574 aura.interrupt_flags
575 .contains(SpellAuraInterruptFlags::DAMAGE)
576 });
577 let direct_damage_break = select(|_, aura| {
578 aura.interrupt_flags.intersects(
579 SpellAuraInterruptFlags::DAMAGE | SpellAuraInterruptFlags::NON_PERIODIC_DAMAGE,
580 )
581 });
582 let mut aura_state = select(|_, aura| aura.granted_aura_state.is_some());
583
584 for bucket in &mut aura_state {
585 bucket.sort_unstable_by_key(|entry| {
586 entry.granted_aura_state.map_or(0, |state| state as i32)
587 });
588 }
589
590 let shapeshift = select(|_, aura| aura.shapeshift_form > 0);
591 let mut dispellable = select(|entry, _| entry.dispel_type != 0);
592
593 for bucket in &mut dispellable {
594 bucket.sort_unstable_by_key(|entry| entry.dispel_type);
595 }
596
597 let mechanic = select(|entry, _| !entry.mechanic_mask.is_empty());
598
599 self.owner_duplicates = (0..auras.len())
600 .map(|raw_local| {
601 let local = local_aura(raw_local);
602
603 bindings
604 .aura_bindings()
605 .filter(|binding| {
606 binding.local == local && binding.key.affected() == ActorId::Player
607 })
608 .map(|binding| binding.handle)
609 .collect::<Vec<_>>()
610 .into_boxed_slice()
611 })
612 .collect();
613
614 self.actor_bindings = ActorEffectBindings {
615 all: flatten_actor_buckets(buckets),
616 static_totals: flatten_actor_buckets(static_totals),
617 dynamic_totals: flatten_actor_buckets(dynamic_totals),
618 by_family: family_buckets
619 .into_iter()
620 .map(flatten_actor_buckets)
621 .collect(),
622 action_break: flatten_actor_buckets(action_break),
623 periodic_damage_break: flatten_actor_buckets(periodic_damage_break),
624 direct_damage_break: flatten_actor_buckets(direct_damage_break),
625 aura_state: flatten_actor_buckets(aura_state),
626 shapeshift: flatten_actor_buckets(shapeshift),
627 dispellable: flatten_actor_buckets(dispellable),
628 mechanic: flatten_actor_buckets(mechanic),
629 };
630 }
631
632 pub(crate) fn bind_spell_modifier_actions(
636 &mut self,
637 damage: &super::DamagePlan,
638 auras: &[AuraData],
639 passive_drivers: &[PassiveDriverEffect],
640 ) {
641 let action_count = damage.action_locals().count();
642 let binding_count = action_count.saturating_mul(MODIFIER_PROPERTY_SLOT_COUNT);
643 let mut passive_indexes = vec![Vec::new(); binding_count];
644 let mut player_auras = vec![Vec::new(); binding_count];
645 let mut scoped_passive_indexes = vec![Vec::new(); action_count];
646 let mut damage_passive_indexes = vec![Vec::new(); action_count];
647 let mut cost_passive_indexes = vec![Vec::new(); action_count];
648
649 for &passive_index in self.passive_family_indexes(EffectFamily::SPELL_MODIFIER) {
650 let effect = &passive_drivers[passive_index].effect;
651 let binding = damage
652 .passive_buff_binding(passive_index)
653 .expect("every finalized passive effect has a damage binding record");
654
655 for action in damage.action_locals() {
656 if spell_scoped_effect_applies_to_action(effect, binding, action, damage) {
657 scoped_passive_indexes[action.index()].push(passive_index);
658 }
659
660 if damage_scoped_effect_applies_to_action(effect, binding, action, damage) {
661 damage_passive_indexes[action.index()].push(passive_index);
662 }
663
664 if let Some(property) = action_modifier_property(effect)
665 && binding
666 .source_effect
667 .is_some_and(|source| damage.effect_affects_action(source, action))
668 {
669 passive_indexes[spell_modifier_binding_index(action, property)]
670 .push(passive_index);
671 }
672 }
673 }
674
675 for family in [EffectFamily::CAST_TIME, EffectFamily::GLOBAL_COOLDOWN] {
676 for &passive_index in self.passive_family_indexes(family) {
677 let effect = &passive_drivers[passive_index].effect;
678 let Some(property) = configured_timing_property(effect) else {
679 continue;
680 };
681 let binding = damage
682 .passive_buff_binding(passive_index)
683 .expect("every finalized passive effect has a damage binding record");
684
685 for action in damage.action_locals() {
686 if binding.lists_action(action) {
687 passive_indexes[spell_modifier_binding_index(action, property)]
688 .push(passive_index);
689 }
690 }
691 }
692 }
693
694 for family in [
695 EffectFamily::COOLDOWN_RECHARGE_RATE,
696 EffectFamily::COOLDOWN_DURATION,
697 ] {
698 for &passive_index in self.passive_family_indexes(family) {
699 let effect = &passive_drivers[passive_index].effect;
700 let binding = damage
701 .passive_buff_binding(passive_index)
702 .expect("every finalized passive effect has a damage binding record");
703
704 for action in damage.action_locals() {
705 if cooldown_effect_applies_to_action(effect, binding, action, damage) {
706 passive_indexes
707 [spell_modifier_binding_index(action, ModifierPropertyKind::Cooldown)]
708 .push(passive_index);
709 }
710 }
711 }
712 }
713
714 for &passive_index in self.passive_family_indexes(EffectFamily::RESOURCE_COST) {
715 let effect = &passive_drivers[passive_index].effect;
716 let binding = damage
717 .passive_buff_binding(passive_index)
718 .expect("every finalized passive effect has a damage binding record");
719
720 for action in damage.action_locals() {
721 if cost_effect_applies_to_action(effect, binding, action, damage) {
722 cost_passive_indexes[action.index()].push(passive_index);
723 }
724 }
725 }
726
727 let player_entries = self
728 .actor_family_effects(ActorId::Player, EffectFamily::SPELL_MODIFIER)
729 .to_vec();
730
731 for entry in player_entries {
732 let Some(effect) = auras
733 .get(entry.local.as_usize())
734 .and_then(|aura| aura.effects.get(usize::from(entry.position)))
735 .and_then(Option::as_ref)
736 else {
737 continue;
738 };
739 let binding = damage
740 .aura_buff_binding(entry.local, entry.position)
741 .expect("every finalized aura effect has a damage binding record");
742
743 for action in damage.action_locals() {
744 if let Some(property) = action_modifier_property(effect)
745 && binding
746 .source_effect
747 .is_some_and(|source| damage.effect_affects_action(source, action))
748 {
749 player_auras[spell_modifier_binding_index(action, property)].push(entry);
750 }
751 }
752 }
753
754 for family in [EffectFamily::CAST_TIME, EffectFamily::GLOBAL_COOLDOWN] {
755 let player_entries = self.actor_family_effects(ActorId::Player, family).to_vec();
756
757 for entry in player_entries {
758 let Some(effect) = auras
759 .get(entry.local.as_usize())
760 .and_then(|aura| aura.effects.get(usize::from(entry.position)))
761 .and_then(Option::as_ref)
762 else {
763 continue;
764 };
765 let Some(property) = configured_timing_property(effect) else {
766 continue;
767 };
768 let binding = damage
769 .aura_buff_binding(entry.local, entry.position)
770 .expect("every finalized aura effect has a damage binding record");
771
772 for action in damage.action_locals() {
773 if binding.lists_action(action) {
774 player_auras[spell_modifier_binding_index(action, property)].push(entry);
775 }
776 }
777 }
778 }
779
780 for family in [
781 EffectFamily::COOLDOWN_RECHARGE_RATE,
782 EffectFamily::COOLDOWN_DURATION,
783 ] {
784 let player_entries = self.actor_family_effects(ActorId::Player, family).to_vec();
785
786 for entry in player_entries {
787 let effect = auras[entry.local.as_usize()].effects[usize::from(entry.position)]
788 .as_ref()
789 .expect("planned effect position remains occupied after finalization");
790 let binding = damage
791 .aura_buff_binding(entry.local, entry.position)
792 .expect("every finalized aura effect has a damage binding record");
793
794 for action in damage.action_locals() {
795 if cooldown_effect_applies_to_action(effect, binding, action, damage) {
796 player_auras
797 [spell_modifier_binding_index(action, ModifierPropertyKind::Cooldown)]
798 .push(entry);
799 }
800 }
801 }
802 }
803
804 let scoped_source = self
805 .actor_bindings
806 .by_family
807 .get(
808 single_family_index(EffectFamily::SPELL_MODIFIER)
809 .expect("spell modifiers occupy one finalized family"),
810 )
811 .expect("spell-modifier actor bindings are finalized");
812 let mut scoped_actor_auras = Vec::with_capacity(action_count);
813 let mut damage_actor_auras = Vec::with_capacity(action_count);
814 let cost_source = self
815 .actor_bindings
816 .by_family
817 .get(
818 single_family_index(EffectFamily::RESOURCE_COST)
819 .expect("resource costs occupy one finalized family"),
820 )
821 .expect("resource-cost actor bindings are finalized");
822 let mut cost_actor_auras = Vec::with_capacity(action_count);
823
824 for action in damage.action_locals() {
825 scoped_actor_auras.push(filter_actor_ranges(scoped_source, |entry| {
826 let effect = auras[entry.local.as_usize()].effects[usize::from(entry.position)]
827 .as_ref()
828 .expect("planned effect position remains occupied after finalization");
829 let binding = damage
830 .aura_buff_binding(entry.local, entry.position)
831 .expect("every finalized aura effect has a damage binding record");
832
833 spell_scoped_effect_applies_to_action(effect, binding, action, damage)
834 }));
835 damage_actor_auras.push(filter_actor_ranges(scoped_source, |entry| {
836 let effect = auras[entry.local.as_usize()].effects[usize::from(entry.position)]
837 .as_ref()
838 .expect("planned effect position remains occupied after finalization");
839 let binding = damage
840 .aura_buff_binding(entry.local, entry.position)
841 .expect("every finalized aura effect has a damage binding record");
842
843 damage_scoped_effect_applies_to_action(effect, binding, action, damage)
844 }));
845 cost_actor_auras.push(filter_actor_ranges(cost_source, |entry| {
846 let effect = auras[entry.local.as_usize()].effects[usize::from(entry.position)]
847 .as_ref()
848 .expect("planned effect position remains occupied after finalization");
849 let binding = damage
850 .aura_buff_binding(entry.local, entry.position)
851 .expect("every finalized aura effect has a damage binding record");
852
853 cost_effect_applies_to_action(effect, binding, action, damage)
854 }));
855 }
856
857 self.action_spell_modifiers = ActionSpellModifierBindings {
858 passive_indexes: passive_indexes
859 .into_iter()
860 .map(Vec::into_boxed_slice)
861 .collect(),
862 #[cfg(feature = "rotation-optimizations")]
863 player_aura_plans: player_auras
864 .into_iter()
865 .map(|entries| ActiveAuraEffectPlan::compile(&entries, auras.len()))
866 .collect(),
867 #[cfg(not(feature = "rotation-optimizations"))]
868 player_auras: player_auras
869 .into_iter()
870 .map(Vec::into_boxed_slice)
871 .collect(),
872 scoped_passive_indexes: scoped_passive_indexes
873 .into_iter()
874 .map(Vec::into_boxed_slice)
875 .collect(),
876 #[cfg(feature = "rotation-optimizations")]
877 scoped_aura_plans: scoped_actor_auras
878 .iter()
879 .map(|ranges| ActiveAuraEffectPlan::compile(&ranges.entries, auras.len()))
880 .collect(),
881 #[cfg(not(feature = "rotation-optimizations"))]
882 scoped_actor_auras: scoped_actor_auras.into_boxed_slice(),
883 damage_passive_indexes: damage_passive_indexes
884 .into_iter()
885 .map(Vec::into_boxed_slice)
886 .collect(),
887 #[cfg(feature = "rotation-optimizations")]
888 damage_aura_plans: damage_actor_auras
889 .iter()
890 .map(|ranges| ActiveAuraEffectPlan::compile(&ranges.entries, auras.len()))
891 .collect(),
892 #[cfg(not(feature = "rotation-optimizations"))]
893 damage_actor_auras: damage_actor_auras.into_boxed_slice(),
894 cost_passive_indexes: cost_passive_indexes
895 .into_iter()
896 .map(Vec::into_boxed_slice)
897 .collect(),
898 #[cfg(feature = "rotation-optimizations")]
899 cost_aura_plans: cost_actor_auras
900 .iter()
901 .map(|ranges| ActiveAuraEffectPlan::compile(&ranges.entries, auras.len()))
902 .collect(),
903 #[cfg(not(feature = "rotation-optimizations"))]
904 cost_actor_auras: cost_actor_auras.into_boxed_slice(),
905 };
906 }
907
908 #[cfg(any(test, not(feature = "rotation-optimizations")))]
910 pub(crate) fn lifecycle_actor_auras(&self, actor: ActorId) -> &[PlannedAuraEffects] {
911 self.actor_bindings.all.for_actor(actor)
912 }
913
914 pub(crate) fn actor_family_effects(
915 &self,
916 actor: ActorId,
917 family: EffectFamily,
918 ) -> &[PlannedAuraEffect] {
919 let Some(index) = single_family_index(family) else {
920 return &[];
921 };
922
923 self.actor_bindings.by_family[index].for_actor(actor)
926 }
927
928 #[cfg(feature = "rotation-optimizations")]
929 pub(crate) fn family_aura_plan(&self, family: EffectFamily) -> &ActiveAuraEffectPlan {
930 let index = single_family_index(family).expect("effect family plan requires one family");
931
932 &self.family_auras[index]
933 }
934
935 #[cfg(feature = "rotation-optimizations")]
936 pub(crate) const fn static_total_aura_plan(&self) -> &ActiveAuraEffectPlan {
937 &self.static_total_auras
938 }
939
940 #[cfg(feature = "rotation-optimizations")]
941 pub(crate) const fn dynamic_total_aura_plan(&self) -> &ActiveAuraEffectPlan {
942 &self.dynamic_total_auras
943 }
944
945 pub(crate) fn passive_family_indexes(&self, family: EffectFamily) -> &[usize] {
946 single_family_index(family).map_or(&[], |index| {
947 &self.passive_by_family[index]
949 })
950 }
951
952 pub(crate) fn health_threshold_capacity(&self) -> usize {
953 let index = single_family_index(EffectFamily::HEALTH_THRESHOLD_TRIGGER)
954 .expect("health thresholds occupy one finalized family");
955 let auras = &self.actor_bindings.by_family[index];
956 let player = auras.for_actor(ActorId::Player).len().saturating_add(
957 self.passive_family_indexes(EffectFamily::HEALTH_THRESHOLD_TRIGGER)
958 .len(),
959 );
960
961 auras.maximum_actor_len().max(player)
962 }
963
964 pub(crate) fn spell_modifier_passive_indexes(
965 &self,
966 action: super::LocalDamageActionIdx,
967 property: ModifierPropertyKind,
968 ) -> &[usize] {
969 self.action_spell_modifiers
970 .passive_indexes
971 .get(spell_modifier_binding_index(action, property))
972 .map_or(&[], Box::as_ref)
973 }
974
975 #[cfg(feature = "rotation-optimizations")]
976 pub(crate) fn spell_modifier_player_aura_plan(
977 &self,
978 action: super::LocalDamageActionIdx,
979 property: ModifierPropertyKind,
980 ) -> &ActiveAuraEffectPlan {
981 self.action_spell_modifiers
982 .player_aura_plans
983 .get(spell_modifier_binding_index(action, property))
984 .expect("finalized action/property has an active-aura effect plan")
985 }
986
987 #[cfg(not(feature = "rotation-optimizations"))]
988 pub(crate) fn spell_modifier_player_auras(
989 &self,
990 action: super::LocalDamageActionIdx,
991 property: ModifierPropertyKind,
992 ) -> &[PlannedAuraEffect] {
993 self.action_spell_modifiers
994 .player_auras
995 .get(spell_modifier_binding_index(action, property))
996 .map_or(&[], Box::as_ref)
997 }
998
999 pub(crate) fn scoped_spell_modifier_passive_indexes(
1000 &self,
1001 action: super::LocalDamageActionIdx,
1002 ) -> &[usize] {
1003 self.action_spell_modifiers
1004 .scoped_passive_indexes
1005 .get(action.index())
1006 .map_or(&[], Box::as_ref)
1007 }
1008
1009 #[cfg(feature = "rotation-optimizations")]
1010 pub(crate) fn scoped_spell_modifier_aura_plan(
1011 &self,
1012 action: super::LocalDamageActionIdx,
1013 ) -> &ActiveAuraEffectPlan {
1014 self.action_spell_modifiers
1015 .scoped_aura_plans
1016 .get(action.index())
1017 .expect("finalized action has a scoped active-aura effect plan")
1018 }
1019
1020 #[cfg(not(feature = "rotation-optimizations"))]
1021 pub(crate) fn scoped_spell_modifier_actor_auras(
1022 &self,
1023 actor: ActorId,
1024 action: super::LocalDamageActionIdx,
1025 ) -> &[PlannedAuraEffect] {
1026 self.action_spell_modifiers
1027 .scoped_actor_auras
1028 .get(action.index())
1029 .map_or(&[], |ranges| ranges.for_actor(actor))
1030 }
1031
1032 pub(crate) fn damage_spell_modifier_passive_indexes(
1033 &self,
1034 action: super::LocalDamageActionIdx,
1035 ) -> &[usize] {
1036 self.action_spell_modifiers
1037 .damage_passive_indexes
1038 .get(action.index())
1039 .map_or(&[], Box::as_ref)
1040 }
1041
1042 #[cfg(feature = "rotation-optimizations")]
1043 pub(crate) fn damage_spell_modifier_aura_plan(
1044 &self,
1045 action: super::LocalDamageActionIdx,
1046 ) -> &ActiveAuraEffectPlan {
1047 self.action_spell_modifiers
1048 .damage_aura_plans
1049 .get(action.index())
1050 .expect("finalized action has a damage active-aura effect plan")
1051 }
1052
1053 #[cfg(not(feature = "rotation-optimizations"))]
1054 pub(crate) fn damage_spell_modifier_actor_auras(
1055 &self,
1056 actor: ActorId,
1057 action: super::LocalDamageActionIdx,
1058 ) -> &[PlannedAuraEffect] {
1059 self.action_spell_modifiers
1060 .damage_actor_auras
1061 .get(action.index())
1062 .map_or(&[], |ranges| ranges.for_actor(actor))
1063 }
1064
1065 pub(crate) fn cost_modifier_passive_indexes(
1066 &self,
1067 action: super::LocalDamageActionIdx,
1068 ) -> &[usize] {
1069 self.action_spell_modifiers
1070 .cost_passive_indexes
1071 .get(action.index())
1072 .map_or(&[], Box::as_ref)
1073 }
1074
1075 #[cfg(feature = "rotation-optimizations")]
1076 pub(crate) fn cost_modifier_aura_plan(
1077 &self,
1078 action: super::LocalDamageActionIdx,
1079 ) -> &ActiveAuraEffectPlan {
1080 self.action_spell_modifiers
1081 .cost_aura_plans
1082 .get(action.index())
1083 .expect("finalized action has a cost active-aura effect plan")
1084 }
1085
1086 #[cfg(not(feature = "rotation-optimizations"))]
1087 pub(crate) fn cost_modifier_actor_auras(
1088 &self,
1089 actor: ActorId,
1090 action: super::LocalDamageActionIdx,
1091 ) -> &[PlannedAuraEffect] {
1092 self.action_spell_modifiers
1093 .cost_actor_auras
1094 .get(action.index())
1095 .map_or(&[], |ranges| ranges.for_actor(actor))
1096 }
1097
1098 #[cfg(not(feature = "rotation-optimizations"))]
1099 pub(crate) fn action_break_auras(&self, actor: ActorId) -> &[PlannedAuraEffects] {
1100 self.actor_bindings.action_break.for_actor(actor)
1101 }
1102
1103 #[cfg(not(feature = "rotation-optimizations"))]
1104 pub(crate) fn damage_break_auras(
1105 &self,
1106 actor: ActorId,
1107 periodic: bool,
1108 ) -> &[PlannedAuraEffects] {
1109 if periodic {
1110 self.actor_bindings.periodic_damage_break.for_actor(actor)
1111 } else {
1112 self.actor_bindings.direct_damage_break.for_actor(actor)
1113 }
1114 }
1115
1116 pub(crate) fn aura_state_auras(
1117 &self,
1118 actor: ActorId,
1119 required: crate::state::AuraState,
1120 ) -> &[PlannedAuraEffects] {
1121 let entries = self.actor_bindings.aura_state.for_actor(actor);
1122 let raw = required as i32;
1123 let state = |entry: &PlannedAuraEffects| {
1124 entry
1125 .granted_aura_state
1126 .map_or(0, |candidate| candidate as i32)
1127 };
1128 let start = entries.partition_point(|entry| state(entry) < raw);
1129 let end = entries.partition_point(|entry| state(entry) <= raw);
1130
1131 &entries[start..end]
1134 }
1135
1136 pub(crate) fn shapeshift_auras(&self) -> &[PlannedAuraEffects] {
1137 self.actor_bindings.shapeshift.for_actor(ActorId::Player)
1138 }
1139
1140 pub(crate) fn dispellable_auras(
1141 &self,
1142 actor: ActorId,
1143 dispel_type: i32,
1144 ) -> &[PlannedAuraEffects] {
1145 let entries = self.actor_bindings.dispellable.for_actor(actor);
1146 let start = entries.partition_point(|entry| entry.dispel_type < dispel_type);
1147 let end = entries.partition_point(|entry| entry.dispel_type <= dispel_type);
1148
1149 &entries[start..end]
1152 }
1153
1154 pub(crate) fn mechanic_auras(&self, actor: ActorId) -> &[PlannedAuraEffects] {
1155 self.actor_bindings.mechanic.for_actor(actor)
1156 }
1157
1158 pub(crate) fn owner_duplicate_handles(&self, local: LocalAuraIdx) -> &[AuraSlotHandle] {
1159 self.owner_duplicates
1160 .get(local.as_usize())
1161 .map_or(&[], Box::as_ref)
1162 }
1163
1164 #[cfg(not(feature = "rotation-optimizations"))]
1165 pub(crate) fn static_total_actor_effects(&self, actor: ActorId) -> &[PlannedAuraEffects] {
1166 self.actor_bindings.static_totals.for_actor(actor)
1167 }
1168
1169 #[cfg(not(feature = "rotation-optimizations"))]
1170 pub(crate) fn dynamic_total_actor_effects(&self, actor: ActorId) -> &[PlannedAuraEffects] {
1171 self.actor_bindings.dynamic_totals.for_actor(actor)
1172 }
1173
1174 pub(crate) fn passive_driver_indexes(&self, class: EffectConsumerClass) -> &[usize] {
1175 &self.relevant(class).passive_driver_indexes
1176 }
1177
1178 pub(crate) fn static_total_passive_driver_indexes(&self) -> &[usize] {
1179 self.passive_driver_indexes(EffectConsumerClass::GeneralTotals)
1180 }
1181
1182 pub(crate) fn dynamic_total_passive_driver_indexes(&self) -> &[usize] {
1183 self.passive_driver_indexes(EffectConsumerClass::DynamicTotals)
1184 }
1185
1186 pub(crate) fn aura(&self, local: LocalAuraIdx) -> AuraEffectMasks {
1187 self.aura_masks[local.as_usize()]
1189 }
1190
1191 pub(crate) const fn global_capabilities(&self) -> EffectFamily {
1192 self.global_capabilities
1193 }
1194
1195 pub(crate) fn validate(&self, aura_count: usize, passive_count: usize) -> bool {
1196 self.aura_masks.len() == aura_count
1197 && self.owner_duplicates.len() == aura_count
1198 && self.optimized_plans_valid(aura_count, passive_count)
1199 && self.actor_bindings.by_family.len() == EFFECT_FAMILY_COUNT
1200 && self.actor_bindings.by_family.iter().all(|ranges| {
1201 ranges.entries.iter().all(|entry| {
1202 entry.local.as_usize() < aura_count
1203 && usize::from(entry.position) < MAX_AURA_BUFF_EFFECTS
1204 })
1205 })
1206 && [
1207 &self.actor_bindings.all,
1208 &self.actor_bindings.static_totals,
1209 &self.actor_bindings.dynamic_totals,
1210 &self.actor_bindings.action_break,
1211 &self.actor_bindings.periodic_damage_break,
1212 &self.actor_bindings.direct_damage_break,
1213 &self.actor_bindings.aura_state,
1214 &self.actor_bindings.shapeshift,
1215 &self.actor_bindings.dispellable,
1216 &self.actor_bindings.mechanic,
1217 ]
1218 .into_iter()
1219 .all(|ranges| {
1220 ranges
1221 .entries
1222 .iter()
1223 .all(|entry| entry.local.as_usize() < aura_count)
1224 })
1225 && EffectConsumerClass::ALL.into_iter().all(|class| {
1226 let relevant = self.relevant(class);
1227
1228 relevant
1229 .aura_locals
1230 .iter()
1231 .all(|local| local.as_usize() < aura_count)
1232 && relevant
1233 .passive_driver_indexes
1234 .iter()
1235 .all(|index| *index < passive_count)
1236 })
1237 && self.passive_by_family.len() == EFFECT_FAMILY_COUNT
1238 && self
1239 .passive_by_family
1240 .iter()
1241 .flatten()
1242 .all(|index| *index < passive_count)
1243 }
1244
1245 #[cfg(test)]
1246 pub(super) fn relevant_auras(&self, class: EffectConsumerClass) -> &[LocalAuraIdx] {
1247 &self.relevant(class).aura_locals
1248 }
1249
1250 #[cfg(test)]
1251 pub(super) fn relevant_passives(&self, class: EffectConsumerClass) -> &[usize] {
1252 &self.relevant(class).passive_driver_indexes
1253 }
1254
1255 #[cfg(not(feature = "rotation-optimizations"))]
1256 #[expect(
1257 clippy::unused_self,
1258 reason = "feature variants share one validation API"
1259 )]
1260 const fn optimized_plans_valid(&self, _aura_count: usize, _passive_count: usize) -> bool {
1261 true
1262 }
1263
1264 fn relevant(&self, class: EffectConsumerClass) -> &RelevantEffectList {
1265 &self.relevant[class.index()]
1267 }
1268}
1269
1270fn spell_modifier_binding_index(
1271 action: super::LocalDamageActionIdx,
1272 property: ModifierPropertyKind,
1273) -> usize {
1274 action
1275 .index()
1276 .saturating_mul(MODIFIER_PROPERTY_SLOT_COUNT)
1277 .saturating_add(property as usize)
1278}
1279
1280fn action_modifier_property(effect: &BuffEffect) -> Option<ModifierPropertyKind> {
1281 match effect {
1282 BuffEffect::SpellModifierFromEffect { property, .. }
1283 | BuffEffect::SpellTargetingFromEffect { property, .. } => Some(*property),
1284 BuffEffect::SpellCastTimeFromEffect { .. } => Some(ModifierPropertyKind::CastTime),
1285 BuffEffect::SpellGcdFromEffect { .. } => Some(ModifierPropertyKind::GlobalCooldown),
1286 BuffEffect::SpellDurationFromEffect { .. } => Some(ModifierPropertyKind::Duration),
1287 BuffEffect::SpellTickTimeFromEffect { .. } => Some(ModifierPropertyKind::TickTime),
1288 BuffEffect::ResistPushbackFromEffect { .. } => Some(ModifierPropertyKind::ResistPushback),
1289 _ => None,
1290 }
1291}
1292
1293fn configured_timing_property(effect: &BuffEffect) -> Option<ModifierPropertyKind> {
1294 match effect {
1295 BuffEffect::CastTimePercent(..) | BuffEffect::CastTimePercentPerStack(..) => {
1296 Some(ModifierPropertyKind::CastTime)
1297 }
1298 BuffEffect::GcdPercent(..) => Some(ModifierPropertyKind::GlobalCooldown),
1299 _ => None,
1300 }
1301}
1302
1303fn cooldown_effect_applies_to_action(
1304 effect: &BuffEffect,
1305 binding: &super::FinalizedBuffDamageBinding,
1306 action: super::LocalDamageActionIdx,
1307 damage: &super::DamagePlan,
1308) -> bool {
1309 match effect {
1310 BuffEffect::RechargeRatePercent(..) => binding.lists_action(action),
1311 BuffEffect::CooldownRechargeRateFromEffect { .. }
1312 | BuffEffect::CooldownDurationPercent { .. } => binding
1313 .source_effect
1314 .is_some_and(|source| damage.effect_affects_action(source, action)),
1315 BuffEffect::ChargeRechargeRateCategory { .. } => true,
1316 _ => false,
1317 }
1318}
1319
1320fn cost_effect_applies_to_action(
1321 effect: &BuffEffect,
1322 binding: &super::FinalizedBuffDamageBinding,
1323 action: super::LocalDamageActionIdx,
1324 damage: &super::DamagePlan,
1325) -> bool {
1326 match effect {
1327 BuffEffect::ResourceCostPercent(..) => true,
1328 BuffEffect::SpellCostPercent(..) | BuffEffect::SpellCostFlat(..) => {
1329 binding.lists_action(action)
1330 }
1331 BuffEffect::SpellCostFromEffect { .. }
1332 | BuffEffect::AdditionalPowerCostFromEffect { .. } => binding
1333 .source_effect
1334 .is_some_and(|source| damage.effect_affects_action(source, action)),
1335 _ => false,
1336 }
1337}
1338
1339fn spell_scoped_effect_applies_to_action(
1340 effect: &BuffEffect,
1341 binding: &super::FinalizedBuffDamageBinding,
1342 action: super::LocalDamageActionIdx,
1343 damage: &super::DamagePlan,
1344) -> bool {
1345 if matches!(effect, BuffEffect::SpellModifierFromEffect { .. }) {
1346 return false;
1347 }
1348
1349 if effect.listed_spell_ids().is_some() {
1350 return binding.lists_action(action);
1351 }
1352
1353 match effect {
1354 BuffEffect::SpellDamageFromEffect { .. }
1355 | BuffEffect::DamageTakenFromSourceSpells { .. }
1356 | BuffEffect::SpellCritChanceFromEffect { .. }
1357 | BuffEffect::SpellCritDamageFromEffect { .. } => binding
1358 .source_effect
1359 .is_some_and(|source| damage.effect_affects_action(source, action)),
1360 _ => true,
1361 }
1362}
1363
1364fn damage_scoped_effect_applies_to_action(
1365 effect: &BuffEffect,
1366 binding: &super::FinalizedBuffDamageBinding,
1367 action: super::LocalDamageActionIdx,
1368 damage: &super::DamagePlan,
1369) -> bool {
1370 if !matches!(
1371 effect,
1372 BuffEffect::DamageMultSpells(..)
1373 | BuffEffect::DamageMultSpellsActive(..)
1374 | BuffEffect::SpellDamageFromEffect { .. }
1375 | BuffEffect::DamageMultSpellsLinear(..)
1376 | BuffEffect::DynamicDamageMultSpells(..)
1377 | BuffEffect::DamageToTargetAuraMechanicPercent { .. }
1378 | BuffEffect::DamageTakenFromSource(..)
1379 | BuffEffect::DamageTakenFromSourceSpells { .. }
1380 | BuffEffect::MechanicDamageTakenPercent { .. }
1381 | BuffEffect::CreatureAoeDamageAvoidance { .. }
1382 | BuffEffect::IncomingDamagePercent { .. }
1383 | BuffEffect::SpellCritChanceFromEffect { .. }
1384 | BuffEffect::SpellCritDamageFromEffect { .. }
1385 | BuffEffect::CritChanceSpells(..)
1386 | BuffEffect::CritDamageSpells(..)
1387 ) {
1388 return false;
1389 }
1390
1391 spell_scoped_effect_applies_to_action(effect, binding, action, damage)
1392}
1393
1394impl PlannedAuraEffect {
1395 pub(crate) const fn is_canonical_for(
1396 self,
1397 requested: EffectFamily,
1398 selected: EffectFamily,
1399 ) -> bool {
1400 let matching = self.families.intersection(requested).bits();
1401
1402 matching != 0 && matching.trailing_zeros() == selected.bits().trailing_zeros()
1403 }
1404}
1405
1406fn flatten_actor_buckets<T>(buckets: Vec<Vec<T>>) -> ActorRanges<T> {
1407 let mut entries = Vec::new();
1408 let mut ranges = Vec::with_capacity(buckets.len());
1409
1410 for bucket in buckets {
1411 let start = entries.len();
1412
1413 entries.extend(bucket);
1414 ranges.push(start..entries.len());
1415 }
1416
1417 let player = ranges[PLAYER_BUCKET].clone();
1420 let external = ranges[EXTERNAL_BUCKET].clone();
1422 let enemies_start = PET_BUCKETS_START + PetIdx::MAX;
1423 let pets = ranges[PET_BUCKETS_START..enemies_start]
1425 .to_vec()
1426 .into_boxed_slice();
1427 let enemies = ranges[enemies_start..].to_vec().into_boxed_slice();
1429
1430 ActorRanges {
1431 entries: entries.into_boxed_slice(),
1432 player,
1433 external,
1434 pets,
1435 enemies,
1436 }
1437}
1438
1439fn filter_actor_ranges<T>(
1440 source: &ActorRanges<T>,
1441 mut keep: impl FnMut(&T) -> bool,
1442) -> ActorRanges<T>
1443where
1444 T: Copy,
1445{
1446 let mut buckets =
1447 Vec::with_capacity(FIXED_ACTOR_BUCKET_COUNT + source.pets.len() + source.enemies.len());
1448
1449 buckets.push(
1450 source.entries[source.player.clone()]
1451 .iter()
1452 .copied()
1453 .filter(&mut keep)
1454 .collect(),
1455 );
1456 buckets.push(
1457 source.entries[source.external.clone()]
1458 .iter()
1459 .copied()
1460 .filter(&mut keep)
1461 .collect(),
1462 );
1463
1464 for range in source.pets.iter().chain(&source.enemies) {
1465 buckets.push(
1466 source.entries[range.start..range.end]
1467 .iter()
1468 .copied()
1469 .filter(&mut keep)
1470 .collect(),
1471 );
1472 }
1473
1474 flatten_actor_buckets(buckets)
1475}
1476
1477fn local_aura(raw: usize) -> LocalAuraIdx {
1478 LocalAuraIdx::new(u8::try_from(raw).expect("finalized aura count is validated below 256"))
1479}
1480
1481fn finalized_aura(auras: &[AuraData], local: LocalAuraIdx) -> &AuraData {
1482 &auras[local.as_usize()]
1484}
1485
1486fn actor_bucket(actor: ActorId, enemy_count: usize) -> Option<usize> {
1487 match actor {
1488 ActorId::Player => Some(PLAYER_BUCKET),
1489 ActorId::External => Some(EXTERNAL_BUCKET),
1490 ActorId::Pet(pet) => {
1491 (pet.as_usize() < PetIdx::MAX).then_some(PET_BUCKETS_START + pet.as_usize())
1492 }
1493 ActorId::Enemy(enemy) => (enemy.as_usize() < enemy_count)
1494 .then_some(PET_BUCKETS_START + PetIdx::MAX + enemy.as_usize()),
1495 }
1496}
1497
1498#[cfg(test)]
1499#[path = "effects/tests.rs"]
1500mod tests;