1use 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 #[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 #[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 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 #[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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 require_cost -> f64, "cost" => |data, spell| data.spell_resources(spell).map(|resources| resources.primary_cost),
529 require_cooldown_s -> f64, "cooldown_s" => |data, spell| data.cooldown_s(spell),
533 require_aura_max_stacks -> u8, "aura_max_stacks" => |data, spell| data.aura_max_stacks(spell),
537 require_aura_duration_ms -> u32, "aura_duration_ms" => |data, spell| data.aura_duration_ms(spell),
541 require_cast_time_ms -> u32, "cast_time_ms" => |data, spell| data.cast_time_ms(spell),
545 }
546 }
547
548 require_accessor! {
549 spell {
550 require_charge_cd_s -> f64, "charge_cd_s" => |data, spell| data.charge_cd_s(spell),
554 require_proc_category_recovery_ms -> u32, "proc_category_recovery_ms" => |data, spell| data.proc_category_recovery_ms(spell),
558 require_proc_type_mask -> u64, "proc_type_mask" => |data, spell| data.proc_type_mask(spell),
562 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 proc_attributes -> u32 => proc_attributes,
612 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}