Skip to main content

wowlab_engine_gamedata/game_data/
effect_access.rs

1//! Effect-coordinate access to resolved game data.
2
3use wowlab_types::sim::{EffectRef, SpellIdx};
4
5use super::ResolvedGameData;
6
7#[expect(
8    clippy::cast_possible_truncation,
9    clippy::cast_sign_loss,
10    reason = "the finite positive u32 range is checked before conversion"
11)]
12fn rounded_u32(value: f64) -> Option<u32> {
13    (value.is_finite() && value > 0.0 && value <= f64::from(u32::MAX)).then(|| value.round() as u32)
14}
15
16macro_rules! effect_accessor {
17    (
18        $ty:ty, $default:expr;
19        $($(#[$meta:meta])* $method:ident => $field:ident),+ $(,)?
20    ) => {
21        $(
22            $(#[$meta])*
23            #[must_use]
24            pub fn $method(&self, spell_id: SpellIdx, effect_index: u8) -> $ty {
25                self.inner
26                    .effects.values
27                    .get(&EffectRef::new(spell_id, effect_index))
28                    .map_or($default, |values| values.$field)
29            }
30        )+
31    };
32}
33
34impl ResolvedGameData {
35    const EFFECT_CLASS_MASK_WORDS: usize = 4;
36
37    /// Resolve an effect's live point value; attribute-285 spells add `mastery_points * sp_coef` to base points.
38    #[must_use]
39    pub fn effect_points(&self, spell_id: SpellIdx, effect_index: u8, mastery_points: f64) -> f64 {
40        let base = self.base_points(spell_id, effect_index);
41
42        if self
43            .spell_props(spell_id)
44            .is_some_and(|props| props.mastery_affects_points)
45        {
46            base + mastery_points * self.sp_coef(spell_id, effect_index)
47        } else {
48            base
49        }
50    }
51
52    /// Resolved radius for one effect, or `None` when that effect has no radius row.
53    #[must_use]
54    pub fn effect_radius(&self, spell_id: SpellIdx, effect_index: u8) -> Option<f64> {
55        self.inner
56            .effects
57            .values
58            .get(&EffectRef::new(spell_id, effect_index))
59            .map(|values| values.radius)
60            .filter(|radius| *radius > 0.0)
61    }
62
63    /// Highest populated 1-based effect index for a spell; zero when no effects were resolved.
64    #[must_use]
65    pub fn max_effect_index(&self, spell_id: SpellIdx) -> u8 {
66        self.inner
67            .effects
68            .values
69            .keys()
70            .filter_map(|effect| (effect.spell == spell_id).then_some(effect.effect_index))
71            .max()
72            .unwrap_or(0)
73    }
74
75    /// DBC child spell referenced by an effect; `None` for absent, zero, or invalid IDs.
76    pub fn trigger_spell(&self, spell_id: SpellIdx, effect_index: u8) -> Option<SpellIdx> {
77        let raw = self
78            .inner
79            .effects
80            .values
81            .get(&EffectRef::new(spell_id, effect_index))?
82            .trigger_spell;
83
84        u32::try_from(raw)
85            .ok()
86            .filter(|&id| id != 0)
87            .map(SpellIdx::from_raw)
88    }
89
90    /// Positive integral spell identity encoded in an effect's base points.
91    #[must_use]
92    pub fn effect_base_points_spell(
93        &self,
94        spell_id: SpellIdx,
95        effect_index: u8,
96    ) -> Option<SpellIdx> {
97        let effect = self
98            .inner
99            .effects
100            .values
101            .get(&EffectRef::new(spell_id, effect_index))?;
102
103        if !effect.base_points.is_finite()
104            || effect.base_points <= 0.0
105            || effect.base_points > f64::from(u32::MAX)
106        {
107            return None;
108        }
109
110        Some(SpellIdx::from_raw(rounded_u32(effect.base_points)?))
111    }
112
113    /// Per-hop chain decay; DBC `0.0` means "no decay" and is treated as `1.0`.
114    #[must_use]
115    pub fn chain_multiplier(&self, spell_id: SpellIdx, effect_index: u8) -> f64 {
116        self.inner
117            .effects
118            .values
119            .get(&EffectRef::new(spell_id, effect_index))
120            .map_or(1.0, |v| {
121                if v.chain_multiplier > 0.0 {
122                    v.chain_multiplier
123                } else {
124                    1.0
125                }
126            })
127    }
128
129    /// School mask for a spell; `Some(0)` for empty data, `None` when the spell is absent.
130    #[must_use]
131    pub fn school_mask(&self, spell_id: SpellIdx) -> Option<i32> {
132        self.inner.effects.school_masks.get(&spell_id).copied()
133    }
134
135    /// True when the effect and target spell have intersecting class-family masks.
136    #[must_use]
137    pub fn effect_class_mask_affects_spell(
138        &self,
139        source: SpellIdx,
140        effect_index: u8,
141        target: SpellIdx,
142    ) -> bool {
143        let Some(ev) = self
144            .inner
145            .effects
146            .values
147            .get(&EffectRef::new(source, effect_index))
148        else {
149            return false;
150        };
151
152        let Some(sf) = self.inner.spells.class_flags.get(&source) else {
153            return false;
154        };
155        let Some(tf) = self.inner.spells.class_flags.get(&target) else {
156            return false;
157        };
158
159        if sf.class_set != tf.class_set {
160            return false;
161        }
162
163        ev.class_mask
164            .iter()
165            .zip(tf.class_mask.iter())
166            .any(|(effect_flags, spell_flags)| effect_flags & spell_flags != 0)
167    }
168
169    require_accessor! {
170        effect {
171            /// `base_points` for a required effect; errors on populated data when absent, tolerant `0.0` on the empty introspection map.
172            /// # Errors
173            /// Returns an error when populated data does not contain the requested effect.
174            require_base_points -> f64 => base_points, "base_points",
175            /// `points_per_resource` for a resource-scaled coefficient (e.g. combo points); errors on populated data when absent, tolerant `0.0` on the empty introspection map.
176            /// # Errors
177            /// Returns an error when populated data does not contain the requested effect.
178            require_points_per_resource -> f64 => points_per_resource, "points_per_resource",
179        }
180    }
181
182    effect_accessor! {
183        i32, 0;
184        /// Raw DBC scaling class for an effect; zero when the effect is absent or unscaled.
185        scaling_class => scaling_class,
186        chain_targets => chain_targets,
187        /// DBC `Effect` kind id (2 = school damage, 30 = energize, ...); `0` when absent. 1-based index.
188        effect_type => effect_type,
189        /// Raw DBC `EffectMechanic`; zero means the effect inherits the spell mechanic.
190        effect_mechanic => effect_mechanic,
191        effect_attributes => effect_attributes,
192        implicit_target_a => implicit_target_a,
193        implicit_target_b => implicit_target_b,
194        /// DBC `EffectAura` kind id (29 = mod stat, 189 = mod rating, ...); `0` when absent. 1-based index.
195        effect_aura => effect_aura,
196        /// Effect `misc_value_0` (school/mechanic/etc.); `0` when absent. `effect_index` is 1-based.
197        effect_misc_value_0 => misc_value_0,
198        /// Effect `misc_value_1` (ordering/label/etc.); `0` when absent. `effect_index` is 1-based.
199        effect_misc_value_1 => misc_value_1,
200        /// Raw `SpellShapeshiftForm.Flags` for the form referenced by an effect.
201        shapeshift_form_flags => shapeshift_form_flags,
202        /// Raw `SpellShapeshiftForm.CombatRoundTime` in milliseconds for the form referenced by an effect.
203        shapeshift_combat_round_time_ms => shapeshift_combat_round_time_ms,
204    }
205
206    effect_accessor! {
207        f64, 0.0;
208        ap_coef             => ap_coef,
209        sp_coef             => sp_coef,
210        power_coefficient_mastery_add => power_coefficient_mastery_add,
211        base_points         => base_points,
212        amplitude           => amplitude,
213        period              => period,
214        coefficient         => coefficient,
215        points_per_resource => points_per_resource,
216    }
217
218    effect_accessor! {
219        [i32; Self::EFFECT_CLASS_MASK_WORDS], [0; Self::EFFECT_CLASS_MASK_WORDS];
220        /// `EffectSpellClassMask` words for a 1-based effect index.
221        effect_class_mask => class_mask,
222    }
223}