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::{AoeProperties, 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                wowlab_types::bench_metrics::record_game_data_hot_read();
26                self.inner
27                    .effects.values
28                    .get(&EffectRef::new(spell_id, effect_index))
29                    .map_or($default, |values| values.$field)
30            }
31        )+
32    };
33}
34
35impl ResolvedGameData {
36    const EFFECT_CLASS_MASK_WORDS: usize = 4;
37
38    /// Resolve an effect's live point value; attribute-285 spells add `mastery_points * sp_coef` to base points.
39    #[must_use]
40    pub fn effect_points(&self, spell_id: SpellIdx, effect_index: u8, mastery_points: f64) -> f64 {
41        let base = self.base_points(spell_id, effect_index);
42
43        if self
44            .spell_props(spell_id)
45            .is_some_and(|props| props.mastery_affects_points)
46        {
47            base + mastery_points * self.sp_coef(spell_id, effect_index)
48        } else {
49            base
50        }
51    }
52
53    /// Resolved radius for one effect, or `None` when that effect has no radius row.
54    #[must_use]
55    pub fn effect_radius(&self, spell_id: SpellIdx, effect_index: u8) -> Option<f64> {
56        wowlab_types::bench_metrics::record_game_data_hot_read();
57
58        self.inner
59            .effects
60            .values
61            .get(&EffectRef::new(spell_id, effect_index))
62            .map(|values| values.radius)
63            .filter(|radius| *radius > 0.0)
64    }
65
66    /// Highest populated 1-based effect index for a spell; zero when no effects were resolved.
67    #[must_use]
68    pub fn max_effect_index(&self, spell_id: SpellIdx) -> u8 {
69        wowlab_types::bench_metrics::record_game_data_hot_read();
70
71        self.inner
72            .effects
73            .max_effect_index_by_spell
74            .get(&spell_id)
75            .copied()
76            .unwrap_or(0)
77    }
78
79    /// Effect and spell coordinates consumed together while compiling `AoE` behavior.
80    #[must_use]
81    pub fn aoe_properties(&self, spell_id: SpellIdx, effect_index: u8) -> AoeProperties {
82        wowlab_types::bench_metrics::record_game_data_hot_read();
83        let effect = self
84            .inner
85            .effects
86            .values
87            .get(&EffectRef::new(spell_id, effect_index));
88        let spell = self.inner.spells.props.get(&spell_id);
89
90        AoeProperties {
91            effect_type: effect.map_or(0, |values| values.effect_type),
92            implicit_target_a: effect.map_or(0, |values| values.implicit_target_a),
93            implicit_target_b: effect.map_or(0, |values| values.implicit_target_b),
94            chain_targets: effect.map_or(0, |values| values.chain_targets),
95            chain_multiplier: effect.map_or(1.0, |values| {
96                if values.chain_multiplier > 0.0 {
97                    values.chain_multiplier
98                } else {
99                    1.0
100                }
101            }),
102            radius: effect.map_or(0.0, |values| values.radius),
103            max_affected_targets: spell.map_or(0, |props| props.max_affected_targets),
104            treat_as_area_effect: spell.is_some_and(|props| props.treat_as_area_effect),
105        }
106    }
107
108    /// DBC child spell referenced by an effect; `None` for absent, zero, or invalid IDs.
109    pub fn trigger_spell(&self, spell_id: SpellIdx, effect_index: u8) -> Option<SpellIdx> {
110        wowlab_types::bench_metrics::record_game_data_hot_read();
111        let raw = self
112            .inner
113            .effects
114            .values
115            .get(&EffectRef::new(spell_id, effect_index))?
116            .trigger_spell;
117
118        u32::try_from(raw)
119            .ok()
120            .filter(|&id| id != 0)
121            .map(SpellIdx::from_raw)
122    }
123
124    /// Positive integral spell identity encoded in an effect's base points.
125    #[must_use]
126    pub fn effect_base_points_spell(
127        &self,
128        spell_id: SpellIdx,
129        effect_index: u8,
130    ) -> Option<SpellIdx> {
131        wowlab_types::bench_metrics::record_game_data_hot_read();
132        let effect = self
133            .inner
134            .effects
135            .values
136            .get(&EffectRef::new(spell_id, effect_index))?;
137
138        if !effect.base_points.is_finite()
139            || effect.base_points <= 0.0
140            || effect.base_points > f64::from(u32::MAX)
141        {
142            return None;
143        }
144
145        Some(SpellIdx::from_raw(rounded_u32(effect.base_points)?))
146    }
147
148    /// Per-hop chain decay; DBC `0.0` means "no decay" and is treated as `1.0`.
149    #[must_use]
150    pub fn chain_multiplier(&self, spell_id: SpellIdx, effect_index: u8) -> f64 {
151        wowlab_types::bench_metrics::record_game_data_hot_read();
152
153        self.inner
154            .effects
155            .values
156            .get(&EffectRef::new(spell_id, effect_index))
157            .map_or(1.0, |v| {
158                if v.chain_multiplier > 0.0 {
159                    v.chain_multiplier
160                } else {
161                    1.0
162                }
163            })
164    }
165
166    /// School mask for a spell; `Some(0)` for empty data, `None` when the spell is absent.
167    #[must_use]
168    pub fn school_mask(&self, spell_id: SpellIdx) -> Option<i32> {
169        wowlab_types::bench_metrics::record_game_data_hot_read();
170
171        self.inner.effects.school_masks.get(&spell_id).copied()
172    }
173
174    /// True when the effect and target spell have intersecting class-family masks.
175    #[must_use]
176    pub fn effect_class_mask_affects_spell(
177        &self,
178        source: SpellIdx,
179        effect_index: u8,
180        target: SpellIdx,
181    ) -> bool {
182        wowlab_types::bench_metrics::record_game_data_hot_read();
183        let Some(ev) = self
184            .inner
185            .effects
186            .values
187            .get(&EffectRef::new(source, effect_index))
188        else {
189            return false;
190        };
191
192        let Some(tf) = self.inner.spells.class_flags.get(&target) else {
193            return false;
194        };
195
196        if ev.source_class_set == 0 || ev.source_class_set != tf.class_set {
197            return false;
198        }
199
200        ev.class_mask
201            .iter()
202            .zip(tf.class_mask.iter())
203            .any(|(effect_flags, spell_flags)| effect_flags & spell_flags != 0)
204    }
205
206    require_accessor! {
207        effect {
208            /// `base_points` for a required effect; errors on populated data when absent, tolerant `0.0` on the empty introspection map.
209            /// # Errors
210            /// Returns an error when populated data does not contain the requested effect.
211            require_base_points -> f64 => base_points, "base_points",
212            /// `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.
213            /// # Errors
214            /// Returns an error when populated data does not contain the requested effect.
215            require_points_per_resource -> f64 => points_per_resource, "points_per_resource",
216        }
217    }
218
219    effect_accessor! {
220        i32, 0;
221        /// Raw DBC scaling class for an effect; zero when the effect is absent or unscaled.
222        scaling_class => scaling_class,
223        chain_targets => chain_targets,
224        /// DBC `Effect` kind id (2 = school damage, 30 = energize, ...); `0` when absent. 1-based index.
225        effect_type => effect_type,
226        /// Raw DBC `EffectMechanic`; zero means the effect inherits the spell mechanic.
227        effect_mechanic => effect_mechanic,
228        effect_attributes => effect_attributes,
229        implicit_target_a => implicit_target_a,
230        implicit_target_b => implicit_target_b,
231        /// DBC `EffectAura` kind id (29 = mod stat, 189 = mod rating, ...); `0` when absent. 1-based index.
232        effect_aura => effect_aura,
233        /// Effect `misc_value_0` (school/mechanic/etc.); `0` when absent. `effect_index` is 1-based.
234        effect_misc_value_0 => misc_value_0,
235        /// Effect `misc_value_1` (ordering/label/etc.); `0` when absent. `effect_index` is 1-based.
236        effect_misc_value_1 => misc_value_1,
237        /// Raw `SpellShapeshiftForm.Flags` for the form referenced by an effect.
238        shapeshift_form_flags => shapeshift_form_flags,
239        /// Raw `SpellShapeshiftForm.CombatRoundTime` in milliseconds for the form referenced by an effect.
240        shapeshift_combat_round_time_ms => shapeshift_combat_round_time_ms,
241    }
242
243    effect_accessor! {
244        f64, 0.0;
245        ap_coef             => ap_coef,
246        sp_coef             => sp_coef,
247        power_coefficient_mastery_add => power_coefficient_mastery_add,
248        base_points         => base_points,
249        amplitude           => amplitude,
250        period              => period,
251        coefficient         => coefficient,
252        points_per_resource => points_per_resource,
253    }
254
255    effect_accessor! {
256        [i32; Self::EFFECT_CLASS_MASK_WORDS], [0; Self::EFFECT_CLASS_MASK_WORDS];
257        /// `EffectSpellClassMask` words for a 1-based effect index.
258        effect_class_mask => class_mask,
259    }
260}