1use super::*;
2
3impl DamagePlan {
4 #[expect(
9 clippy::too_many_arguments,
10 reason = "cold plan compilation keeps independent finalized registries explicit"
11 )]
12 pub(crate) fn compile(
13 config: &ProgramConfig,
14 spells: &[SpellData],
15 auras: &[AuraData],
16 passive_driver_effects: &[PassiveDriverEffect],
17 spell_effects: &[SpellEffectData],
18 declared_spell_ids: &[u32],
19 generated_identities: &[DamageActionId],
20 reachable_spell_ids: &[u32],
21 required_target_spell_ids: &[u32],
22 effect_plan: &EffectPlan,
23 driver_spell_modifiers: &[DriverSpellModifier],
24 impact_effect_procs: &[crate::state::ImpactEffectProc],
25 periodic_drivers: &[crate::state::PeriodicDriver],
26 landed_impact_procs: &[LandedImpactProc],
27 auto_attacks: &[crate::state::AutoAttackData],
28 hook_flat_damage: &wowlab_types::sim::FastMap<u32, f64>,
29 hook_scalars: &wowlab_types::sim::FastMap<u32, f64>,
30 ) -> Result<Self, String> {
31 let data = &config.game_data;
32 let mut required_effects = Vec::new();
33
34 required_effects.extend(spells.iter().filter_map(|spell| {
35 (!matches!(spell.damage, crate::state::RuntimeDamageDef::None))
36 .then_some(spell.damage_effect)
37 }));
38 required_effects.extend(spells.iter().flat_map(|spell| {
39 [
40 (spell.channel.tick_damage, spell.channel.tick_spell_id),
41 (
42 spell.channel.tick_damage_alt,
43 spell.channel.tick_damage_alt_spell_id,
44 ),
45 ]
46 .into_iter()
47 .filter_map(|(damage, spell_id)| {
48 (!matches!(damage, crate::state::RuntimeDamageDef::None) && spell_id != 0)
49 .then_some(crate::state::DamageEffectRef::new(spell_id, 1))
50 })
51 }));
52 required_effects.extend(spell_effects.iter().filter_map(|effect| match effect {
53 SpellEffectData::Damage { effect, .. } => Some(*effect),
54 SpellEffectData::Heal { effect, .. } | SpellEffectData::Energize { effect, .. } => {
55 *effect
56 }
57 _ => None,
58 }));
59 required_effects.extend(
60 auto_attacks
61 .iter()
62 .map(|attack| crate::state::DamageEffectRef::new(attack.spell_id, 1)),
63 );
64
65 for aura in auras {
66 if let Some(periodic) = aura.periodic
67 && matches!(
68 periodic.effect,
69 crate::state::PeriodicKind::FlatDamage { .. }
70 | crate::state::PeriodicKind::RollingFlatDamage { .. }
71 | crate::state::PeriodicKind::DamageAp { .. }
72 | crate::state::PeriodicKind::RollingDamageAp { .. }
73 | crate::state::PeriodicKind::DamageSp { .. }
74 | crate::state::PeriodicKind::RollingDamageSp { .. }
75 | crate::state::PeriodicKind::MaxHealthDamage { .. }
76 | crate::state::PeriodicKind::Leech { .. }
77 | crate::state::PeriodicKind::ResidualDamage
78 )
79 {
80 let spell_id = periodic.damage_spell_id_or(aura.aura_id);
81
82 required_effects.push(
83 periodic
84 .effect_ref
85 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 1)),
86 );
87 required_effects.push(
88 periodic
89 .effect_ref
90 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 0)),
91 );
92 }
93
94 for effect in aura.effects.iter().flatten() {
95 if let Some((spell_id, effect_index)) = effect.dbc_source() {
96 required_effects
97 .push(crate::state::DamageEffectRef::new(spell_id, effect_index));
98 }
99
100 match effect {
101 crate::state::BuffEffect::AutoAttackReplacement(spell_id) => {
102 required_effects.push(crate::state::DamageEffectRef::new(*spell_id, 1));
103 }
104 crate::state::BuffEffect::DamageReplication(definition) => {
105 required_effects.push(definition.effect);
106 }
107 _ => {}
108 }
109 }
110 }
111
112 for driver in passive_driver_effects {
113 if let Some((spell_id, effect_index)) = driver.effect.dbc_source() {
114 required_effects.push(crate::state::DamageEffectRef::new(spell_id, effect_index));
115 }
116
117 match driver.effect {
118 crate::state::BuffEffect::AutoAttackReplacement(spell_id) => {
119 required_effects.push(crate::state::DamageEffectRef::new(spell_id, 1));
120 }
121 crate::state::BuffEffect::DamageReplication(definition) => {
122 required_effects.push(definition.effect);
123 }
124 _ => {}
125 }
126 }
127
128 let mut executable_damage_effects = spells
129 .iter()
130 .filter_map(|spell| {
131 (!matches!(spell.damage, crate::state::RuntimeDamageDef::None))
132 .then_some(spell.damage_effect)
133 })
134 .chain(spells.iter().flat_map(|spell| {
135 [
136 (spell.channel.tick_damage, spell.channel.tick_spell_id),
137 (
138 spell.channel.tick_damage_alt,
139 spell.channel.tick_damage_alt_spell_id,
140 ),
141 ]
142 .into_iter()
143 .filter_map(|(damage, spell_id)| {
144 (!matches!(damage, crate::state::RuntimeDamageDef::None) && spell_id != 0)
145 .then_some(crate::state::DamageEffectRef::new(spell_id, 1))
146 })
147 }))
148 .chain(spell_effects.iter().filter_map(|effect| match effect {
149 SpellEffectData::Damage { effect, .. } => Some(*effect),
150 _ => None,
151 }))
152 .chain(
153 auto_attacks
154 .iter()
155 .map(|attack| crate::state::DamageEffectRef::new(attack.spell_id, 1)),
156 )
157 .chain(
158 required_target_spell_ids
159 .iter()
160 .copied()
161 .map(|spell_id| crate::state::DamageEffectRef::new(spell_id, 1)),
162 )
163 .collect::<Vec<_>>();
164
165 for aura in auras {
166 if let Some(periodic) = aura.periodic
167 && matches!(
168 periodic.effect,
169 crate::state::PeriodicKind::FlatDamage { .. }
170 | crate::state::PeriodicKind::RollingFlatDamage { .. }
171 | crate::state::PeriodicKind::DamageAp { .. }
172 | crate::state::PeriodicKind::RollingDamageAp { .. }
173 | crate::state::PeriodicKind::DamageSp { .. }
174 | crate::state::PeriodicKind::RollingDamageSp { .. }
175 | crate::state::PeriodicKind::MaxHealthDamage { .. }
176 | crate::state::PeriodicKind::Leech { .. }
177 | crate::state::PeriodicKind::ResidualDamage
178 )
179 {
180 let spell_id = periodic.damage_spell_id_or(aura.aura_id);
181
182 executable_damage_effects.push(
183 periodic
184 .effect_ref
185 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 1)),
186 );
187 }
188
189 executable_damage_effects.extend(aura.effects.iter().flatten().filter_map(|effect| {
190 match effect {
191 crate::state::BuffEffect::AutoAttackReplacement(spell_id) => {
192 Some(crate::state::DamageEffectRef::new(*spell_id, 1))
193 }
194 crate::state::BuffEffect::DamageReplication(definition) => {
195 Some(definition.effect)
196 }
197 _ => None,
198 }
199 }));
200 }
201
202 executable_damage_effects.extend(passive_driver_effects.iter().filter_map(|driver| {
203 match driver.effect {
204 crate::state::BuffEffect::AutoAttackReplacement(spell_id) => {
205 Some(crate::state::DamageEffectRef::new(spell_id, 1))
206 }
207 crate::state::BuffEffect::DamageReplication(definition) => Some(definition.effect),
208 _ => None,
209 }
210 }));
211 required_effects.extend(executable_damage_effects.iter().copied());
212
213 let mut required_max_effect_indices = wowlab_types::sim::FastMap::default();
214
215 for effect in required_effects {
216 required_max_effect_indices
217 .entry(effect.spell_id)
218 .and_modify(|index: &mut u8| *index = (*index).max(effect.effect_index))
219 .or_insert(effect.effect_index);
220 }
221
222 let required_max_effect_index = |spell: SpellIdx| {
223 required_max_effect_indices
224 .get(&spell.as_u32())
225 .copied()
226 .unwrap_or_default()
227 .max(data.max_effect_index(spell))
228 .max(1)
229 };
230 let mut spell_ids = Vec::new();
231
232 for &spell_id in declared_spell_ids {
233 let spell = SpellIdx::from_raw(spell_id);
234
235 if spell_id != 0 && !spell_ids.contains(&spell) {
236 spell_ids.push(spell);
237 }
238 }
239
240 let declared_action_count = spell_ids.len();
241
242 for spell in spells {
243 let spell = SpellIdx::from_raw(spell.spell_id);
244
245 if !spell_ids.contains(&spell) {
246 spell_ids.push(spell);
247 }
248 }
249
250 for aura in auras {
251 let spell = SpellIdx::from_raw(aura.aura_id);
252
253 if !spell_ids.contains(&spell) {
254 spell_ids.push(spell);
255 }
256 }
257
258 if config.mastery_spell.as_u32() != 0 && !spell_ids.contains(&config.mastery_spell) {
259 spell_ids.push(config.mastery_spell);
260 }
261
262 let mut extra_spell_ids = reachable_spell_ids
263 .iter()
264 .copied()
265 .filter(|spell_id| *spell_id != 0)
266 .map(SpellIdx::from_raw)
267 .collect::<Vec<_>>();
268
269 extra_spell_ids.extend(spell_effects.iter().filter_map(|effect| match effect {
270 SpellEffectData::Damage { effect, .. }
271 | SpellEffectData::Heal {
272 effect: Some(effect),
273 ..
274 }
275 | SpellEffectData::Energize {
276 effect: Some(effect),
277 ..
278 } => Some(SpellIdx::from_raw(effect.spell_id)),
279 SpellEffectData::Delayed {
280 profile_spell_id, ..
281 } => Some(SpellIdx::from_raw(*profile_spell_id)),
282 _ => None,
283 }));
284 extra_spell_ids.extend(
285 required_target_spell_ids
286 .iter()
287 .copied()
288 .map(SpellIdx::from_raw),
289 );
290 extra_spell_ids.extend(
291 periodic_drivers
292 .iter()
293 .map(|driver| SpellIdx::from_raw(driver.driver_spell_id)),
294 );
295 extra_spell_ids.extend(
296 impact_effect_procs
297 .iter()
298 .flat_map(|proc| [proc.driver_spell_id, proc.profile_spell_id])
299 .filter(|spell_id| *spell_id != 0)
300 .map(SpellIdx::from_raw),
301 );
302 extra_spell_ids.extend(
303 auras
304 .iter()
305 .flat_map(|aura| aura.effects.iter().flatten())
306 .filter_map(crate::state::BuffEffect::listed_spell_ids)
307 .flatten()
308 .copied()
309 .map(SpellIdx::from_raw),
310 );
311 extra_spell_ids.extend(auras.iter().flat_map(|aura| {
312 [aura.apply_profile_spell_id, aura.tick_profile_spell_id]
313 .into_iter()
314 .filter(|spell_id| *spell_id != 0)
315 .map(SpellIdx::from_raw)
316 }));
317 extra_spell_ids.extend(
318 passive_driver_effects
319 .iter()
320 .filter_map(|driver| driver.effect.listed_spell_ids())
321 .flatten()
322 .copied()
323 .map(SpellIdx::from_raw),
324 );
325 extra_spell_ids.extend(
326 driver_spell_modifiers
327 .iter()
328 .flat_map(|modifier| modifier.spells.iter().copied())
329 .map(SpellIdx::from_raw),
330 );
331 extra_spell_ids.extend(
332 auras
333 .iter()
334 .flat_map(|aura| aura.effects.iter().flatten())
335 .filter_map(crate::state::BuffEffect::dbc_source)
336 .map(|(spell_id, _)| SpellIdx::from_raw(spell_id)),
337 );
338 extra_spell_ids.extend(
339 auras
340 .iter()
341 .flat_map(|aura| aura.effects.iter().flatten())
342 .filter_map(|effect| match effect {
343 crate::state::BuffEffect::AutoAttackReplacement(spell_id) => {
344 Some(SpellIdx::from_raw(*spell_id))
345 }
346 _ => None,
347 }),
348 );
349 extra_spell_ids.extend(
350 passive_driver_effects
351 .iter()
352 .filter_map(|driver| driver.effect.dbc_source())
353 .map(|(spell_id, _)| SpellIdx::from_raw(spell_id)),
354 );
355 extra_spell_ids.sort_unstable();
356 extra_spell_ids.dedup();
357
358 for spell in extra_spell_ids {
359 if !spell_ids.contains(&spell) {
360 spell_ids.push(spell);
361 }
362 }
363
364 let mut cursor = 0;
365
366 while cursor < spell_ids.len() {
367 let coordinate_spell = spell_ids[cursor];
369 let max_effect_index = required_max_effect_index(coordinate_spell);
370
371 for effect_index in 0..=max_effect_index {
372 let coordinate = EffectRef::new(coordinate_spell, effect_index);
373
374 if let Ok(triggered) = wowlab_engine_domain::dbc::resolve_triggered_damage(
375 wowlab_engine_domain::dbc::EffectLookup::new(data, coordinate),
376 ) && !spell_ids.contains(&triggered.spell_id)
377 {
378 spell_ids.push(triggered.spell_id);
379 }
380 }
381
382 cursor += 1;
383 }
384
385 let mut extra_spell_ids = spell_ids.split_off(declared_action_count);
386
387 extra_spell_ids.sort_unstable();
388 extra_spell_ids.dedup();
389 extra_spell_ids.retain(|spell| !spell_ids.contains(spell));
390 spell_ids.extend(extra_spell_ids);
391
392 let mut actions = Vec::with_capacity(spell_ids.len());
393 let mut effects = Vec::new();
394 let mut next_effect_start = 0_u32;
395 let action_effect_starts = spell_ids
396 .iter()
397 .map(|&spell| {
398 let start = next_effect_start;
399
400 next_effect_start += u32::from(required_max_effect_index(spell)) + 1;
401
402 start
403 })
404 .collect::<Vec<_>>();
405 let mut folded_passive_effects = Vec::with_capacity(spell_ids.len());
406 let capabilities = effect_plan.global_capabilities();
407 let reflection_capable =
408 capabilities.intersects(EffectFamily::REFLECTION | EffectFamily::DEFLECTION);
409
410 for (action_ordinal, &spell) in spell_ids.iter().enumerate() {
411 let raw = spell.as_u32();
412 let local = LocalDamageActionIdx::new(action_ordinal)?;
413 let school = wowlab_engine_domain::dbc::damage_school(data, spell)
414 .unwrap_or(wowlab_types::combat::DamageSchool::Physical);
415 let spell_props = data.spell_props_for_damage_plan(spell);
416 let attributes = data.spell_attributes(spell).unwrap_or_default();
417 let effect_start = LocalDamageEffectIdx::new(effects.len())?;
418 let max_effect_index = required_max_effect_index(spell);
419 let has_attack_effect = (1..=max_effect_index).any(|effect_index| {
420 wowlab_engine_domain::dbc::spell_effect_is(
421 data.effect_type(spell, effect_index),
422 wowlab_engine_domain::dbc::SpellEffectKind::Attack,
423 )
424 });
425 let has_direct_damage = wowlab_engine_domain::dbc::damage_def(data, spell).is_some();
426 let casting_disabled_exception = wowlab_engine_domain::dbc::spell_attribute_is(
427 attributes,
428 wowlab_engine_domain::dbc::SpellAttributeKind::RangedAbility,
429 ) || wowlab_engine_domain::dbc::spell_attribute_is(
430 attributes,
431 wowlab_engine_domain::dbc::SpellAttributeKind::IgnoreCastingDisabled,
432 ) || has_attack_effect;
433 let registered = spells.iter().find(|definition| definition.spell_id == raw);
434 let profile_aoe =
435 registered.map_or_else(DamageProfileAoe::default, |definition| DamageProfileAoe {
436 is_aoe: definition.targeting.is_aoe,
437 split: definition.targeting.split,
438 chain_targeting: definition.targeting.chain_targeting,
439 chain_multiplier: definition.targeting.chain_multiplier,
440 base_mult: definition.targeting.aoe_base_mult,
441 reduced_targets: definition.targeting.aoe_reduced_targets,
442 full_targets: definition.targeting.aoe_full_targets,
443 max_targets: definition.targeting.aoe_max_targets,
444 });
445
446 actions.push(DamageActionPlan {
447 spell,
448 effect_start,
449 effect_count: u16::from(max_effect_index) + 1,
450 passive: data.passive_spell_modifiers(spell),
451 proc_chance: data.proc_chance(spell).unwrap_or_default(),
452 gcd_ms: data.gcd_ms(spell).unwrap_or_default(),
453 aura_duration_ms: data.aura_duration_ms(spell).unwrap_or_default(),
454 aura_tick_ms: data.aura_tick_ms(spell).unwrap_or_default(),
455 duration_hasted: data.duration_hasted(spell).unwrap_or(false),
456 hasted_ticks: data.hasted_ticks(spell).unwrap_or(false),
457 secondary_gain: data.secondary_gain(spell).unwrap_or_default(),
458 primary_resource_gain: registered
459 .map_or(0.0, |definition| definition.cost.resource_gain),
460 hook_flat_damage: hook_flat_damage.get(&raw).copied(),
461 hook_scalar: hook_scalars.get(&raw).copied(),
462 profile_base_points: registered.map_or(0.0, |definition| definition.base_points),
463 profile_aoe,
464 registered_damage: registered.and_then(|definition| {
465 (!matches!(definition.damage, crate::state::RuntimeDamageDef::None))
466 .then_some((definition.base_points, definition.damage))
467 }),
468 school,
469 school_mask: SpellSchoolMask::from(school),
470 mechanic_mask: data.spell_mechanic_mask(spell),
471 attributes: attributes.to_vec().into_boxed_slice(),
472 labels: data.spell_labels(spell).to_vec().into_boxed_slice(),
473 proc_policy: crate::ProcPolicy::from_attributes(attributes),
474 primary_cost_entry: spell_props.primary_cost_entry,
475 secondary_cost_entry: spell_props.secondary_cost_entry,
476 has_attack_effect,
477 has_direct_damage,
478 guaranteed_crit: registered
479 .is_some_and(|definition| definition.damage_policy.guaranteed_crit),
480 casting_disabled_exception,
481 reflection_eligible: reflection_capable
482 && reflection_eligible(attributes, spell_props.defense_type),
483 defense_type: spell_props.defense_type,
484 hit_chance_flat: spell_props.hit_chance_flat,
485 hit_chance_multiplier: spell_props.hit_chance_multiplier,
486 target_resistance_flat: spell_props.target_resistance_flat,
487 target_resistance_multiplier: spell_props.target_resistance_multiplier,
488 treat_as_periodic: data.treat_as_periodic(spell).unwrap_or(false),
489 mastery_affects_points: spell_props.mastery_affects_points,
490 damage_attribute_flags: crate::systems::damage_flags_from_data(data, spell, false),
491 chain_target_range: data.chain_target_range(spell).unwrap_or_default(),
492 ignores_line_of_sight: data.ignores_line_of_sight(spell).unwrap_or(false),
493 });
494
495 for effect_index in 0..=max_effect_index {
496 let coordinate = EffectRef::new(spell, effect_index);
497 let aoe = data.aoe_properties(spell, effect_index);
498 let mut affected_actions = vec![0_u64; spell_ids.len().div_ceil(AFFECT_WORD_BITS)];
499
500 for (target_ordinal, target) in spell_ids.iter().copied().enumerate() {
501 if data.effect_affects_spell(spell, effect_index, target) {
502 affected_actions[target_ordinal / AFFECT_WORD_BITS] |=
504 1_u64 << (target_ordinal % AFFECT_WORD_BITS);
505 }
506 }
507
508 let direct_damage = wowlab_engine_domain::dbc::damage_def(data, spell)
509 .filter(|damage| damage.effect_index == effect_index);
510 let triggered_damage = direct_damage
511 .is_none()
512 .then(|| {
513 wowlab_engine_domain::dbc::resolve_triggered_damage(
514 wowlab_engine_domain::dbc::EffectLookup::new(data, coordinate),
515 )
516 .ok()
517 })
518 .flatten();
519 let geometry = triggered_damage.map_or(coordinate, |triggered| {
520 EffectRef::new(triggered.spell_id, triggered.damage.effect_index)
521 });
522 let resolved_damage =
523 direct_damage.or_else(|| triggered_damage.map(|triggered| triggered.damage));
524 let runtime_damage = resolved_damage.and_then(|damage| {
525 crate::builder::lower_resolved_damage(
526 damage,
527 wowlab_engine_domain::constants::OFF_HAND_DAMAGE_MULT,
528 )
529 });
530 let resolved_damage_base_points = resolved_damage.map_or(0.0, |damage| {
531 crate::state::resolved_damage_base_points(damage, 1.0)
532 });
533
534 let local_effect = LocalDamageEffectIdx::new(effects.len())?;
535 let geometry_action = spell_ids
536 .iter()
537 .position(|candidate| *candidate == geometry.spell)
538 .ok_or_else(|| {
539 format!(
541 "resolved damage geometry {}:{} is absent from the finalized action universe",
542 geometry.spell.as_u32(),
543 geometry.effect_index
544 )
545 })?;
546 let geometry_effect_start = action_effect_starts[geometry_action];
548 let geometry_local =
549 LocalDamageEffectIdx(geometry_effect_start + u32::from(geometry.effect_index));
550 let target = (|| {
551 let target_plan = compile_target_plan(TargetPlanInput {
552 spell_id: raw,
553 effect_index,
554 effect_type: aoe.effect_type,
555 target_a: aoe.implicit_target_a,
556 target_b: aoe.implicit_target_b,
557 })
558 .map_err(|error| {
559 format!("failed to compile target plan for spell {raw} effect {effect_index}: {error}")
561 })?;
562 let is_multi_target =
563 wowlab_engine_domain::targeting::target_plan_is_multi_target(&target_plan);
564 let executable = lower_target_plan(&target_plan).map_err(|error| {
565 format!(
567 "failed to lower target plan for spell {raw} effect {effect_index}: {error}"
568 )
569 })?;
570 let overlay = registered_target_geometry_overlay(raw, effect_index).map_err(
571 |error| {
572 format!("failed to resolve target geometry for spell {raw} effect {effect_index}: {error}")
574 },
575 )?;
576 let data_radius = if aoe.radius > 0.0 {
577 aoe.radius
578 } else {
579 data.radius(spell).unwrap_or_default()
580 };
581
582 Ok::<_, String>(FinalizedTargetExecution {
583 executable,
584 is_multi_target,
585 radius: overlay.and_then(|value| value.radius).unwrap_or(data_radius),
586 range: overlay
587 .and_then(|value| value.range)
588 .unwrap_or_else(|| {
589 data.hostile_max_range(spell).unwrap_or(data_radius)
590 }),
591 cone_half_angle: overlay
592 .and_then(|value| value.cone_half_angle)
593 .unwrap_or_else(|| data.cone_half_angle(spell).unwrap_or_default()),
594 face_explicit_target: overlay
595 .is_some_and(|value| value.face_explicit_target)
596 || data.spell_attributes(spell).is_some_and(|attributes| {
597 wowlab_engine_domain::dbc::spell_attribute_is(
598 attributes,
599 wowlab_engine_domain::dbc::SpellAttributeKind::EnforceFacingPrimaryTargetOnly,
600 )
601 }),
602 })
603 })();
604 let target_required = executable_damage_effects
605 .iter()
606 .any(|effect| effect.spell_id == raw && effect.effect_index == effect_index)
607 || [
608 wowlab_engine_domain::dbc::SpellEffectKind::SchoolDamage,
609 wowlab_engine_domain::dbc::SpellEffectKind::HealthLeech,
610 wowlab_engine_domain::dbc::SpellEffectKind::WeaponDamageNoSchool,
611 wowlab_engine_domain::dbc::SpellEffectKind::WeaponPercentDamage,
612 wowlab_engine_domain::dbc::SpellEffectKind::WeaponDamage,
613 wowlab_engine_domain::dbc::SpellEffectKind::NormalizedWeaponDamage,
614 ]
615 .iter()
616 .any(|kind| *kind as i32 == aoe.effect_type);
617 let target = match target {
618 Ok(target) => Some(target),
619 Err(error) if target_required => return Err(error),
620 Err(_) => None,
621 };
622
623 effects.push(DamageEffectPlan {
624 action: local,
625 effect_index,
626 aoe,
627 target,
628 mechanic_mask: data.effect_mechanic_mask(spell, effect_index),
629 base_points: data.base_points(spell, effect_index),
630 ap_coefficient: data.ap_coef(spell, effect_index),
631 sp_coefficient: data.sp_coef(spell, effect_index),
632 amplitude: data.amplitude(spell, effect_index),
633 period: data.period(spell, effect_index),
634 power_coefficient_mastery_add: data
635 .power_coefficient_mastery_add(spell, effect_index),
636 aura_subtype: data.effect_aura(spell, effect_index),
637 modifier_property: data.effect_misc_value_0(spell, effect_index),
638 attributes: data.effect_attributes(spell, effect_index),
639 geometry,
640 geometry_local,
641 resolved_damage: runtime_damage,
642 resolved_damage_base_points,
643 affected_actions: affected_actions.into_boxed_slice(),
644 });
645
646 if data.is_folded_passive(spell) {
647 folded_passive_effects.push(local_effect);
648 }
649 }
650 }
651
652 let mut generated_action_bindings = wowlab_types::sim::FastMap::default();
653 let mut missing_generated_identity = None;
654
655 for identity in generated_identities.iter().copied() {
656 let Some(ordinal) = spell_ids
657 .iter()
658 .position(|spell| spell.as_u32() == identity.spell_id())
659 else {
660 missing_generated_identity = Some(identity);
661 break;
662 };
663
664 generated_action_bindings.insert(identity, LocalDamageActionIdx::new(ordinal)?);
665 }
666
667 if let Some(identity) = missing_generated_identity {
668 return Err(format!(
669 "generated damage identity is absent from finalized action universe: {}",
670 identity.spell_id()
671 ));
672 }
673
674 let has_replication = capabilities.contains(EffectFamily::DAMAGE_REPLICATION);
675 let power_type_id = |resource: Option<wowlab_types::combat::ResourceType>| {
676 resource.map(|resource| i32::from(u8::from(resource)))
677 };
678 let primary_power_type = power_type_id(config.base_stats.resource_type);
679 let secondary_power_type = power_type_id(config.base_stats.secondary_resource_type);
680 let power_display_divisor = |power_type: Option<i32>| {
681 power_type.map_or(1.0, |power_type| data.power_display_divisor(power_type))
682 };
683 let power_cost_percent = |power_type: Option<i32>| {
684 power_type.map_or(0.0, |power_type| data.power_cost_percent(power_type))
685 };
686
687 let resolve_request = |effect: crate::state::DamageEffectRef,
688 geometry_effect: crate::state::DamageEffectRef,
689 profile_spell_id: u32|
690 -> Result<DamageRequestBinding, String> {
691 let finalize_action = |spell_id: u32| {
692 spell_ids
693 .iter()
694 .position(|spell| spell.as_u32() == spell_id)
695 .ok_or_else(|| format!("unresolved finalized damage action {spell_id}"))
696 .and_then(LocalDamageActionIdx::new)
697 };
698 let bind_effect = |effect: crate::state::DamageEffectRef| {
699 let action = finalize_action(effect.spell_id)?;
700 let plan = &actions[action.index()];
702
703 if u16::from(effect.effect_index) >= plan.effect_count {
704 return Err(format!(
705 "unresolved finalized damage effect {}:{}",
706 effect.spell_id, effect.effect_index
707 ));
708 }
709
710 Ok(LocalDamageEffectIdx(
711 plan.effect_start.0 + u32::from(effect.effect_index),
712 ))
713 };
714 let effect = bind_effect(effect)?;
715 let geometry_effect = bind_effect(geometry_effect)?;
716 let target = effects[geometry_effect.index()].target.ok_or_else(|| {
718 format!(
719 "damage effect {}:{} has no finalized target execution",
720 actions[effects[geometry_effect.index()].action.index()]
722 .spell
723 .as_u32(),
724 effects[geometry_effect.index()].effect_index
725 )
726 })?;
727
728 Ok(DamageRequestBinding {
729 effect,
730 effect_action: effects[effect.index()].action,
732 geometry_effect,
733 profile_action: finalize_action(profile_spell_id)?,
734 target,
735 })
736 };
737 let spell_requests = spells
738 .iter()
739 .map(|spell| {
740 (!matches!(spell.damage, crate::state::RuntimeDamageDef::None))
741 .then(|| {
742 resolve_request(spell.damage_effect, spell.damage_effect, spell.spell_id)
743 .map(|request| SpellDamageBinding { request })
744 })
745 .transpose()
746 })
747 .collect::<Result<Vec<_>, _>>()?;
748 let spell_actions = spells
749 .iter()
750 .map(|spell| {
751 spell_ids
752 .iter()
753 .position(|candidate| candidate.as_u32() == spell.spell_id)
754 .ok_or_else(|| format!("unresolved finalized spell action {}", spell.spell_id))
755 .and_then(LocalDamageActionIdx::new)
756 })
757 .collect::<Result<Vec<_>, _>>()?;
758 let periodic_requests = auras
759 .iter()
760 .map(|aura| {
761 let Some(periodic) = aura.periodic else {
762 return Ok(None);
763 };
764 let deals_damage = matches!(
765 periodic.effect,
766 crate::state::PeriodicKind::FlatDamage { .. }
767 | crate::state::PeriodicKind::RollingFlatDamage { .. }
768 | crate::state::PeriodicKind::DamageAp { .. }
769 | crate::state::PeriodicKind::RollingDamageAp { .. }
770 | crate::state::PeriodicKind::DamageSp { .. }
771 | crate::state::PeriodicKind::RollingDamageSp { .. }
772 | crate::state::PeriodicKind::MaxHealthDamage { .. }
773 | crate::state::PeriodicKind::Leech { .. }
774 | crate::state::PeriodicKind::ResidualDamage
775 );
776
777 if !deals_damage {
778 return Ok(None);
779 }
780
781 let spell_id = periodic.damage_spell_id_or(aura.aura_id);
782 let effect = periodic
783 .effect_ref
784 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 1));
785
786 let request = resolve_request(effect, effect, spell_id)?;
787 let snapshot_coordinate = periodic
788 .effect_ref
789 .unwrap_or_else(|| crate::state::DamageEffectRef::new(spell_id, 0));
790 let snapshot_action = spell_ids
791 .iter()
792 .position(|spell| spell.as_u32() == snapshot_coordinate.spell_id)
793 .ok_or_else(|| {
794 format!(
795 "unresolved periodic snapshot action {}",
796 snapshot_coordinate.spell_id
797 )
798 })
799 .and_then(LocalDamageActionIdx::new)?;
800 let action = &actions[snapshot_action.index()];
802
803 if u16::from(snapshot_coordinate.effect_index) >= action.effect_count {
804 return Err(format!(
805 "unresolved periodic snapshot effect {}:{}",
806 snapshot_coordinate.spell_id, snapshot_coordinate.effect_index
807 ));
808 }
809
810 let snapshot_effect = LocalDamageEffectIdx(
811 action.effect_start.0 + u32::from(snapshot_coordinate.effect_index),
812 );
813
814 Ok(Some(PeriodicDamageBinding {
815 request,
816 snapshot_effect,
817 }))
818 })
819 .collect::<Result<Vec<_>, _>>()?;
820 let aura_actions = auras
821 .iter()
822 .map(|aura| {
823 spell_ids
824 .iter()
825 .position(|candidate| candidate.as_u32() == aura.aura_id)
826 .ok_or_else(|| format!("unresolved finalized aura action {}", aura.aura_id))
827 .and_then(LocalDamageActionIdx::new)
828 })
829 .collect::<Result<Vec<_>, _>>()?;
830 let aura_profile_action = |aura: &AuraData, profile_spell_id: u32| {
831 let profile_spell_id = if profile_spell_id == 0 {
832 aura.aura_id
833 } else {
834 profile_spell_id
835 };
836
837 spell_ids
838 .iter()
839 .position(|candidate| candidate.as_u32() == profile_spell_id)
840 .ok_or_else(|| {
841 format!("unresolved finalized aura profile action {profile_spell_id}")
842 })
843 .and_then(LocalDamageActionIdx::new)
844 };
845 let aura_apply_profile_actions = auras
846 .iter()
847 .map(|aura| aura_profile_action(aura, aura.apply_profile_spell_id))
848 .collect::<Result<Vec<_>, _>>()?;
849 let aura_tick_profile_actions = auras
850 .iter()
851 .map(|aura| aura_profile_action(aura, aura.tick_profile_spell_id))
852 .collect::<Result<Vec<_>, _>>()?;
853 let effect_requests = spell_effects
854 .iter()
855 .map(|effect| match effect {
856 SpellEffectData::Damage { effect, .. } => {
857 resolve_request(*effect, *effect, effect.spell_id).map(Some)
858 }
859 _ => Ok(None),
860 })
861 .collect::<Result<Vec<_>, _>>()?;
862 let spell_effect_sources = spell_effects
863 .iter()
864 .map(|effect| {
865 let coordinate = match effect {
866 SpellEffectData::Damage { effect, .. } => Some(*effect),
867 SpellEffectData::Heal { effect, .. }
868 | SpellEffectData::Energize { effect, .. } => *effect,
869 _ => None,
870 };
871
872 coordinate
873 .map(|coordinate| {
874 let action = spell_ids
875 .iter()
876 .position(|spell| spell.as_u32() == coordinate.spell_id)
877 .ok_or_else(|| {
878 format!(
879 "unresolved spell-effect source action {}",
880 coordinate.spell_id
881 )
882 })
883 .and_then(LocalDamageActionIdx::new)?;
884 let plan = &actions[action.index()];
886
887 if u16::from(coordinate.effect_index) >= plan.effect_count {
888 return Err(format!(
889 "unresolved spell-effect source {}:{}",
890 coordinate.spell_id, coordinate.effect_index
891 ));
892 }
893
894 Ok::<_, String>(LocalDamageEffectIdx(
895 plan.effect_start.0 + u32::from(coordinate.effect_index),
896 ))
897 })
898 .transpose()
899 })
900 .collect::<Result<Vec<_>, _>>()?;
901 let delayed_profile_actions = spell_effects
902 .iter()
903 .map(|effect| match effect {
904 SpellEffectData::Delayed {
905 profile_spell_id, ..
906 } => spell_ids
907 .iter()
908 .position(|spell| spell.as_u32() == *profile_spell_id)
909 .ok_or_else(|| {
910 format!("unresolved delayed effect profile action {profile_spell_id}")
911 })
912 .and_then(LocalDamageActionIdx::new)
913 .map(Some),
914 _ => Ok(None),
915 })
916 .collect::<Result<Vec<_>, _>>()?;
917 let mastery_action = (config.mastery_spell.as_u32() != 0)
918 .then(|| {
919 spell_ids
920 .iter()
921 .position(|spell| *spell == config.mastery_spell)
922 .ok_or_else(|| {
923 format!(
924 "unresolved finalized mastery action {}",
925 config.mastery_spell.as_u32()
926 )
927 })
928 .and_then(LocalDamageActionIdx::new)
929 })
930 .transpose()?;
931 let auto_attack_requests = auto_attacks
932 .iter()
933 .map(|attack| {
934 let effect = crate::state::DamageEffectRef::new(attack.spell_id, 1);
935
936 resolve_request(effect, effect, attack.spell_id)
937 })
938 .collect::<Result<Vec<_>, _>>()?;
939 let main_hand = data.main_hand();
940 let off_hand = data.off_hand();
941 let dual_wield = off_hand.is_some();
942 let mut player_auto_attack_index = 0_usize;
943 let auto_attack_profiles = auto_attacks
944 .iter()
945 .zip(auto_attack_requests.iter().copied())
946 .map(|(attack, request)| {
947 let assigned_player_index = (!attack.is_pet).then(|| {
948 let index = player_auto_attack_index;
949
950 player_auto_attack_index = player_auto_attack_index.saturating_add(1);
951
952 index
953 });
954 let (weapon, hand) = match assigned_player_index {
955 Some(0) => (Some(main_hand), crate::state::SwingHand::MainHand),
956 Some(1) => (off_hand, crate::state::SwingHand::OffHand),
957 None | Some(_) => (None, crate::state::SwingHand::MainHand),
958 };
959 let action = &actions[request.effect_action().index()];
961 let resolved_damage = action.registered_damage.or_else(|| {
962 (0..action.effect_count).find_map(|offset| {
963 let effect = &effects[action.effect_start.index() + usize::from(offset)];
965
966 effect
967 .resolved_damage
968 .map(|damage| (effect.resolved_damage_base_points, damage))
969 })
970 });
971 let weapon_payload = resolved_damage.and_then(|(_, damage)| match damage {
972 crate::state::RuntimeDamageDef::Weapon {
973 multiplier,
974 normalized,
975 is_physical,
976 ..
977 } => Some((multiplier, normalized, is_physical)),
978 _ => None,
979 });
980 let stat_inheritance =
981 attack
982 .npc_id
983 .map_or(crate::state::PetStatInheritance::IDENTITY, |npc_id| {
984 crate::state::PetStatInheritance::new(
985 data.pet_attack_power_inheritance_mult(npc_id),
986 data.pet_spell_power_inheritance_mult(npc_id),
987 )
988 });
989
990 FinalizedAutoAttackProfile {
991 request,
992 weapon,
993 hand,
994 dual_wield,
995 weapon_multiplier: weapon_payload.map_or(1.0, |payload| payload.0),
996 normalized: weapon_payload.is_some_and(|payload| payload.1),
997 is_physical: weapon_payload.map_or(attack.is_physical, |payload| payload.2),
998 stat_inheritance,
999 }
1000 })
1001 .collect::<Vec<_>>();
1002 let channel_requests = spells
1003 .iter()
1004 .map(|spell| {
1005 let bind = |damage: crate::state::RuntimeDamageDef, damage_spell_id: u32| {
1006 if matches!(damage, crate::state::RuntimeDamageDef::None) {
1007 return Ok(None);
1008 }
1009
1010 let effect = crate::state::DamageEffectRef::new(damage_spell_id, 1);
1011
1012 resolve_request(effect, effect, spell.spell_id).map(Some)
1013 };
1014
1015 Ok::<_, String>([
1016 bind(spell.channel.tick_damage, spell.channel.tick_spell_id)?,
1017 bind(
1018 spell.channel.tick_damage_alt,
1019 spell.channel.tick_damage_alt_spell_id,
1020 )?,
1021 ])
1022 })
1023 .collect::<Result<Vec<_>, _>>()?;
1024 let mut replications = Vec::new();
1025 let mut compile_replication = |definition: crate::state::DamageReplication| {
1026 let request = resolve_request(
1027 definition.effect,
1028 definition.effect,
1029 definition.effect.spell_id,
1030 )?;
1031 let mut source_actions = vec![0_u64; actions.len().div_ceil(AFFECT_WORD_BITS)];
1032
1033 if definition.source_spells.is_empty() {
1034 source_actions.fill(u64::MAX);
1035 } else {
1036 for &spell_id in definition.source_spells {
1037 let action = spell_ids
1038 .iter()
1039 .position(|spell| spell.as_u32() == spell_id)
1040 .ok_or_else(|| {
1041 format!("unresolved replication source damage action {spell_id}")
1043 })
1044 .and_then(LocalDamageActionIdx::new)?;
1045
1046 source_actions[action.index() / AFFECT_WORD_BITS] |=
1049 1_u64 << (action.index() % AFFECT_WORD_BITS);
1050 }
1051 }
1052
1053 let index = u16::try_from(replications.len())
1054 .map_err(|error| format!("replication plan exceeds u16 capacity: {error}"))?;
1055
1056 replications.push(FinalizedDamageReplication {
1057 definition,
1058 request,
1059 source_actions: source_actions.into_boxed_slice(),
1060 });
1061
1062 Ok::<_, String>(index)
1063 };
1064 let aura_replications = auras
1065 .iter()
1066 .map(|aura| {
1067 let mut bindings = [None; crate::state::MAX_AURA_BUFF_EFFECTS];
1068
1069 for (position, effect) in aura.effects.iter().enumerate() {
1070 if let Some(crate::state::BuffEffect::DamageReplication(definition)) = effect {
1071 bindings[position] = Some(compile_replication(*definition)?);
1073 }
1074 }
1075
1076 Ok::<_, String>(bindings)
1077 })
1078 .collect::<Result<Vec<_>, _>>()?;
1079 let passive_replications = passive_driver_effects
1080 .iter()
1081 .map(|driver| match driver.effect {
1082 crate::state::BuffEffect::DamageReplication(definition) => {
1083 compile_replication(definition).map(Some)
1084 }
1085 _ => Ok(None),
1086 })
1087 .collect::<Result<Vec<_>, _>>()?;
1088 let compile_buff_binding = |effect: &crate::state::BuffEffect| {
1089 let finalize_action = |spell_id: u32| {
1090 spell_ids
1091 .iter()
1092 .position(|spell| spell.as_u32() == spell_id)
1093 .ok_or_else(|| format!("unresolved finalized modifier action {spell_id}"))
1094 .and_then(LocalDamageActionIdx::new)
1095 };
1096 let bind_effect = |spell_id: u32, effect_index: u8| {
1097 let action = finalize_action(spell_id)?;
1098 let plan = &actions[action.index()];
1100
1101 if u16::from(effect_index) >= plan.effect_count {
1102 return Err(format!(
1103 "unresolved finalized modifier effect {spell_id}:{effect_index}"
1104 ));
1105 }
1106
1107 Ok(LocalDamageEffectIdx(
1108 plan.effect_start.0 + u32::from(effect_index),
1109 ))
1110 };
1111 let source_effect = effect
1112 .dbc_source()
1113 .map(|(spell_id, effect_index)| bind_effect(spell_id, effect_index))
1114 .transpose()?;
1115 let replacement_request = match effect {
1116 crate::state::BuffEffect::AutoAttackReplacement(spell_id) => {
1117 let coordinate = crate::state::DamageEffectRef::new(*spell_id, 1);
1118
1119 Some(resolve_request(coordinate, coordinate, *spell_id)?)
1120 }
1121 _ => None,
1122 };
1123 let mut listed_actions = vec![0_u64; actions.len().div_ceil(AFFECT_WORD_BITS)];
1124
1125 if let Some(spells) = effect.listed_spell_ids() {
1126 for &spell_id in spells {
1127 let action = finalize_action(spell_id)?;
1128
1129 listed_actions[action.index() / AFFECT_WORD_BITS] |=
1132 1_u64 << (action.index() % AFFECT_WORD_BITS);
1133 }
1134 }
1135
1136 Ok::<_, String>(FinalizedBuffDamageBinding {
1137 source_effect,
1138 replacement_request,
1139 listed_actions: listed_actions.into_boxed_slice(),
1140 })
1141 };
1142 let aura_buff_bindings = auras
1143 .iter()
1144 .map(|aura| {
1145 let mut bindings = std::array::from_fn(|_| None);
1146
1147 for (position, effect) in aura.effects.iter().enumerate() {
1148 if let Some(effect) = effect {
1149 bindings[position] = Some(compile_buff_binding(effect)?);
1151 }
1152 }
1153
1154 Ok::<_, String>(bindings)
1155 })
1156 .collect::<Result<Vec<_>, _>>()?;
1157 let passive_buff_bindings = passive_driver_effects
1158 .iter()
1159 .map(|driver| compile_buff_binding(&driver.effect).map(Some))
1160 .collect::<Result<Vec<_>, _>>()?;
1161 let driver_modifier_actions = driver_spell_modifiers
1162 .iter()
1163 .map(|modifier| {
1164 let mut actions_set = vec![0_u64; actions.len().div_ceil(AFFECT_WORD_BITS)];
1165
1166 for &spell_id in modifier.spells {
1167 let action = spell_ids
1168 .iter()
1169 .position(|spell| spell.as_u32() == spell_id)
1170 .ok_or_else(|| {
1171 format!("unresolved finalized driver modifier action {spell_id}")
1173 })?;
1174
1175 actions_set[action / AFFECT_WORD_BITS] |= 1_u64 << (action % AFFECT_WORD_BITS);
1178 }
1179
1180 Ok::<_, String>(actions_set.into_boxed_slice())
1181 })
1182 .collect::<Result<Vec<_>, _>>()?;
1183 #[cfg(feature = "rotation-optimizations")]
1184 let driver_modifiers_by_action = {
1185 let mut by_action = vec![Vec::new(); actions.len()];
1186
1187 for (modifier, listed_actions) in driver_modifier_actions.iter().enumerate() {
1188 for (action, modifiers) in by_action.iter_mut().enumerate() {
1189 if listed_actions
1190 .get(action / AFFECT_WORD_BITS)
1191 .is_some_and(|word| word & (1_u64 << (action % AFFECT_WORD_BITS)) != 0)
1192 {
1193 modifiers.push(modifier);
1194 }
1195 }
1196 }
1197
1198 by_action
1199 .into_iter()
1200 .map(Vec::into_boxed_slice)
1201 .collect::<Vec<_>>()
1202 .into_boxed_slice()
1203 };
1204 #[cfg(feature = "rotation-optimizations")]
1205 let folded_passive_effects_by_action = {
1206 let mut by_action = vec![Vec::new(); actions.len()];
1207
1208 for &effect in &folded_passive_effects {
1209 let effect_plan = finalized_effect_plan(&effects, effect);
1210
1211 for (action, relevant) in by_action.iter_mut().enumerate() {
1212 if effect_plan.affects_action(LocalDamageActionIdx::new(action)?) {
1213 relevant.push(effect);
1214 }
1215 }
1216 }
1217
1218 by_action
1219 .into_iter()
1220 .map(Vec::into_boxed_slice)
1221 .collect::<Vec<_>>()
1222 .into_boxed_slice()
1223 };
1224 let impact_effect_driver_actions = impact_effect_procs
1225 .iter()
1226 .map(|proc| {
1227 (proc.driver_spell_id != 0)
1228 .then(|| {
1229 spell_ids
1230 .iter()
1231 .position(|spell| spell.as_u32() == proc.driver_spell_id)
1232 .ok_or_else(|| {
1233 format!(
1234 "unresolved finalized impact proc driver action {}",
1235 proc.driver_spell_id
1236 )
1237 })
1238 .and_then(LocalDamageActionIdx::new)
1239 })
1240 .transpose()
1241 })
1242 .collect::<Result<Vec<_>, _>>()?;
1243 let impact_effect_profile_actions = impact_effect_procs
1244 .iter()
1245 .map(|proc| {
1246 let spell_id = if proc.profile_spell_id == 0 {
1247 proc.driver_spell_id
1248 } else {
1249 proc.profile_spell_id
1250 };
1251
1252 (spell_id != 0)
1253 .then(|| {
1254 spell_ids
1255 .iter()
1256 .position(|spell| spell.as_u32() == spell_id)
1257 .ok_or_else(|| {
1258 format!(
1259 "unresolved finalized impact proc profile action {spell_id}"
1260 )
1261 })
1262 .and_then(LocalDamageActionIdx::new)
1263 })
1264 .transpose()
1265 })
1266 .collect::<Result<Vec<_>, _>>()?;
1267 let periodic_driver_actions = periodic_drivers
1268 .iter()
1269 .map(|driver| {
1270 spell_ids
1271 .iter()
1272 .position(|spell| spell.as_u32() == driver.driver_spell_id)
1273 .ok_or_else(|| {
1274 format!(
1275 "unresolved finalized periodic driver action {}",
1276 driver.driver_spell_id
1277 )
1278 })
1279 .and_then(LocalDamageActionIdx::new)
1280 })
1281 .collect::<Result<Vec<_>, _>>()?;
1282
1283 Ok(Self {
1284 actions: actions.into_boxed_slice(),
1285 effects: effects.into_boxed_slice(),
1286 generated_action_bindings,
1287 #[cfg(not(feature = "rotation-optimizations"))]
1288 folded_passive_effects: folded_passive_effects.into_boxed_slice(),
1289 #[cfg(feature = "rotation-optimizations")]
1290 folded_passive_effects_by_action,
1291 crit_damage_per_crit_chance: data
1292 .crit_damage_per_crit_chance()
1293 .iter()
1294 .map(|(schools, percent)| (SpellSchoolMask::from_dbc(*schools), *percent))
1295 .collect(),
1296 spell_requests: spell_requests.into_boxed_slice(),
1297 spell_actions: spell_actions.into_boxed_slice(),
1298 periodic_requests: periodic_requests.into_boxed_slice(),
1299 aura_actions: aura_actions.into_boxed_slice(),
1300 aura_apply_profile_actions: aura_apply_profile_actions.into_boxed_slice(),
1301 aura_tick_profile_actions: aura_tick_profile_actions.into_boxed_slice(),
1302 effect_requests: effect_requests.into_boxed_slice(),
1303 spell_effect_sources: spell_effect_sources.into_boxed_slice(),
1304 delayed_profile_actions: delayed_profile_actions.into_boxed_slice(),
1305 mastery_action,
1306 auto_attacks: auto_attack_profiles.into_boxed_slice(),
1307 channel_requests: channel_requests.into_boxed_slice(),
1308 replications: replications.into_boxed_slice(),
1309 aura_replications: aura_replications.into_boxed_slice(),
1310 passive_replications: passive_replications.into_boxed_slice(),
1311 aura_buff_bindings: aura_buff_bindings.into_boxed_slice(),
1312 passive_buff_bindings: passive_buff_bindings.into_boxed_slice(),
1313 #[cfg(not(feature = "rotation-optimizations"))]
1314 driver_modifier_actions: driver_modifier_actions.into_boxed_slice(),
1315 #[cfg(feature = "rotation-optimizations")]
1316 driver_modifiers_by_action,
1317 impact_effect_driver_actions: impact_effect_driver_actions.into_boxed_slice(),
1318 impact_effect_profile_actions: impact_effect_profile_actions.into_boxed_slice(),
1319 periodic_driver_actions: periodic_driver_actions.into_boxed_slice(),
1320 auto_attack_damage_mult: data.auto_attack_damage_mult(),
1321 pet_damage_mult: data.pet_damage_mult(),
1322 guardian_damage_mult: data.guardian_damage_mult(),
1323 target_armor_mult: data.target_armor_mult(),
1324 player_level: i32::try_from(data.level()).unwrap_or(i32::MAX),
1325 main_hand: data.main_hand(),
1326 off_hand: data.off_hand(),
1327 primary_power_display_divisor: power_display_divisor(primary_power_type),
1328 secondary_power_display_divisor: power_display_divisor(secondary_power_type),
1329 primary_power_cost_percent: power_cost_percent(primary_power_type),
1330 secondary_power_cost_percent: power_cost_percent(secondary_power_type),
1331 rage_auto_attack_gain_multiplier: data.power_auto_attack_gain_multiplier(i32::from(
1332 u8::from(wowlab_types::combat::ResourceType::Rage),
1333 )),
1334 health_per_stamina: data.health_per_stamina(data.level()),
1335 has_spell_modifiers: capabilities.contains(EffectFamily::SPELL_MODIFIER)
1336 || !driver_spell_modifiers.is_empty(),
1337 has_armor_bypass: capabilities.contains(EffectFamily::ARMOR_BYPASS),
1338 has_health_critical: capabilities
1339 .contains(EffectFamily::HEALTH_DEPENDENT_CRITICAL_CHANCE),
1340 has_replication,
1341 has_reflection: capabilities.contains(EffectFamily::REFLECTION),
1342 has_deflection: capabilities.contains(EffectFamily::DEFLECTION),
1343 retains_landed_impacts: !landed_impact_procs.is_empty(),
1344 })
1345 }
1346}
1347
1348#[cfg(feature = "rotation-optimizations")]
1349fn finalized_effect_plan(
1350 effects: &[DamageEffectPlan],
1351 effect: LocalDamageEffectIdx,
1352) -> &DamageEffectPlan {
1353 effects
1354 .get(effect.index())
1355 .expect("folded passive belongs to the finalized effect registry")
1356}