Skip to main content

wowlab_parsers/parsers/transform/
spell.rs

1#[cfg(feature = "dbc")]
2use wowlab_engine_domain::dbc::{
3    AuraSubtypeKind, SpellAttributeKind, SpellEffectKind, aura_subtype_kind, spell_attribute_is,
4    spell_attribute_is_any, spell_effect_is, spell_effect_is_any,
5};
6#[cfg(feature = "dbc")]
7use wowlab_types::constants::DEFAULT_GCD_MS_I32;
8#[cfg(feature = "dbc")]
9use wowlab_types::data::{
10    EmpowerStage, KnowledgeSource, LearnSpell, PeriodicType, PowerCostEntry, RefreshBehavior,
11    RppmMod, SpellDataFlat, SpellEffect, SpellLabel,
12};
13
14#[cfg(feature = "dbc")]
15use super::super::dbc::DbcData;
16#[cfg(feature = "dbc")]
17use super::super::dbc::rows::SpellEffectRow;
18#[cfg(feature = "dbc")]
19use super::super::errors::TransformError;
20
21#[cfg(all(feature = "dbc", test))]
22const TEST_MAX_TARGETS: i32 = 7;
23
24#[cfg(feature = "dbc")]
25#[expect(
26    clippy::struct_excessive_bools,
27    reason = "these independent flags mirror orthogonal spell attributes and are consumed by name"
28)]
29struct AuraFlags {
30    duration_hasted: bool,
31    hasted_ticks: bool,
32    tick_may_crit: bool,
33    tick_on_application: bool,
34    pandemic_refresh: bool,
35    rolling_periodic: bool,
36}
37
38#[cfg(feature = "dbc")]
39struct PeriodicInfo {
40    periodic_type: Option<PeriodicType>,
41    tick_period_ms: i32,
42}
43
44#[cfg(feature = "dbc")]
45fn extract_aura_flags(attributes: &[i32]) -> AuraFlags {
46    AuraFlags {
47        duration_hasted: spell_attribute_is(attributes, SpellAttributeKind::HasteAffectsDuration),
48        hasted_ticks: spell_attribute_is_any(
49            attributes,
50            &[
51                SpellAttributeKind::SpellHasteAffectsPeriodic,
52                SpellAttributeKind::MeleeHasteAffectsPeriodic,
53            ],
54        ),
55        tick_may_crit: spell_attribute_is(attributes, SpellAttributeKind::PeriodicCanCrit),
56        tick_on_application: spell_attribute_is(attributes, SpellAttributeKind::TickOnApplication),
57        pandemic_refresh: spell_attribute_is(
58            attributes,
59            SpellAttributeKind::PeriodicRefreshExtendsDuration,
60        ),
61        rolling_periodic: spell_attribute_is(attributes, SpellAttributeKind::RollingPeriodic),
62    }
63}
64
65#[cfg(feature = "dbc")]
66fn extract_periodic_info(dbc: &DbcData, spell_id: i32) -> PeriodicInfo {
67    let effects = dbc
68        .spell_effect
69        .get(&spell_id)
70        .map_or(&[] as &[SpellEffectRow], Vec::as_slice);
71
72    for effect in effects {
73        if !spell_effect_is(effect.Effect, SpellEffectKind::ApplyAura) {
74            continue;
75        }
76
77        let periodic_type = match aura_subtype_kind(effect.EffectAura) {
78            Some(AuraSubtypeKind::PeriodicDamage) => Some(PeriodicType::Damage),
79            Some(AuraSubtypeKind::PeriodicHeal) => Some(PeriodicType::Heal),
80            Some(AuraSubtypeKind::PeriodicLeech) => Some(PeriodicType::Leech),
81            Some(AuraSubtypeKind::PeriodicEnergize) => Some(PeriodicType::Energize),
82            Some(AuraSubtypeKind::PeriodicTriggerSpell) => Some(PeriodicType::TriggerSpell),
83            _ => None,
84        };
85
86        if periodic_type.is_some() {
87            return PeriodicInfo {
88                periodic_type,
89                tick_period_ms: effect.EffectAuraPeriod,
90            };
91        }
92    }
93
94    PeriodicInfo {
95        periodic_type: None,
96        tick_period_ms: 0,
97    }
98}
99
100#[cfg(feature = "dbc")]
101fn effect_radius(dbc: &DbcData, effect: &SpellEffectRow) -> Option<(f32, f32)> {
102    let index = [effect.EffectRadiusIndex_0, effect.EffectRadiusIndex_1]
103        .into_iter()
104        .find(|index| *index > 0)?;
105
106    dbc.spell_radius
107        .get(&index)
108        .map(|radius| (radius.RadiusMin, radius.RadiusMax))
109}
110
111#[cfg(feature = "dbc")]
112fn knowledge_source(spell_id: i32, context: Option<&SpellKnowledgeContext>) -> KnowledgeSource {
113    let Some(context) = context else {
114        return KnowledgeSource::Unknown;
115    };
116
117    if let Some(definition_id) = context
118        .talent_spell_id_to_trait_definition_id
119        .as_ref()
120        .and_then(|talents| talents.get(&spell_id))
121    {
122        return KnowledgeSource::Talent {
123            trait_definition_id: *definition_id,
124        };
125    }
126
127    if let (Some(class_id), Some(class_spells)) = (context.class_id, &context.class_spell_ids) {
128        if class_spells.contains(&spell_id) {
129            return KnowledgeSource::Class { class_id };
130        }
131    }
132
133    context
134        .spec_id
135        .map_or(KnowledgeSource::Unknown, |spec_id| KnowledgeSource::Spec {
136            spec_id,
137        })
138}
139
140#[cfg(feature = "dbc")]
141const fn determine_refresh_behavior(flags: &AuraFlags, tick_period_ms: i32) -> RefreshBehavior {
142    if flags.pandemic_refresh {
143        RefreshBehavior::Pandemic
144    } else if tick_period_ms > 0 {
145        RefreshBehavior::Tick
146    } else {
147        RefreshBehavior::Duration
148    }
149}
150
151/// Optional class- and spec-scoped knowledge used to enrich a spell with class/spec/talent linkage during transformation.
152#[derive(Debug)]
153pub struct SpellKnowledgeContext {
154    pub class_id: Option<i32>,
155    pub class_spell_ids: Option<wowlab_types::sim::FastSet<i32>>,
156    pub spec_id: Option<i32>,
157    pub talent_spell_id_to_trait_definition_id: Option<wowlab_types::sim::IntMap<i32, i32>>,
158}
159
160/// Build a fully-resolved `SpellDataFlat` for `spell_id` by joining 20+ DBC tables and applying optional class/spec context.
161///
162/// # Errors
163///
164/// Returns [`TransformError`] when `spell_id` is absent from the DBC data.
165#[cfg(feature = "dbc")]
166// #t(rust_max_fn_lines) spell transform gathers 20+ DBC tables into a single flat struct sequentially
167pub fn transform_spell(
168    dbc: &DbcData,
169    spell_id: i32,
170    context: Option<&SpellKnowledgeContext>,
171) -> Result<SpellDataFlat, TransformError> {
172    let name_row = dbc
173        .spell_name
174        .get(&spell_id)
175        .ok_or_else(|| TransformError::spell_not_found(spell_id))?;
176
177    let spell_row = dbc.spell.get(&spell_id);
178    let misc = dbc.spell_misc.get(&spell_id);
179    let effects = dbc
180        .spell_effect
181        .get(&spell_id)
182        .map_or(&[] as &[SpellEffectRow], Vec::as_slice);
183    let categories = dbc.spell_categories.get(&spell_id).and_then(|rows| {
184        rows.iter()
185            .find(|row| {
186                wowlab_engine_domain::dbc::DifficultyId::try_from(row.DifficultyID).ok()
187                    == Some(wowlab_engine_domain::dbc::DifficultyId::Base)
188            })
189            .or_else(|| rows.first())
190    });
191
192    let attributes: Vec<i32> = misc
193        .map(super::super::dbc::rows::spell::SpellMiscRow::attributes_vec)
194        .unwrap_or_default();
195
196    let (range_min_0, range_min_1, range_max_0, range_max_1) = misc
197        .and_then(|m| dbc.spell_range.get(&m.RangeIndex))
198        .map_or((0.0, 0.0, 0.0, 0.0), |r| {
199            (r.RangeMin_0, r.RangeMin_1, r.RangeMax_0, r.RangeMax_1)
200        });
201
202    let (radius_min, radius_max) = effects
203        .iter()
204        .find_map(|effect| effect_radius(dbc, effect))
205        .unwrap_or((0.0, 0.0));
206
207    let cooldown = dbc.spell_cooldowns.get(&spell_id).and_then(|rows| {
208        rows.iter()
209            .find(|row| {
210                wowlab_engine_domain::dbc::DifficultyId::try_from(row.DifficultyID).ok()
211                    == Some(wowlab_engine_domain::dbc::DifficultyId::Base)
212            })
213            .or_else(|| rows.first())
214    });
215    let recovery_time = cooldown.map_or(0, |c| c.RecoveryTime);
216    let category_recovery_time = cooldown.map_or(0, |c| c.CategoryRecoveryTime);
217    let start_recovery_time = cooldown.map_or(DEFAULT_GCD_MS_I32, |c| c.StartRecoveryTime);
218    let start_recovery_category = categories.map_or(0, |c| c.StartRecoveryCategory);
219
220    let cast_time = misc
221        .and_then(|m| dbc.spell_cast_times.get(&m.CastingTimeIndex))
222        .map_or(0, |ct| ct.Base);
223
224    let (duration, max_duration) = misc
225        .and_then(|m| dbc.spell_duration.get(&m.DurationIndex))
226        .map_or((0, 0), |d| (d.Duration, d.MaxDuration));
227
228    let charge_category = categories
229        .filter(|c| c.ChargeCategory > 0)
230        .and_then(|c| dbc.spell_category.get(&c.ChargeCategory));
231    let (max_charges, charge_recovery_time) =
232        charge_category.map_or((0, 0), |cat| (cat.MaxCharges, cat.ChargeRecoveryTime));
233
234    let power_costs: Vec<PowerCostEntry> = dbc
235        .spell_power
236        .get(&spell_id)
237        .map(|powers| {
238            powers
239                .iter()
240                .map(|p| PowerCostEntry {
241                    power_type: p.PowerType,
242                    cost: p.ManaCost,
243                    cost_pct: p.PowerCostPct,
244                    max_cost_pct: f64::from(p.PowerCostMaxPct),
245                    optional_cost: p.OptionalCost,
246                    optional_cost_pct: f64::from(p.OptionalCostPct),
247                    required_aura_spell_id: p.RequiredAuraSpellID,
248                })
249                .collect()
250        })
251        .unwrap_or_default();
252
253    let mana_cost = effects
254        .iter()
255        .find(|e| {
256            spell_effect_is(e.Effect, SpellEffectKind::PowerDrain) && e.EffectMiscValue_0 == 0
257        })
258        .map_or(0, |e| {
259            wowlab_types::numeric::f64_to_i32_saturating_round(e.EffectBasePointsF.abs())
260        });
261
262    let class_options = dbc.spell_class_options.get(&spell_id);
263    let spell_class_set = class_options.map_or(0, |c| c.SpellClassSet);
264    let spell_class_mask_1 = class_options.map_or(0, |c| c.SpellClassMask_0);
265    let spell_class_mask_2 = class_options.map_or(0, |c| c.SpellClassMask_1);
266    let spell_class_mask_3 = class_options.map_or(0, |c| c.SpellClassMask_2);
267    let spell_class_mask_4 = class_options.map_or(0, |c| c.SpellClassMask_3);
268
269    let target_restrictions = dbc.spell_target_restrictions.get(&spell_id);
270    let cone_degrees = target_restrictions.map_or(0.0, |t| t.ConeDegrees);
271    let target_flags = target_restrictions.map_or(0, |t| t.Targets);
272    let max_targets = target_restrictions.map_or(0, |t| t.MaxTargets);
273
274    let aura = dbc.spell_aura_restrictions.get(&spell_id);
275    let caster_aura_spell = aura.map_or(0, |a| a.CasterAuraSpell);
276    let caster_aura_state = aura.map_or(0, |a| a.CasterAuraState);
277    let exclude_caster_aura_spell = aura.map_or(0, |a| a.ExcludeCasterAuraSpell);
278    let exclude_caster_aura_state = aura.map_or(0, |a| a.ExcludeCasterAuraState);
279    let exclude_target_aura_spell = aura.map_or(0, |a| a.ExcludeTargetAuraSpell);
280    let exclude_target_aura_state = aura.map_or(0, |a| a.ExcludeTargetAuraState);
281    let target_aura_spell = aura.map_or(0, |a| a.TargetAuraSpell);
282    let target_aura_state = aura.map_or(0, |a| a.TargetAuraState);
283
284    let levels = dbc
285        .spell_levels
286        .get(&spell_id)
287        .and_then(|lvls| lvls.first());
288    let base_level = levels.map_or(0, |l| l.BaseLevel);
289    let max_level = levels.map_or(0, |l| l.MaxLevel);
290    let spell_level = levels.map_or(0, |l| l.SpellLevel);
291    let max_passive_aura_level = levels.map_or(0, |l| l.MaxPassiveAuraLevel);
292
293    let learn_spells: Vec<LearnSpell> = dbc
294        .spell_learn_spell
295        .get(&spell_id)
296        .map(|v| {
297            v.iter()
298                .map(|ls| LearnSpell {
299                    learn_spell_id: ls.LearnSpellID,
300                    overrides_spell_id: ls.OverridesSpellID,
301                })
302                .collect()
303        })
304        .unwrap_or_default();
305
306    let replacement_spell_id = dbc
307        .spell_replacement
308        .get(&spell_id)
309        .map_or(0, |r| r.ReplacementSpellID);
310
311    let shapeshift = dbc.spell_shapeshift.get(&spell_id);
312    let shapeshift_exclude_0 = shapeshift.map_or(0, |s| s.ShapeshiftExclude_0);
313    let shapeshift_exclude_1 = shapeshift.map_or(0, |s| s.ShapeshiftExclude_1);
314    let shapeshift_mask_0 = shapeshift.map_or(0, |s| s.ShapeshiftMask_0);
315    let shapeshift_mask_1 = shapeshift.map_or(0, |s| s.ShapeshiftMask_1);
316    let stance_bar_order = shapeshift.map_or(0, |s| s.StanceBarOrder);
317
318    let totems = dbc.spell_totems.get(&spell_id).and_then(|t| t.first());
319    let totem_0 = totems.map_or(0, |t| t.Totem_0);
320    let totem_1 = totems.map_or(0, |t| t.Totem_1);
321    let required_totem_category_0 = totems.map_or(0, |t| t.RequiredTotemCategoryID_0);
322    let required_totem_category_1 = totems.map_or(0, |t| t.RequiredTotemCategoryID_1);
323
324    let (can_empower, empower_stages) =
325        dbc.spell_empower
326            .get(&spell_id)
327            .map_or((false, Vec::new()), |empower| {
328                let stages: Vec<EmpowerStage> = dbc
329                    .spell_empower_stage
330                    .get(&empower.ID)
331                    .map(|stages| {
332                        let mut sorted: Vec<_> = stages
333                            .iter()
334                            .map(|s| EmpowerStage {
335                                stage: s.Stage,
336                                duration_ms: s.DurationMs,
337                            })
338                            .collect();
339
340                        sorted.sort_by_key(|s| s.stage);
341
342                        sorted
343                    })
344                    .unwrap_or_default();
345
346                (true, stages)
347            });
348
349    let spell_scaling = dbc.spell_scaling.get(&spell_id);
350    let min_scaling_level = spell_scaling.map_or(0, |s| s.MinScalingLevel);
351    let max_scaling_level = spell_scaling.map_or(0, |s| s.MaxScalingLevel);
352
353    let (effect_bonus_coefficient, bonus_coefficient_from_ap) = effects
354        .iter()
355        .find(|e| {
356            spell_effect_is_any(
357                e.Effect,
358                &[SpellEffectKind::SchoolDamage, SpellEffectKind::Heal],
359            )
360        })
361        .map_or((0.0, 0.0), |e| {
362            (e.EffectBonusCoefficient, e.BonusCoefficientFromAP)
363        });
364
365    let effect_trigger_spell: Vec<i32> = effects
366        .iter()
367        .map(|e| e.EffectTriggerSpell)
368        .filter(|&t| t != 0)
369        .collect();
370
371    let implicit_target: Vec<i32> = effects
372        .iter()
373        .flat_map(|e| [e.ImplicitTarget_0, e.ImplicitTarget_1])
374        .filter(|&t| t != 0)
375        .collect();
376
377    let equipped_item_requirement = dbc.spell_equipped_items.get(&spell_id).map(|row| {
378        wowlab_types::data::EquippedItemRequirement {
379            item_class: row.EquippedItemClass,
380            inventory_type_mask: row.EquippedItemInvTypes,
381            subclass_mask: row.EquippedItemSubclass,
382        }
383    });
384
385    let interrupts = dbc.spell_interrupts.get(&spell_id);
386    let interrupt_flags = interrupts.map_or(0, |i| i.InterruptFlags);
387    let interrupt_aura_0 = interrupts.map_or(0, |i| i.AuraInterruptFlags_0);
388    let interrupt_aura_1 = interrupts.map_or(0, |i| i.AuraInterruptFlags_1);
389    let interrupt_channel_0 = interrupts.map_or(0, |i| i.ChannelInterruptFlags_0);
390    let interrupt_channel_1 = interrupts.map_or(0, |i| i.ChannelInterruptFlags_1);
391
392    let description_variables_row = dbc
393        .spell_x_description_variables
394        .get(&spell_id)
395        .and_then(|xvars| xvars.first())
396        .and_then(|xvar| {
397            dbc.spell_description_variables
398                .get(&xvar.SpellDescriptionVariablesID)
399        });
400    let description_variables = description_variables_row
401        .map(|dv| dv.Variables.clone())
402        .unwrap_or_default();
403
404    let icon_file_data_id = misc.map_or(0, |m| m.SpellIconFileDataID);
405    let file_name = super::resolve_icon_file_name(dbc, icon_file_data_id);
406
407    // DBC Passive attribute is authoritative; an instant+apply-aura heuristic mislabels instant self-buffs.
408    let is_passive = spell_attribute_is(&attributes, SpellAttributeKind::Passive);
409
410    let spell_effects: Vec<SpellEffect> = effects
411        .iter()
412        .map(|e| {
413            let (radius_min, radius_max) = effect_radius(dbc, e).unwrap_or((0.0, 0.0));
414
415            SpellEffect {
416                index: e.EffectIndex,
417                effect: e.Effect,
418                mechanic: e.EffectMechanic,
419                effect_attributes: e.EffectAttributes,
420                aura: e.EffectAura,
421                base_points: e.EffectBasePointsF,
422                period: e.EffectAuraPeriod,
423                chain_targets: e.EffectChainTargets,
424                chain_multiplier: f64::from(e.EffectChainAmplitude),
425                trigger_spell: e.EffectTriggerSpell,
426                misc_value_0: e.EffectMiscValue_0,
427                misc_value_1: e.EffectMiscValue_1,
428                shapeshift_form_flags: dbc
429                    .spell_shapeshift_form
430                    .get(&e.EffectMiscValue_0)
431                    .map_or(0, |form| form.Flags),
432                shapeshift_combat_round_time_ms: dbc
433                    .spell_shapeshift_form
434                    .get(&e.EffectMiscValue_0)
435                    .map_or(0, |form| form.CombatRoundTime),
436                radius_min,
437                radius_max,
438                coefficient: e.Coefficient,
439                scaling_class: e.ScalingClass,
440                points_per_resource: e.EffectPointsPerResource,
441                variance: e.Variance,
442                bonus_coefficient: e.EffectBonusCoefficient,
443                bonus_coefficient_from_ap: e.BonusCoefficientFromAP,
444                amplitude: e.EffectAmplitude,
445                pvp_multiplier: e.PvpMultiplier,
446                effect_class_mask_1: e.EffectSpellClassMask_0,
447                effect_class_mask_2: e.EffectSpellClassMask_1,
448                effect_class_mask_3: e.EffectSpellClassMask_2,
449                effect_class_mask_4: e.EffectSpellClassMask_3,
450                implicit_target_a: e.ImplicitTarget_0,
451                implicit_target_b: e.ImplicitTarget_1,
452            }
453        })
454        .collect();
455
456    let aura_options = dbc.spell_aura_options.get(&spell_id);
457    let aura_max_stacks = aura_options.map_or(1, |ao| ao.CumulativeAura).max(1);
458    let proc_charges = aura_options.map_or(0, |ao| ao.ProcCharges);
459    let proc_chance = aura_options.map_or(0, |ao| ao.ProcChance);
460    let proc_type_mask = aura_options.map_or(0, |ao| {
461        let mask = u64::from(u32::from_ne_bytes(ao.ProcTypeMask_0.to_ne_bytes()))
462            | (u64::from(u32::from_ne_bytes(ao.ProcTypeMask_1.to_ne_bytes())) << u32::BITS);
463
464        i64::try_from(mask).unwrap_or_default()
465    });
466    let proc_category_recovery_ms = aura_options.map_or(0, |ao| ao.ProcCategoryRecovery);
467    let periodic_info = extract_periodic_info(dbc, spell_id);
468    let aura_flags = extract_aura_flags(&attributes);
469    let refresh_behavior = determine_refresh_behavior(&aura_flags, periodic_info.tick_period_ms);
470
471    let (rppm_base_rate, rppm_flags, rppm_mods) = aura_options
472        .filter(|ao| ao.SpellProcsPerMinuteID > 0)
473        .and_then(|ao| {
474            dbc.spell_procs_per_minute
475                .get(&ao.SpellProcsPerMinuteID)
476                .map(|ppm| {
477                    let mods: Vec<RppmMod> = dbc
478                        .spell_procs_per_minute_mod
479                        .get(&ao.SpellProcsPerMinuteID)
480                        .map(|mods| {
481                            mods.iter()
482                                .map(|m| RppmMod {
483                                    mod_type: m.Type,
484                                    param: m.Param,
485                                    coeff: m.Coeff,
486                                })
487                                .collect()
488                        })
489                        .unwrap_or_default();
490
491                    (ppm.BaseProcRate, ppm.Flags, mods)
492                })
493        })
494        .unwrap_or((0.0, 0, Vec::new()));
495
496    let knowledge_source = knowledge_source(spell_id, context);
497
498    Ok(SpellDataFlat {
499        id: spell_id,
500        name: name_row.Name_lang.clone().unwrap_or_default().into(),
501        description: spell_row
502            .and_then(|s| s.Description_lang.clone())
503            .unwrap_or_default(),
504        aura_description: spell_row
505            .and_then(|s| s.AuraDescription_lang.clone())
506            .unwrap_or_default(),
507        description_variables,
508        file_name: file_name.into(),
509        is_passive,
510        proc_chance,
511        proc_type_mask,
512        proc_category_recovery_ms,
513        knowledge_source,
514        cast_time,
515        recovery_time,
516        category_recovery_time,
517        category_id: categories.map_or(0, |c| c.Category),
518        charge_category_id: categories.map_or(0, |c| c.ChargeCategory),
519        start_recovery_category,
520        start_recovery_time,
521        mana_cost,
522        power_costs,
523        charge_recovery_time,
524        max_charges,
525        range_max_0,
526        range_max_1,
527        range_min_0,
528        range_min_1,
529        cone_degrees,
530        target_flags,
531        max_targets,
532        radius_max,
533        radius_min,
534        defense_type: categories.map_or(0, |c| c.DefenseType),
535        school_mask: misc.map_or(0, |m| m.SchoolMask),
536        bonus_coefficient_from_ap,
537        effect_bonus_coefficient,
538        min_scaling_level,
539        max_scaling_level,
540        interrupt_aura_0,
541        interrupt_aura_1,
542        interrupt_channel_0,
543        interrupt_channel_1,
544        interrupt_flags,
545        duration,
546        max_duration,
547        can_empower,
548        empower_stages,
549        dispel_type: categories.map_or(0, |c| c.DispelType),
550        mechanic: categories.map_or(0, |c| c.Mechanic),
551        facing_caster_flags: 0,
552        speed: misc.map_or(0.0, |m| m.Speed),
553        launch_delay: misc.map_or(0.0, |m| m.LaunchDelay),
554        spell_class_mask_1,
555        spell_class_mask_2,
556        spell_class_mask_3,
557        spell_class_mask_4,
558        spell_class_set,
559        base_level,
560        max_level,
561        max_passive_aura_level,
562        spell_level,
563        caster_aura_spell,
564        caster_aura_state,
565        exclude_caster_aura_spell,
566        exclude_caster_aura_state,
567        exclude_target_aura_spell,
568        exclude_target_aura_state,
569        target_aura_spell,
570        target_aura_state,
571        replacement_spell_id,
572        shapeshift_exclude_0,
573        shapeshift_exclude_1,
574        shapeshift_mask_0,
575        shapeshift_mask_1,
576        stance_bar_order,
577        required_totem_category_0,
578        required_totem_category_1,
579        totem_0,
580        totem_1,
581        attributes,
582        equipped_item_requirement,
583        effect_trigger_spell,
584        implicit_target,
585        learn_spells,
586        effects: spell_effects,
587        proc_charges,
588        max_stacks: aura_max_stacks,
589        periodic_type: periodic_info.periodic_type,
590        tick_period_ms: periodic_info.tick_period_ms,
591        refresh_behavior,
592        duration_hasted: aura_flags.duration_hasted,
593        hasted_ticks: aura_flags.hasted_ticks,
594        pandemic_refresh: aura_flags.pandemic_refresh,
595        rolling_periodic: aura_flags.rolling_periodic,
596        tick_may_crit: aura_flags.tick_may_crit,
597        tick_on_application: aura_flags.tick_on_application,
598        rppm_base_rate,
599        rppm_flags,
600        rppm_mods,
601        labels: dbc
602            .spell_label
603            .get(&spell_id)
604            .map(|rows| rows.iter().map(|r| SpellLabel::from(r.LabelID)).collect())
605            .unwrap_or_default(),
606    })
607}
608
609#[cfg(all(test, feature = "dbc"))]
610#[path = "spell/tests.rs"]
611mod tests;