Skip to main content

wowlab_engine_combat/builder/lower/
mod.rs

1//! Lowering context types and helper functions for `#[derive(LowerData)]`.
2
3use wowlab_engine_domain::rotation::to_simc_key;
4use wowlab_types::{
5    constants::MS_PER_SECOND,
6    game::{CdrCondition, CdrEffect},
7    sim::{AuraOn, FastMap, IntMap},
8};
9
10use super::{
11    AutoAttackDefinitionDraft, ImpactEffectProcDefinition,
12    def::{
13        BuilderAuraDef, BuilderDamageDef, BuilderSpellDef, BuilderSpellEffect, PeriodicDef,
14        PeriodicEffectDef,
15    },
16};
17use crate::state::{
18    AuraData, AutoAttackData, ImpactEffectProc, LocalAuraIdx, LocalSpellIdx, MAX_CDR_EFFECTS,
19    PeriodicData, PeriodicKind, RuntimeDamageDef, SpellAvailability, SpellBehavior, SpellChannel,
20    SpellChannelBehavior, SpellChannelCompletion, SpellChannelTiming, SpellCooldown, SpellCost,
21    SpellDamagePolicy, SpellData, SpellEffectData, SpellEffectRange, SpellEquipmentRequirements,
22    SpellFormRequirements, SpellOverride, SpellRequirements, SpellTargeting,
23};
24
25#[cfg(test)]
26mod tests;
27
28pub(crate) struct RuntimeStateParts {
29    pub(crate) spell_data: Vec<SpellData>,
30    pub(crate) empower_cast_times_ms: Vec<u32>,
31    pub(crate) spell_effects: Vec<SpellEffectData>,
32    pub(crate) spell_by_id: IntMap<u32, LocalSpellIdx>,
33    pub(crate) aura_data: Vec<AuraData>,
34    pub(crate) aura_by_id: IntMap<u32, LocalAuraIdx>,
35    pub(crate) aura_by_identity: FastMap<(u32, AuraOn), LocalAuraIdx>,
36    pub(crate) auto_attack_data: Vec<AutoAttackData>,
37    pub(crate) impact_effect_procs: Vec<ImpactEffectProc>,
38}
39
40pub(crate) fn lower_builder_state(
41    spells: &[BuilderSpellDef],
42    auras: &[BuilderAuraDef],
43    auto_attacks: &[AutoAttackDefinitionDraft],
44    impact_effect_procs: &[ImpactEffectProcDefinition],
45    names: &mut Vec<Box<str>>,
46) -> RuntimeStateParts {
47    let mut spell_effects = Vec::new();
48    let mut empower_cast_times_ms = Vec::new();
49    let mut ctx = BuilderToRuntimeCtx {
50        names,
51        spell_effects: &mut spell_effects,
52        empower_cast_times_ms: &mut empower_cast_times_ms,
53    };
54
55    let mut spell_by_id: IntMap<u32, LocalSpellIdx> = IntMap::default();
56    let mut spell_data: Vec<SpellData> = Vec::with_capacity(spells.len());
57
58    for (i, def) in spells.iter().enumerate() {
59        spell_by_id.insert(
60            def.spell_id,
61            LocalSpellIdx::new(u8::try_from(i).expect("spell count is validated before lowering")),
62        );
63        spell_data.push(def.into_data(&mut ctx));
64    }
65
66    let mut aura_by_id: IntMap<u32, LocalAuraIdx> = IntMap::default();
67    let mut aura_by_identity: FastMap<(u32, AuraOn), LocalAuraIdx> = FastMap::default();
68    let mut aura_data: Vec<AuraData> = Vec::with_capacity(auras.len());
69
70    for (i, def) in auras.iter().enumerate() {
71        let local =
72            LocalAuraIdx::new(u8::try_from(i).expect("aura count is validated before lowering"));
73
74        aura_by_id.insert(def.aura_id, local);
75        aura_by_identity
76            .entry((def.aura_id, def.on))
77            .or_insert(local);
78
79        if def.propagates_to_pet {
80            aura_by_identity
81                .entry((def.aura_id, AuraOn::Pet))
82                .or_insert(local);
83        }
84
85        aura_data.push(def.into_data(&mut ctx));
86    }
87
88    let auto_attack_data: Vec<AutoAttackData> = auto_attacks
89        .iter()
90        .map(|def| def.into_data(&mut ctx))
91        .collect();
92    let impact_effect_procs = impact_effect_procs
93        .iter()
94        .map(|proc| ImpactEffectProc {
95            driver_spell_id: proc.driver_spell_id,
96            driver_effect_index: proc.driver_effect_index,
97            driver: proc.driver,
98            actor_filter: proc.actor_filter,
99            source: proc.source,
100            chance: proc.chance,
101            chance_scale: proc.chance_scale,
102            icd_ms: proc.icd_ms,
103            target_icd_ms: proc.target_icd_ms,
104            phase_mask: proc.phase_mask,
105            hit_mask: proc.hit_mask,
106            charges: proc.charges,
107            uses_stacks_for_charges: proc.uses_stacks_for_charges,
108            charge_aura: aura_by_id.get(&proc.driver_spell_id).copied(),
109            periodic_only: proc.periodic_only,
110            skip_periodic: proc.skip_periodic,
111            physical_only: proc.physical_only,
112            crit_only: proc.crit_only,
113            profile_spell_id: proc.profile_spell_id,
114            effects: lower_effect_range(&proc.effects, &mut ctx),
115            fire: proc.fire,
116        })
117        .collect();
118
119    RuntimeStateParts {
120        spell_data,
121        empower_cast_times_ms,
122        spell_effects,
123        spell_by_id,
124        aura_data,
125        aura_by_id,
126        aura_by_identity,
127        auto_attack_data,
128        impact_effect_procs,
129    }
130}
131
132pub(crate) struct BuilderToRuntimeCtx<'a> {
133    pub(crate) names: &'a mut Vec<Box<str>>,
134    pub(crate) spell_effects: &'a mut Vec<SpellEffectData>,
135    pub(crate) empower_cast_times_ms: &'a mut Vec<u32>,
136}
137
138impl BuilderToRuntimeCtx<'_> {
139    fn push_name(&mut self, name: &str) -> u16 {
140        let idx = u16::try_from(self.names.len()).expect("name table length fits in u16");
141
142        self.names.push(name.into());
143
144        idx
145    }
146}
147
148pub(crate) struct InfoCtx<'a> {
149    pub(crate) names: &'a [Box<str>],
150    pub(crate) auras: &'a [AuraData],
151}
152
153pub(crate) fn resolve_name_idx(name: &str, ctx: &mut BuilderToRuntimeCtx<'_>) -> u16 {
154    ctx.push_name(name)
155}
156
157pub(crate) fn lower_builder_damage(def: &BuilderDamageDef) -> RuntimeDamageDef {
158    match def {
159        BuilderDamageDef::None => RuntimeDamageDef::None,
160        BuilderDamageDef::Flat(amount) => RuntimeDamageDef::Flat(*amount),
161        BuilderDamageDef::ApCoefficient {
162            coef,
163            is_physical,
164            ap_type,
165        } => RuntimeDamageDef::ApCoefficient {
166            coef: *coef,
167            is_physical: *is_physical,
168            ap_type: *ap_type,
169        },
170        BuilderDamageDef::SpCoefficient { coef, is_physical } => RuntimeDamageDef::SpCoefficient {
171            coef: *coef,
172            is_physical: *is_physical,
173        },
174        BuilderDamageDef::Weapon {
175            multiplier,
176            flat_bonus,
177            normalized,
178            is_physical,
179            hand,
180        } => RuntimeDamageDef::Weapon {
181            multiplier: *multiplier,
182            flat_bonus: *flat_bonus,
183            normalized: *normalized,
184            is_physical: *is_physical,
185            hand: *hand,
186        },
187    }
188}
189
190pub(crate) fn lower_resolved_damage(
191    def: wowlab_engine_gamedata::ResolvedDamageDef,
192    off_hand_ap_multiplier: f64,
193) -> Option<RuntimeDamageDef> {
194    BuilderDamageDef::from_resolved(def, off_hand_ap_multiplier)
195        .map(|builder| lower_builder_damage(&builder))
196}
197
198pub(crate) fn spell_damage(
199    def: &BuilderSpellDef,
200    _ctx: &mut BuilderToRuntimeCtx<'_>,
201) -> RuntimeDamageDef {
202    lower_builder_damage(&def.damage)
203}
204
205pub(crate) fn spell_off_gcd(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> bool {
206    def.start_recovery_category != Some(wowlab_types::data::CooldownCategoryId::GLOBAL)
207}
208
209pub(crate) fn spell_empower_cast_time_offset(
210    def: &BuilderSpellDef,
211    ctx: &mut BuilderToRuntimeCtx<'_>,
212) -> u32 {
213    let offset = u32::try_from(ctx.empower_cast_times_ms.len())
214        .expect("empower cast-time pool length fits in u32");
215
216    ctx.empower_cast_times_ms
217        .extend_from_slice(&def.empower_cast_times_ms);
218
219    offset
220}
221
222pub(crate) fn spell_empower_rank_count(
223    def: &BuilderSpellDef,
224    _ctx: &mut BuilderToRuntimeCtx<'_>,
225) -> u8 {
226    u8::try_from(def.empower_cast_times_ms.len()).expect("empower rank count fits in u8")
227}
228
229pub(crate) fn spell_channel_tick_damage(
230    def: &BuilderSpellDef,
231    _ctx: &mut BuilderToRuntimeCtx<'_>,
232) -> RuntimeDamageDef {
233    match def.channel_tick_damage {
234        BuilderDamageDef::None => lower_builder_damage(&def.damage),
235        ref d => lower_builder_damage(d),
236    }
237}
238
239pub(crate) fn spell_channel_tick_damage_alt(
240    def: &BuilderSpellDef,
241    _ctx: &mut BuilderToRuntimeCtx<'_>,
242) -> RuntimeDamageDef {
243    lower_builder_damage(&def.channel_tick_damage_alt)
244}
245
246pub(crate) fn spell_has_cooldown(
247    def: &BuilderSpellDef,
248    _ctx: &mut BuilderToRuntimeCtx<'_>,
249) -> bool {
250    def.cooldown.is_some()
251}
252
253pub(crate) fn spell_cooldown_duration_ms(
254    def: &BuilderSpellDef,
255    _ctx: &mut BuilderToRuntimeCtx<'_>,
256) -> u32 {
257    match &def.cooldown {
258        Some(cd) => {
259            wowlab_types::numeric::f64_to_u32_saturating_trunc(cd.duration_secs * MS_PER_SECOND)
260        }
261        None => 0,
262    }
263}
264
265pub(crate) fn spell_cooldown_category(
266    def: &BuilderSpellDef,
267    _ctx: &mut BuilderToRuntimeCtx<'_>,
268) -> Option<wowlab_types::data::CooldownCategoryId> {
269    def.cooldown.as_ref().and_then(|cooldown| cooldown.category)
270}
271
272pub(crate) fn spell_category_cooldown_duration_ms(
273    def: &BuilderSpellDef,
274    _ctx: &mut BuilderToRuntimeCtx<'_>,
275) -> u32 {
276    def.cooldown.as_ref().map_or(0, |cooldown| {
277        wowlab_types::numeric::f64_to_u32_saturating_trunc(
278            cooldown.category_duration_secs * MS_PER_SECOND,
279        )
280    })
281}
282
283pub(crate) fn spell_charge_category(
284    def: &BuilderSpellDef,
285    _ctx: &mut BuilderToRuntimeCtx<'_>,
286) -> Option<wowlab_types::data::CooldownCategoryId> {
287    def.cooldown
288        .as_ref()
289        .and_then(|cooldown| cooldown.charge_category)
290}
291
292pub(crate) fn spell_max_charges(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> u8 {
293    match &def.cooldown {
294        Some(cd) => cd.max_charges,
295        None => 0,
296    }
297}
298
299pub(crate) fn spell_recharge_ms(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> u32 {
300    match &def.cooldown {
301        Some(cd) => {
302            wowlab_types::numeric::f64_to_u32_saturating_trunc(cd.recharge_secs * MS_PER_SECOND)
303        }
304        None => 0,
305    }
306}
307
308/// Recomputed post-build once aura defs exist (`mark_dynamic_max_charges`).
309pub(crate) const fn spell_dynamic_max_charges(
310    _def: &BuilderSpellDef,
311    _ctx: &mut BuilderToRuntimeCtx<'_>,
312) -> bool {
313    false
314}
315
316pub(crate) fn spell_cdr_effects(
317    def: &BuilderSpellDef,
318    _ctx: &mut BuilderToRuntimeCtx<'_>,
319) -> [CdrEffect; MAX_CDR_EFFECTS] {
320    let mut out = [CdrEffect {
321        target_spell_local: 0,
322        amount_ms: 0,
323        condition: CdrCondition::Always,
324    }; MAX_CDR_EFFECTS];
325
326    for (j, eff) in def.cdr_effects.iter().take(MAX_CDR_EFFECTS).enumerate() {
327        // BOUNDS: j bounded by take(MAX_CDR_EFFECTS).
328        out[j] = *eff;
329    }
330
331    out
332}
333
334pub(crate) fn spell_cdr_count(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> u8 {
335    u8::try_from(def.cdr_effects.len().min(MAX_CDR_EFFECTS)).expect("MAX_CDR_EFFECTS fits in u8")
336}
337
338pub(crate) fn spell_followup_effects(
339    def: &BuilderSpellDef,
340    ctx: &mut BuilderToRuntimeCtx<'_>,
341) -> SpellEffectRange {
342    lower_effect_range(&def.followup_effects, ctx)
343}
344
345pub(crate) fn spell_channel_tick_effects(
346    def: &BuilderSpellDef,
347    ctx: &mut BuilderToRuntimeCtx<'_>,
348) -> SpellEffectRange {
349    lower_effect_range(&def.channel_tick_effects, ctx)
350}
351
352pub(crate) fn spell_channel_complete_effects(
353    def: &BuilderSpellDef,
354    ctx: &mut BuilderToRuntimeCtx<'_>,
355) -> SpellEffectRange {
356    lower_effect_range(&def.channel_complete_effects, ctx)
357}
358
359pub(crate) const fn spell_cost(
360    def: &BuilderSpellDef,
361    _ctx: &mut BuilderToRuntimeCtx<'_>,
362) -> SpellCost {
363    SpellCost {
364        resource_cost: def.resource_cost,
365        optional_resource_cost: def.optional_resource_cost,
366        resource_cost_pct: def.resource_cost_pct,
367        maximum_resource_cost_pct: def.maximum_resource_cost_pct,
368        optional_resource_cost_pct: def.optional_resource_cost_pct,
369        resource_gain: def.resource_gain,
370        secondary_resource_cost: def.secondary_resource_cost,
371        secondary_optional_resource_cost: def.secondary_optional_resource_cost,
372        secondary_resource_gain: def.secondary_resource_gain,
373        health_cost: def.health_cost,
374        health_cost_pct: def.health_cost_pct,
375        health_max_cost_pct: def.health_max_cost_pct,
376        health_optional_cost: def.health_optional_cost,
377        health_optional_cost_pct: def.health_optional_cost_pct,
378    }
379}
380
381pub(crate) fn spell_cooldown(
382    def: &BuilderSpellDef,
383    ctx: &mut BuilderToRuntimeCtx<'_>,
384) -> SpellCooldown {
385    SpellCooldown {
386        cooldown_hasted: def.cooldown_hasted,
387        start_recovery_category: def.start_recovery_category,
388        has_cooldown: spell_has_cooldown(def, ctx),
389        cooldown_duration_ms: spell_cooldown_duration_ms(def, ctx),
390        cooldown_category: spell_cooldown_category(def, ctx),
391        category_cooldown_duration_ms: spell_category_cooldown_duration_ms(def, ctx),
392        charge_category: spell_charge_category(def, ctx),
393        max_charges: spell_max_charges(def, ctx),
394        recharge_ms: spell_recharge_ms(def, ctx),
395        dynamic_max_charges: spell_dynamic_max_charges(def, ctx),
396        cdr_effects: spell_cdr_effects(def, ctx),
397        cdr_count: spell_cdr_count(def, ctx),
398    }
399}
400
401pub(crate) fn spell_channel(
402    def: &BuilderSpellDef,
403    ctx: &mut BuilderToRuntimeCtx<'_>,
404) -> SpellChannel {
405    SpellChannel {
406        tick_count: def.channel_tick_count,
407        tick_interval_ms: def.channel_tick_interval_ms,
408        tick_damage: spell_channel_tick_damage(def, ctx),
409        tick_spell_id: def.channel_tick_spell_id,
410        tick_damage_alt: spell_channel_tick_damage_alt(def, ctx),
411        tick_damage_alt_spell_id: def.channel_tick_damage_alt_spell_id,
412        tick_damage_aura: def.channel_tick_damage_aura,
413        tick_cost: def.channel_tick_cost,
414        tick_cost_alt: def.channel_tick_cost_alt,
415        tick_cost_aura: def.channel_tick_cost_aura,
416        tick_effects: spell_channel_tick_effects(def, ctx),
417        complete_effects: spell_channel_complete_effects(def, ctx),
418        timing: SpellChannelTiming {
419            tick_zero: def.channel_tick_zero,
420            tick_is_periodic: def.channel_tick_is_periodic,
421            hasted_ticks: def.channel_hasted_ticks,
422        },
423        behavior: SpellChannelBehavior {
424            is_channel: def.is_channel,
425            duration_hasted: def.channel_duration_hasted,
426            tick_may_crit: def.channel_tick_may_crit,
427        },
428        completion: SpellChannelCompletion {
429            tick_damage_alt_may_crit: def.channel_tick_damage_alt_may_crit,
430            gcd_on_start: def.channel_gcd_on_start,
431            apply_lag: def.channel_apply_lag,
432        },
433    }
434}
435
436pub(crate) const fn spell_targeting(
437    def: &BuilderSpellDef,
438    _ctx: &mut BuilderToRuntimeCtx<'_>,
439) -> SpellTargeting {
440    SpellTargeting {
441        explicit_target_mask: def.explicit_target_mask,
442        required_explicit_target_mask: def.required_explicit_target_mask,
443        aoe_max_targets: def.aoe_max_targets,
444        aoe_reduced_targets: def.aoe_reduced_targets,
445        aoe_full_targets: def.aoe_full_targets,
446        aoe_base_mult: def.aoe_base_mult,
447        chain_targeting: def.chain_targeting,
448        chain_multiplier: def.chain_multiplier,
449        split: def.split,
450        is_aoe: def.is_aoe,
451    }
452}
453
454pub(crate) const fn spell_requirements(
455    def: &BuilderSpellDef,
456    _ctx: &mut BuilderToRuntimeCtx<'_>,
457) -> SpellRequirements {
458    SpellRequirements {
459        shapeshift_required_mask: def.shapeshift_required_mask,
460        shapeshift_excluded_mask: def.shapeshift_excluded_mask,
461        caster_aura_spell: def.caster_aura_spell,
462        caster_aura_state: def.caster_aura_state,
463        exclude_caster_aura_spell: def.exclude_caster_aura_spell,
464        exclude_caster_aura_state: def.exclude_caster_aura_state,
465        target_aura_spell: def.target_aura_spell,
466        target_aura_state: def.target_aura_state,
467        exclude_target_aura_spell: def.exclude_target_aura_spell,
468        exclude_target_aura_state: def.exclude_target_aura_state,
469        equipped_item_requirement: def.equipped_item_requirement,
470        gating_aura_id: def.gating_aura_id,
471        gating_aura_min_stacks: def.gating_aura_min_stacks,
472        cost_bypass_aura_id: def.cost_bypass_aura_id,
473        consume_aura_on_cast_id: def.consume_aura_on_cast_id,
474        cooldown_bypass_aura_id: def.cooldown_bypass_aura_id,
475        execute_below_pct: def.execute_below_pct,
476        instant_cast_aura_id: def.instant_cast_aura_id,
477        availability: SpellAvailability {
478            usable_while_casting: def.usable_while_casting,
479            usable_while_moving: def.usable_while_moving,
480            requires_unshifted: def.requires_unshifted,
481        },
482        form: SpellFormRequirements {
483            allow_while_unshifted: def.allow_while_unshifted,
484            requires_stealth: def.requires_stealth,
485        },
486        equipment: SpellEquipmentRequirements {
487            requires_behind_target: def.requires_behind_target,
488            requires_main_hand: def.requires_main_hand,
489            requires_off_hand: def.requires_off_hand,
490        },
491    }
492}
493
494pub(crate) const fn spell_override(
495    def: &BuilderSpellDef,
496    _ctx: &mut BuilderToRuntimeCtx<'_>,
497) -> SpellOverride {
498    SpellOverride {
499        aura_id: def.override_aura_id,
500        spell_id: def.override_spell_id,
501        aura_min_stacks: def.override_aura_min_stacks,
502        shares_base_cooldown: def.override_shares_base_cooldown,
503        preserves_aura: def.override_preserves_aura,
504    }
505}
506
507pub(crate) fn spell_behavior(
508    def: &BuilderSpellDef,
509    ctx: &mut BuilderToRuntimeCtx<'_>,
510) -> SpellBehavior {
511    SpellBehavior {
512        off_gcd: spell_off_gcd(def, ctx),
513        is_pet: def.is_pet,
514        breaks_stealth: def.breaks_stealth,
515    }
516}
517
518pub(crate) const fn spell_damage_policy(
519    def: &BuilderSpellDef,
520    _ctx: &mut BuilderToRuntimeCtx<'_>,
521) -> SpellDamagePolicy {
522    SpellDamagePolicy {
523        may_crit: def.damage_may_crit,
524        guaranteed_crit: def.guaranteed_crit,
525    }
526}
527
528pub(crate) fn aura_on_consume(
529    def: &BuilderAuraDef,
530    ctx: &mut BuilderToRuntimeCtx<'_>,
531) -> SpellEffectRange {
532    lower_effect_range(&def.on_consume, ctx)
533}
534
535pub(crate) fn aura_apply_effects(
536    def: &BuilderAuraDef,
537    ctx: &mut BuilderToRuntimeCtx<'_>,
538) -> SpellEffectRange {
539    lower_effect_range(&def.apply_effects, ctx)
540}
541
542pub(crate) fn aura_tick_effects(
543    def: &BuilderAuraDef,
544    ctx: &mut BuilderToRuntimeCtx<'_>,
545) -> SpellEffectRange {
546    lower_effect_range(&def.tick_effects, ctx)
547}
548
549fn lower_effect(effect: &BuilderSpellEffect, ctx: &mut BuilderToRuntimeCtx<'_>) -> SpellEffectData {
550    match effect {
551        BuilderSpellEffect::Conditional { predicate, effects } => SpellEffectData::Conditional {
552            predicate: *predicate,
553            effects: lower_effect_range(effects, ctx),
554        },
555        BuilderSpellEffect::ApplyAura { aura_local } => SpellEffectData::ApplyAura {
556            aura_local: *aura_local,
557        },
558        BuilderSpellEffect::AddAuraStack { aura_local } => SpellEffectData::AddAuraStack {
559            aura_local: *aura_local,
560        },
561        BuilderSpellEffect::RemoveAura { aura_local } => SpellEffectData::RemoveAura {
562            aura_local: *aura_local,
563        },
564        BuilderSpellEffect::InterruptCast { lockout_ms } => SpellEffectData::InterruptCast {
565            lockout_ms: *lockout_ms,
566        },
567        BuilderSpellEffect::Dispel { dispel_type, steal } => SpellEffectData::Dispel {
568            dispel_type: *dispel_type,
569            steal: *steal,
570        },
571        BuilderSpellEffect::Damage {
572            effect,
573            damage,
574            base_points,
575            may_crit,
576            attribute_flags,
577            requires_main_hand,
578            requires_off_hand,
579            equipped_item_requirement,
580        } => SpellEffectData::Damage {
581            effect: *effect,
582            damage: lower_builder_damage(damage),
583            base_points: *base_points,
584            may_crit: *may_crit,
585            attribute_flags: *attribute_flags,
586            requires_main_hand: *requires_main_hand,
587            requires_off_hand: *requires_off_hand,
588            equipped_item_requirement: *equipped_item_requirement,
589        },
590        BuilderSpellEffect::Heal {
591            effect,
592            base,
593            ap_coef,
594            sp_coef,
595            percent_of_max,
596            may_crit,
597        } => SpellEffectData::Heal {
598            effect: *effect,
599            base: *base,
600            ap_coef: *ap_coef,
601            sp_coef: *sp_coef,
602            percent_of_max: *percent_of_max,
603            may_crit: *may_crit,
604        },
605        BuilderSpellEffect::Energize {
606            effect,
607            pool,
608            amount,
609        } => SpellEffectData::Energize {
610            effect: *effect,
611            pool: *pool,
612            amount: *amount,
613        },
614        BuilderSpellEffect::EnergizePercent {
615            resource_type,
616            percent,
617            effect_index,
618        } => SpellEffectData::EnergizePercent {
619            resource_type: *resource_type,
620            percent: *percent,
621            effect_index: *effect_index,
622        },
623        BuilderSpellEffect::ExtendAura {
624            aura_local,
625            amount_ms,
626        } => SpellEffectData::ExtendAura {
627            aura_local: *aura_local,
628            amount_ms: *amount_ms,
629        },
630        BuilderSpellEffect::MutateCooldown {
631            spell_local,
632            operation,
633        } => SpellEffectData::MutateCooldown {
634            spell_local: *spell_local,
635            operation: *operation,
636        },
637        BuilderSpellEffect::Delayed {
638            delay_ms,
639            profile_spell_id,
640            effects,
641        } => SpellEffectData::Delayed {
642            delay_ms: *delay_ms,
643            profile_spell_id: *profile_spell_id,
644            effects: lower_effect_range(effects, ctx),
645        },
646    }
647}
648
649fn lower_effect_range(
650    effects: &[BuilderSpellEffect],
651    ctx: &mut BuilderToRuntimeCtx<'_>,
652) -> SpellEffectRange {
653    let lowered: Vec<_> = effects
654        .iter()
655        .map(|effect| lower_effect(effect, ctx))
656        .collect();
657    let start = ctx.spell_effects.len();
658
659    ctx.spell_effects.extend(lowered);
660    let len = ctx.spell_effects.len() - start;
661
662    SpellEffectRange {
663        start: u32::try_from(start).expect("effect arena start fits in u32"),
664        len: u16::try_from(len).expect("effect range length fits in u16"),
665    }
666}
667
668pub(crate) fn aura_periodic(
669    def: &BuilderAuraDef,
670    _ctx: &mut BuilderToRuntimeCtx<'_>,
671) -> Option<PeriodicData> {
672    def.periodic.as_ref().map(periodic_data)
673}
674
675fn periodic_data(def: &PeriodicDef) -> PeriodicData {
676    let effect = match &def.effect {
677        PeriodicEffectDef::FlatDamage {
678            amount,
679            is_physical,
680        } => PeriodicKind::FlatDamage {
681            amount: *amount,
682            is_physical: *is_physical,
683        },
684        PeriodicEffectDef::RollingFlatDamage {
685            amount,
686            is_physical,
687        } => PeriodicKind::RollingFlatDamage {
688            amount: *amount,
689            is_physical: *is_physical,
690        },
691        PeriodicEffectDef::Damage {
692            ap_coef,
693            is_physical,
694        } => PeriodicKind::DamageAp {
695            coef: *ap_coef,
696            is_physical: *is_physical,
697        },
698        PeriodicEffectDef::RollingDamage {
699            ap_coef,
700            is_physical,
701        } => PeriodicKind::RollingDamageAp {
702            coef: *ap_coef,
703            is_physical: *is_physical,
704        },
705        PeriodicEffectDef::SpDamage {
706            sp_coef,
707            is_physical,
708        } => PeriodicKind::DamageSp {
709            coef: *sp_coef,
710            is_physical: *is_physical,
711        },
712        PeriodicEffectDef::RollingSpDamage {
713            sp_coef,
714            is_physical,
715        } => PeriodicKind::RollingDamageSp {
716            coef: *sp_coef,
717            is_physical: *is_physical,
718        },
719        PeriodicEffectDef::MaxHealthDamage {
720            percent,
721            is_physical,
722        } => PeriodicKind::MaxHealthDamage {
723            percent: *percent,
724            is_physical: *is_physical,
725        },
726        PeriodicEffectDef::Heal {
727            base,
728            ap_coef,
729            sp_coef,
730            percent_of_max,
731        } => PeriodicKind::Heal {
732            base: *base,
733            ap_coef: *ap_coef,
734            sp_coef: *sp_coef,
735            percent_of_max: *percent_of_max,
736        },
737        PeriodicEffectDef::Leech {
738            base,
739            ap_coef,
740            sp_coef,
741            is_physical,
742            heal_multiplier,
743        } => PeriodicKind::Leech {
744            base: *base,
745            ap_coef: *ap_coef,
746            sp_coef: *sp_coef,
747            is_physical: *is_physical,
748            heal_multiplier: *heal_multiplier,
749        },
750        PeriodicEffectDef::ResourceGain {
751            amount,
752            resource_type,
753        } => PeriodicKind::ResourceGain {
754            amount: *amount,
755            resource_type: *resource_type,
756        },
757        PeriodicEffectDef::ResourceDrain { amount } => {
758            PeriodicKind::ResourceDrain { amount: *amount }
759        }
760        PeriodicEffectDef::ApplyAura { target_aura_local } => PeriodicKind::ApplyAura {
761            target_aura_local: *target_aura_local,
762        },
763        PeriodicEffectDef::ResidualDamage => PeriodicKind::ResidualDamage,
764        PeriodicEffectDef::RemoveStack => PeriodicKind::RemoveStack,
765        PeriodicEffectDef::Hook => PeriodicKind::Hook,
766    };
767
768    PeriodicData {
769        tick_interval_ms: def.tick_ms,
770        damage_spell_id: def.damage_spell_id.unwrap_or(0),
771        effect_ref: def.effect_ref,
772        hasted_ticks: def.hasted,
773        partial_tick: def.partial_tick,
774        may_crit: def.may_crit,
775        damage_is_periodic: def.damage_is_periodic,
776        tick_on_application: def.tick_on_application,
777        effect,
778        resource_gain: def.resource_gain,
779        crit_resource_gain: def.crit_resource_gain,
780        crit_resource_chance: def.crit_resource_chance,
781    }
782}
783
784fn name_from_idx(name_idx: u16, ctx: &InfoCtx<'_>) -> String {
785    ctx.names
786        .get(name_idx as usize)
787        .map(ToString::to_string)
788        .unwrap_or_default()
789}
790
791pub(crate) fn spell_name(data: &SpellData, ctx: &InfoCtx<'_>) -> String {
792    name_from_idx(data.name_idx, ctx)
793}
794
795pub(crate) fn spell_slug(data: &SpellData, ctx: &InfoCtx<'_>) -> String {
796    to_simc_key(&name_from_idx(data.name_idx, ctx))
797}
798
799pub(crate) fn spell_damage_info(
800    data: &SpellData,
801    _ctx: &InfoCtx<'_>,
802) -> wowlab_types::game::DamageInfo {
803    use wowlab_types::game::{DamageInfo, DamageKind};
804    let kind = match data.damage {
805        RuntimeDamageDef::None => DamageKind::None,
806        RuntimeDamageDef::Flat(amount) => DamageKind::Flat { amount },
807        RuntimeDamageDef::ApCoefficient {
808            coef, is_physical, ..
809        } => DamageKind::ApCoefficient { coef, is_physical },
810        RuntimeDamageDef::SpCoefficient { coef, is_physical } => {
811            DamageKind::SpCoefficient { coef, is_physical }
812        }
813        RuntimeDamageDef::Weapon {
814            multiplier,
815            flat_bonus,
816            normalized,
817            is_physical,
818            ..
819        } => DamageKind::Weapon {
820            multiplier,
821            flat_bonus,
822            normalized,
823            is_physical,
824        },
825    };
826
827    DamageInfo { kind }
828}
829
830pub(crate) fn spell_applies_aura_id(data: &SpellData, ctx: &InfoCtx<'_>) -> Option<u32> {
831    data.applies_aura
832        // #t(rust_unchecked_indexing) applies_aura is set during build, always in bounds
833        .map(|idx| ctx.auras[idx.as_usize()].aura_id)
834}
835
836pub(crate) const fn spell_info_start_recovery_category(
837    data: &SpellData,
838    _ctx: &InfoCtx<'_>,
839) -> Option<wowlab_types::data::CooldownCategoryId> {
840    data.cooldown.start_recovery_category
841}
842
843pub(crate) const fn spell_info_off_gcd(data: &SpellData, _ctx: &InfoCtx<'_>) -> bool {
844    data.behavior.off_gcd
845}
846
847pub(crate) const fn spell_info_is_pet(data: &SpellData, _ctx: &InfoCtx<'_>) -> bool {
848    data.behavior.is_pet
849}
850
851pub(crate) const fn spell_info_breaks_stealth(data: &SpellData, _ctx: &InfoCtx<'_>) -> bool {
852    data.behavior.breaks_stealth
853}
854
855pub(crate) const fn spell_info_resource_cost(data: &SpellData, _ctx: &InfoCtx<'_>) -> f64 {
856    data.cost.resource_cost
857}
858
859pub(crate) const fn spell_info_resource_gain(data: &SpellData, _ctx: &InfoCtx<'_>) -> f64 {
860    data.cost.resource_gain
861}
862
863pub(crate) const fn spell_info_secondary_resource_cost(
864    data: &SpellData,
865    _ctx: &InfoCtx<'_>,
866) -> f64 {
867    data.cost.secondary_resource_cost
868}
869
870pub(crate) const fn spell_info_secondary_resource_gain(
871    data: &SpellData,
872    _ctx: &InfoCtx<'_>,
873) -> f64 {
874    data.cost.secondary_resource_gain
875}
876
877pub(crate) fn spell_cooldown_info(
878    data: &SpellData,
879    _ctx: &InfoCtx<'_>,
880) -> Option<wowlab_types::game::CooldownInfo> {
881    use wowlab_types::game::CooldownInfo;
882
883    data.cooldown.has_cooldown.then(|| CooldownInfo {
884        duration_secs: f64::from(data.cooldown.cooldown_duration_ms) / MS_PER_SECOND,
885        max_charges: data.cooldown.max_charges,
886        recharge_secs: f64::from(data.cooldown.recharge_ms) / MS_PER_SECOND,
887    })
888}
889
890pub(crate) fn spell_cdr_effects_info(data: &SpellData, _ctx: &InfoCtx<'_>) -> Vec<CdrEffect> {
891    // BOUNDS: cdr_count set at build time, <= cdr_effects.len().
892
893    data.cooldown.cdr_effects[..data.cooldown.cdr_count as usize].to_vec()
894}
895
896pub(crate) fn aura_name(data: &AuraData, ctx: &InfoCtx<'_>) -> String {
897    name_from_idx(data.name_idx, ctx)
898}
899
900pub(crate) fn aura_slug(data: &AuraData, ctx: &InfoCtx<'_>) -> String {
901    to_simc_key(&name_from_idx(data.name_idx, ctx))
902}
903
904pub(crate) fn aura_on(data: &AuraData, _ctx: &InfoCtx<'_>) -> String {
905    let on: AuraOn = data.on;
906
907    on.to_string()
908}
909
910pub(crate) fn aura_haste_buff_pct(data: &AuraData, _ctx: &InfoCtx<'_>) -> f64 {
911    let mut haste_buff_pct = 0.0;
912
913    for effect in data.effects.iter().flatten() {
914        if let crate::BuffEffect::Haste(v) | crate::BuffEffect::HasteMult(v) = effect {
915            haste_buff_pct += *v;
916        }
917    }
918
919    haste_buff_pct
920}
921
922pub(crate) fn aura_damage_mult_pct(data: &AuraData, _ctx: &InfoCtx<'_>) -> f64 {
923    let mut damage_mult_pct = 0.0;
924
925    for effect in data.effects.iter().flatten() {
926        if let crate::BuffEffect::DamageMult(v) = effect {
927            damage_mult_pct = *v;
928        }
929    }
930
931    damage_mult_pct
932}
933
934pub(crate) fn aura_periodic_info(
935    data: &AuraData,
936    _ctx: &InfoCtx<'_>,
937) -> Option<wowlab_types::game::PeriodicInfo> {
938    use wowlab_types::game::{PeriodicEffect, PeriodicInfo};
939
940    data.periodic.as_ref().map(|p| {
941        let effect = match p.effect {
942            PeriodicKind::FlatDamage {
943                amount,
944                is_physical,
945            }
946            | PeriodicKind::RollingFlatDamage {
947                amount,
948                is_physical,
949            } => PeriodicEffect::FlatDamage {
950                amount,
951                is_physical,
952            },
953            PeriodicKind::DamageAp { coef, is_physical }
954            | PeriodicKind::RollingDamageAp { coef, is_physical } => PeriodicEffect::Damage {
955                ap_coef: coef,
956                is_physical,
957            },
958            PeriodicKind::DamageSp { coef, is_physical }
959            | PeriodicKind::RollingDamageSp { coef, is_physical } => PeriodicEffect::SpDamage {
960                sp_coef: coef,
961                is_physical,
962            },
963            PeriodicKind::MaxHealthDamage {
964                percent,
965                is_physical,
966            } => PeriodicEffect::MaxHealthDamage {
967                percent,
968                is_physical,
969            },
970            PeriodicKind::Heal { .. } | PeriodicKind::Leech { .. } | PeriodicKind::Hook => {
971                PeriodicEffect::Hook
972            }
973            PeriodicKind::ResourceGain { amount, .. } => PeriodicEffect::ResourceGain { amount },
974            PeriodicKind::ResourceDrain { amount } => PeriodicEffect::ResourceDrain { amount },
975            PeriodicKind::ApplyAura { target_aura_local } => PeriodicEffect::ApplyAura {
976                target_aura_local: target_aura_local.0,
977            },
978            PeriodicKind::ResidualDamage => PeriodicEffect::ResidualDamage,
979            PeriodicKind::RemoveStack => PeriodicEffect::RemoveStack,
980        };
981
982        PeriodicInfo {
983            tick_ms: p.tick_interval_ms,
984            effect,
985        }
986    })
987}