Skip to main content

wowlab_engine_content/hooks/frost_death_knight/
hooks.rs

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_rng::proc_chance;
8use wowlab_types::combat::DamageSchool;
9
10use crate::generated::specs::frost_death_knight::{
11    AURA_BONEGRINDER_CRIT, AURA_BONEGRINDER_FROST, AURA_BRITTLE, AURA_CHOSEN_OF_FROSTBROOD_HASTE,
12    AURA_DARK_TALONS_ICY_TALONS, AURA_EMPOWER_RUNE_WEAPON, AURA_FREEZING_TEMPEST,
13    AURA_EXTERMINATE, AURA_FROST_FEVER, AURA_FROSTREAPER, AURA_FROZEN_DOMINION_MASTERY,
14    AURA_ICY_ONSLAUGHT, AURA_ICY_TALONS, AURA_KILLING_MACHINE, AURA_KILLING_STREAK,
15    AURA_PILLAR_OF_FROST, AURA_RAZORICE, AURA_REMORSELESS_WINTER, AURA_RIME, AURA_RUNE_MASTERY,
16    EFFECT, SPELL_EMPOWER_RUNE_WEAPON,
17};
18
19/// Runic-power display divisor (power type 6 `display_modifier`).
20const RP_DISPLAY_DIV: f64 = 10.0;
21
22/// Frost Strike RP cost (49143 `power_costs` 350 / 10).
23const FROST_STRIKE_RP_COST: f64 = 35.0;
24
25/// Breath of Sindragosa RP cost (1249658 `power_costs` 600 / 10).
26const BOS_RP_COST: f64 = 60.0;
27
28/// Runic Empowerment chance uses spell 81229 effect 1 per RP spent.
29const RUNIC_EMPOWERMENT_RP_FACTOR: f64 = 0.001;
30
31/// Hyperpyrexia proc chance (spell 456238 proc chance 35%).
32const HYPERPYREXIA_PROC_CHANCE: f64 = 0.35;
33
34/// Frostbound Will recharge reduction (spell 1238680 effect 1).
35const FROSTBOUND_WILL_ERW_MS: u32 = 6_000;
36
37/// Bonegrinder conversion threshold (spell 377101 max stacks minus one).
38const BONEGRINDER_CRIT_MAX: i32 = 5;
39
40const ERW_RECHARGE_MS: u32 = 24_000;
41
42/// Applies Frozen Heart mastery, including Frostreaper's Obliterate conversion.
43#[must_use]
44pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
45    if ctx.school == DamageSchool::Frost || ctx.spell_id == EFFECT::OBLITERATE_PHYS.0 {
46        ctx.bonus(1)
47    } else {
48        1.0
49    }
50}
51
52fn static_frost_mult(ctx: &HookCtx<'_>) -> f64 {
53    (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::MOTFW_ALL_PCT))
54        * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROST_DK_SPEC_PCT))
55}
56
57fn deal_effect(ctx: &mut HookCtx<'_>, effect: wowlab_engine_combat::DamageEffectId, mult: f64) {
58    let coef = ctx.effect_ap_coefficient(effect);
59
60    ctx.deal_damage_ap(effect.action(), coef * mult, DamageFlags::empty());
61}
62
63/// An effect's AP coefficient scaled by its spell's static passive fold.
64fn effect_folded_coef(ctx: &HookCtx<'_>, effect: wowlab_engine_combat::DamageEffectId) -> f64 {
65    ctx.effect_ap_coefficient(effect)
66        * ctx.action_passive_direct_damage_mult(effect.action())
67}
68
69fn roll_hyperpyrexia(ctx: &mut HookCtx<'_>, trigger_coef: f64) {
70    if !proc_chance(ctx.rng(), HYPERPYREXIA_PROC_CHANCE) {
71        return;
72    }
73
74    let coef = trigger_coef * ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::HYPERPYREXIA_PORTION_PCT);
75
76    ctx.deal_damage_ap(crate::generated::specs::frost_death_knight::DAMAGE_REPORTED::HYPERPYREXIA, coef, DamageFlags::empty());
77}
78
79fn roll_dark_talons(ctx: &mut HookCtx<'_>) {
80    let chance = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::DARK_TALONS_CHANCE);
81
82    if proc_chance(ctx.rng(), chance) {
83        ctx.apply_aura(AURA_DARK_TALONS_ICY_TALONS);
84    }
85}
86
87/// An RE proc expires Icy Onslaught and suppresses the caster's IO stack gain.
88fn runic_empowerment(ctx: &mut HookCtx<'_>, rp_spent: f64) -> bool {
89    let chance = ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::RUNIC_EMPOWERMENT_PCT)
90        * RUNIC_EMPOWERMENT_RP_FACTOR
91        * rp_spent;
92
93    if !proc_chance(ctx.rng(), chance) {
94        return false;
95    }
96
97    ctx.gain_resource(1.0);
98    ctx.expire_aura(AURA_ICY_ONSLAUGHT);
99
100    let chance = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FRIGID_EXECUTIONER_PCT);
101
102    if proc_chance(ctx.rng(), chance) {
103        ctx.gain_resource(1.0);
104    }
105
106    true
107}
108
109fn obliteration(ctx: &mut HookCtx<'_>) {
110    if !ctx.is_aura_active(AURA_PILLAR_OF_FROST) {
111        return;
112    }
113
114    ctx.apply_aura(AURA_KILLING_MACHINE);
115
116    let chance = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::OBLITERATION_RUNE_PCT);
117
118    if proc_chance(ctx.rng(), chance) {
119        ctx.gain_resource(1.0);
120    }
121}
122
123fn trigger_frostreaper(
124    ctx: &mut HookCtx<'_>,
125    driver: wowlab_engine_combat::SystemRppmBinding,
126    aura_local: wowlab_engine_combat::LocalAuraIdx,
127) -> bool {
128    if !ctx.roll_system_rppm(driver) {
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    // Both hits go out under their child ids: the generic static fold already
141    // carries MotFW/Frostreaper/spec passives, and Killing Machine's e1/e3
142    // affect lists name the children (325461/1264084), never the parent.
143    deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::OBLITERATE_PHYS, 1.0);
144    deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::OBLITERATE_FROST, 1.0);
145
146    if trigger_frostreaper(
147        ctx,
148        wowlab_engine_combat::generated_identities::system_rppm::FROSTREAPER,
149        AURA_FROSTREAPER,
150    ) {
151        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROSTREAPER_DAMAGE, static_frost_mult(ctx));
152    }
153
154    ctx.consume_aura(AURA_EMPOWER_RUNE_WEAPON);
155
156    if ctx.consume_aura_stack(AURA_EXTERMINATE) {
157        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::EXTERMINATE_HIT, 1.0);
158        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::EXTERMINATE_AOE, 1.0);
159        ctx.apply_aura(AURA_KILLING_MACHINE);
160    }
161
162    let km_stacks = ctx.aura_stacks(AURA_KILLING_MACHINE);
163
164    if km_stacks > 0 {
165        ctx.consume_aura(AURA_KILLING_MACHINE);
166        ctx.apply_aura_n(AURA_KILLING_STREAK, km_stacks);
167        roll_dark_talons(ctx);
168        let aa_mult = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::ARCTIC_ASSAULT_PCT)
169            * f64::from(km_stacks)
170            * static_frost_mult(ctx);
171
172        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::GLACIAL_ADVANCE_DAMAGE, aa_mult);
173        ctx.apply_aura(AURA_RAZORICE);
174
175        let chance = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::MURDEROUS_EFFICIENCY_PCT);
176
177        if proc_chance(ctx.rng(), chance) {
178            ctx.gain_resource(1.0);
179        }
180
181        ctx.apply_aura(AURA_BONEGRINDER_CRIT);
182
183        if ctx.aura_stacks(AURA_BONEGRINDER_CRIT) >= BONEGRINDER_CRIT_MAX {
184            ctx.expire_aura(AURA_BONEGRINDER_CRIT);
185            ctx.apply_aura(AURA_BONEGRINDER_FROST);
186        }
187    }
188
189    let chance = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::RIME_PROC_PCT);
190
191    if proc_chance(ctx.rng(), chance) {
192        ctx.apply_aura(AURA_RIME);
193    }
194
195    ctx.apply_aura(AURA_RUNE_MASTERY);
196
197    roll_hyperpyrexia(ctx, effect_folded_coef(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::OBLITERATE_PHYS));
198    roll_hyperpyrexia(ctx, effect_folded_coef(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::OBLITERATE_FROST));
199}
200
201pub(crate) fn frost_strike_hook(ctx: &mut HookCtx<'_>) {
202    let fs_mult = static_frost_mult(ctx)
203        * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::RUNIC_OVERFLOW_FS_PCT))
204        * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::DEATHLY_BLOWS_FS_PCT))
205        * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::COF_SPELLS_PCT));
206    let fs_coef = ctx.effect_ap_coefficient(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROST_STRIKE_2H);
207
208    ctx.deal_damage_ap(crate::generated::specs::frost_death_knight::DAMAGE_SPELL::FROST_STRIKE, fs_coef * fs_mult, DamageFlags::empty());
209
210    if ctx.consume_aura(AURA_FROSTREAPER) {
211        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROSTREAPER_DAMAGE, static_frost_mult(ctx));
212    }
213
214    ctx.apply_aura(AURA_ICY_TALONS);
215
216    if !runic_empowerment(ctx, FROST_STRIKE_RP_COST) {
217        ctx.apply_aura(AURA_ICY_ONSLAUGHT);
218    }
219
220    obliteration(ctx);
221    roll_hyperpyrexia(ctx, fs_coef * fs_mult);
222}
223
224pub(crate) fn howling_blast_hook(ctx: &mut HookCtx<'_>) {
225    ctx.apply_aura(AURA_FROST_FEVER);
226
227    let mut hb_mult = static_frost_mult(ctx) * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::COF_SPELLS_PCT));
228
229    if ctx.consume_aura(AURA_RIME) {
230        roll_dark_talons(ctx);
231        let rime_pct = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::RIME_HB_BONUS_PCT)
232            + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::NORTHWINDS_RIME_PCT);
233
234        hb_mult *= (1.0 + rime_pct)
235            * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::ICEBREAKER_PCT))
236            * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::BIND_IN_DARKNESS_HB_PCT));
237
238        let km_chance = ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::HOWLING_BLADES_KM_PCT);
239
240        for blade in [
241            crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::HOWLING_BLADES_FIRST,
242            crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::HOWLING_BLADES_SECOND,
243        ] {
244            deal_effect(ctx, blade, 1.0);
245
246            if proc_chance(ctx.rng(), km_chance) {
247                ctx.apply_aura(AURA_KILLING_MACHINE);
248            }
249        }
250
251        let rfc_rp = ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::RAGE_OF_FROZEN_CHAMPION_RP) / RP_DISPLAY_DIV;
252
253        ctx.gain_secondary_resource(rfc_rp);
254        ctx.reduce_cooldown(SPELL_EMPOWER_RUNE_WEAPON, FROSTBOUND_WILL_ERW_MS);
255    } else {
256        ctx.apply_aura(AURA_RUNE_MASTERY);
257    }
258
259    deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::HOWLING_BLAST_DAMAGE, hb_mult);
260    obliteration(ctx);
261}
262
263/// Brittle rolls spell 374504's proc chance on every disease tick.
264pub(crate) fn frost_fever_tick_hook(ctx: &mut HookCtx<'_>) {
265    let chance = ctx
266        .spec_config::<super::config::FrostDkConfig>()
267        .map_or(0.0, |config| config.brittle_proc_chance);
268
269    if proc_chance(ctx.rng(), chance) {
270        ctx.apply_aura(AURA_BRITTLE);
271    }
272}
273
274pub(crate) fn remorseless_winter_hook(ctx: &mut HookCtx<'_>) {
275    if mid2_2pc(ctx) {
276        ctx.expire_aura(AURA_FREEZING_TEMPEST);
277    }
278
279    ctx.apply_aura(AURA_REMORSELESS_WINTER);
280}
281
282pub(crate) fn remorseless_winter_tick_hook(ctx: &mut HookCtx<'_>) {
283    if mid2_2pc(ctx) {
284        ctx.apply_aura(AURA_FREEZING_TEMPEST);
285    }
286}
287
288fn trigger_frozen_dominion(
289    ctx: &mut HookCtx<'_>,
290    talent: wowlab_engine_combat::DamageActionId,
291    remorseless_winter_local: wowlab_engine_combat::LocalAuraIdx,
292    mastery_local: wowlab_engine_combat::LocalAuraIdx,
293) {
294    if !ctx.talent_selected(talent) {
295        return;
296    }
297
298    ctx.apply_aura(remorseless_winter_local);
299    ctx.apply_aura(mastery_local);
300}
301
302/// Applies Frozen Dominion's Remorseless Winter.
303pub(crate) fn pillar_of_frost_hook(ctx: &mut HookCtx<'_>) {
304    // This activation grants one 377253 stack per distinct enemy hit. The
305    // engine currently resolves a single target, and the
306    // periodic aura ticks immediately on application.
307    trigger_frozen_dominion(
308        ctx,
309        crate::generated::specs::frost_death_knight::DAMAGE_TALENT::FROZEN_DOMINION,
310        AURA_REMORSELESS_WINTER,
311        AURA_FROZEN_DOMINION_MASTERY,
312    );
313
314}
315
316pub(crate) fn empower_rune_weapon_hook(ctx: &mut HookCtx<'_>) {
317    let rp = ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::ERW_RP) / RP_DISPLAY_DIV;
318
319    ctx.gain_secondary_resource(rp);
320    ctx.apply_aura(AURA_EMPOWER_RUNE_WEAPON);
321    ctx.apply_aura(AURA_KILLING_MACHINE);
322
323}
324
325fn mid2_2pc(ctx: &HookCtx<'_>) -> bool {
326    ctx.program()
327        .spec_config::<super::config::FrostDkConfig>()
328        .is_some_and(|config| config.mid2_2pc)
329}
330
331pub(crate) fn breath_of_sindragosa_hook(ctx: &mut HookCtx<'_>) {
332    breath_of_sindragosa_damage(ctx);
333    ctx.apply_aura(AURA_ICY_TALONS);
334    runic_empowerment(ctx, BOS_RP_COST);
335    ctx.reduce_cooldown(SPELL_EMPOWER_RUNE_WEAPON, ERW_RECHARGE_MS);
336}
337
338pub(crate) fn breath_of_sindragosa_tick_hook(ctx: &mut HookCtx<'_>) {
339    breath_of_sindragosa_damage(ctx);
340}
341
342pub(crate) fn breath_of_sindragosa_expire_hook(ctx: &mut HookCtx<'_>) {
343    ctx.gain_resource(ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::BOS_RUNE_REFUND));
344}
345
346fn breath_of_sindragosa_damage(ctx: &mut HookCtx<'_>) {
347    ctx.deal_effect_damage_ap_with_geometry(
348        crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::BOS_DAMAGE,
349        crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::BOS_DAMAGE,
350        1.0,
351        DamageFlags::empty(),
352    );
353}
354
355pub(crate) fn frostwyrms_fury_hook(ctx: &mut HookCtx<'_>) {
356    let multiplier = static_frost_mult(ctx) * (1.0 + ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::COF_FWF_PCT));
357
358    ctx.deal_effect_damage_ap_with_geometry(
359        crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROSTWYRMS_FURY_DAMAGE,
360        crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROSTWYRMS_FURY_DAMAGE,
361        multiplier,
362        DamageFlags::empty(),
363    );
364    let coef = ctx.effect_ap_coefficient(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::FROSTWYRMS_FURY_DAMAGE) * multiplier;
365
366    ctx.deal_damage_ap(
367        crate::generated::specs::frost_death_knight::DAMAGE_REPORTED::CHOSEN_OF_FROSTBROOD_RECALL,
368        coef,
369        DamageFlags::empty(),
370    );
371    ctx.apply_aura(AURA_CHOSEN_OF_FROSTBROOD_HASTE);
372}
373
374/// Collapses Reaper's Mark's single-target package to cast time.
375pub(crate) fn reapers_mark_hook(ctx: &mut HookCtx<'_>) {
376    deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::WAVE_OF_SOULS_DAMAGE, 1.0);
377    deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::WAVE_OF_SOULS_DAMAGE, 1.0);
378
379    let hp_pct = ctx.target_health_fraction();
380    let grim_reaper = 1.0 + (1.0 - hp_pct) * ctx.effect_percent(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::GRIM_REAPER_MAX_PCT);
381    let stacks = ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::REAPERS_MARK_STACKS);
382
383    deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::REAPERS_MARK_EXPLOSION, stacks * grim_reaper);
384
385    // Echoing Fury's cast-time charge is consumed before the delayed explosion.
386
387    for _ in 0..wowlab_types::numeric::f64_to_i64_saturating_trunc(
388        ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::ECHOING_FURY_STACKS),
389    ) {
390        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::EXTERMINATE_HIT, 1.0);
391        deal_effect(ctx, crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::EXTERMINATE_AOE, 1.0);
392        ctx.apply_aura(AURA_KILLING_MACHINE);
393    }
394
395    ctx.apply_aura_n(
396        AURA_EXTERMINATE,
397        wowlab_types::numeric::f64_to_i32_saturating_trunc(
398            ctx.effect_base_points(crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::EXTERMINATE_STACKS),
399        ),
400    );
401
402    ctx.apply_aura(AURA_KILLING_MACHINE);
403    ctx.apply_aura(AURA_FROST_FEVER);
404    ctx.apply_aura(AURA_RUNE_MASTERY);
405}
406
407pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
408    let hand = event.hand;
409    let is_crit = event.is_crit;
410
411    if hand == SwingHand::OffHand {
412        return;
413    }
414
415    if is_crit {
416        ctx.apply_aura(AURA_KILLING_MACHINE);
417        ctx.deal_effect_damage_ap_with_geometry(
418            crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::ICY_DEATH_TORRENT_DAMAGE,
419            crate::generated::specs::frost_death_knight::DAMAGE_EFFECT::ICY_DEATH_TORRENT_DAMAGE,
420            1.0,
421            DamageFlags::empty(),
422        );
423    }
424}
425
426#[cfg(test)]
427mod tests {
428    use googletest::prelude::*;
429    use wowlab_engine_combat::AuraOps as _;
430    use wowlab_engine_combat::{BuffEffect, HookCtx};
431    use wowlab_engine_domain::dbc::{AuraSubtypeKind, SpellEffectKind};
432    use wowlab_engine_ports::{CombatStats, TalentSelection};
433    use wowlab_engine_telemetry::{TelemetrySink};
434use wowlab_engine_gamedata::{AuraProps, ResolvedGameData, SpellProps};
435    use wowlab_types::sim::{SimTime, SpellIdx};
436
437    use super::trigger_frozen_dominion;
438    use crate::generated::specs::frost_death_knight::DAMAGE_TALENT;
439
440    const FROZEN_DOMINION: u32 = 377_226;
441    const FROZEN_DOMINION_MASTERY: u32 = 377_253;
442    const FROZEN_DOMINION_MASTERY_PER_STACK: f64 = 4.0;
443    const REMORSELESS_WINTER: u32 = 1_233_152;
444    const REMORSELESS_WINTER_LOCAL: wowlab_engine_combat::LocalAuraIdx =
445        wowlab_engine_combat::test_support::local_aura(0);
446    const MASTERY_LOCAL: wowlab_engine_combat::LocalAuraIdx =
447        wowlab_engine_combat::test_support::local_aura(1);
448
449    fn activation_fixture(selected: bool) -> wowlab_engine_combat::BuiltCombatSystem {
450        let talents = selected.then_some(TalentSelection {
451            spell_id: FROZEN_DOMINION,
452            override_spell_id: 0,
453            replaces_spell_id: 0,
454            ranks: 1,
455            precombat_aura: false,
456            hero_tree: None,
457            effect_overrides: Vec::new(),
458        });
459
460        crate::test_combat_builder_for(wowlab_engine_combat::generated_identities::specs::frost_death_knight::PROOF, CombatStats::default())
461            .aura("remorseless_winter", REMORSELESS_WINTER, |a| {
462                Ok(a.on_player().duration_ms(12_000))
463            })
464            .aura("frozen_dominion_mastery", FROZEN_DOMINION_MASTERY, |a| {
465                Ok(a.on_player().duration_ms(15_000).max_stacks(5))
466            })
467            .effect_identities(wowlab_engine_combat::generated_identities::specs::frost_death_knight::damage::ALL)
468            .talent_spell_ids(&[("Frozen Dominion", FROZEN_DOMINION)])
469            .talent_selections(talents.as_slice())
470            .into_draft().expect("valid combat draft").finalize(&crate::test_support::wait_rotation())
471            .expect("build Frozen Dominion activation fixture")
472    }
473
474    #[gtest]
475    fn frozen_dominion_activation_requires_selected_talent() -> Result<()> {
476        let mut built = activation_fixture(false);
477        let mut telemetry = TelemetrySink::default();
478        let mut rng = || 0.5;
479        let ctx = &mut HookCtx::new(
480            wowlab_engine_combat::HookCtxServices {
481                program: &built.program,
482                instance: &mut built.instance,
483                sink: &mut telemetry,
484                rng: &mut rng,
485            },
486            wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
487        );
488
489        trigger_frozen_dominion(
490            ctx,
491            DAMAGE_TALENT::FROZEN_DOMINION,
492            REMORSELESS_WINTER_LOCAL,
493            MASTERY_LOCAL,
494        );
495
496        verify_that!(ctx.aura_stacks(REMORSELESS_WINTER_LOCAL), eq(0))?;
497
498        verify_that!(ctx.aura_stacks(MASTERY_LOCAL), eq(0))
499    }
500
501    #[gtest]
502    fn selected_frozen_dominion_activates_winter_and_one_mastery_stack() -> Result<()> {
503        let mut built = activation_fixture(true);
504        let mut telemetry = TelemetrySink::default();
505        let mut rng = || 0.5;
506        let ctx = &mut HookCtx::new(
507            wowlab_engine_combat::HookCtxServices {
508                program: &built.program,
509                instance: &mut built.instance,
510                sink: &mut telemetry,
511                rng: &mut rng,
512            },
513            wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
514        );
515
516        trigger_frozen_dominion(
517            ctx,
518            DAMAGE_TALENT::FROZEN_DOMINION,
519            REMORSELESS_WINTER_LOCAL,
520            MASTERY_LOCAL,
521        );
522
523        verify_that!(ctx.aura_stacks(REMORSELESS_WINTER_LOCAL), eq(1))?;
524        verify_that!(ctx.aura_stacks(MASTERY_LOCAL), eq(1))?;
525        verify_that!(
526            ctx.aura_remaining_ms(REMORSELESS_WINTER_LOCAL),
527            some(eq(12_000))
528        )?;
529
530        verify_that!(ctx.aura_remaining_ms(MASTERY_LOCAL), some(eq(15_000)))
531    }
532
533    #[gtest]
534    fn frozen_dominion_mastery_definition_is_lowered_from_dbc() -> Result<()> {
535        let spell = SpellIdx::from_raw(FROZEN_DOMINION_MASTERY);
536        let mut data = ResolvedGameData::builder();
537
538        data.insert_spell_props(spell, SpellProps::default());
539        data.insert_aura_props(
540            spell,
541            AuraProps {
542                duration_ms: 15_000,
543                doses: 1,
544                max_stacks: 5,
545                tick_period_ms: 0,
546                pandemic: false,
547            },
548        );
549        data.insert_effect_type(
550            wowlab_types::sim::EffectRef::new(spell, 1),
551            SpellEffectKind::ApplyAura as i32,
552        );
553        data.insert_effect_aura(
554            wowlab_types::sim::EffectRef::new(spell, 1),
555            AuraSubtypeKind::ModMasteryPercent as i32,
556        );
557        data.insert_base_points(spell, 1, FROZEN_DOMINION_MASTERY_PER_STACK);
558        let data = data.build();
559        let built = crate::test_combat_builder_for(wowlab_engine_combat::generated_identities::specs::frost_death_knight::PROOF, CombatStats::default())
560            .game_data(data.clone())
561            .aura("frozen_dominion_mastery", FROZEN_DOMINION_MASTERY, |a| {
562                Ok(a.on_player()
563                    .apply_base_from_data(&data, FROZEN_DOMINION_MASTERY)?
564                    .max_stacks(data.require_aura_max_stacks(spell)?))
565            })
566            .into_draft().expect("valid combat draft").finalize(&crate::test_support::wait_rotation())
567            .expect("build DBC-backed Frozen Dominion mastery fixture");
568        let local = built.program.diagnostic_aura_local(FROZEN_DOMINION_MASTERY).or_fail()?;
569        let aura = built.program.aura(local);
570
571        verify_that!(aura.base_duration_ms, eq(15_000))?;
572        verify_that!(aura.max_stacks, eq(5))?;
573
574        verify_true!(aura.effects.iter().flatten().any(|effect| {
575            matches!(
576                effect,
577                BuffEffect::Mastery(value)
578                    if (*value - FROZEN_DOMINION_MASTERY_PER_STACK).abs() <= f64::EPSILON
579            )
580        }))
581    }
582}
583
584#[cfg(test)]
585#[path = "hooks/bonegrinder_tests.rs"]
586mod bonegrinder_tests;
587
588#[cfg(test)]
589#[path = "hooks/frostreaper_tests.rs"]
590mod frostreaper_tests;
591
592#[cfg(test)]
593#[path = "hooks/icy_talons_tests.rs"]
594mod icy_talons_tests;