Skip to main content

wowlab_engine_content/expansion_traits/
omnium.rs

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