1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::ProcOps as _;
5use wowlab_engine_combat::AuraOps as _;
6use wowlab_engine_combat::{DamageFlags, HookCtx, MasteryCtx, SwingEvent, SwingHand};
7use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
8use wowlab_engine_rng::proc_chance;
9use wowlab_types::{combat::DamageSchool, sim::SpellIdx};
10
11use crate::generated::specs::frost_death_knight::{
12 AURA_BONEGRINDER_CRIT, AURA_BONEGRINDER_FROST, AURA_BRITTLE, AURA_CHOSEN_OF_FROSTBROOD_HASTE,
13 AURA_DARK_TALONS_ICY_TALONS, AURA_EMPOWER_RUNE_WEAPON, AURA_EMPOWERED_STRIKES,
14 AURA_EXTERMINATE, AURA_FROST_FEVER, AURA_FROSTREAPER, AURA_FROZEN_DOMINION_MASTERY,
15 AURA_ICY_ONSLAUGHT, AURA_ICY_TALONS, AURA_KILLING_MACHINE, AURA_KILLING_STREAK, AURA_MID1_4PC,
16 AURA_PILLAR_OF_FROST, AURA_RAZORICE, AURA_REMORSELESS_WINTER, AURA_RIME, AURA_RUNE_MASTERY,
17 EFFECT, REPORTED_SPELL, SPELL, SPELL_EMPOWER_RUNE_WEAPON, TALENT,
18};
19
20const RP_DISPLAY_DIV: f64 = 10.0;
22
23const FROST_STRIKE_RP_COST: f64 = 35.0;
25
26const BOS_RP_COST: f64 = 60.0;
28
29const RUNIC_EMPOWERMENT_RP_FACTOR: f64 = 0.001;
31
32const HYPERPYREXIA_PROC_CHANCE: f64 = 0.35;
34
35const FROSTBOUND_WILL_ERW_MS: u32 = 6_000;
37
38const BONEGRINDER_CRIT_MAX: i32 = 5;
40
41const ERW_RECHARGE_MS: u32 = 24_000;
42
43#[must_use]
45pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
46 if ctx.school == DamageSchool::Frost || ctx.spell_id == EFFECT::OBLITERATE_PHYS.0 {
47 ctx.bonus(1)
48 } else {
49 1.0
50 }
51}
52
53fn static_frost_mult(ctx: &HookCtx<'_>) -> f64 {
54 (1.0 + ctx.effect_percent(EFFECT::MOTFW_ALL_PCT))
55 * (1.0 + ctx.effect_percent(EFFECT::FROST_DK_SPEC_PCT))
56}
57
58fn deal_effect(ctx: &mut HookCtx<'_>, effect: (u32, u8), mult: f64) {
59 let coef = ctx
60 .game_data()
61 .ap_coef(SpellIdx::from_raw(effect.0), effect.1);
62
63 ctx.deal_damage_ap(effect.0, coef * mult, DamageFlags::empty());
64}
65
66fn effect_folded_coef(ctx: &HookCtx<'_>, effect: (u32, u8)) -> f64 {
68 let idx = SpellIdx::from_raw(effect.0);
69
70 ctx.game_data().ap_coef(idx, effect.1) * ctx.game_data().passive_damage_mult(idx, false)
71}
72
73fn roll_hyperpyrexia(ctx: &mut HookCtx<'_>, trigger_coef: f64) {
74 if !proc_chance(ctx.rng(), HYPERPYREXIA_PROC_CHANCE) {
75 return;
76 }
77
78 let coef = trigger_coef * ctx.effect_percent(EFFECT::HYPERPYREXIA_PORTION_PCT);
79
80 ctx.deal_damage_ap(REPORTED_SPELL::HYPERPYREXIA, coef, DamageFlags::empty());
81}
82
83fn roll_dark_talons(ctx: &mut HookCtx<'_>) {
84 let chance = ctx.effect_percent(EFFECT::DARK_TALONS_CHANCE);
85
86 if proc_chance(ctx.rng(), chance) {
87 ctx.apply_aura(AURA_DARK_TALONS_ICY_TALONS.raw());
88 }
89}
90
91fn runic_empowerment(ctx: &mut HookCtx<'_>, rp_spent: f64) -> bool {
93 let chance = ctx.effect_base_points(EFFECT::RUNIC_EMPOWERMENT_PCT)
94 * RUNIC_EMPOWERMENT_RP_FACTOR
95 * rp_spent;
96
97 if !proc_chance(ctx.rng(), chance) {
98 return false;
99 }
100
101 ctx.gain_resource(1.0);
102 ctx.expire_aura(AURA_ICY_ONSLAUGHT.raw());
103
104 let chance = ctx.effect_percent(EFFECT::FRIGID_EXECUTIONER_PCT);
105
106 if proc_chance(ctx.rng(), chance) {
107 ctx.gain_resource(1.0);
108 }
109
110 true
111}
112
113fn obliteration(ctx: &mut HookCtx<'_>) {
114 if !ctx.is_aura_active(AURA_PILLAR_OF_FROST.raw()) {
115 return;
116 }
117
118 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
119
120 let chance = ctx.effect_percent(EFFECT::OBLITERATION_RUNE_PCT);
121
122 if proc_chance(ctx.rng(), chance) {
123 ctx.gain_resource(1.0);
124 }
125}
126
127fn trigger_frostreaper(ctx: &mut HookCtx<'_>, driver_spell_id: u32, aura_local: u8) -> bool {
128 if !ctx.roll_system_rppm(driver_spell_id) {
129 return false;
130 }
131
132 let refreshed = ctx.is_aura_active(aura_local);
133
134 ctx.apply_aura(aura_local);
135
136 refreshed
137}
138
139pub(crate) fn obliterate_hook(ctx: &mut HookCtx<'_>) {
140 deal_effect(ctx, EFFECT::OBLITERATE_PHYS, 1.0);
144 deal_effect(ctx, EFFECT::OBLITERATE_FROST, 1.0);
145
146 if trigger_frostreaper(ctx, TALENT::FROSTREAPER, AURA_FROSTREAPER.raw()) {
147 deal_effect(ctx, EFFECT::FROSTREAPER_DAMAGE, static_frost_mult(ctx));
148 }
149
150 ctx.consume_aura_stack(AURA_EMPOWERED_STRIKES.raw());
151 ctx.consume_aura(AURA_EMPOWER_RUNE_WEAPON.raw());
152
153 if ctx.consume_aura_stack(AURA_EXTERMINATE.raw()) {
154 deal_effect(ctx, EFFECT::EXTERMINATE_HIT, 1.0);
155 deal_effect(ctx, EFFECT::EXTERMINATE_AOE, 1.0);
156 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
157 }
158
159 let km_stacks = ctx.aura_stacks(AURA_KILLING_MACHINE.raw());
160
161 if km_stacks > 0 {
162 ctx.consume_aura(AURA_KILLING_MACHINE.raw());
163 ctx.apply_aura_n(AURA_KILLING_STREAK.raw(), km_stacks);
164 roll_dark_talons(ctx);
165 let aa_mult = ctx.effect_percent(EFFECT::ARCTIC_ASSAULT_PCT)
166 * f64::from(km_stacks)
167 * static_frost_mult(ctx);
168
169 deal_effect(ctx, EFFECT::GLACIAL_ADVANCE_DAMAGE, aa_mult);
170 ctx.apply_aura(AURA_RAZORICE.raw());
171
172 let chance = ctx.effect_percent(EFFECT::MURDEROUS_EFFICIENCY_PCT);
173
174 if proc_chance(ctx.rng(), chance) {
175 ctx.gain_resource(1.0);
176 }
177
178 ctx.apply_aura(AURA_BONEGRINDER_CRIT.raw());
179
180 if ctx.aura_stacks(AURA_BONEGRINDER_CRIT.raw()) >= BONEGRINDER_CRIT_MAX {
181 ctx.expire_aura(AURA_BONEGRINDER_CRIT.raw());
182 ctx.apply_aura(AURA_BONEGRINDER_FROST.raw());
183 }
184 }
185
186 let chance = ctx.effect_percent(EFFECT::RIME_PROC_PCT);
187
188 if proc_chance(ctx.rng(), chance) {
189 ctx.apply_aura(AURA_RIME.raw());
190 }
191
192 ctx.apply_aura(AURA_RUNE_MASTERY.raw());
193
194 roll_hyperpyrexia(ctx, effect_folded_coef(ctx, EFFECT::OBLITERATE_PHYS));
195 roll_hyperpyrexia(ctx, effect_folded_coef(ctx, EFFECT::OBLITERATE_FROST));
196}
197
198pub(crate) fn frost_strike_hook(ctx: &mut HookCtx<'_>) {
199 let fs_mult = static_frost_mult(ctx)
200 * (1.0 + ctx.effect_percent(EFFECT::RUNIC_OVERFLOW_FS_PCT))
201 * (1.0 + ctx.effect_percent(EFFECT::DEATHLY_BLOWS_FS_PCT))
202 * (1.0 + ctx.effect_percent(EFFECT::COF_SPELLS_PCT));
203 let fs_coef = ctx
204 .game_data()
205 .effect_ap_coefficient(EFFECT::FROST_STRIKE_2H);
206
207 ctx.deal_damage_ap(SPELL::FROST_STRIKE, fs_coef * fs_mult, DamageFlags::empty());
208
209 if ctx.consume_aura(AURA_FROSTREAPER.raw()) {
210 deal_effect(ctx, EFFECT::FROSTREAPER_DAMAGE, static_frost_mult(ctx));
211 }
212
213 ctx.apply_aura(AURA_ICY_TALONS.raw());
214
215 if !runic_empowerment(ctx, FROST_STRIKE_RP_COST) {
216 ctx.apply_aura(AURA_ICY_ONSLAUGHT.raw());
217 }
218
219 obliteration(ctx);
220 roll_hyperpyrexia(ctx, fs_coef * fs_mult);
221}
222
223pub(crate) fn howling_blast_hook(ctx: &mut HookCtx<'_>) {
224 ctx.apply_aura(AURA_FROST_FEVER.raw());
225
226 let mut hb_mult = static_frost_mult(ctx) * (1.0 + ctx.effect_percent(EFFECT::COF_SPELLS_PCT));
227
228 if ctx.consume_aura(AURA_RIME.raw()) {
229 roll_dark_talons(ctx);
230 let rime_pct = ctx.effect_percent(EFFECT::RIME_HB_BONUS_PCT)
231 + ctx.effect_percent(EFFECT::NORTHWINDS_RIME_PCT);
232
233 hb_mult *= (1.0 + rime_pct)
234 * (1.0 + ctx.effect_percent(EFFECT::ICEBREAKER_PCT))
235 * (1.0 + ctx.effect_percent(EFFECT::BIND_IN_DARKNESS_HB_PCT));
236
237 let km_chance = ctx.effect_percent(EFFECT::HOWLING_BLADES_KM_PCT);
238
239 for blade in [EFFECT::HOWLING_BLADES_FIRST, EFFECT::HOWLING_BLADES_SECOND] {
240 deal_effect(ctx, blade, 1.0);
241
242 if proc_chance(ctx.rng(), km_chance) {
243 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
244 }
245 }
246
247 let rfc_rp = ctx.effect_base_points(EFFECT::RAGE_OF_FROZEN_CHAMPION_RP) / RP_DISPLAY_DIV;
248
249 ctx.gain_secondary_resource(rfc_rp);
250 ctx.reduce_cooldown(SPELL_EMPOWER_RUNE_WEAPON.raw(), FROSTBOUND_WILL_ERW_MS);
251 } else {
252 ctx.apply_aura(AURA_RUNE_MASTERY.raw());
253 }
254
255 deal_effect(ctx, EFFECT::HOWLING_BLAST_DAMAGE, hb_mult);
256 obliteration(ctx);
257}
258
259pub(crate) fn frost_fever_tick_hook(ctx: &mut HookCtx<'_>) {
261 let chance = ctx.selected_talent_proc_chance(TALENT::BRITTLE);
262
263 if proc_chance(ctx.rng(), chance) {
264 ctx.apply_aura(AURA_BRITTLE.raw());
265 }
266}
267
268pub(crate) fn remorseless_winter_hook(ctx: &mut HookCtx<'_>) {
269 ctx.apply_aura(AURA_REMORSELESS_WINTER.raw());
270}
271
272fn trigger_frozen_dominion(
273 ctx: &mut HookCtx<'_>,
274 talent_id: u32,
275 remorseless_winter_local: u8,
276 mastery_local: u8,
277) {
278 if !ctx.talent_selected(talent_id) {
279 return;
280 }
281
282 ctx.apply_aura(remorseless_winter_local);
283 ctx.apply_aura(mastery_local);
284}
285
286pub(crate) fn pillar_of_frost_hook(ctx: &mut HookCtx<'_>) {
288 trigger_frozen_dominion(
292 ctx,
293 TALENT::FROZEN_DOMINION,
294 AURA_REMORSELESS_WINTER.raw(),
295 AURA_FROZEN_DOMINION_MASTERY.raw(),
296 );
297
298 if mid1_4pc(ctx) {
299 ctx.apply_aura(AURA_MID1_4PC.raw());
300 }
301}
302
303pub(crate) fn empower_rune_weapon_hook(ctx: &mut HookCtx<'_>) {
304 let rp = ctx.effect_base_points(EFFECT::ERW_RP) / RP_DISPLAY_DIV;
305
306 ctx.gain_secondary_resource(rp);
307 ctx.apply_aura(AURA_EMPOWER_RUNE_WEAPON.raw());
308 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
309
310 if mid1_2pc(ctx) {
311 ctx.apply_aura(AURA_EMPOWERED_STRIKES.raw());
312 }
313}
314
315fn mid1_2pc(ctx: &HookCtx<'_>) -> bool {
316 ctx.state()
317 .spec_config::<super::config::FrostDkConfig>()
318 .is_some_and(|config| config.mid1_2pc)
319}
320
321fn mid1_4pc(ctx: &HookCtx<'_>) -> bool {
322 ctx.state()
323 .spec_config::<super::config::FrostDkConfig>()
324 .is_some_and(|config| config.mid1_4pc)
325}
326
327pub(crate) fn breath_of_sindragosa_hook(ctx: &mut HookCtx<'_>) {
328 breath_of_sindragosa_damage(ctx);
329 ctx.apply_aura(AURA_ICY_TALONS.raw());
330 runic_empowerment(ctx, BOS_RP_COST);
331 ctx.reduce_cooldown(SPELL_EMPOWER_RUNE_WEAPON.raw(), ERW_RECHARGE_MS);
332}
333
334pub(crate) fn breath_of_sindragosa_tick_hook(ctx: &mut HookCtx<'_>) {
335 breath_of_sindragosa_damage(ctx);
336}
337
338pub(crate) fn breath_of_sindragosa_expire_hook(ctx: &mut HookCtx<'_>) {
339 ctx.gain_resource(ctx.effect_base_points(EFFECT::BOS_RUNE_REFUND));
340}
341
342fn breath_of_sindragosa_damage(ctx: &mut HookCtx<'_>) {
343 ctx.deal_effect_damage_ap_with_geometry(
344 EFFECT::BOS_DAMAGE,
345 EFFECT::BOS_DAMAGE,
346 1.0,
347 DamageFlags::empty(),
348 );
349}
350
351pub(crate) fn frostwyrms_fury_hook(ctx: &mut HookCtx<'_>) {
352 let multiplier = static_frost_mult(ctx) * (1.0 + ctx.effect_percent(EFFECT::COF_FWF_PCT));
353
354 ctx.deal_effect_damage_ap_with_geometry(
355 EFFECT::FROSTWYRMS_FURY_DAMAGE,
356 EFFECT::FROSTWYRMS_FURY_DAMAGE,
357 multiplier,
358 DamageFlags::empty(),
359 );
360 let coef = ctx.effect_ap_coefficient(EFFECT::FROSTWYRMS_FURY_DAMAGE) * multiplier;
361
362 ctx.deal_damage_ap(
363 REPORTED_SPELL::CHOSEN_OF_FROSTBROOD_RECALL,
364 coef,
365 DamageFlags::empty(),
366 );
367 ctx.apply_aura(AURA_CHOSEN_OF_FROSTBROOD_HASTE.raw());
368}
369
370pub(crate) fn reapers_mark_hook(ctx: &mut HookCtx<'_>) {
372 deal_effect(ctx, EFFECT::WAVE_OF_SOULS_DAMAGE, 1.0);
373 deal_effect(ctx, EFFECT::WAVE_OF_SOULS_DAMAGE, 1.0);
374
375 let hp_pct = ctx.target_health_fraction();
376 let grim_reaper = 1.0 + (1.0 - hp_pct) * ctx.effect_percent(EFFECT::GRIM_REAPER_MAX_PCT);
377 let stacks = ctx.effect_base_points(EFFECT::REAPERS_MARK_STACKS);
378
379 deal_effect(ctx, EFFECT::REAPERS_MARK_EXPLOSION, stacks * grim_reaper);
380
381 for _ in 0..wowlab_types::numeric::f64_to_i64_saturating_trunc(
384 ctx.effect_base_points(EFFECT::ECHOING_FURY_STACKS),
385 ) {
386 deal_effect(ctx, EFFECT::EXTERMINATE_HIT, 1.0);
387 deal_effect(ctx, EFFECT::EXTERMINATE_AOE, 1.0);
388 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
389 }
390
391 ctx.apply_aura_n(
392 AURA_EXTERMINATE.raw(),
393 wowlab_types::numeric::f64_to_i32_saturating_trunc(
394 ctx.effect_base_points(EFFECT::EXTERMINATE_STACKS),
395 ),
396 );
397
398 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
399 ctx.apply_aura(AURA_FROST_FEVER.raw());
400 ctx.apply_aura(AURA_RUNE_MASTERY.raw());
401}
402
403pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
404 let hand = event.hand;
405 let is_crit = event.is_crit;
406
407 if hand == SwingHand::OffHand {
408 return;
409 }
410
411 if is_crit {
412 ctx.apply_aura(AURA_KILLING_MACHINE.raw());
413 ctx.deal_effect_damage_ap_with_geometry(
414 EFFECT::ICY_DEATH_TORRENT_DAMAGE,
415 EFFECT::ICY_DEATH_TORRENT_DAMAGE,
416 1.0,
417 DamageFlags::empty(),
418 );
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use googletest::prelude::*;
425 use wowlab_engine_combat::AuraOps as _;
426 use wowlab_engine_combat::{BuffEffect, BuiltCombatSystem, HookCtx, LocalAuraIdx};
427 use wowlab_engine_domain::dbc::{AuraSubtypeKind, SpellEffectKind};
428 use wowlab_engine_ports::{CombatStats, TalentSelection};
429 use wowlab_engine_telemetry::{TelemetrySink};
430use wowlab_engine_gamedata::{AuraProps, ResolvedGameData, SpellProps};
431 use wowlab_types::sim::{SimTime, SpellIdx};
432
433 use super::trigger_frozen_dominion;
434
435 const FROZEN_DOMINION: u32 = 377_226;
436 const FROZEN_DOMINION_MASTERY: u32 = 377_253;
437 const FROZEN_DOMINION_MASTERY_PER_STACK: f64 = 4.0;
438 const REMORSELESS_WINTER: u32 = 1_233_152;
439 const REMORSELESS_WINTER_LOCAL: LocalAuraIdx = LocalAuraIdx::new(0);
440 const MASTERY_LOCAL: LocalAuraIdx = LocalAuraIdx::new(1);
441
442 fn activation_fixture(selected: bool) -> BuiltCombatSystem {
443 let talents = selected.then_some(TalentSelection {
444 spell_id: FROZEN_DOMINION,
445 override_spell_id: 0,
446 replaces_spell_id: 0,
447 ranks: 1,
448 precombat_aura: false,
449 hero_tree: None,
450 effect_overrides: Vec::new(),
451 });
452
453 crate::test_combat_builder(CombatStats::default())
454 .aura("remorseless_winter", REMORSELESS_WINTER, |a| {
455 Ok(a.on_player().duration_ms(12_000))
456 })
457 .aura("frozen_dominion_mastery", FROZEN_DOMINION_MASTERY, |a| {
458 Ok(a.on_player().duration_ms(15_000).max_stacks(5))
459 })
460 .talent_spell_ids(&[("Frozen Dominion", FROZEN_DOMINION)])
461 .talent_selections(talents.as_slice())
462 .build(crate::test_support::wait_rotation())
463 .expect("build Frozen Dominion activation fixture")
464 }
465
466 #[gtest]
467 fn frozen_dominion_activation_requires_selected_talent() -> Result<()> {
468 let mut built = activation_fixture(false);
469 let mut telemetry = TelemetrySink::default();
470 let mut rng = || 0.5;
471 let ctx = &mut HookCtx::new(
472 wowlab_engine_combat::HookCtxServices {
473 state: &mut built.state,
474 buf: &mut built.buffer,
475 sink: &mut telemetry,
476 rng: &mut rng,
477 },
478 wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
479 );
480
481 trigger_frozen_dominion(
482 ctx,
483 FROZEN_DOMINION,
484 REMORSELESS_WINTER_LOCAL.raw(),
485 MASTERY_LOCAL.raw(),
486 );
487
488 verify_that!(ctx.aura_stacks(REMORSELESS_WINTER_LOCAL.raw()), eq(0))?;
489
490 verify_that!(ctx.aura_stacks(MASTERY_LOCAL.raw()), eq(0))
491 }
492
493 #[gtest]
494 fn selected_frozen_dominion_activates_winter_and_one_mastery_stack() -> Result<()> {
495 let mut built = activation_fixture(true);
496 let mut telemetry = TelemetrySink::default();
497 let mut rng = || 0.5;
498 let ctx = &mut HookCtx::new(
499 wowlab_engine_combat::HookCtxServices {
500 state: &mut built.state,
501 buf: &mut built.buffer,
502 sink: &mut telemetry,
503 rng: &mut rng,
504 },
505 wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
506 );
507
508 trigger_frozen_dominion(
509 ctx,
510 FROZEN_DOMINION,
511 REMORSELESS_WINTER_LOCAL.raw(),
512 MASTERY_LOCAL.raw(),
513 );
514
515 verify_that!(ctx.aura_stacks(REMORSELESS_WINTER_LOCAL.raw()), eq(1))?;
516 verify_that!(ctx.aura_stacks(MASTERY_LOCAL.raw()), eq(1))?;
517 verify_that!(
518 ctx.aura_remaining_ms(REMORSELESS_WINTER_LOCAL.raw()),
519 some(eq(12_000))
520 )?;
521
522 verify_that!(ctx.aura_remaining_ms(MASTERY_LOCAL.raw()), some(eq(15_000)))
523 }
524
525 #[gtest]
526 fn frozen_dominion_mastery_definition_is_lowered_from_dbc() -> Result<()> {
527 let spell = SpellIdx::from_raw(FROZEN_DOMINION_MASTERY);
528 let mut data = ResolvedGameData::builder();
529
530 data.insert_spell_props(spell, SpellProps::default());
531 data.insert_aura_props(
532 spell,
533 AuraProps {
534 duration_ms: 15_000,
535 doses: 1,
536 max_stacks: 5,
537 tick_period_ms: 0,
538 pandemic: false,
539 },
540 );
541 data.insert_effect_type(
542 wowlab_types::sim::EffectRef::new(spell, 1),
543 SpellEffectKind::ApplyAura as i32,
544 );
545 data.insert_effect_aura(
546 wowlab_types::sim::EffectRef::new(spell, 1),
547 AuraSubtypeKind::ModMasteryPercent as i32,
548 );
549 data.insert_base_points(spell, 1, FROZEN_DOMINION_MASTERY_PER_STACK);
550 let data = data.build();
551 let built = crate::test_combat_builder(CombatStats::default())
552 .game_data(data.clone())
553 .aura("frozen_dominion_mastery", FROZEN_DOMINION_MASTERY, |a| {
554 Ok(a.on_player()
555 .apply_base_from_data(&data, FROZEN_DOMINION_MASTERY)?
556 .max_stacks(data.require_aura_max_stacks(spell)?))
557 })
558 .build(crate::test_support::wait_rotation())
559 .expect("build DBC-backed Frozen Dominion mastery fixture");
560 let local = built.state.aura_local(FROZEN_DOMINION_MASTERY).or_fail()?;
561 let aura = built.aura(local).or_fail()?;
562
563 verify_that!(aura.base_duration_ms, eq(15_000))?;
564 verify_that!(aura.max_stacks, eq(5))?;
565
566 verify_true!(aura.effects.iter().flatten().any(|effect| {
567 matches!(
568 effect,
569 BuffEffect::Mastery(value)
570 if (*value - FROZEN_DOMINION_MASTERY_PER_STACK).abs() <= f64::EPSILON
571 )
572 }))
573 }
574}
575
576#[cfg(test)]
577#[path = "hooks/bonegrinder_tests.rs"]
578mod bonegrinder_tests;
579
580#[cfg(test)]
581#[path = "hooks/frostreaper_tests.rs"]
582mod frostreaper_tests;
583
584#[cfg(test)]
585#[path = "hooks/icy_talons_tests.rs"]
586mod icy_talons_tests;