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