1use 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, ¶ms.game_data)?;
110 }
111
112 if let Some(stat) = selected_stat_rune(params) {
113 register_stat_buff(built, ¶ms.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, ¶ms.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(¶ms.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 ¶ms.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, ¶ms.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}