Skip to main content

wowlab_engine_gamedata/game_data/
spell_access.rs

1//! Spell, aura, power, and spell-relation access to resolved game data.
2
3use wowlab_types::{
4    constants::{HUNDRED, MS_PER_SECOND},
5    data::SpellResources,
6    sim::SpellIdx,
7};
8
9use super::{AuraProps, DamageSpellProperties, PowerTypeProps, ResolvedGameData, SpellProps};
10
11fn missing_field(spell_id: SpellIdx, field: &'static str) -> crate::GameDataError {
12    crate::GameDataError::missing_spell_field(spell_id, field)
13}
14
15macro_rules! spell_prop_accessor {
16    (
17        copy {
18            $(
19                $(#[$copy_meta:meta])*
20                $copy_method:ident -> $copy_ty:ty => $copy_field:ident
21            ),* $(,)?
22        }
23        flatten {
24            $(
25                $(#[$flatten_meta:meta])*
26                $flatten_method:ident -> $flatten_ty:ty => $flatten_field:ident
27            ),* $(,)?
28        }
29        slice {
30            $(
31                $(#[$slice_meta:meta])*
32                $slice_method:ident -> $slice_ty:ty => $slice_field:ident
33            ),* $(,)?
34        }
35    ) => {
36        $(
37            $(#[$copy_meta])*
38            #[must_use]
39            pub fn $copy_method(&self, spell_id: SpellIdx) -> Option<$copy_ty> {
40                self.spell_props(spell_id).map(|props| props.$copy_field)
41            }
42        )*
43        $(
44            $(#[$flatten_meta])*
45            #[must_use]
46            pub fn $flatten_method(&self, spell_id: SpellIdx) -> Option<$flatten_ty> {
47                self.spell_props(spell_id)
48                    .and_then(|props| props.$flatten_field)
49            }
50        )*
51        $(
52            $(#[$slice_meta])*
53            #[must_use]
54            pub fn $slice_method(&self, spell_id: SpellIdx) -> Option<&[$slice_ty]> {
55                self.spell_props(spell_id)
56                    .map(|props| props.$slice_field.as_slice())
57            }
58        )*
59    };
60}
61
62fn empower_stages_are_contiguous(stages: &[wowlab_types::data::EmpowerStage]) -> bool {
63    stages
64        .iter()
65        .enumerate()
66        .all(|(index, stage)| i32::try_from(index).is_ok_and(|expected| stage.stage == expected))
67}
68
69impl ResolvedGameData {
70    /// Copy-only spell scalars consumed together by combat damage-plan finalization.
71    #[must_use]
72    pub fn spell_props_for_damage_plan(&self, spell_id: SpellIdx) -> DamageSpellProperties {
73        self.spell_props(spell_id)
74            .map_or_else(Default::default, |props| DamageSpellProperties {
75                defense_type: props.defense_type,
76                hit_chance_flat: props.hit_chance_flat,
77                hit_chance_multiplier: props.hit_chance_multiplier,
78                target_resistance_flat: props.target_resistance_flat,
79                target_resistance_multiplier: props.target_resistance_multiplier,
80                dispel_type: props.dispel_type,
81                dispel_resistance_flat: props.dispel_resistance_flat,
82                dispel_resistance_multiplier: props.dispel_resistance_multiplier,
83                mastery_affects_points: props.mastery_affects_points,
84                primary_cost_entry: props.primary_cost_entry,
85                secondary_cost_entry: props.secondary_cost_entry,
86            })
87    }
88
89    /// Cumulative hold time needed to release a DBC empower spell at `rank`.
90    ///
91    /// One-based ranks cumulatively consume zero-based DBC duration segments.
92    #[must_use]
93    pub fn empower_cast_time_ms(&self, spell_id: SpellIdx, rank: u8) -> Option<u32> {
94        if rank == 0 {
95            return None;
96        }
97
98        if self.inner.spells.props.is_empty() {
99            return Some(0);
100        }
101
102        let props = self.spell_props(spell_id)?;
103
104        if !props.can_empower {
105            return None;
106        }
107
108        let stages = &props.empower_stages;
109
110        if !empower_stages_are_contiguous(stages) {
111            return None;
112        }
113
114        let count = usize::from(rank);
115
116        if count > stages.len() {
117            return None;
118        }
119
120        stages.iter().take(count).try_fold(0_u32, |total, stage| {
121            u32::try_from(stage.duration_ms)
122                .ok()
123                .and_then(|duration| total.checked_add(duration))
124        })
125    }
126
127    /// Cumulative hold times for every one-based release rank of a DBC empower spell.
128    #[must_use]
129    pub fn empower_cast_times_ms(&self, spell_id: SpellIdx) -> Option<Vec<u32>> {
130        if self.inner.spells.props.is_empty() {
131            return Some(Vec::new());
132        }
133
134        let props = self.spell_props(spell_id)?;
135
136        if !props.can_empower
137            || props.empower_stages.is_empty()
138            || !empower_stages_are_contiguous(&props.empower_stages)
139        {
140            return None;
141        }
142
143        let mut total = 0_u32;
144        let mut cast_times = Vec::with_capacity(props.empower_stages.len());
145
146        for stage in &props.empower_stages {
147            let duration = u32::try_from(stage.duration_ms).ok()?;
148
149            total = total.checked_add(duration)?;
150            cast_times.push(total);
151        }
152
153        Some(cast_times)
154    }
155
156    /// Required cumulative hold times for every release rank of a DBC empower spell.
157    /// # Errors
158    /// Returns an error when the spell has no valid resolved empower stages.
159    pub fn require_empower_cast_times_ms(
160        &self,
161        spell_id: SpellIdx,
162    ) -> Result<Vec<u32>, crate::GameDataError> {
163        self.empower_cast_times_ms(spell_id)
164            .ok_or_else(|| crate::GameDataError::invalid_empower_timings(spell_id))
165    }
166
167    /// Whether the spell has a `SpellEmpower` row in structured game data.
168    #[must_use]
169    pub fn is_empower_spell(&self, spell_id: SpellIdx) -> bool {
170        self.spell_props(spell_id)
171            .is_some_and(|props| props.can_empower)
172    }
173
174    /// Required cumulative empower hold time for a one-based release rank.
175    /// # Errors
176    /// Returns an error when the spell has no such resolved empower rank.
177    pub fn require_empower_cast_time_ms(
178        &self,
179        spell_id: SpellIdx,
180        rank: u8,
181    ) -> Result<u32, crate::GameDataError> {
182        self.empower_cast_time_ms(spell_id, rank)
183            .ok_or_else(|| crate::GameDataError::missing_empower_rank(spell_id, rank))
184    }
185
186    /// Number of release ranks defined by a spell's DBC empower stages.
187    #[must_use]
188    pub fn empower_max_rank(&self, spell_id: SpellIdx) -> Option<u8> {
189        if self.inner.spells.props.is_empty() {
190            return Some(0);
191        }
192
193        let props = self.spell_props(spell_id)?;
194
195        if !props.can_empower
196            || props.empower_stages.is_empty()
197            || !empower_stages_are_contiguous(&props.empower_stages)
198        {
199            return None;
200        }
201
202        u8::try_from(props.empower_stages.len()).ok()
203    }
204
205    /// Required number of release ranks defined by a spell's DBC empower stages.
206    /// # Errors
207    /// Returns an error when the spell has no resolved empower stages or their count exceeds `u8`.
208    pub fn require_empower_max_rank(&self, spell_id: SpellIdx) -> Result<u8, crate::GameDataError> {
209        self.empower_max_rank(spell_id)
210            .ok_or_else(|| crate::GameDataError::invalid_empower_rank_count(spell_id))
211    }
212
213    #[must_use]
214    pub fn is_empty(&self) -> bool {
215        self.inner.effects.values.is_empty()
216    }
217
218    #[must_use]
219    pub fn spell_resources(&self, spell_id: SpellIdx) -> Option<SpellResources> {
220        self.spell_props(spell_id).map(|props| SpellResources {
221            primary_cost: props.cost,
222            primary_optional_cost: props.optional_cost,
223            primary_cost_pct: props.cost_pct,
224            primary_max_cost_pct: props.max_cost_pct,
225            primary_optional_cost_pct: props.optional_cost_pct,
226            primary_gain: props.gain,
227            secondary_cost: props.secondary_cost,
228            secondary_optional_cost: props.secondary_optional_cost,
229            secondary_gain: props.secondary_gain,
230            health_cost: props.health_cost,
231            health_cost_pct: props.health_cost_pct,
232            health_max_cost_pct: props.health_max_cost_pct,
233            health_optional_cost: props.health_optional_cost,
234            health_optional_cost_pct: props.health_optional_cost_pct,
235            primary_cost_entry: props.primary_cost_entry,
236            secondary_cost_entry: props.secondary_cost_entry,
237        })
238    }
239
240    #[must_use]
241    pub fn hit_chance_modifier(&self, spell_id: SpellIdx, initial: f64) -> f64 {
242        self.spell_props(spell_id).map_or(initial, |props| {
243            (initial + props.hit_chance_flat) * props.hit_chance_multiplier
244        })
245    }
246
247    #[must_use]
248    pub fn target_resistance_modifier(&self, spell_id: SpellIdx, initial: f64) -> f64 {
249        self.spell_props(spell_id).map_or(initial, |props| {
250            (initial + props.target_resistance_flat) * props.target_resistance_multiplier
251        })
252    }
253
254    #[must_use]
255    pub fn dispel_resistance_modifier(&self, spell_id: SpellIdx, initial: f64) -> f64 {
256        self.spell_props(spell_id).map_or(initial, |props| {
257            (initial + props.dispel_resistance_flat) * props.dispel_resistance_multiplier
258        })
259    }
260
261    /// Raw DBC dispel family assigned to this spell.
262    #[must_use]
263    pub fn dispel_type(&self, spell_id: SpellIdx) -> i32 {
264        self.spell_props(spell_id)
265            .map_or(0, |props| props.dispel_type)
266    }
267
268    /// Raw DBC `SpellCategories.Mechanic` identity.
269    #[must_use]
270    pub fn spell_mechanic(&self, spell_id: SpellIdx) -> i32 {
271        self.spell_props(spell_id).map_or(0, |props| props.mechanic)
272    }
273
274    #[must_use]
275    pub fn cooldown_s(&self, spell_id: SpellIdx) -> Option<f64> {
276        self.spell_props(spell_id)
277            .map(|p| f64::from(p.cooldown_ms) / MS_PER_SECOND)
278    }
279
280    #[must_use]
281    pub fn cast_time_ms(&self, spell_id: SpellIdx) -> Option<u32> {
282        self.spell_props(spell_id)
283            .and_then(|p| u32::try_from(p.cast_time_ms).ok())
284    }
285
286    #[must_use]
287    pub fn breaks_stealth(&self, spell_id: SpellIdx) -> Option<bool> {
288        self.spell_props(spell_id)
289            .map(|p| !p.does_not_break_stealth)
290    }
291
292    /// `SpellAuraOptions.ProcChance` as a 0..=1 fraction; `None` when the spell is unresolved.
293    #[must_use]
294    pub fn proc_chance(&self, spell_id: SpellIdx) -> Option<f64> {
295        self.proc_chance_with_flat_bonus(spell_id, 0.0)
296    }
297
298    /// `SpellAuraOptions.ProcCharges`; zero means unlimited.
299    #[must_use]
300    pub fn proc_charges(&self, spell_id: SpellIdx) -> Option<u8> {
301        self.spell_props(spell_id)
302            .and_then(|props| u8::try_from(props.proc_charges).ok())
303    }
304
305    /// Non-negative `SpellAuraOptions.ProcCategoryRecovery` in milliseconds.
306    #[must_use]
307    pub fn proc_category_recovery_ms(&self, spell_id: SpellIdx) -> Option<u32> {
308        self.spell_props(spell_id)
309            .map(|props| u32::try_from(props.proc_category_recovery_ms.max(0)).unwrap_or(0))
310    }
311
312    /// Base real-procs-per-minute rate and raw scaling flags.
313    #[must_use]
314    pub fn rppm(&self, spell_id: SpellIdx) -> Option<(f64, i32)> {
315        self.spell_props(spell_id)
316            .map(|props| (props.rppm_base_rate, props.rppm_flags))
317    }
318
319    /// Proc chance after adding a runtime flat fraction before passive percentage modifiers.
320    #[must_use]
321    pub fn proc_chance_with_flat_bonus(
322        &self,
323        spell_id: SpellIdx,
324        runtime_flat: f64,
325    ) -> Option<f64> {
326        self.spell_props(spell_id).map(|props| {
327            (f64::from(props.proc_chance_pct) / HUNDRED + runtime_flat)
328                * (1.0 + props.proc_chance_multiplier_delta)
329        })
330    }
331
332    #[must_use]
333    pub fn charges(&self, spell_id: SpellIdx) -> Option<u8> {
334        self.spell_props(spell_id)
335            .and_then(|p| u8::try_from(p.max_charges).ok())
336    }
337
338    #[must_use]
339    pub fn charge_cd_s(&self, spell_id: SpellIdx) -> Option<f64> {
340        self.spell_props(spell_id)
341            .map(|p| f64::from(p.charge_cd_ms) / MS_PER_SECOND)
342    }
343
344    #[must_use]
345    pub fn category_cooldown_s(&self, spell_id: SpellIdx) -> Option<f64> {
346        self.spell_props(spell_id)
347            .map(|p| f64::from(p.category_cooldown_ms) / MS_PER_SECOND)
348    }
349
350    #[must_use]
351    pub fn gcd_ms(&self, spell_id: SpellIdx) -> Option<u32> {
352        self.spell_props(spell_id)
353            .and_then(|p| u32::try_from(p.gcd_ms).ok())
354    }
355
356    #[must_use]
357    pub fn aura_duration_ms(&self, spell_id: SpellIdx) -> Option<u32> {
358        // Game-data `-1` (no fixed duration) collapses to the aura system's `0` permanent sentinel.
359        let raw = self
360            .aura_props(spell_id)
361            .map(|props| props.duration_ms)
362            .or_else(|| {
363                self.spell_props(spell_id)
364                    .map(|props| props.duration_ms)
365                    .filter(|duration_ms| *duration_ms != 0)
366            })?;
367
368        Some(u32::try_from(raw).unwrap_or(0))
369    }
370
371    #[must_use]
372    pub fn aura_max_stacks(&self, spell_id: SpellIdx) -> Option<u8> {
373        let configured_stacks = self
374            .aura_props(spell_id)
375            .map(|props| props.max_stacks)
376            .or_else(|| {
377                self.spell_props(spell_id)
378                    .map(|props| props.max_stacks)
379                    .filter(|max_stacks| *max_stacks != 0)
380            });
381
382        configured_stacks.and_then(|max_stacks| u8::try_from(max_stacks).ok())
383    }
384
385    #[must_use]
386    pub fn aura_doses(&self, spell_id: SpellIdx) -> Option<u8> {
387        self.aura_props(spell_id)
388            .and_then(|props| u8::try_from(props.doses).ok())
389    }
390
391    #[must_use]
392    pub fn aura_tick_ms(&self, spell_id: SpellIdx) -> Option<u32> {
393        self.aura_props(spell_id)
394            .and_then(|p| u32::try_from(p.tick_period_ms).ok())
395    }
396
397    /// Tick count of a channel. `cast_time_ms` IS the channel duration.
398    #[must_use]
399    pub fn channel_tick_count(&self, spell_id: SpellIdx) -> Option<u8> {
400        let dur = self.spell_props(spell_id)?.cast_time_ms;
401        let aura_period = self
402            .aura_props(spell_id)
403            .map(|props| props.tick_period_ms)
404            .filter(|period| *period > 0);
405        let effect_period = self
406            .inner
407            .effects
408            .values
409            .iter()
410            .filter_map(|(key, effect)| {
411                (key.spell == spell_id && effect.period > 0.0).then_some(effect.period)
412            })
413            .min_by(f64::total_cmp)
414            .and_then(|period| {
415                (period <= f64::from(i32::MAX))
416                    .then(|| wowlab_types::numeric::f64_to_i32_saturating_round(period))
417            });
418        let period = aura_period.or(effect_period).unwrap_or(0);
419
420        if dur <= 0 || period <= 0 {
421            return Some(0);
422        }
423
424        let n = dur / period;
425
426        u8::try_from(n.min(i32::from(u8::MAX))).ok()
427    }
428
429    /// Max base power for a power type (`power_type_enum`); `None` when the type is absent.
430    #[must_use]
431    pub fn power_max(&self, type_id: i32) -> Option<f64> {
432        self.power_props(type_id).map(|p| p.max_base_power)
433    }
434
435    /// Display divisor for raw DBC flat power amounts (runic power and rage store tenths); `1.0` when the type is absent.
436    #[must_use]
437    pub fn power_display_divisor(&self, type_id: i32) -> f64 {
438        self.power_props(type_id)
439            .map_or(1.0, |p| p.display_modifier.max(1.0))
440    }
441
442    /// 1-based DBC power-cost entry backing the spell's cost in the given pool; `0` = no such cost, and cost modifiers route to entries by this index.
443    #[must_use]
444    pub fn cost_entry(&self, spell_id: SpellIdx, secondary: bool) -> u8 {
445        self.spell_props(spell_id).map_or(0, |p| {
446            if secondary {
447                p.secondary_cost_entry
448            } else {
449                p.primary_cost_entry
450            }
451        })
452    }
453
454    /// Combat regeneration when the DBC expresses regeneration as a percentage of maximum power (notably mana).
455    #[must_use]
456    pub fn power_regen_for_max(&self, type_id: i32, maximum: f64) -> Option<f64> {
457        self.power_props(type_id)
458            .map(|props| props.regen_combat + maximum * props.regen_percent_of_max / HUNDRED)
459    }
460
461    /// Multiplier for auto-attack gains of the named resource (aura 213 passives).
462    #[must_use]
463    pub fn power_auto_attack_gain_multiplier(&self, type_id: i32) -> f64 {
464        self.power_props(type_id)
465            .map_or(1.0, |props| 1.0 + props.auto_attack_gain_percent / HUNDRED)
466    }
467
468    /// Additive spell-cost percent for the named resource (aura 423 passives).
469    #[must_use]
470    pub fn power_cost_percent(&self, type_id: i32) -> f64 {
471        self.power_props(type_id)
472            .map_or(0.0, |props| props.cost_percent)
473    }
474
475    /// Default (starting) power for a power type; `None` when the type is absent.
476    #[must_use]
477    pub fn power_default(&self, type_id: i32) -> Option<f64> {
478        self.power_props(type_id).map(|p| p.default_power)
479    }
480
481    /// Descale a raw DBC power amount for `type_id` using its display modifier.
482    #[must_use]
483    pub fn scaled_power_amount(&self, type_id: i32, raw: f64) -> f64 {
484        raw / self
485            .power_props(type_id)
486            .map_or(1.0, |props| props.display_modifier.max(1.0))
487    }
488
489    /// Returns the replacement for an overridden spell.
490    ///
491    /// ```compile_fail
492    /// use wowlab_engine_gamedata::ResolvedGameData;
493    /// use wowlab_types::sim::AuraIdx;
494    ///
495    /// let data = ResolvedGameData::default();
496    /// let aura = AuraIdx::from_raw(1);
497    /// data.spell_override(aura);
498    /// ```
499    #[must_use]
500    pub fn spell_override(&self, overridden_id: SpellIdx) -> Option<SpellIdx> {
501        wowlab_types::bench_metrics::record_game_data_hot_read();
502
503        self.inner.spells.overrides.get(&overridden_id).copied()
504    }
505
506    /// Iterates learned spells for `teacher` in resolver insertion order (deduplicated, first-occurrence order retained); missing teachers yield an empty iterator.
507    #[must_use]
508    pub fn learned_spells(
509        &self,
510        teacher: SpellIdx,
511    ) -> impl ExactSizeIterator<Item = SpellIdx> + '_ {
512        wowlab_types::bench_metrics::record_game_data_hot_read();
513
514        self.inner
515            .spells
516            .learns
517            .get(&teacher)
518            .map(Vec::as_slice)
519            .unwrap_or_default()
520            .iter()
521            .copied()
522    }
523
524    /// Whether `spell` carries the given `SpellLabel` id (affect-list gating).
525    #[must_use]
526    pub fn has_label(&self, spell: SpellIdx, label: i32) -> bool {
527        wowlab_types::bench_metrics::record_game_data_hot_read();
528
529        self.inner
530            .spells
531            .labels
532            .get(&spell)
533            .is_some_and(|labels| labels.contains(&label))
534    }
535
536    /// Spell labels frozen by engine finalization for direct runtime membership checks.
537    #[must_use]
538    pub fn spell_labels(&self, spell: SpellIdx) -> &[i32] {
539        wowlab_types::bench_metrics::record_game_data_hot_read();
540
541        self.inner
542            .spells
543            .labels
544            .get(&spell)
545            .map(Vec::as_slice)
546            .unwrap_or_default()
547    }
548
549    pub(super) fn spell_props(&self, spell_id: SpellIdx) -> Option<&SpellProps> {
550        wowlab_types::bench_metrics::record_game_data_hot_read();
551
552        self.inner.spells.props.get(&spell_id)
553    }
554
555    fn aura_props(&self, spell_id: SpellIdx) -> Option<&AuraProps> {
556        wowlab_types::bench_metrics::record_game_data_hot_read();
557
558        self.inner.spells.aura_props.get(&spell_id)
559    }
560
561    fn power_props(&self, type_id: i32) -> Option<&PowerTypeProps> {
562        wowlab_types::bench_metrics::record_game_data_hot_read();
563
564        self.inner.spells.power_types.get(&type_id)
565    }
566
567    require_accessor! {
568        spell {
569            /// Resource cost, required to be present on populated game data.
570            /// # Errors
571            /// Returns an error when the spell has no resolved cost.
572            require_cost -> f64, "cost" => |data, spell| data.spell_resources(spell).map(|resources| resources.primary_cost),
573            /// Cooldown in seconds, required on populated data.
574            /// # Errors
575            /// Returns an error when the spell has no resolved cooldown.
576            require_cooldown_s -> f64, "cooldown_s" => |data, spell| data.cooldown_s(spell),
577            /// Max aura stacks, required to be present on populated game data.
578            /// # Errors
579            /// Returns an error when the spell has no resolved aura stack limit.
580            require_aura_max_stacks -> u8, "aura_max_stacks" => |data, spell| data.aura_max_stacks(spell),
581            /// Aura duration in milliseconds, required on populated game data.
582            /// # Errors
583            /// Returns an error when the spell has no resolved aura duration.
584            require_aura_duration_ms -> u32, "aura_duration_ms" => |data, spell| data.aura_duration_ms(spell),
585            /// Cast time in milliseconds, required on populated game data.
586            /// # Errors
587            /// Returns an error when the spell has no resolved cast time.
588            require_cast_time_ms -> u32, "cast_time_ms" => |data, spell| data.cast_time_ms(spell),
589        }
590    }
591
592    require_accessor! {
593        spell {
594            /// Charge cooldown in seconds, required on populated game data.
595            /// # Errors
596            /// Returns an error when the spell has no resolved charge cooldown.
597            require_charge_cd_s -> f64, "charge_cd_s" => |data, spell| data.charge_cd_s(spell),
598            /// Proc-category internal cooldown in milliseconds, required on populated data.
599            /// # Errors
600            /// Returns an error when the driver spell is absent.
601            require_proc_category_recovery_ms -> u32, "proc_category_recovery_ms" => |data, spell| data.proc_category_recovery_ms(spell),
602            /// Combined DBC proc event mask, required on populated data.
603            /// # Errors
604            /// Returns an error when the driver spell is absent.
605            require_proc_type_mask -> u64, "proc_type_mask" => |data, spell| data.proc_type_mask(spell),
606            /// Channel tick count, required to be present on populated game data.
607            /// # Errors
608            /// Returns an error when the spell has no resolved channel tick count.
609            require_channel_tick_count -> u8, "channel_tick_count" => |data, spell| data.channel_tick_count(spell),
610        }
611    }
612
613    spell_prop_accessor! {
614        copy {
615            defense_type -> i32 => defense_type,
616            shapeshift_required_mask -> u64 => shapeshift_required_mask,
617            shapeshift_excluded_mask -> u64 => shapeshift_excluded_mask,
618            gain -> f64 => gain,
619            secondary_gain -> f64 => secondary_gain,
620            cooldown_hasted -> bool => cooldown_hasted,
621            gcd_haste_type -> wowlab_types::combat::GcdHasteType => gcd_haste_type,
622            hasted_ticks -> bool => hasted_ticks,
623            duration_hasted -> bool => duration_hasted,
624            cannot_crit -> bool => cannot_crit,
625            requires_stealth -> bool => requires_stealth,
626            requires_behind_target -> bool => requires_behind_target,
627            requires_unshifted -> bool => requires_unshifted,
628            allow_while_unshifted -> bool => allow_while_unshifted,
629            treat_as_periodic -> bool => treat_as_periodic,
630            treat_as_area_effect -> bool => treat_as_area_effect,
631            usable_while_casting -> bool => usable_while_casting,
632            usable_while_moving -> bool => usable_while_moving,
633            asynchronous_stacks -> bool => asynchronous_stacks,
634            disable_player_damage_multiplier -> bool => disable_player_damage_multiplier,
635            disable_target_damage_multiplier -> bool => disable_target_damage_multiplier,
636            disable_positive_target_damage_multiplier -> bool => disable_positive_target_damage_multiplier,
637            requires_off_hand -> bool => requires_off_hand,
638            requires_main_hand -> bool => requires_main_hand,
639            refresh_behavior -> wowlab_types::data::RefreshBehavior => refresh_behavior,
640            caster_aura_state -> i32 => caster_aura_state,
641            caster_aura_spell -> i32 => caster_aura_spell,
642            exclude_caster_aura_state -> i32 => exclude_caster_aura_state,
643            exclude_caster_aura_spell -> i32 => exclude_caster_aura_spell,
644            target_aura_state -> i32 => target_aura_state,
645            target_aura_spell -> i32 => target_aura_spell,
646            exclude_target_aura_state -> i32 => exclude_target_aura_state,
647            exclude_target_aura_spell -> i32 => exclude_target_aura_spell,
648            aura_interrupt_flags -> u32 => aura_interrupt_flags,
649            spell_interrupt_flags -> u32 => spell_interrupt_flags,
650            channel_interrupt_flags -> u32 => channel_interrupt_flags,
651            rolling_periodic -> bool => rolling_periodic,
652            tick_may_crit -> bool => tick_may_crit,
653            tick_on_application -> bool => tick_on_application,
654            /// Raw `SpellProcEntry.AttributesMask`; zero means no proc attributes.
655            proc_attributes -> u32 => proc_attributes,
656            /// Combined `SpellAuraOptions.ProcTypeMask_0/1` event mask.
657            proc_type_mask -> u64 => proc_type_mask,
658            rppm_haste_scales -> bool => rppm_haste_scales,
659            rppm_crit_scales -> bool => rppm_crit_scales,
660        }
661        flatten {
662            equipped_item_requirement -> wowlab_types::data::EquippedItemRequirement => equipped_item_requirement,
663            cooldown_category -> wowlab_types::data::CooldownCategoryId => cooldown_category,
664            charge_category -> wowlab_types::data::CooldownCategoryId => charge_category,
665            start_recovery_category -> wowlab_types::data::CooldownCategoryId => start_recovery_category,
666        }
667        slice {
668            spell_attributes -> i32 => attributes,
669        }
670    }
671
672    spell_prop_accessor! {
673        copy {
674            projectile_speed -> f64 => projectile_speed,
675            launch_delay_s -> f64 => launch_delay_s,
676            hostile_min_range -> f64 => hostile_min_range,
677            hostile_max_range -> f64 => hostile_max_range,
678            radius -> f64 => radius,
679            chain_target_range -> f64 => chain_target_range,
680            max_affected_targets -> u8 => max_affected_targets,
681            cone_half_angle -> f64 => cone_half_angle,
682            fixed_travel_time -> bool => fixed_travel_time,
683            ignores_line_of_sight -> bool => ignores_line_of_sight,
684            explicit_target_mask -> u32 => explicit_target_mask,
685            required_explicit_target_mask -> u32 => required_explicit_target_mask,
686        }
687        flatten {
688        }
689        slice {
690        }
691    }
692}