1use wowlab_engine_domain::rotation::to_simc_key;
4use wowlab_types::{
5 constants::MS_PER_SECOND,
6 game::{CdrCondition, CdrEffect},
7 sim::{AuraOn, FastMap, IntMap},
8};
9
10use super::{
11 AutoAttackDefinitionDraft, ImpactEffectProcDefinition,
12 def::{
13 BuilderAuraDef, BuilderDamageDef, BuilderSpellDef, BuilderSpellEffect, PeriodicDef,
14 PeriodicEffectDef,
15 },
16};
17use crate::state::{
18 AuraData, AutoAttackData, ImpactEffectProc, LocalAuraIdx, LocalSpellIdx, MAX_CDR_EFFECTS,
19 PeriodicData, PeriodicKind, RuntimeDamageDef, SpellAvailability, SpellBehavior, SpellChannel,
20 SpellChannelBehavior, SpellChannelCompletion, SpellChannelTiming, SpellCooldown, SpellCost,
21 SpellDamagePolicy, SpellData, SpellEffectData, SpellEffectRange, SpellEquipmentRequirements,
22 SpellFormRequirements, SpellOverride, SpellRequirements, SpellTargeting,
23};
24
25#[cfg(test)]
26mod tests;
27
28pub(crate) struct RuntimeStateParts {
29 pub(crate) spell_data: Vec<SpellData>,
30 pub(crate) empower_cast_times_ms: Vec<u32>,
31 pub(crate) spell_effects: Vec<SpellEffectData>,
32 pub(crate) spell_by_id: IntMap<u32, LocalSpellIdx>,
33 pub(crate) aura_data: Vec<AuraData>,
34 pub(crate) aura_by_id: IntMap<u32, LocalAuraIdx>,
35 pub(crate) aura_by_identity: FastMap<(u32, AuraOn), LocalAuraIdx>,
36 pub(crate) auto_attack_data: Vec<AutoAttackData>,
37 pub(crate) impact_effect_procs: Vec<ImpactEffectProc>,
38}
39
40pub(crate) fn lower_builder_state(
41 spells: &[BuilderSpellDef],
42 auras: &[BuilderAuraDef],
43 auto_attacks: &[AutoAttackDefinitionDraft],
44 impact_effect_procs: &[ImpactEffectProcDefinition],
45 names: &mut Vec<Box<str>>,
46) -> RuntimeStateParts {
47 let mut spell_effects = Vec::new();
48 let mut empower_cast_times_ms = Vec::new();
49 let mut ctx = BuilderToRuntimeCtx {
50 names,
51 spell_effects: &mut spell_effects,
52 empower_cast_times_ms: &mut empower_cast_times_ms,
53 };
54
55 let mut spell_by_id: IntMap<u32, LocalSpellIdx> = IntMap::default();
56 let mut spell_data: Vec<SpellData> = Vec::with_capacity(spells.len());
57
58 for (i, def) in spells.iter().enumerate() {
59 spell_by_id.insert(
60 def.spell_id,
61 LocalSpellIdx::new(u8::try_from(i).expect("spell count is validated before lowering")),
62 );
63 spell_data.push(def.into_data(&mut ctx));
64 }
65
66 let mut aura_by_id: IntMap<u32, LocalAuraIdx> = IntMap::default();
67 let mut aura_by_identity: FastMap<(u32, AuraOn), LocalAuraIdx> = FastMap::default();
68 let mut aura_data: Vec<AuraData> = Vec::with_capacity(auras.len());
69
70 for (i, def) in auras.iter().enumerate() {
71 let local =
72 LocalAuraIdx::new(u8::try_from(i).expect("aura count is validated before lowering"));
73
74 aura_by_id.insert(def.aura_id, local);
75 aura_by_identity
76 .entry((def.aura_id, def.on))
77 .or_insert(local);
78
79 if def.propagates_to_pet {
80 aura_by_identity
81 .entry((def.aura_id, AuraOn::Pet))
82 .or_insert(local);
83 }
84
85 aura_data.push(def.into_data(&mut ctx));
86 }
87
88 let auto_attack_data: Vec<AutoAttackData> = auto_attacks
89 .iter()
90 .map(|def| def.into_data(&mut ctx))
91 .collect();
92 let impact_effect_procs = impact_effect_procs
93 .iter()
94 .map(|proc| ImpactEffectProc {
95 driver_spell_id: proc.driver_spell_id,
96 driver_effect_index: proc.driver_effect_index,
97 driver: proc.driver,
98 actor_filter: proc.actor_filter,
99 source: proc.source,
100 chance: proc.chance,
101 chance_scale: proc.chance_scale,
102 icd_ms: proc.icd_ms,
103 target_icd_ms: proc.target_icd_ms,
104 phase_mask: proc.phase_mask,
105 hit_mask: proc.hit_mask,
106 charges: proc.charges,
107 uses_stacks_for_charges: proc.uses_stacks_for_charges,
108 charge_aura: aura_by_id.get(&proc.driver_spell_id).copied(),
109 periodic_only: proc.periodic_only,
110 skip_periodic: proc.skip_periodic,
111 physical_only: proc.physical_only,
112 crit_only: proc.crit_only,
113 profile_spell_id: proc.profile_spell_id,
114 effects: lower_effect_range(&proc.effects, &mut ctx),
115 fire: proc.fire,
116 })
117 .collect();
118
119 RuntimeStateParts {
120 spell_data,
121 empower_cast_times_ms,
122 spell_effects,
123 spell_by_id,
124 aura_data,
125 aura_by_id,
126 aura_by_identity,
127 auto_attack_data,
128 impact_effect_procs,
129 }
130}
131
132pub(crate) struct BuilderToRuntimeCtx<'a> {
133 pub(crate) names: &'a mut Vec<Box<str>>,
134 pub(crate) spell_effects: &'a mut Vec<SpellEffectData>,
135 pub(crate) empower_cast_times_ms: &'a mut Vec<u32>,
136}
137
138impl BuilderToRuntimeCtx<'_> {
139 fn push_name(&mut self, name: &str) -> u16 {
140 let idx = u16::try_from(self.names.len()).expect("name table length fits in u16");
141
142 self.names.push(name.into());
143
144 idx
145 }
146}
147
148pub(crate) struct InfoCtx<'a> {
149 pub(crate) names: &'a [Box<str>],
150 pub(crate) auras: &'a [AuraData],
151}
152
153pub(crate) fn resolve_name_idx(name: &str, ctx: &mut BuilderToRuntimeCtx<'_>) -> u16 {
154 ctx.push_name(name)
155}
156
157pub(crate) fn lower_builder_damage(def: &BuilderDamageDef) -> RuntimeDamageDef {
158 match def {
159 BuilderDamageDef::None => RuntimeDamageDef::None,
160 BuilderDamageDef::Flat(amount) => RuntimeDamageDef::Flat(*amount),
161 BuilderDamageDef::ApCoefficient {
162 coef,
163 is_physical,
164 ap_type,
165 } => RuntimeDamageDef::ApCoefficient {
166 coef: *coef,
167 is_physical: *is_physical,
168 ap_type: *ap_type,
169 },
170 BuilderDamageDef::SpCoefficient { coef, is_physical } => RuntimeDamageDef::SpCoefficient {
171 coef: *coef,
172 is_physical: *is_physical,
173 },
174 BuilderDamageDef::Weapon {
175 multiplier,
176 flat_bonus,
177 normalized,
178 is_physical,
179 hand,
180 } => RuntimeDamageDef::Weapon {
181 multiplier: *multiplier,
182 flat_bonus: *flat_bonus,
183 normalized: *normalized,
184 is_physical: *is_physical,
185 hand: *hand,
186 },
187 }
188}
189
190pub(crate) fn lower_resolved_damage(
191 def: wowlab_engine_gamedata::ResolvedDamageDef,
192 off_hand_ap_multiplier: f64,
193) -> Option<RuntimeDamageDef> {
194 BuilderDamageDef::from_resolved(def, off_hand_ap_multiplier)
195 .map(|builder| lower_builder_damage(&builder))
196}
197
198pub(crate) fn spell_damage(
199 def: &BuilderSpellDef,
200 _ctx: &mut BuilderToRuntimeCtx<'_>,
201) -> RuntimeDamageDef {
202 lower_builder_damage(&def.damage)
203}
204
205pub(crate) fn spell_off_gcd(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> bool {
206 def.start_recovery_category != Some(wowlab_types::data::CooldownCategoryId::GLOBAL)
207}
208
209pub(crate) fn spell_empower_cast_time_offset(
210 def: &BuilderSpellDef,
211 ctx: &mut BuilderToRuntimeCtx<'_>,
212) -> u32 {
213 let offset = u32::try_from(ctx.empower_cast_times_ms.len())
214 .expect("empower cast-time pool length fits in u32");
215
216 ctx.empower_cast_times_ms
217 .extend_from_slice(&def.empower_cast_times_ms);
218
219 offset
220}
221
222pub(crate) fn spell_empower_rank_count(
223 def: &BuilderSpellDef,
224 _ctx: &mut BuilderToRuntimeCtx<'_>,
225) -> u8 {
226 u8::try_from(def.empower_cast_times_ms.len()).expect("empower rank count fits in u8")
227}
228
229pub(crate) fn spell_channel_tick_damage(
230 def: &BuilderSpellDef,
231 _ctx: &mut BuilderToRuntimeCtx<'_>,
232) -> RuntimeDamageDef {
233 match def.channel_tick_damage {
234 BuilderDamageDef::None => lower_builder_damage(&def.damage),
235 ref d => lower_builder_damage(d),
236 }
237}
238
239pub(crate) fn spell_channel_tick_damage_alt(
240 def: &BuilderSpellDef,
241 _ctx: &mut BuilderToRuntimeCtx<'_>,
242) -> RuntimeDamageDef {
243 lower_builder_damage(&def.channel_tick_damage_alt)
244}
245
246pub(crate) fn spell_has_cooldown(
247 def: &BuilderSpellDef,
248 _ctx: &mut BuilderToRuntimeCtx<'_>,
249) -> bool {
250 def.cooldown.is_some()
251}
252
253pub(crate) fn spell_cooldown_duration_ms(
254 def: &BuilderSpellDef,
255 _ctx: &mut BuilderToRuntimeCtx<'_>,
256) -> u32 {
257 match &def.cooldown {
258 Some(cd) => {
259 wowlab_types::numeric::f64_to_u32_saturating_trunc(cd.duration_secs * MS_PER_SECOND)
260 }
261 None => 0,
262 }
263}
264
265pub(crate) fn spell_cooldown_category(
266 def: &BuilderSpellDef,
267 _ctx: &mut BuilderToRuntimeCtx<'_>,
268) -> Option<wowlab_types::data::CooldownCategoryId> {
269 def.cooldown.as_ref().and_then(|cooldown| cooldown.category)
270}
271
272pub(crate) fn spell_category_cooldown_duration_ms(
273 def: &BuilderSpellDef,
274 _ctx: &mut BuilderToRuntimeCtx<'_>,
275) -> u32 {
276 def.cooldown.as_ref().map_or(0, |cooldown| {
277 wowlab_types::numeric::f64_to_u32_saturating_trunc(
278 cooldown.category_duration_secs * MS_PER_SECOND,
279 )
280 })
281}
282
283pub(crate) fn spell_charge_category(
284 def: &BuilderSpellDef,
285 _ctx: &mut BuilderToRuntimeCtx<'_>,
286) -> Option<wowlab_types::data::CooldownCategoryId> {
287 def.cooldown
288 .as_ref()
289 .and_then(|cooldown| cooldown.charge_category)
290}
291
292pub(crate) fn spell_max_charges(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> u8 {
293 match &def.cooldown {
294 Some(cd) => cd.max_charges,
295 None => 0,
296 }
297}
298
299pub(crate) fn spell_recharge_ms(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> u32 {
300 match &def.cooldown {
301 Some(cd) => {
302 wowlab_types::numeric::f64_to_u32_saturating_trunc(cd.recharge_secs * MS_PER_SECOND)
303 }
304 None => 0,
305 }
306}
307
308pub(crate) const fn spell_dynamic_max_charges(
310 _def: &BuilderSpellDef,
311 _ctx: &mut BuilderToRuntimeCtx<'_>,
312) -> bool {
313 false
314}
315
316pub(crate) fn spell_cdr_effects(
317 def: &BuilderSpellDef,
318 _ctx: &mut BuilderToRuntimeCtx<'_>,
319) -> [CdrEffect; MAX_CDR_EFFECTS] {
320 let mut out = [CdrEffect {
321 target_spell_local: 0,
322 amount_ms: 0,
323 condition: CdrCondition::Always,
324 }; MAX_CDR_EFFECTS];
325
326 for (j, eff) in def.cdr_effects.iter().take(MAX_CDR_EFFECTS).enumerate() {
327 out[j] = *eff;
329 }
330
331 out
332}
333
334pub(crate) fn spell_cdr_count(def: &BuilderSpellDef, _ctx: &mut BuilderToRuntimeCtx<'_>) -> u8 {
335 u8::try_from(def.cdr_effects.len().min(MAX_CDR_EFFECTS)).expect("MAX_CDR_EFFECTS fits in u8")
336}
337
338pub(crate) fn spell_followup_effects(
339 def: &BuilderSpellDef,
340 ctx: &mut BuilderToRuntimeCtx<'_>,
341) -> SpellEffectRange {
342 lower_effect_range(&def.followup_effects, ctx)
343}
344
345pub(crate) fn spell_channel_tick_effects(
346 def: &BuilderSpellDef,
347 ctx: &mut BuilderToRuntimeCtx<'_>,
348) -> SpellEffectRange {
349 lower_effect_range(&def.channel_tick_effects, ctx)
350}
351
352pub(crate) fn spell_channel_complete_effects(
353 def: &BuilderSpellDef,
354 ctx: &mut BuilderToRuntimeCtx<'_>,
355) -> SpellEffectRange {
356 lower_effect_range(&def.channel_complete_effects, ctx)
357}
358
359pub(crate) const fn spell_cost(
360 def: &BuilderSpellDef,
361 _ctx: &mut BuilderToRuntimeCtx<'_>,
362) -> SpellCost {
363 SpellCost {
364 resource_cost: def.resource_cost,
365 optional_resource_cost: def.optional_resource_cost,
366 resource_cost_pct: def.resource_cost_pct,
367 maximum_resource_cost_pct: def.maximum_resource_cost_pct,
368 optional_resource_cost_pct: def.optional_resource_cost_pct,
369 resource_gain: def.resource_gain,
370 secondary_resource_cost: def.secondary_resource_cost,
371 secondary_optional_resource_cost: def.secondary_optional_resource_cost,
372 secondary_resource_gain: def.secondary_resource_gain,
373 health_cost: def.health_cost,
374 health_cost_pct: def.health_cost_pct,
375 health_max_cost_pct: def.health_max_cost_pct,
376 health_optional_cost: def.health_optional_cost,
377 health_optional_cost_pct: def.health_optional_cost_pct,
378 }
379}
380
381pub(crate) fn spell_cooldown(
382 def: &BuilderSpellDef,
383 ctx: &mut BuilderToRuntimeCtx<'_>,
384) -> SpellCooldown {
385 SpellCooldown {
386 cooldown_hasted: def.cooldown_hasted,
387 start_recovery_category: def.start_recovery_category,
388 has_cooldown: spell_has_cooldown(def, ctx),
389 cooldown_duration_ms: spell_cooldown_duration_ms(def, ctx),
390 cooldown_category: spell_cooldown_category(def, ctx),
391 category_cooldown_duration_ms: spell_category_cooldown_duration_ms(def, ctx),
392 charge_category: spell_charge_category(def, ctx),
393 max_charges: spell_max_charges(def, ctx),
394 recharge_ms: spell_recharge_ms(def, ctx),
395 dynamic_max_charges: spell_dynamic_max_charges(def, ctx),
396 cdr_effects: spell_cdr_effects(def, ctx),
397 cdr_count: spell_cdr_count(def, ctx),
398 }
399}
400
401pub(crate) fn spell_channel(
402 def: &BuilderSpellDef,
403 ctx: &mut BuilderToRuntimeCtx<'_>,
404) -> SpellChannel {
405 SpellChannel {
406 tick_count: def.channel_tick_count,
407 tick_interval_ms: def.channel_tick_interval_ms,
408 tick_damage: spell_channel_tick_damage(def, ctx),
409 tick_spell_id: def.channel_tick_spell_id,
410 tick_damage_alt: spell_channel_tick_damage_alt(def, ctx),
411 tick_damage_alt_spell_id: def.channel_tick_damage_alt_spell_id,
412 tick_damage_aura: def.channel_tick_damage_aura,
413 tick_cost: def.channel_tick_cost,
414 tick_cost_alt: def.channel_tick_cost_alt,
415 tick_cost_aura: def.channel_tick_cost_aura,
416 tick_effects: spell_channel_tick_effects(def, ctx),
417 complete_effects: spell_channel_complete_effects(def, ctx),
418 timing: SpellChannelTiming {
419 tick_zero: def.channel_tick_zero,
420 tick_is_periodic: def.channel_tick_is_periodic,
421 hasted_ticks: def.channel_hasted_ticks,
422 },
423 behavior: SpellChannelBehavior {
424 is_channel: def.is_channel,
425 duration_hasted: def.channel_duration_hasted,
426 tick_may_crit: def.channel_tick_may_crit,
427 },
428 completion: SpellChannelCompletion {
429 tick_damage_alt_may_crit: def.channel_tick_damage_alt_may_crit,
430 gcd_on_start: def.channel_gcd_on_start,
431 apply_lag: def.channel_apply_lag,
432 },
433 }
434}
435
436pub(crate) const fn spell_targeting(
437 def: &BuilderSpellDef,
438 _ctx: &mut BuilderToRuntimeCtx<'_>,
439) -> SpellTargeting {
440 SpellTargeting {
441 explicit_target_mask: def.explicit_target_mask,
442 required_explicit_target_mask: def.required_explicit_target_mask,
443 aoe_max_targets: def.aoe_max_targets,
444 aoe_reduced_targets: def.aoe_reduced_targets,
445 aoe_full_targets: def.aoe_full_targets,
446 aoe_base_mult: def.aoe_base_mult,
447 chain_targeting: def.chain_targeting,
448 chain_multiplier: def.chain_multiplier,
449 split: def.split,
450 is_aoe: def.is_aoe,
451 }
452}
453
454pub(crate) const fn spell_requirements(
455 def: &BuilderSpellDef,
456 _ctx: &mut BuilderToRuntimeCtx<'_>,
457) -> SpellRequirements {
458 SpellRequirements {
459 shapeshift_required_mask: def.shapeshift_required_mask,
460 shapeshift_excluded_mask: def.shapeshift_excluded_mask,
461 caster_aura_spell: def.caster_aura_spell,
462 caster_aura_state: def.caster_aura_state,
463 exclude_caster_aura_spell: def.exclude_caster_aura_spell,
464 exclude_caster_aura_state: def.exclude_caster_aura_state,
465 target_aura_spell: def.target_aura_spell,
466 target_aura_state: def.target_aura_state,
467 exclude_target_aura_spell: def.exclude_target_aura_spell,
468 exclude_target_aura_state: def.exclude_target_aura_state,
469 equipped_item_requirement: def.equipped_item_requirement,
470 gating_aura_id: def.gating_aura_id,
471 gating_aura_min_stacks: def.gating_aura_min_stacks,
472 cost_bypass_aura_id: def.cost_bypass_aura_id,
473 consume_aura_on_cast_id: def.consume_aura_on_cast_id,
474 cooldown_bypass_aura_id: def.cooldown_bypass_aura_id,
475 execute_below_pct: def.execute_below_pct,
476 instant_cast_aura_id: def.instant_cast_aura_id,
477 availability: SpellAvailability {
478 usable_while_casting: def.usable_while_casting,
479 usable_while_moving: def.usable_while_moving,
480 requires_unshifted: def.requires_unshifted,
481 },
482 form: SpellFormRequirements {
483 allow_while_unshifted: def.allow_while_unshifted,
484 requires_stealth: def.requires_stealth,
485 },
486 equipment: SpellEquipmentRequirements {
487 requires_behind_target: def.requires_behind_target,
488 requires_main_hand: def.requires_main_hand,
489 requires_off_hand: def.requires_off_hand,
490 },
491 }
492}
493
494pub(crate) const fn spell_override(
495 def: &BuilderSpellDef,
496 _ctx: &mut BuilderToRuntimeCtx<'_>,
497) -> SpellOverride {
498 SpellOverride {
499 aura_id: def.override_aura_id,
500 spell_id: def.override_spell_id,
501 aura_min_stacks: def.override_aura_min_stacks,
502 shares_base_cooldown: def.override_shares_base_cooldown,
503 preserves_aura: def.override_preserves_aura,
504 }
505}
506
507pub(crate) fn spell_behavior(
508 def: &BuilderSpellDef,
509 ctx: &mut BuilderToRuntimeCtx<'_>,
510) -> SpellBehavior {
511 SpellBehavior {
512 off_gcd: spell_off_gcd(def, ctx),
513 is_pet: def.is_pet,
514 breaks_stealth: def.breaks_stealth,
515 }
516}
517
518pub(crate) const fn spell_damage_policy(
519 def: &BuilderSpellDef,
520 _ctx: &mut BuilderToRuntimeCtx<'_>,
521) -> SpellDamagePolicy {
522 SpellDamagePolicy {
523 may_crit: def.damage_may_crit,
524 guaranteed_crit: def.guaranteed_crit,
525 }
526}
527
528pub(crate) fn aura_on_consume(
529 def: &BuilderAuraDef,
530 ctx: &mut BuilderToRuntimeCtx<'_>,
531) -> SpellEffectRange {
532 lower_effect_range(&def.on_consume, ctx)
533}
534
535pub(crate) fn aura_apply_effects(
536 def: &BuilderAuraDef,
537 ctx: &mut BuilderToRuntimeCtx<'_>,
538) -> SpellEffectRange {
539 lower_effect_range(&def.apply_effects, ctx)
540}
541
542pub(crate) fn aura_tick_effects(
543 def: &BuilderAuraDef,
544 ctx: &mut BuilderToRuntimeCtx<'_>,
545) -> SpellEffectRange {
546 lower_effect_range(&def.tick_effects, ctx)
547}
548
549fn lower_effect(effect: &BuilderSpellEffect, ctx: &mut BuilderToRuntimeCtx<'_>) -> SpellEffectData {
550 match effect {
551 BuilderSpellEffect::Conditional { predicate, effects } => SpellEffectData::Conditional {
552 predicate: *predicate,
553 effects: lower_effect_range(effects, ctx),
554 },
555 BuilderSpellEffect::ApplyAura { aura_local } => SpellEffectData::ApplyAura {
556 aura_local: *aura_local,
557 },
558 BuilderSpellEffect::AddAuraStack { aura_local } => SpellEffectData::AddAuraStack {
559 aura_local: *aura_local,
560 },
561 BuilderSpellEffect::RemoveAura { aura_local } => SpellEffectData::RemoveAura {
562 aura_local: *aura_local,
563 },
564 BuilderSpellEffect::InterruptCast { lockout_ms } => SpellEffectData::InterruptCast {
565 lockout_ms: *lockout_ms,
566 },
567 BuilderSpellEffect::Dispel { dispel_type, steal } => SpellEffectData::Dispel {
568 dispel_type: *dispel_type,
569 steal: *steal,
570 },
571 BuilderSpellEffect::Damage {
572 effect,
573 damage,
574 base_points,
575 may_crit,
576 attribute_flags,
577 requires_main_hand,
578 requires_off_hand,
579 equipped_item_requirement,
580 } => SpellEffectData::Damage {
581 effect: *effect,
582 damage: lower_builder_damage(damage),
583 base_points: *base_points,
584 may_crit: *may_crit,
585 attribute_flags: *attribute_flags,
586 requires_main_hand: *requires_main_hand,
587 requires_off_hand: *requires_off_hand,
588 equipped_item_requirement: *equipped_item_requirement,
589 },
590 BuilderSpellEffect::Heal {
591 effect,
592 base,
593 ap_coef,
594 sp_coef,
595 percent_of_max,
596 may_crit,
597 } => SpellEffectData::Heal {
598 effect: *effect,
599 base: *base,
600 ap_coef: *ap_coef,
601 sp_coef: *sp_coef,
602 percent_of_max: *percent_of_max,
603 may_crit: *may_crit,
604 },
605 BuilderSpellEffect::Energize {
606 effect,
607 pool,
608 amount,
609 } => SpellEffectData::Energize {
610 effect: *effect,
611 pool: *pool,
612 amount: *amount,
613 },
614 BuilderSpellEffect::EnergizePercent {
615 resource_type,
616 percent,
617 effect_index,
618 } => SpellEffectData::EnergizePercent {
619 resource_type: *resource_type,
620 percent: *percent,
621 effect_index: *effect_index,
622 },
623 BuilderSpellEffect::ExtendAura {
624 aura_local,
625 amount_ms,
626 } => SpellEffectData::ExtendAura {
627 aura_local: *aura_local,
628 amount_ms: *amount_ms,
629 },
630 BuilderSpellEffect::MutateCooldown {
631 spell_local,
632 operation,
633 } => SpellEffectData::MutateCooldown {
634 spell_local: *spell_local,
635 operation: *operation,
636 },
637 BuilderSpellEffect::Delayed {
638 delay_ms,
639 profile_spell_id,
640 effects,
641 } => SpellEffectData::Delayed {
642 delay_ms: *delay_ms,
643 profile_spell_id: *profile_spell_id,
644 effects: lower_effect_range(effects, ctx),
645 },
646 }
647}
648
649fn lower_effect_range(
650 effects: &[BuilderSpellEffect],
651 ctx: &mut BuilderToRuntimeCtx<'_>,
652) -> SpellEffectRange {
653 let lowered: Vec<_> = effects
654 .iter()
655 .map(|effect| lower_effect(effect, ctx))
656 .collect();
657 let start = ctx.spell_effects.len();
658
659 ctx.spell_effects.extend(lowered);
660 let len = ctx.spell_effects.len() - start;
661
662 SpellEffectRange {
663 start: u32::try_from(start).expect("effect arena start fits in u32"),
664 len: u16::try_from(len).expect("effect range length fits in u16"),
665 }
666}
667
668pub(crate) fn aura_periodic(
669 def: &BuilderAuraDef,
670 _ctx: &mut BuilderToRuntimeCtx<'_>,
671) -> Option<PeriodicData> {
672 def.periodic.as_ref().map(periodic_data)
673}
674
675fn periodic_data(def: &PeriodicDef) -> PeriodicData {
676 let effect = match &def.effect {
677 PeriodicEffectDef::FlatDamage {
678 amount,
679 is_physical,
680 } => PeriodicKind::FlatDamage {
681 amount: *amount,
682 is_physical: *is_physical,
683 },
684 PeriodicEffectDef::RollingFlatDamage {
685 amount,
686 is_physical,
687 } => PeriodicKind::RollingFlatDamage {
688 amount: *amount,
689 is_physical: *is_physical,
690 },
691 PeriodicEffectDef::Damage {
692 ap_coef,
693 is_physical,
694 } => PeriodicKind::DamageAp {
695 coef: *ap_coef,
696 is_physical: *is_physical,
697 },
698 PeriodicEffectDef::RollingDamage {
699 ap_coef,
700 is_physical,
701 } => PeriodicKind::RollingDamageAp {
702 coef: *ap_coef,
703 is_physical: *is_physical,
704 },
705 PeriodicEffectDef::SpDamage {
706 sp_coef,
707 is_physical,
708 } => PeriodicKind::DamageSp {
709 coef: *sp_coef,
710 is_physical: *is_physical,
711 },
712 PeriodicEffectDef::RollingSpDamage {
713 sp_coef,
714 is_physical,
715 } => PeriodicKind::RollingDamageSp {
716 coef: *sp_coef,
717 is_physical: *is_physical,
718 },
719 PeriodicEffectDef::MaxHealthDamage {
720 percent,
721 is_physical,
722 } => PeriodicKind::MaxHealthDamage {
723 percent: *percent,
724 is_physical: *is_physical,
725 },
726 PeriodicEffectDef::Heal {
727 base,
728 ap_coef,
729 sp_coef,
730 percent_of_max,
731 } => PeriodicKind::Heal {
732 base: *base,
733 ap_coef: *ap_coef,
734 sp_coef: *sp_coef,
735 percent_of_max: *percent_of_max,
736 },
737 PeriodicEffectDef::Leech {
738 base,
739 ap_coef,
740 sp_coef,
741 is_physical,
742 heal_multiplier,
743 } => PeriodicKind::Leech {
744 base: *base,
745 ap_coef: *ap_coef,
746 sp_coef: *sp_coef,
747 is_physical: *is_physical,
748 heal_multiplier: *heal_multiplier,
749 },
750 PeriodicEffectDef::ResourceGain {
751 amount,
752 resource_type,
753 } => PeriodicKind::ResourceGain {
754 amount: *amount,
755 resource_type: *resource_type,
756 },
757 PeriodicEffectDef::ResourceDrain { amount } => {
758 PeriodicKind::ResourceDrain { amount: *amount }
759 }
760 PeriodicEffectDef::ApplyAura { target_aura_local } => PeriodicKind::ApplyAura {
761 target_aura_local: *target_aura_local,
762 },
763 PeriodicEffectDef::ResidualDamage => PeriodicKind::ResidualDamage,
764 PeriodicEffectDef::RemoveStack => PeriodicKind::RemoveStack,
765 PeriodicEffectDef::Hook => PeriodicKind::Hook,
766 };
767
768 PeriodicData {
769 tick_interval_ms: def.tick_ms,
770 damage_spell_id: def.damage_spell_id.unwrap_or(0),
771 effect_ref: def.effect_ref,
772 hasted_ticks: def.hasted,
773 partial_tick: def.partial_tick,
774 may_crit: def.may_crit,
775 damage_is_periodic: def.damage_is_periodic,
776 tick_on_application: def.tick_on_application,
777 effect,
778 resource_gain: def.resource_gain,
779 crit_resource_gain: def.crit_resource_gain,
780 crit_resource_chance: def.crit_resource_chance,
781 }
782}
783
784fn name_from_idx(name_idx: u16, ctx: &InfoCtx<'_>) -> String {
785 ctx.names
786 .get(name_idx as usize)
787 .map(ToString::to_string)
788 .unwrap_or_default()
789}
790
791pub(crate) fn spell_name(data: &SpellData, ctx: &InfoCtx<'_>) -> String {
792 name_from_idx(data.name_idx, ctx)
793}
794
795pub(crate) fn spell_slug(data: &SpellData, ctx: &InfoCtx<'_>) -> String {
796 to_simc_key(&name_from_idx(data.name_idx, ctx))
797}
798
799pub(crate) fn spell_damage_info(
800 data: &SpellData,
801 _ctx: &InfoCtx<'_>,
802) -> wowlab_types::game::DamageInfo {
803 use wowlab_types::game::{DamageInfo, DamageKind};
804 let kind = match data.damage {
805 RuntimeDamageDef::None => DamageKind::None,
806 RuntimeDamageDef::Flat(amount) => DamageKind::Flat { amount },
807 RuntimeDamageDef::ApCoefficient {
808 coef, is_physical, ..
809 } => DamageKind::ApCoefficient { coef, is_physical },
810 RuntimeDamageDef::SpCoefficient { coef, is_physical } => {
811 DamageKind::SpCoefficient { coef, is_physical }
812 }
813 RuntimeDamageDef::Weapon {
814 multiplier,
815 flat_bonus,
816 normalized,
817 is_physical,
818 ..
819 } => DamageKind::Weapon {
820 multiplier,
821 flat_bonus,
822 normalized,
823 is_physical,
824 },
825 };
826
827 DamageInfo { kind }
828}
829
830pub(crate) fn spell_applies_aura_id(data: &SpellData, ctx: &InfoCtx<'_>) -> Option<u32> {
831 data.applies_aura
832 .map(|idx| ctx.auras[idx.as_usize()].aura_id)
834}
835
836pub(crate) const fn spell_info_start_recovery_category(
837 data: &SpellData,
838 _ctx: &InfoCtx<'_>,
839) -> Option<wowlab_types::data::CooldownCategoryId> {
840 data.cooldown.start_recovery_category
841}
842
843pub(crate) const fn spell_info_off_gcd(data: &SpellData, _ctx: &InfoCtx<'_>) -> bool {
844 data.behavior.off_gcd
845}
846
847pub(crate) const fn spell_info_is_pet(data: &SpellData, _ctx: &InfoCtx<'_>) -> bool {
848 data.behavior.is_pet
849}
850
851pub(crate) const fn spell_info_breaks_stealth(data: &SpellData, _ctx: &InfoCtx<'_>) -> bool {
852 data.behavior.breaks_stealth
853}
854
855pub(crate) const fn spell_info_resource_cost(data: &SpellData, _ctx: &InfoCtx<'_>) -> f64 {
856 data.cost.resource_cost
857}
858
859pub(crate) const fn spell_info_resource_gain(data: &SpellData, _ctx: &InfoCtx<'_>) -> f64 {
860 data.cost.resource_gain
861}
862
863pub(crate) const fn spell_info_secondary_resource_cost(
864 data: &SpellData,
865 _ctx: &InfoCtx<'_>,
866) -> f64 {
867 data.cost.secondary_resource_cost
868}
869
870pub(crate) const fn spell_info_secondary_resource_gain(
871 data: &SpellData,
872 _ctx: &InfoCtx<'_>,
873) -> f64 {
874 data.cost.secondary_resource_gain
875}
876
877pub(crate) fn spell_cooldown_info(
878 data: &SpellData,
879 _ctx: &InfoCtx<'_>,
880) -> Option<wowlab_types::game::CooldownInfo> {
881 use wowlab_types::game::CooldownInfo;
882
883 data.cooldown.has_cooldown.then(|| CooldownInfo {
884 duration_secs: f64::from(data.cooldown.cooldown_duration_ms) / MS_PER_SECOND,
885 max_charges: data.cooldown.max_charges,
886 recharge_secs: f64::from(data.cooldown.recharge_ms) / MS_PER_SECOND,
887 })
888}
889
890pub(crate) fn spell_cdr_effects_info(data: &SpellData, _ctx: &InfoCtx<'_>) -> Vec<CdrEffect> {
891 data.cooldown.cdr_effects[..data.cooldown.cdr_count as usize].to_vec()
894}
895
896pub(crate) fn aura_name(data: &AuraData, ctx: &InfoCtx<'_>) -> String {
897 name_from_idx(data.name_idx, ctx)
898}
899
900pub(crate) fn aura_slug(data: &AuraData, ctx: &InfoCtx<'_>) -> String {
901 to_simc_key(&name_from_idx(data.name_idx, ctx))
902}
903
904pub(crate) fn aura_on(data: &AuraData, _ctx: &InfoCtx<'_>) -> String {
905 let on: AuraOn = data.on;
906
907 on.to_string()
908}
909
910pub(crate) fn aura_haste_buff_pct(data: &AuraData, _ctx: &InfoCtx<'_>) -> f64 {
911 let mut haste_buff_pct = 0.0;
912
913 for effect in data.effects.iter().flatten() {
914 if let crate::BuffEffect::Haste(v) | crate::BuffEffect::HasteMult(v) = effect {
915 haste_buff_pct += *v;
916 }
917 }
918
919 haste_buff_pct
920}
921
922pub(crate) fn aura_damage_mult_pct(data: &AuraData, _ctx: &InfoCtx<'_>) -> f64 {
923 let mut damage_mult_pct = 0.0;
924
925 for effect in data.effects.iter().flatten() {
926 if let crate::BuffEffect::DamageMult(v) = effect {
927 damage_mult_pct = *v;
928 }
929 }
930
931 damage_mult_pct
932}
933
934pub(crate) fn aura_periodic_info(
935 data: &AuraData,
936 _ctx: &InfoCtx<'_>,
937) -> Option<wowlab_types::game::PeriodicInfo> {
938 use wowlab_types::game::{PeriodicEffect, PeriodicInfo};
939
940 data.periodic.as_ref().map(|p| {
941 let effect = match p.effect {
942 PeriodicKind::FlatDamage {
943 amount,
944 is_physical,
945 }
946 | PeriodicKind::RollingFlatDamage {
947 amount,
948 is_physical,
949 } => PeriodicEffect::FlatDamage {
950 amount,
951 is_physical,
952 },
953 PeriodicKind::DamageAp { coef, is_physical }
954 | PeriodicKind::RollingDamageAp { coef, is_physical } => PeriodicEffect::Damage {
955 ap_coef: coef,
956 is_physical,
957 },
958 PeriodicKind::DamageSp { coef, is_physical }
959 | PeriodicKind::RollingDamageSp { coef, is_physical } => PeriodicEffect::SpDamage {
960 sp_coef: coef,
961 is_physical,
962 },
963 PeriodicKind::MaxHealthDamage {
964 percent,
965 is_physical,
966 } => PeriodicEffect::MaxHealthDamage {
967 percent,
968 is_physical,
969 },
970 PeriodicKind::Heal { .. } | PeriodicKind::Leech { .. } | PeriodicKind::Hook => {
971 PeriodicEffect::Hook
972 }
973 PeriodicKind::ResourceGain { amount, .. } => PeriodicEffect::ResourceGain { amount },
974 PeriodicKind::ResourceDrain { amount } => PeriodicEffect::ResourceDrain { amount },
975 PeriodicKind::ApplyAura { target_aura_local } => PeriodicEffect::ApplyAura {
976 target_aura_local: target_aura_local.0,
977 },
978 PeriodicKind::ResidualDamage => PeriodicEffect::ResidualDamage,
979 PeriodicKind::RemoveStack => PeriodicEffect::RemoveStack,
980 };
981
982 PeriodicInfo {
983 tick_ms: p.tick_interval_ms,
984 effect,
985 }
986 })
987}