Skip to main content

wowlab_engine_combat/state/program/damage/
compile.rs

1use super::*;
2
3impl DamagePlan {
4    // #t(fn: rust_large_fn_params) cold compilation borrows independent finalized definition families without an owning compatibility aggregate.
5    // #t(fn: rust_max_fn_lines) one cold pass keeps identity assignment, scalar lowering, target lowering, and dense layout auditable together.
6    // #t(fn: rust_cyclomatic_complexity) exhaustive cold compilation follows every reachable trigger and effect-local lowering branch.
7    // #t(fn: rust_string_error) the private compiler supplies exact spell/effect context before the draft wraps it in EngineError.
8    #[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            // BOUNDS: the loop condition validates the cursor before each indexed read.
368            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                        // BOUNDS: the bitset is sized from this exact local action universe.
503                        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                        // #t(block: rust_alloc_in_loop) allocation retains the exact coordinate only on construction failure.
540                        format!(
541                            "resolved damage geometry {}:{} is absent from the finalized action universe",
542                            geometry.spell.as_u32(),
543                            geometry.effect_index
544                        )
545                    })?;
546                // BOUNDS: geometry_action is a position in spell_ids and starts has identical length.
547                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                        // #t(rust_alloc_in_loop) allocation occurs only on construction failure and retains the exact effect coordinate.
560                        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                        // #t(rust_alloc_in_loop) allocation occurs only on construction failure and retains the exact effect coordinate.
566                        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                            // #t(rust_alloc_in_loop) allocation occurs only on construction failure and retains the exact effect coordinate.
573                            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                // BOUNDS: finalize_action constructs indices only from positions in actions' source registry.
701                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            // BOUNDS: bind_effect validates coordinates against the dense effects ranges.
717            let target = effects[geometry_effect.index()].target.ok_or_else(|| {
718                format!(
719                    "damage effect {}:{} has no finalized target execution",
720                    // BOUNDS: every effect plan stores an action assigned from the actions registry.
721                    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                // BOUNDS: bind_effect validated effect against the dense effects ranges.
731                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                // BOUNDS: the action index is constructed from a position in the actions source registry.
801                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                        // BOUNDS: the action index is constructed from a position in the actions source registry.
885                        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                // BOUNDS: every request action was constructed from this finalized action array.
960                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                        // BOUNDS: effect_start and effect_count were assigned from this finalized effect array.
964                        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                            // #t(block: rust_alloc_in_loop) allocation retains the unresolved identity only on construction failure.
1042                            format!("unresolved replication source damage action {spell_id}")
1043                        })
1044                        .and_then(LocalDamageActionIdx::new)?;
1045
1046                    // BOUNDS: source_actions is sized for every finalized action bit.
1047
1048                    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                        // BOUNDS: aura effect iteration is bounded by MAX_AURA_BUFF_EFFECTS.
1072                        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                // BOUNDS: finalize_action constructs indices only from positions in actions' source registry.
1099                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                    // BOUNDS: listed_actions is sized for every finalized action bit.
1130
1131                    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                        // BOUNDS: aura effect iteration is bounded by MAX_AURA_BUFF_EFFECTS.
1150                        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                            // #t(block: rust_alloc_in_loop) allocation retains the unresolved identity only on construction failure.
1172                            format!("unresolved finalized driver modifier action {spell_id}")
1173                        })?;
1174
1175                    // BOUNDS: actions_set is sized for every finalized action bit.
1176
1177                    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}