wowlab_engine_gamedata/game_data/
effect_access.rs1use 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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 require_base_points -> f64 => base_points, "base_points",
212 require_points_per_resource -> f64 => points_per_resource, "points_per_resource",
216 }
217 }
218
219 effect_accessor! {
220 i32, 0;
221 scaling_class => scaling_class,
223 chain_targets => chain_targets,
224 effect_type => effect_type,
226 effect_mechanic => effect_mechanic,
228 effect_attributes => effect_attributes,
229 implicit_target_a => implicit_target_a,
230 implicit_target_b => implicit_target_b,
231 effect_aura => effect_aura,
233 effect_misc_value_0 => misc_value_0,
235 effect_misc_value_1 => misc_value_1,
237 shapeshift_form_flags => shapeshift_form_flags,
239 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 effect_class_mask => class_mask,
259 }
260}