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
19const RP_DISPLAY_DIV: f64 = 10.0;
21
22const FROST_STRIKE_RP_COST: f64 = 35.0;
24
25const BOS_RP_COST: f64 = 60.0;
27
28const RUNIC_EMPOWERMENT_RP_FACTOR: f64 = 0.001;
30
31const HYPERPYREXIA_PROC_CHANCE: f64 = 0.35;
33
34const FROSTBOUND_WILL_ERW_MS: u32 = 6_000;
36
37const BONEGRINDER_CRIT_MAX: i32 = 5;
39
40const ERW_RECHARGE_MS: u32 = 24_000;
41
42#[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
63fn 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
87fn 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 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
263pub(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
302pub(crate) fn pillar_of_frost_hook(ctx: &mut HookCtx<'_>) {
304 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
374pub(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 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;