wowlab_engine_gamedata/game_data/
effect_access.rs1use 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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 require_base_points -> f64 => base_points, "base_points",
175 require_points_per_resource -> f64 => points_per_resource, "points_per_resource",
179 }
180 }
181
182 effect_accessor! {
183 i32, 0;
184 scaling_class => scaling_class,
186 chain_targets => chain_targets,
187 effect_type => effect_type,
189 effect_mechanic => effect_mechanic,
191 effect_attributes => effect_attributes,
192 implicit_target_a => implicit_target_a,
193 implicit_target_b => implicit_target_b,
194 effect_aura => effect_aura,
196 effect_misc_value_0 => misc_value_0,
198 effect_misc_value_1 => misc_value_1,
200 shapeshift_form_flags => shapeshift_form_flags,
202 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 effect_class_mask => class_mask,
222 }
223}