1use wowlab_types::{
4 constants::{HUNDRED, MS_PER_SECOND},
5 data::SpellResources,
6 sim::SpellIdx,
7};
8
9use super::{AuraProps, DamageSpellProperties, 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]
72 pub fn spell_props_for_damage_plan(&self, spell_id: SpellIdx) -> DamageSpellProperties {
73 self.spell_props(spell_id)
74 .map_or_else(Default::default, |props| DamageSpellProperties {
75 defense_type: props.defense_type,
76 hit_chance_flat: props.hit_chance_flat,
77 hit_chance_multiplier: props.hit_chance_multiplier,
78 target_resistance_flat: props.target_resistance_flat,
79 target_resistance_multiplier: props.target_resistance_multiplier,
80 dispel_type: props.dispel_type,
81 dispel_resistance_flat: props.dispel_resistance_flat,
82 dispel_resistance_multiplier: props.dispel_resistance_multiplier,
83 mastery_affects_points: props.mastery_affects_points,
84 primary_cost_entry: props.primary_cost_entry,
85 secondary_cost_entry: props.secondary_cost_entry,
86 })
87 }
88
89 #[must_use]
93 pub fn empower_cast_time_ms(&self, spell_id: SpellIdx, rank: u8) -> Option<u32> {
94 if rank == 0 {
95 return None;
96 }
97
98 if self.inner.spells.props.is_empty() {
99 return Some(0);
100 }
101
102 let props = self.spell_props(spell_id)?;
103
104 if !props.can_empower {
105 return None;
106 }
107
108 let stages = &props.empower_stages;
109
110 if !empower_stages_are_contiguous(stages) {
111 return None;
112 }
113
114 let count = usize::from(rank);
115
116 if count > stages.len() {
117 return None;
118 }
119
120 stages.iter().take(count).try_fold(0_u32, |total, stage| {
121 u32::try_from(stage.duration_ms)
122 .ok()
123 .and_then(|duration| total.checked_add(duration))
124 })
125 }
126
127 #[must_use]
129 pub fn empower_cast_times_ms(&self, spell_id: SpellIdx) -> Option<Vec<u32>> {
130 if self.inner.spells.props.is_empty() {
131 return Some(Vec::new());
132 }
133
134 let props = self.spell_props(spell_id)?;
135
136 if !props.can_empower
137 || props.empower_stages.is_empty()
138 || !empower_stages_are_contiguous(&props.empower_stages)
139 {
140 return None;
141 }
142
143 let mut total = 0_u32;
144 let mut cast_times = Vec::with_capacity(props.empower_stages.len());
145
146 for stage in &props.empower_stages {
147 let duration = u32::try_from(stage.duration_ms).ok()?;
148
149 total = total.checked_add(duration)?;
150 cast_times.push(total);
151 }
152
153 Some(cast_times)
154 }
155
156 pub fn require_empower_cast_times_ms(
160 &self,
161 spell_id: SpellIdx,
162 ) -> Result<Vec<u32>, crate::GameDataError> {
163 self.empower_cast_times_ms(spell_id)
164 .ok_or_else(|| crate::GameDataError::invalid_empower_timings(spell_id))
165 }
166
167 #[must_use]
169 pub fn is_empower_spell(&self, spell_id: SpellIdx) -> bool {
170 self.spell_props(spell_id)
171 .is_some_and(|props| props.can_empower)
172 }
173
174 pub fn require_empower_cast_time_ms(
178 &self,
179 spell_id: SpellIdx,
180 rank: u8,
181 ) -> Result<u32, crate::GameDataError> {
182 self.empower_cast_time_ms(spell_id, rank)
183 .ok_or_else(|| crate::GameDataError::missing_empower_rank(spell_id, rank))
184 }
185
186 #[must_use]
188 pub fn empower_max_rank(&self, spell_id: SpellIdx) -> Option<u8> {
189 if self.inner.spells.props.is_empty() {
190 return Some(0);
191 }
192
193 let props = self.spell_props(spell_id)?;
194
195 if !props.can_empower
196 || props.empower_stages.is_empty()
197 || !empower_stages_are_contiguous(&props.empower_stages)
198 {
199 return None;
200 }
201
202 u8::try_from(props.empower_stages.len()).ok()
203 }
204
205 pub fn require_empower_max_rank(&self, spell_id: SpellIdx) -> Result<u8, crate::GameDataError> {
209 self.empower_max_rank(spell_id)
210 .ok_or_else(|| crate::GameDataError::invalid_empower_rank_count(spell_id))
211 }
212
213 #[must_use]
214 pub fn is_empty(&self) -> bool {
215 self.inner.effects.values.is_empty()
216 }
217
218 #[must_use]
219 pub fn spell_resources(&self, spell_id: SpellIdx) -> Option<SpellResources> {
220 self.spell_props(spell_id).map(|props| SpellResources {
221 primary_cost: props.cost,
222 primary_optional_cost: props.optional_cost,
223 primary_cost_pct: props.cost_pct,
224 primary_max_cost_pct: props.max_cost_pct,
225 primary_optional_cost_pct: props.optional_cost_pct,
226 primary_gain: props.gain,
227 secondary_cost: props.secondary_cost,
228 secondary_optional_cost: props.secondary_optional_cost,
229 secondary_gain: props.secondary_gain,
230 health_cost: props.health_cost,
231 health_cost_pct: props.health_cost_pct,
232 health_max_cost_pct: props.health_max_cost_pct,
233 health_optional_cost: props.health_optional_cost,
234 health_optional_cost_pct: props.health_optional_cost_pct,
235 primary_cost_entry: props.primary_cost_entry,
236 secondary_cost_entry: props.secondary_cost_entry,
237 })
238 }
239
240 #[must_use]
241 pub fn hit_chance_modifier(&self, spell_id: SpellIdx, initial: f64) -> f64 {
242 self.spell_props(spell_id).map_or(initial, |props| {
243 (initial + props.hit_chance_flat) * props.hit_chance_multiplier
244 })
245 }
246
247 #[must_use]
248 pub fn target_resistance_modifier(&self, spell_id: SpellIdx, initial: f64) -> f64 {
249 self.spell_props(spell_id).map_or(initial, |props| {
250 (initial + props.target_resistance_flat) * props.target_resistance_multiplier
251 })
252 }
253
254 #[must_use]
255 pub fn dispel_resistance_modifier(&self, spell_id: SpellIdx, initial: f64) -> f64 {
256 self.spell_props(spell_id).map_or(initial, |props| {
257 (initial + props.dispel_resistance_flat) * props.dispel_resistance_multiplier
258 })
259 }
260
261 #[must_use]
263 pub fn dispel_type(&self, spell_id: SpellIdx) -> i32 {
264 self.spell_props(spell_id)
265 .map_or(0, |props| props.dispel_type)
266 }
267
268 #[must_use]
270 pub fn spell_mechanic(&self, spell_id: SpellIdx) -> i32 {
271 self.spell_props(spell_id).map_or(0, |props| props.mechanic)
272 }
273
274 #[must_use]
275 pub fn cooldown_s(&self, spell_id: SpellIdx) -> Option<f64> {
276 self.spell_props(spell_id)
277 .map(|p| f64::from(p.cooldown_ms) / MS_PER_SECOND)
278 }
279
280 #[must_use]
281 pub fn cast_time_ms(&self, spell_id: SpellIdx) -> Option<u32> {
282 self.spell_props(spell_id)
283 .and_then(|p| u32::try_from(p.cast_time_ms).ok())
284 }
285
286 #[must_use]
287 pub fn breaks_stealth(&self, spell_id: SpellIdx) -> Option<bool> {
288 self.spell_props(spell_id)
289 .map(|p| !p.does_not_break_stealth)
290 }
291
292 #[must_use]
294 pub fn proc_chance(&self, spell_id: SpellIdx) -> Option<f64> {
295 self.proc_chance_with_flat_bonus(spell_id, 0.0)
296 }
297
298 #[must_use]
300 pub fn proc_charges(&self, spell_id: SpellIdx) -> Option<u8> {
301 self.spell_props(spell_id)
302 .and_then(|props| u8::try_from(props.proc_charges).ok())
303 }
304
305 #[must_use]
307 pub fn proc_category_recovery_ms(&self, spell_id: SpellIdx) -> Option<u32> {
308 self.spell_props(spell_id)
309 .map(|props| u32::try_from(props.proc_category_recovery_ms.max(0)).unwrap_or(0))
310 }
311
312 #[must_use]
314 pub fn rppm(&self, spell_id: SpellIdx) -> Option<(f64, i32)> {
315 self.spell_props(spell_id)
316 .map(|props| (props.rppm_base_rate, props.rppm_flags))
317 }
318
319 #[must_use]
321 pub fn proc_chance_with_flat_bonus(
322 &self,
323 spell_id: SpellIdx,
324 runtime_flat: f64,
325 ) -> Option<f64> {
326 self.spell_props(spell_id).map(|props| {
327 (f64::from(props.proc_chance_pct) / HUNDRED + runtime_flat)
328 * (1.0 + props.proc_chance_multiplier_delta)
329 })
330 }
331
332 #[must_use]
333 pub fn charges(&self, spell_id: SpellIdx) -> Option<u8> {
334 self.spell_props(spell_id)
335 .and_then(|p| u8::try_from(p.max_charges).ok())
336 }
337
338 #[must_use]
339 pub fn charge_cd_s(&self, spell_id: SpellIdx) -> Option<f64> {
340 self.spell_props(spell_id)
341 .map(|p| f64::from(p.charge_cd_ms) / MS_PER_SECOND)
342 }
343
344 #[must_use]
345 pub fn category_cooldown_s(&self, spell_id: SpellIdx) -> Option<f64> {
346 self.spell_props(spell_id)
347 .map(|p| f64::from(p.category_cooldown_ms) / MS_PER_SECOND)
348 }
349
350 #[must_use]
351 pub fn gcd_ms(&self, spell_id: SpellIdx) -> Option<u32> {
352 self.spell_props(spell_id)
353 .and_then(|p| u32::try_from(p.gcd_ms).ok())
354 }
355
356 #[must_use]
357 pub fn aura_duration_ms(&self, spell_id: SpellIdx) -> Option<u32> {
358 let raw = self
360 .aura_props(spell_id)
361 .map(|props| props.duration_ms)
362 .or_else(|| {
363 self.spell_props(spell_id)
364 .map(|props| props.duration_ms)
365 .filter(|duration_ms| *duration_ms != 0)
366 })?;
367
368 Some(u32::try_from(raw).unwrap_or(0))
369 }
370
371 #[must_use]
372 pub fn aura_max_stacks(&self, spell_id: SpellIdx) -> Option<u8> {
373 let configured_stacks = self
374 .aura_props(spell_id)
375 .map(|props| props.max_stacks)
376 .or_else(|| {
377 self.spell_props(spell_id)
378 .map(|props| props.max_stacks)
379 .filter(|max_stacks| *max_stacks != 0)
380 });
381
382 configured_stacks.and_then(|max_stacks| u8::try_from(max_stacks).ok())
383 }
384
385 #[must_use]
386 pub fn aura_doses(&self, spell_id: SpellIdx) -> Option<u8> {
387 self.aura_props(spell_id)
388 .and_then(|props| u8::try_from(props.doses).ok())
389 }
390
391 #[must_use]
392 pub fn aura_tick_ms(&self, spell_id: SpellIdx) -> Option<u32> {
393 self.aura_props(spell_id)
394 .and_then(|p| u32::try_from(p.tick_period_ms).ok())
395 }
396
397 #[must_use]
399 pub fn channel_tick_count(&self, spell_id: SpellIdx) -> Option<u8> {
400 let dur = self.spell_props(spell_id)?.cast_time_ms;
401 let aura_period = self
402 .aura_props(spell_id)
403 .map(|props| props.tick_period_ms)
404 .filter(|period| *period > 0);
405 let effect_period = self
406 .inner
407 .effects
408 .values
409 .iter()
410 .filter_map(|(key, effect)| {
411 (key.spell == spell_id && effect.period > 0.0).then_some(effect.period)
412 })
413 .min_by(f64::total_cmp)
414 .and_then(|period| {
415 (period <= f64::from(i32::MAX))
416 .then(|| wowlab_types::numeric::f64_to_i32_saturating_round(period))
417 });
418 let period = aura_period.or(effect_period).unwrap_or(0);
419
420 if dur <= 0 || period <= 0 {
421 return Some(0);
422 }
423
424 let n = dur / period;
425
426 u8::try_from(n.min(i32::from(u8::MAX))).ok()
427 }
428
429 #[must_use]
431 pub fn power_max(&self, type_id: i32) -> Option<f64> {
432 self.power_props(type_id).map(|p| p.max_base_power)
433 }
434
435 #[must_use]
437 pub fn power_display_divisor(&self, type_id: i32) -> f64 {
438 self.power_props(type_id)
439 .map_or(1.0, |p| p.display_modifier.max(1.0))
440 }
441
442 #[must_use]
444 pub fn cost_entry(&self, spell_id: SpellIdx, secondary: bool) -> u8 {
445 self.spell_props(spell_id).map_or(0, |p| {
446 if secondary {
447 p.secondary_cost_entry
448 } else {
449 p.primary_cost_entry
450 }
451 })
452 }
453
454 #[must_use]
456 pub fn power_regen_for_max(&self, type_id: i32, maximum: f64) -> Option<f64> {
457 self.power_props(type_id)
458 .map(|props| props.regen_combat + maximum * props.regen_percent_of_max / HUNDRED)
459 }
460
461 #[must_use]
463 pub fn power_auto_attack_gain_multiplier(&self, type_id: i32) -> f64 {
464 self.power_props(type_id)
465 .map_or(1.0, |props| 1.0 + props.auto_attack_gain_percent / HUNDRED)
466 }
467
468 #[must_use]
470 pub fn power_cost_percent(&self, type_id: i32) -> f64 {
471 self.power_props(type_id)
472 .map_or(0.0, |props| props.cost_percent)
473 }
474
475 #[must_use]
477 pub fn power_default(&self, type_id: i32) -> Option<f64> {
478 self.power_props(type_id).map(|p| p.default_power)
479 }
480
481 #[must_use]
483 pub fn scaled_power_amount(&self, type_id: i32, raw: f64) -> f64 {
484 raw / self
485 .power_props(type_id)
486 .map_or(1.0, |props| props.display_modifier.max(1.0))
487 }
488
489 #[must_use]
500 pub fn spell_override(&self, overridden_id: SpellIdx) -> Option<SpellIdx> {
501 wowlab_types::bench_metrics::record_game_data_hot_read();
502
503 self.inner.spells.overrides.get(&overridden_id).copied()
504 }
505
506 #[must_use]
508 pub fn learned_spells(
509 &self,
510 teacher: SpellIdx,
511 ) -> impl ExactSizeIterator<Item = SpellIdx> + '_ {
512 wowlab_types::bench_metrics::record_game_data_hot_read();
513
514 self.inner
515 .spells
516 .learns
517 .get(&teacher)
518 .map(Vec::as_slice)
519 .unwrap_or_default()
520 .iter()
521 .copied()
522 }
523
524 #[must_use]
526 pub fn has_label(&self, spell: SpellIdx, label: i32) -> bool {
527 wowlab_types::bench_metrics::record_game_data_hot_read();
528
529 self.inner
530 .spells
531 .labels
532 .get(&spell)
533 .is_some_and(|labels| labels.contains(&label))
534 }
535
536 #[must_use]
538 pub fn spell_labels(&self, spell: SpellIdx) -> &[i32] {
539 wowlab_types::bench_metrics::record_game_data_hot_read();
540
541 self.inner
542 .spells
543 .labels
544 .get(&spell)
545 .map(Vec::as_slice)
546 .unwrap_or_default()
547 }
548
549 pub(super) fn spell_props(&self, spell_id: SpellIdx) -> Option<&SpellProps> {
550 wowlab_types::bench_metrics::record_game_data_hot_read();
551
552 self.inner.spells.props.get(&spell_id)
553 }
554
555 fn aura_props(&self, spell_id: SpellIdx) -> Option<&AuraProps> {
556 wowlab_types::bench_metrics::record_game_data_hot_read();
557
558 self.inner.spells.aura_props.get(&spell_id)
559 }
560
561 fn power_props(&self, type_id: i32) -> Option<&PowerTypeProps> {
562 wowlab_types::bench_metrics::record_game_data_hot_read();
563
564 self.inner.spells.power_types.get(&type_id)
565 }
566
567 require_accessor! {
568 spell {
569 require_cost -> f64, "cost" => |data, spell| data.spell_resources(spell).map(|resources| resources.primary_cost),
573 require_cooldown_s -> f64, "cooldown_s" => |data, spell| data.cooldown_s(spell),
577 require_aura_max_stacks -> u8, "aura_max_stacks" => |data, spell| data.aura_max_stacks(spell),
581 require_aura_duration_ms -> u32, "aura_duration_ms" => |data, spell| data.aura_duration_ms(spell),
585 require_cast_time_ms -> u32, "cast_time_ms" => |data, spell| data.cast_time_ms(spell),
589 }
590 }
591
592 require_accessor! {
593 spell {
594 require_charge_cd_s -> f64, "charge_cd_s" => |data, spell| data.charge_cd_s(spell),
598 require_proc_category_recovery_ms -> u32, "proc_category_recovery_ms" => |data, spell| data.proc_category_recovery_ms(spell),
602 require_proc_type_mask -> u64, "proc_type_mask" => |data, spell| data.proc_type_mask(spell),
606 require_channel_tick_count -> u8, "channel_tick_count" => |data, spell| data.channel_tick_count(spell),
610 }
611 }
612
613 spell_prop_accessor! {
614 copy {
615 defense_type -> i32 => defense_type,
616 shapeshift_required_mask -> u64 => shapeshift_required_mask,
617 shapeshift_excluded_mask -> u64 => shapeshift_excluded_mask,
618 gain -> f64 => gain,
619 secondary_gain -> f64 => secondary_gain,
620 cooldown_hasted -> bool => cooldown_hasted,
621 gcd_haste_type -> wowlab_types::combat::GcdHasteType => gcd_haste_type,
622 hasted_ticks -> bool => hasted_ticks,
623 duration_hasted -> bool => duration_hasted,
624 cannot_crit -> bool => cannot_crit,
625 requires_stealth -> bool => requires_stealth,
626 requires_behind_target -> bool => requires_behind_target,
627 requires_unshifted -> bool => requires_unshifted,
628 allow_while_unshifted -> bool => allow_while_unshifted,
629 treat_as_periodic -> bool => treat_as_periodic,
630 treat_as_area_effect -> bool => treat_as_area_effect,
631 usable_while_casting -> bool => usable_while_casting,
632 usable_while_moving -> bool => usable_while_moving,
633 asynchronous_stacks -> bool => asynchronous_stacks,
634 disable_player_damage_multiplier -> bool => disable_player_damage_multiplier,
635 disable_target_damage_multiplier -> bool => disable_target_damage_multiplier,
636 disable_positive_target_damage_multiplier -> bool => disable_positive_target_damage_multiplier,
637 requires_off_hand -> bool => requires_off_hand,
638 requires_main_hand -> bool => requires_main_hand,
639 refresh_behavior -> wowlab_types::data::RefreshBehavior => refresh_behavior,
640 caster_aura_state -> i32 => caster_aura_state,
641 caster_aura_spell -> i32 => caster_aura_spell,
642 exclude_caster_aura_state -> i32 => exclude_caster_aura_state,
643 exclude_caster_aura_spell -> i32 => exclude_caster_aura_spell,
644 target_aura_state -> i32 => target_aura_state,
645 target_aura_spell -> i32 => target_aura_spell,
646 exclude_target_aura_state -> i32 => exclude_target_aura_state,
647 exclude_target_aura_spell -> i32 => exclude_target_aura_spell,
648 aura_interrupt_flags -> u32 => aura_interrupt_flags,
649 spell_interrupt_flags -> u32 => spell_interrupt_flags,
650 channel_interrupt_flags -> u32 => channel_interrupt_flags,
651 rolling_periodic -> bool => rolling_periodic,
652 tick_may_crit -> bool => tick_may_crit,
653 tick_on_application -> bool => tick_on_application,
654 proc_attributes -> u32 => proc_attributes,
656 proc_type_mask -> u64 => proc_type_mask,
658 rppm_haste_scales -> bool => rppm_haste_scales,
659 rppm_crit_scales -> bool => rppm_crit_scales,
660 }
661 flatten {
662 equipped_item_requirement -> wowlab_types::data::EquippedItemRequirement => equipped_item_requirement,
663 cooldown_category -> wowlab_types::data::CooldownCategoryId => cooldown_category,
664 charge_category -> wowlab_types::data::CooldownCategoryId => charge_category,
665 start_recovery_category -> wowlab_types::data::CooldownCategoryId => start_recovery_category,
666 }
667 slice {
668 spell_attributes -> i32 => attributes,
669 }
670 }
671
672 spell_prop_accessor! {
673 copy {
674 projectile_speed -> f64 => projectile_speed,
675 launch_delay_s -> f64 => launch_delay_s,
676 hostile_min_range -> f64 => hostile_min_range,
677 hostile_max_range -> f64 => hostile_max_range,
678 radius -> f64 => radius,
679 chain_target_range -> f64 => chain_target_range,
680 max_affected_targets -> u8 => max_affected_targets,
681 cone_half_angle -> f64 => cone_half_angle,
682 fixed_travel_time -> bool => fixed_travel_time,
683 ignores_line_of_sight -> bool => ignores_line_of_sight,
684 explicit_target_mask -> u32 => explicit_target_mask,
685 required_explicit_target_mask -> u32 => required_explicit_target_mask,
686 }
687 flatten {
688 }
689 slice {
690 }
691 }
692}