1use wowlab_engine_combat::{
4 AuraData, AuraOps as _, BuffEffect, BuiltCombatSystem, DamageOps as _, HookCtx, ImpactEvent,
5 ImpactProc, MAX_AURA_BUFF_EFFECTS, PeriodicData, PeriodicKind, ProcOps as _, SpellEffectRange,
6};
7use wowlab_engine_domain::dbc::{EffectLookup, item_scaled_effect_value};
8use wowlab_engine_gamedata::ResolvedGameData;
9use wowlab_engine_ports::{EngineError, HandlerParams, ResolveDependencies};
10use wowlab_types::{
11 constants::{HUNDRED, MS_PER_SECOND},
12 data::{RefreshBehavior, SpellLabel},
13 game::RatingType,
14 sim::{AuraOn, EffectRef, SpellIdx},
15};
16
17const OMNIUM_ITEM_LEVEL: i32 = 289;
18const CORE_COEFFICIENT_EFFECT: u8 = 1;
19const PAYLOAD_EFFECT: u8 = 2;
20const STAT_COEFFICIENT_EFFECT: u8 = 3;
21
22mod spell {
23 pub(super) const VOID_TOUCHED_ORBS: u32 = 1_279_596;
24 pub(super) const UNLEASHED_FIRE: u32 = 1_279_599;
25 pub(super) const LINGERING: u32 = 1_287_555;
26 pub(super) const CRITICAL_POWER: u32 = 1_279_609;
27 pub(super) const BURNING_HASTE: u32 = 1_279_610;
28 pub(super) const MASTERFUL_CUNNING: u32 = 1_279_612;
29 pub(super) const VERSATILE_WARRIOR: u32 = 1_279_613;
30 pub(super) const OVERLOAD: u32 = 1_279_614;
31 pub(super) const RESIDUAL_ENERGY: u32 = 1_279_615;
32 pub(super) const ECHOES: u32 = 1_279_616;
33
34 pub(super) const COEFFICIENTS: u32 = 1_288_183;
35 pub(super) const VOID_TOUCHED_ORBS_DAMAGE: u32 = 1_286_716;
36 pub(super) const VOID_TOUCHED_ORBS_COUNTER: u32 = 1_287_425;
37 pub(super) const UNLEASHED_FIRE_DAMAGE: u32 = 1_286_970;
38 pub(super) const LINGERING_DAMAGE: u32 = 1_287_663;
39 pub(super) const CRITICAL_POWER_BUFF: u32 = 1_287_772;
40 pub(super) const BURNING_HASTE_BUFF: u32 = 1_287_774;
41 pub(super) const MASTERFUL_CUNNING_BUFF: u32 = 1_287_771;
42 pub(super) const VERSATILE_WARRIOR_BUFF: u32 = 1_287_770;
43 pub(super) const ECHOES_DEBUFF: u32 = 1_289_063;
44 pub(super) const ECHOES_DAMAGE: u32 = 1_303_048;
45}
46
47const VOID_TOUCHED_ORBS_IDS: &[u32] = &[
48 spell::COEFFICIENTS,
49 spell::VOID_TOUCHED_ORBS_DAMAGE,
50 spell::VOID_TOUCHED_ORBS_COUNTER,
51];
52const UNLEASHED_FIRE_IDS: &[u32] = &[spell::COEFFICIENTS, spell::UNLEASHED_FIRE_DAMAGE];
53const LINGERING_IDS: &[u32] = &[spell::LINGERING_DAMAGE];
54const CRITICAL_POWER_IDS: &[u32] = &[spell::CRITICAL_POWER_BUFF];
55const BURNING_HASTE_IDS: &[u32] = &[spell::BURNING_HASTE_BUFF];
56const MASTERFUL_CUNNING_IDS: &[u32] = &[spell::MASTERFUL_CUNNING_BUFF];
57const VERSATILE_WARRIOR_IDS: &[u32] = &[spell::VERSATILE_WARRIOR_BUFF];
58const ECHOES_IDS: &[u32] = &[spell::ECHOES_DEBUFF, spell::ECHOES_DAMAGE];
59
60pub(super) const RESOLVE_DEPENDENCIES: &[ResolveDependencies] = &[
61 ResolveDependencies::new(spell::VOID_TOUCHED_ORBS, VOID_TOUCHED_ORBS_IDS),
62 ResolveDependencies::new(spell::UNLEASHED_FIRE, UNLEASHED_FIRE_IDS),
63 ResolveDependencies::new(spell::LINGERING, LINGERING_IDS),
64 ResolveDependencies::new(spell::CRITICAL_POWER, CRITICAL_POWER_IDS),
65 ResolveDependencies::new(spell::BURNING_HASTE, BURNING_HASTE_IDS),
66 ResolveDependencies::new(spell::MASTERFUL_CUNNING, MASTERFUL_CUNNING_IDS),
67 ResolveDependencies::new(spell::VERSATILE_WARRIOR, VERSATILE_WARRIOR_IDS),
68 ResolveDependencies::new(spell::ECHOES, ECHOES_IDS),
69];
70
71pub(super) fn register(
72 built: &mut BuiltCombatSystem,
73 params: &HandlerParams<'_>,
74) -> Result<(), EngineError> {
75 let unleashed_fire = params.talent_picked(spell::UNLEASHED_FIRE);
76 let void_touched_orbs = params.talent_picked(spell::VOID_TOUCHED_ORBS);
77
78 if !unleashed_fire && !void_touched_orbs {
79 return Ok(());
80 }
81
82 let core_proc = register_core_rune(built, ¶ms.game_data, unleashed_fire)?;
83
84 if params.talent_picked(spell::LINGERING) {
85 register_lingering(built, ¶ms.game_data)?;
86 }
87
88 if let Some(stat) = selected_stat_rune(params) {
89 register_stat_buff(built, ¶ms.game_data, stat)?;
90 }
91
92 built.register_impact_proc(ImpactProc::new(core_proc));
93
94 if params.talent_picked(spell::ECHOES) {
95 register_echoes(built, ¶ms.game_data)?;
96 }
97
98 Ok(())
99}
100
101fn register_core_rune(
102 built: &mut BuiltCombatSystem,
103 data: &ResolvedGameData,
104 unleashed_fire: bool,
105) -> Result<wowlab_engine_combat::ImpactProcFn, EngineError> {
106 let damage_spell_id = if unleashed_fire {
107 spell::UNLEASHED_FIRE_DAMAGE
108 } else {
109 spell::VOID_TOUCHED_ORBS_DAMAGE
110 };
111
112 core_scaled_payload(data, damage_spell_id)
113 .filter(|payload| payload.is_finite() && *payload > 0.0)
114 .ok_or_else(|| EngineError::spec_construction("Omnium core payload is unresolved"))?;
115
116 if unleashed_fire {
117 let _ = built.register_system_rppm_from_data(spell::UNLEASHED_FIRE)?;
118
119 Ok(unleashed_fire_proc)
120 } else {
121 register_void_orbs(built, data)?;
122
123 Ok(void_touched_orbs_proc)
124 }
125}
126
127fn register_void_orbs(
128 built: &mut BuiltCombatSystem,
129 data: &ResolvedGameData,
130) -> Result<(), EngineError> {
131 let counter = SpellIdx::from_raw(spell::VOID_TOUCHED_ORBS_COUNTER);
132 let driver = SpellIdx::from_raw(spell::VOID_TOUCHED_ORBS);
133 let max_stacks = data
134 .aura_max_stacks(counter)
135 .filter(|stacks| *stacks > 0)
136 .ok_or_else(|| {
137 EngineError::spec_construction("Omnium Void-Touched Orbs maximum is unresolved")
138 })?;
139 let tick_interval_ms = data
140 .aura_tick_ms(driver)
141 .filter(|period| *period > 0)
142 .ok_or_else(|| {
143 EngineError::spec_construction(
144 "Omnium Void-Touched Orbs generation period is unresolved",
145 )
146 })?;
147 let aura = AuraData {
148 aura_id: spell::VOID_TOUCHED_ORBS_COUNTER,
149 name_idx: 0,
150 on: AuraOn::Player,
151 propagates_to_pet: false,
152 base_duration_ms: 0,
153 doses: 1,
154 max_stacks,
155 pandemic: false,
156 refresh_behavior: RefreshBehavior::Duration,
157 duration_hasted: false,
158 shapeshift_form: 0,
159 shapeshift_form_flags: 0,
160 shapeshift_combat_round_time_ms: 0,
161 effects: [None; MAX_AURA_BUFF_EFFECTS],
162 periodic: Some(PeriodicData {
163 tick_interval_ms,
164 damage_spell_id: 0,
165 effect_ref: None,
166 hasted_ticks: false,
167 partial_tick: false,
168 may_crit: false,
169 damage_is_periodic: true,
170 tick_on_application: false,
171 effect: PeriodicKind::Hook,
172 resource_gain: 0.0,
173 crit_resource_gain: 0.0,
174 crit_resource_chance: 0.0,
175 }),
176 spell_group: None,
177 granted_aura_state: None,
178 crowd_control: None,
179 is_snapshot: false,
180 dynamic_tick_action: wowlab_engine_combat::DynamicTickAction::None,
181 apply_at_max_stacks: true,
182 reverse: false,
183 freeze_stacks: false,
184 tick_behavior: wowlab_engine_combat::AuraTickBehavior::None,
185 tick_stack_change: 1,
186 async_stacks: false,
187 periodic_damage_scales_with_stacks: false,
188 interrupt_flags: wowlab_engine_domain::dbc::SpellAuraInterruptFlags::empty(),
189 reapply_on_expire: false,
190 application_excluded_auras: [0; wowlab_engine_combat::MAX_AURA_APPLICATION_EXCLUSIONS],
191 application_followup_aura_id: 0,
192 apply_effects: SpellEffectRange::default(),
193 apply_profile_spell_id: 0,
194 on_consume: SpellEffectRange::default(),
195 tick_effects: SpellEffectRange::default(),
196 tick_profile_spell_id: 0,
197 expire_trigger_spell_id: 0,
198 expire_trigger_from_caster: false,
199 on_expire: None,
200 on_tick: Some(generate_void_orb),
201 };
202 let local = built.register_aura("Rune of Void-Touched Orbs", aura)?;
203
204 built.push_precombat_aura(local);
205
206 Ok(())
207}
208
209fn generate_void_orb(ctx: &mut HookCtx<'_>) {
210 ctx.add_aura_stack_id(spell::VOID_TOUCHED_ORBS_COUNTER);
211}
212
213fn is_class_ability(data: &ResolvedGameData, spell_id: u32) -> bool {
214 data.has_label(SpellIdx::from_raw(spell_id), SpellLabel::CLASS_SPELLS.0)
215}
216
217fn unleashed_fire_proc(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
218 if !is_class_ability(ctx.game_data(), impact.spell_id) {
219 return;
220 }
221
222 if !ctx.roll_system_rppm(spell::UNLEASHED_FIRE) {
223 return;
224 }
225
226 fire_core_rune(ctx, spell::UNLEASHED_FIRE_DAMAGE);
227}
228
229fn void_touched_orbs_proc(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
230 if impact.amount <= 0.0 || !is_class_ability(ctx.game_data(), impact.spell_id) {
231 return;
232 }
233
234 let orbs = ctx.take_aura_stacks_id(spell::VOID_TOUCHED_ORBS_COUNTER);
235
236 for _ in 0..orbs {
237 fire_core_rune(ctx, spell::VOID_TOUCHED_ORBS_DAMAGE);
238 }
239}
240
241fn fire_core_rune(ctx: &mut HookCtx<'_>, damage_spell_id: u32) {
242 let mut amount = core_scaled_payload(ctx.game_data(), damage_spell_id)
243 .expect("Omnium core payload was validated during construction");
244
245 if spell_present(ctx.game_data(), spell::OVERLOAD) {
246 amount *= 1.0
247 + percent_effect(EffectLookup::new(
248 ctx.game_data(),
249 EffectRef::new(SpellIdx::from_raw(spell::OVERLOAD), CORE_COEFFICIENT_EFFECT),
250 ));
251 }
252
253 ctx.deal_damage_flat(damage_spell_id, amount);
254
255 if spell_present(ctx.game_data(), spell::LINGERING) {
256 ctx.apply_aura_id(spell::LINGERING_DAMAGE);
257 }
258
259 for buff in [
260 spell::CRITICAL_POWER_BUFF,
261 spell::BURNING_HASTE_BUFF,
262 spell::MASTERFUL_CUNNING_BUFF,
263 spell::VERSATILE_WARRIOR_BUFF,
264 ] {
265 if ctx
266 .game_data()
267 .aura_duration_ms(SpellIdx::from_raw(buff))
268 .is_some()
269 {
270 ctx.apply_aura_id(buff);
271 }
272 }
273}
274
275fn register_echoes(
276 built: &mut BuiltCombatSystem,
277 data: &ResolvedGameData,
278) -> Result<(), EngineError> {
279 let duration_ms = data
280 .aura_duration_ms(SpellIdx::from_raw(spell::ECHOES_DEBUFF))
281 .filter(|duration| *duration > 0)
282 .ok_or_else(|| EngineError::spec_construction("Omnium Echoes duration is unresolved"))?;
283 let mut aura = AuraData::simple_player_buff(
284 spell::ECHOES_DEBUFF,
285 0,
286 duration_ms,
287 [None; MAX_AURA_BUFF_EFFECTS],
288 );
289
290 aura.on = AuraOn::Target;
291 aura.on_expire = Some(expire_echoes);
292 let _ = built.register_aura("Rune of Echoes", aura)?;
293
294 built.register_impact_proc(
295 ImpactProc::new(accumulate_echoes).with_spell_filter(is_echoed_omnium_damage),
296 );
297
298 Ok(())
299}
300
301fn is_echoed_omnium_damage(spell_id: u32) -> bool {
302 matches!(
303 spell_id,
304 spell::UNLEASHED_FIRE_DAMAGE | spell::VOID_TOUCHED_ORBS_DAMAGE | spell::LINGERING_DAMAGE
305 )
306}
307
308fn accumulate_echoes(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
309 if ctx.is_aura_active_id(spell::ECHOES_DEBUFF) {
310 let coefficient = percent_effect(EffectLookup::new(
311 ctx.game_data(),
312 EffectRef::new(SpellIdx::from_raw(spell::ECHOES), CORE_COEFFICIENT_EFFECT),
313 ));
314
315 if let Some(local) = ctx.aura_local_index(spell::ECHOES_DEBUFF) {
316 ctx.accumulate_damage(local, impact.amount * coefficient);
317 }
318 } else {
319 ctx.apply_aura_id(spell::ECHOES_DEBUFF);
320 }
321}
322
323fn expire_echoes(ctx: &mut HookCtx<'_>) {
324 let Some(local) = ctx.aura_local_index(spell::ECHOES_DEBUFF) else {
325 return;
326 };
327 let amount = ctx.take_accumulated_damage(local);
328
329 if amount > 0.0 {
330 ctx.deal_damage_flat(spell::ECHOES_DAMAGE, amount);
331 }
332}
333
334fn core_scaled_payload(data: &ResolvedGameData, payload_spell_id: u32) -> Option<f64> {
335 let coefficient = item_scaled_effect_value(
336 EffectLookup::new(
337 data,
338 EffectRef::new(
339 SpellIdx::from_raw(spell::COEFFICIENTS),
340 CORE_COEFFICIENT_EFFECT,
341 ),
342 ),
343 OMNIUM_ITEM_LEVEL,
344 )
345 .map(f64::floor)?
346 / HUNDRED;
347
348 Some(coefficient * data.base_points(SpellIdx::from_raw(payload_spell_id), PAYLOAD_EFFECT))
349}
350
351fn percent_effect(lookup: EffectLookup<'_>) -> f64 {
352 lookup.base_points() / HUNDRED
353}
354
355fn spell_present(data: &ResolvedGameData, spell_id: u32) -> bool {
356 data.max_effect_index(SpellIdx::from_raw(spell_id)) > 0
357}
358
359#[derive(Clone, Copy)]
360struct StatRune {
361 buff_spell_id: u32,
362 rating: RatingType,
363 effect: fn(f64) -> BuffEffect,
364 name: &'static str,
365}
366
367fn selected_stat_rune(params: &HandlerParams<'_>) -> Option<StatRune> {
368 [
369 (
370 spell::CRITICAL_POWER,
371 StatRune {
372 buff_spell_id: spell::CRITICAL_POWER_BUFF,
373 rating: RatingType::Crit,
374 effect: BuffEffect::Crit,
375 name: "Rune of Critical Power",
376 },
377 ),
378 (
379 spell::BURNING_HASTE,
380 StatRune {
381 buff_spell_id: spell::BURNING_HASTE_BUFF,
382 rating: RatingType::Haste,
383 effect: BuffEffect::Haste,
384 name: "Rune of Burning Haste",
385 },
386 ),
387 (
388 spell::MASTERFUL_CUNNING,
389 StatRune {
390 buff_spell_id: spell::MASTERFUL_CUNNING_BUFF,
391 rating: RatingType::Mastery,
392 effect: BuffEffect::Mastery,
393 name: "Rune of Masterful Cunning",
394 },
395 ),
396 (
397 spell::VERSATILE_WARRIOR,
398 StatRune {
399 buff_spell_id: spell::VERSATILE_WARRIOR_BUFF,
400 rating: RatingType::Versatility,
401 effect: BuffEffect::Versatility,
402 name: "Rune of the Versatile Warrior",
403 },
404 ),
405 ]
406 .into_iter()
407 .find_map(|(talent, rune)| params.talent_picked(talent).then_some(rune))
408}
409
410fn register_stat_buff(
411 built: &mut BuiltCombatSystem,
412 data: &ResolvedGameData,
413 rune: StatRune,
414) -> Result<(), EngineError> {
415 let scaled = item_scaled_effect_value(
416 EffectLookup::new(
417 data,
418 EffectRef::new(
419 SpellIdx::from_raw(spell::COEFFICIENTS),
420 STAT_COEFFICIENT_EFFECT,
421 ),
422 ),
423 OMNIUM_ITEM_LEVEL,
424 )
425 .ok_or_else(|| EngineError::spec_construction("Omnium stat coefficient is unresolved"))?
426 .floor()
427 / HUNDRED;
428 let rating = scaled * data.base_points(SpellIdx::from_raw(rune.buff_spell_id), PAYLOAD_EFFECT);
429 let divisor = wowlab_engine_domain::stats::combat_rating_divisor(
430 data.game_tables(),
431 rune.rating,
432 data.level(),
433 )
434 .ok_or_else(|| EngineError::spec_construction("Omnium combat-rating divisor is unresolved"))?;
435 let mut effects = [None; MAX_AURA_BUFF_EFFECTS];
436
437 effects[0] = Some((rune.effect)(rating / divisor));
438
439 let mut aura = AuraData::simple_player_buff(
440 rune.buff_spell_id,
441 0,
442 data.aura_duration_ms(SpellIdx::from_raw(rune.buff_spell_id))
443 .filter(|duration| *duration > 0)
444 .ok_or_else(|| EngineError::spec_construction("Omnium stat duration is unresolved"))?,
445 effects,
446 );
447
448 aura.max_stacks = data
449 .aura_max_stacks(SpellIdx::from_raw(rune.buff_spell_id))
450 .filter(|stacks| *stacks > 0)
451 .ok_or_else(|| EngineError::spec_construction("Omnium stat stack count is unresolved"))?;
452 aura.async_stacks = true;
453 let _ = built.register_aura(rune.name, aura)?;
454
455 Ok(())
456}
457
458fn register_lingering(
459 built: &mut BuiltCombatSystem,
460 data: &ResolvedGameData,
461) -> Result<(), EngineError> {
462 let lingering_payload = core_scaled_payload(data, spell::LINGERING_DAMAGE)
463 .filter(|payload| payload.is_finite() && *payload > 0.0)
464 .ok_or_else(|| {
465 EngineError::spec_construction("Omnium lingering damage payload is unresolved")
466 })?;
467
468 let duration_ms = data
469 .aura_duration_ms(SpellIdx::from_raw(spell::LINGERING_DAMAGE))
470 .filter(|duration| *duration > 0)
471 .ok_or_else(|| EngineError::spec_construction("Omnium Lingering duration is unresolved"))?;
472 let tick_interval_ms = data
473 .aura_tick_ms(SpellIdx::from_raw(spell::LINGERING_DAMAGE))
474 .filter(|period| *period > 0)
475 .ok_or_else(|| EngineError::spec_construction("Omnium Lingering period is unresolved"))?;
476 let residual_mult = if spell_present(data, spell::RESIDUAL_ENERGY) {
477 1.0 + percent_effect(EffectLookup::new(
478 data,
479 EffectRef::new(
480 SpellIdx::from_raw(spell::RESIDUAL_ENERGY),
481 CORE_COEFFICIENT_EFFECT,
482 ),
483 ))
484 } else {
485 1.0
486 };
487 let tick_amount =
488 lingering_payload / (f64::from(duration_ms) / MS_PER_SECOND).max(1.0) * residual_mult;
489 let aura = AuraData {
490 aura_id: spell::LINGERING_DAMAGE,
491 name_idx: 0,
492 on: AuraOn::Target,
493 propagates_to_pet: false,
494 base_duration_ms: duration_ms,
495 doses: 1,
496 max_stacks: 1,
497 pandemic: false,
498 refresh_behavior: RefreshBehavior::Duration,
499 duration_hasted: false,
500 shapeshift_form: 0,
501 shapeshift_form_flags: 0,
502 shapeshift_combat_round_time_ms: 0,
503 effects: [None; MAX_AURA_BUFF_EFFECTS],
504 periodic: Some(PeriodicData {
505 tick_interval_ms,
506 damage_spell_id: spell::LINGERING_DAMAGE,
507 effect_ref: None,
508 hasted_ticks: false,
509 partial_tick: false,
510 may_crit: false,
511 damage_is_periodic: true,
512 tick_on_application: false,
513 effect: PeriodicKind::FlatDamage {
514 amount: tick_amount,
515 is_physical: false,
516 },
517 resource_gain: 0.0,
518 crit_resource_gain: 0.0,
519 crit_resource_chance: 0.0,
520 }),
521 spell_group: None,
522 granted_aura_state: None,
523 crowd_control: None,
524 is_snapshot: false,
525 dynamic_tick_action: wowlab_engine_combat::DynamicTickAction::None,
526 apply_at_max_stacks: false,
527 reverse: false,
528 freeze_stacks: false,
529 tick_behavior: wowlab_engine_combat::AuraTickBehavior::None,
530 tick_stack_change: 1,
531 async_stacks: false,
532 periodic_damage_scales_with_stacks: false,
533 interrupt_flags: wowlab_engine_domain::dbc::SpellAuraInterruptFlags::empty(),
534 reapply_on_expire: false,
535 application_excluded_auras: [0; wowlab_engine_combat::MAX_AURA_APPLICATION_EXCLUSIONS],
536 application_followup_aura_id: 0,
537 apply_effects: SpellEffectRange::default(),
538 apply_profile_spell_id: 0,
539 on_consume: SpellEffectRange::default(),
540 tick_effects: SpellEffectRange::default(),
541 tick_profile_spell_id: 0,
542 expire_trigger_spell_id: 0,
543 expire_trigger_from_caster: false,
544 on_expire: None,
545 on_tick: None,
546 };
547 let _ = built.register_aura("Rune of Lingering", aura)?;
548
549 Ok(())
550}
551
552#[cfg(test)]
553mod tests {
554 use googletest::prelude::*;
555 use rstest::rstest;
556 use wowlab_engine_ports::CombatStats;
557
558 use super::*;
559
560 #[gtest]
561 #[rstest]
562 #[case::class_ability(&[SpellLabel::CLASS_SPELLS.0], true)]
563 #[case::item_effect(&[SpellLabel::ITEM_EFFECTS.0], false)]
564 #[case::unlabelled(&[], false)]
565 fn unleashed_fire_uses_the_class_spell_label(
566 #[case] labels: &[i32],
567 #[case] expected: bool,
568 ) -> Result<()> {
569 let spell_id = SpellIdx::from_raw(123);
570 let mut builder = ResolvedGameData::builder();
571
572 builder.insert_spell_labels(spell_id, labels.to_vec());
573
574 verify_that!(
575 is_class_ability(&builder.build(), spell_id.as_u32()),
576 eq(expected)
577 )
578 }
579
580 #[gtest]
581 fn selected_omnium_mechanics_fail_closed_without_runtime_data() -> Result<()> {
582 let data = ResolvedGameData::default();
583 let mut void_orbs = crate::test_combat_builder(CombatStats::default())
584 .build(wowlab_types::sim::Rotation::empty())
585 .or_fail()?;
586 let mut echoes = crate::test_combat_builder(CombatStats::default())
587 .build(wowlab_types::sim::Rotation::empty())
588 .or_fail()?;
589 let mut lingering = crate::test_combat_builder(CombatStats::default())
590 .build(wowlab_types::sim::Rotation::empty())
591 .or_fail()?;
592
593 verify_that!(register_void_orbs(&mut void_orbs, &data).is_err(), eq(true))?;
594 verify_that!(register_echoes(&mut echoes, &data).is_err(), eq(true))?;
595
596 verify_that!(register_lingering(&mut lingering, &data).is_err(), eq(true))
597 }
598
599 #[gtest]
600 #[rstest]
601 #[case::void_orbs(
602 spell::VOID_TOUCHED_ORBS,
603 &[
604 spell::COEFFICIENTS,
605 spell::VOID_TOUCHED_ORBS_DAMAGE,
606 spell::VOID_TOUCHED_ORBS_COUNTER,
607 ]
608 )]
609 #[case::unleashed_fire(
610 spell::UNLEASHED_FIRE,
611 &[spell::COEFFICIENTS, spell::UNLEASHED_FIRE_DAMAGE]
612 )]
613 #[case::echoes(
614 spell::ECHOES,
615 &[spell::ECHOES_DEBUFF, spell::ECHOES_DAMAGE]
616 )]
617 fn core_runes_resolve_their_runtime_dependencies(
618 #[case] talent: u32,
619 #[case] expected: &[u32],
620 ) -> Result<()> {
621 let actual = RESOLVE_DEPENDENCIES
622 .iter()
623 .find(|dependency| dependency.selector_id() == talent)
624 .map_or(&[][..], |dependency| dependency.spell_ids());
625
626 verify_that!(actual, eq(expected))
627 }
628
629 #[gtest]
630 #[rstest]
631 #[case::unleashed_fire(spell::UNLEASHED_FIRE_DAMAGE, true)]
632 #[case::void_orbs(spell::VOID_TOUCHED_ORBS_DAMAGE, true)]
633 #[case::lingering(spell::LINGERING_DAMAGE, true)]
634 #[case::echo_damage(spell::ECHOES_DAMAGE, false)]
635 #[case::class_spell(123, false)]
636 fn echoes_accumulates_only_core_and_lingering_damage(
637 #[case] spell_id: u32,
638 #[case] expected: bool,
639 ) -> Result<()> {
640 verify_that!(is_echoed_omnium_damage(spell_id), eq(expected))
641 }
642}