Skip to main content

wowlab_engine_content/hooks/unholy_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::SchedulingOps as _;
5use wowlab_engine_combat::GuardianOps as _;
6use wowlab_engine_combat::AuraOps as _;
7use wowlab_engine_combat::{
8    DamageFlags, GuardianAbility, GuardianEvent, GuardianSpec, HookCtx, ImpactEvent, MasteryCtx,
9    PetAbility, PetActionBar, PetResourceModel, SwingEvent, SwingHand,
10};
11use wowlab_engine_rng::proc_chance;
12use wowlab_types::{combat::DamageSchool, constants::HUNDRED, data::SpellLabel};
13
14use super::{
15    config::{UnholyConfig, UnholyRuntime},
16    riders::{
17        extend_riders_from_rp, maybe_summon_random_rider, summon_apocalypse_riders,
18        trigger_icy_fury, trigger_let_terror_reign, trigger_whitemane_famine,
19    },
20};
21use crate::generated::specs::unholy_death_knight::{
22    AURA_BRITTLE, AURA_COMMANDER_OF_THE_DEAD, AURA_DARK_TRANSFORMATION, AURA_DREAD_PLAGUE,
23    AURA_FESTERING_SCYTHE, AURA_FESTERING_SCYTHE_TT, AURA_FORBIDDEN_KNOWLEDGE,
24    AURA_GARGOYLE_ACTIVE, AURA_GARGOYLE_EMPOWERMENT, AURA_ICY_TALONS, AURA_INFECTED_CLAWS,
25    AURA_LESSER_GHOUL, AURA_LESSER_GHOUL_COUNTER, AURA_REAPING, AURA_RUNE_MASTERY,
26    AURA_RUNIC_CORRUPTION, AURA_SUDDEN_DOOM, AURA_UNHOLY_DEVOTION, AURA_VIRULENT_PLAGUE, EFFECT,
27    HERO, SPELL_PUTREFY,
28};
29
30/// Death Coil RP cost (spell 47541 power cost 300 divided by 10).
31const DEATH_COIL_RP_COST: i32 = 30;
32const RC_PER_RP_FACTOR: f64 = 0.0001;
33
34/// Permanent-ghoul attack power is 0.474 times owner attack power.
35const GHOUL_AP: f64 = 0.474;
36const LESSER_GHOUL_NPC_ID: u32 = 237_409;
37
38/// Lesser-ghoul attack power is 0.192975 times owner attack power.
39const LESSER_GHOUL_AP: f64 = 0.192_975;
40const PET_SWING_MS: u32 = 2_000;
41const LESSER_GHOUL_STEP_MS: u32 = 500;
42const LESSER_GHOUL_DURATION_MS: u32 = 8_000;
43const ARMY_GHOUL_DURATION_MS: u32 = 30_000;
44const LESSER_GHOUL_TAG: u32 = 275_430;
45const ARMY_GHOUL_TAG: u32 = 1_294_026;
46const MIDNIGHT_LESSER_GHOUL_AP_MULT: f64 = 1.10;
47
48const CLAW_BASE_MULT: f64 = 0.85;
49pub(super) const PERMANENT_GHOUL_NPC_ID: u32 = 26_125;
50const PERMANENT_GHOUL_ENERGY_REGEN: f64 = 10.0;
51const PERMANENT_GHOUL_ABILITY_COST: f64 = 40.0;
52const PERMANENT_GHOUL_GCD_MS: u32 = 1_500;
53const SWEEPING_CLAWS_GCD_MS: u32 = 1_000;
54const LESSER_GHOUL_CLAW_STEP_INTERVAL: u16 = 3;
55const LESSER_GHOUL_SWING_STEP_INTERVAL: u16 = 4;
56const LESSER_GHOUL_ENERGY_PER_STEP: f64 = 5.0;
57const LESSER_GHOUL_ABILITY_COST: f64 = 40.0;
58
59/// Army of the Dead summons eight ghouls at a 1.75 attack-power multiplier.
60const ARMY_GHOULS: usize = 8;
61const ARMY_GHOUL_AP_MULT: f64 = 1.75 * 0.8;
62
63const GARGOYLE_NPC_ID: u32 = 27_829;
64const GARGOYLE_INITIAL_STUN_MS: u32 = 2_800;
65
66/// Magus package averages 20 Shadow Bolts and nine Frostbolts per summon.
67const MAGUS_SHADOW_BOLTS: f64 = 20.0;
68const MAGUS_FROSTBOLTS: f64 = 9.0;
69
70/// Infected Claws: 15% per ghoul melee to apply the bleed (driver 1241792).
71const INFECTED_CLAWS_CHANCE: f64 = 0.15;
72
73#[must_use]
74pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
75    if ctx.is_pet
76        || (ctx.has_school(DamageSchool::Shadow) && ctx.spell_has_label(SpellLabel::CLASS_SPELLS.0))
77    {
78        ctx.bonus(1)
79    } else {
80        1.0
81    }
82}
83
84const fn compounded_crit_damage_bonus(dreadblade_pct: f64, grave_mastery_pct: f64) -> f64 {
85    ((1.0 + dreadblade_pct / HUNDRED) * (1.0 + grave_mastery_pct / HUNDRED) - 1.0) * HUNDRED
86}
87
88#[must_use]
89pub(crate) fn crit_damage(ctx: &MasteryCtx<'_>) -> f64 {
90    if !ctx.is_pet {
91        return 0.0;
92    }
93
94    let dreadblade_pct = ctx.points(EFFECT::DREADBLADE_PET_CRIT.1);
95    let grave_mastery_pct = ctx.program
96        .spec_config::<UnholyConfig>()
97        .map_or(0.0, |config| config.grave_mastery_pct);
98
99    compounded_crit_damage_bonus(dreadblade_pct, grave_mastery_pct)
100}
101
102fn pet_package_mult(ctx: &HookCtx<'_>) -> f64 {
103    if ctx.is_aura_active(AURA_COMMANDER_OF_THE_DEAD) {
104        1.0 + ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::COMMANDER_PCT)
105    } else {
106        1.0
107    }
108}
109
110const fn lesser_ghoul_ap_mult(tag: u32) -> f64 {
111    let summon_mult = if tag == ARMY_GHOUL_TAG {
112        ARMY_GHOUL_AP_MULT
113    } else {
114        1.0
115    };
116
117    LESSER_GHOUL_AP * MIDNIGHT_LESSER_GHOUL_AP_MULT * summon_mult
118}
119
120fn lesser_ghoul_claw_at(step: u16) -> bool {
121    if !step.is_multiple_of(LESSER_GHOUL_CLAW_STEP_INTERVAL) {
122        return false;
123    }
124
125    let mut energy = HUNDRED;
126    let mut claw = false;
127
128    for current in 0..=step {
129        if current > 0 {
130            energy = (energy + LESSER_GHOUL_ENERGY_PER_STEP).min(HUNDRED);
131        }
132
133        if current % LESSER_GHOUL_CLAW_STEP_INTERVAL == 0 && energy >= LESSER_GHOUL_ABILITY_COST {
134            energy -= LESSER_GHOUL_ABILITY_COST;
135            claw = current == step;
136        }
137    }
138
139    claw
140}
141
142fn roll_infected_claws(ctx: &mut HookCtx<'_>) {
143    if proc_chance(ctx.rng(), INFECTED_CLAWS_CHANCE) {
144        ctx.apply_owner_aura(AURA_INFECTED_CLAWS);
145    }
146}
147
148fn lesser_ghoul_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
149    let ap_mult = lesser_ghoul_ap_mult(event.tag) * pet_package_mult(ctx);
150
151    if event.action_index.is_multiple_of(LESSER_GHOUL_SWING_STEP_INTERVAL) {
152        ctx.deal_damage_ap(
153            crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::MELEE,
154            ap_mult * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS),
155            DamageFlags::PET,
156        );
157        roll_infected_claws(ctx);
158    }
159
160    if !lesser_ghoul_claw_at(event.action_index) {
161        return;
162    }
163
164    let transformed = ctx.is_aura_active(AURA_DARK_TRANSFORMATION);
165    let effect = if transformed {
166        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::LESSER_SWEEPING
167    } else {
168        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::LESSER_CLAW
169    };
170
171    ctx.deal_damage_ap(
172        if transformed {
173            crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::LESSER_SWEEPING_CLAWS
174        } else {
175            crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::LESSER_CLAW
176        },
177        ap_mult * ctx.effect_ap_coefficient(effect),
178        DamageFlags::PET,
179    );
180    roll_infected_claws(ctx);
181}
182
183fn lesser_ghoul_demise(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
184    ctx.consume_owner_aura_stack(AURA_LESSER_GHOUL_COUNTER);
185
186    if event.tag == ARMY_GHOUL_TAG
187        && let Some(runtime) = ctx.spec_runtime_mut::<UnholyRuntime>()
188    {
189        runtime.active_army_ghouls = runtime.active_army_ghouls.saturating_sub(1);
190    }
191
192    ctx.deal_damage_ap(
193        crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::RUPTURED_VISCERA,
194        lesser_ghoul_ap_mult(event.tag)
195            * pet_package_mult(ctx)
196            * ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::RUPTURED_VISCERA),
197        DamageFlags::PET,
198    );
199}
200
201fn summon_lesser_ghoul(ctx: &mut HookCtx<'_>, army: bool) {
202    let (tag, duration_ms) = if army {
203        (ARMY_GHOUL_TAG, ARMY_GHOUL_DURATION_MS)
204    } else {
205        (LESSER_GHOUL_TAG, LESSER_GHOUL_DURATION_MS)
206    };
207
208    ctx.add_aura_stack(AURA_LESSER_GHOUL_COUNTER);
209
210    if army && let Some(runtime) = ctx.spec_runtime_mut::<UnholyRuntime>() {
211        runtime.active_army_ghouls = runtime.active_army_ghouls.saturating_add(1);
212    }
213
214    let _ = ctx.summon_guardian(
215        GuardianSpec::new_inheriting_owner(tag, duration_ms)
216            .npc_id(LESSER_GHOUL_NPC_ID)
217            .stat_inheritance(
218                ctx.spec_config::<UnholyConfig>()
219                    .map_or(wowlab_engine_combat::PetStatInheritance::IDENTITY, |config| {
220                        config.lesser_ghoul_stat_inheritance
221                    }),
222            )
223            .ability(
224                GuardianAbility::new(LESSER_GHOUL_STEP_MS, lesser_ghoul_action).hasted_actions(),
225            )
226            .on_demise(lesser_ghoul_demise),
227    );
228}
229
230fn gargoyle_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
231    if event.action_index == 0 {
232        return;
233    }
234
235    let coefficient = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::GARGOYLE_STRIKE) * pet_package_mult(ctx);
236
237    ctx.deal_damage_ap(
238        crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::GARGOYLE_STRIKE,
239        coefficient,
240        DamageFlags::PET,
241    );
242}
243
244fn summon_gargoyle(ctx: &mut HookCtx<'_>) {
245    let Some(config) = ctx
246        .spec_config::<UnholyConfig>()
247        .and_then(|config| config.gargoyle)
248    else {
249        return;
250    };
251
252    ctx.expire_aura(AURA_GARGOYLE_EMPOWERMENT);
253    ctx.apply_aura(AURA_GARGOYLE_ACTIVE);
254    let _ = ctx.summon_guardian(
255        GuardianSpec::new_inheriting_owner(
256            HERO::UNHOLY_PETS::SPELL::SUMMON_GARGOYLE,
257            config.duration,
258        )
259        .npc_id(GARGOYLE_NPC_ID)
260        .stat_inheritance(config.stat_inheritance)
261        .ability(
262            GuardianAbility::new(config.strike_cast, gargoyle_action)
263                .first_action_delay(GARGOYLE_INITIAL_STUN_MS)
264                .hasted_action_interval(),
265        ),
266    );
267}
268
269fn empower_gargoyle(ctx: &mut HookCtx<'_>, runic_power: i32) {
270    if !ctx.is_aura_active(AURA_GARGOYLE_ACTIVE) {
271        return;
272    }
273
274    for _ in 0..runic_power.max(0) {
275        ctx.add_aura_stack(AURA_GARGOYLE_EMPOWERMENT);
276    }
277}
278
279pub(super) fn sudden_doom_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
280    let chance = ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::SUDDEN_DOOM_PCT)
281        * (1.0 + ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::HARBINGER_SD_PCT))
282        * (1.0 + ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::EBON_FEVER_PCT));
283
284    if proc_chance(ctx.rng(), chance) {
285        ctx.apply_aura(AURA_SUDDEN_DOOM);
286    }
287}
288
289fn runic_corruption(ctx: &mut HookCtx<'_>, rp_spent: f64) {
290    let chance = ctx.effect_base_points(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::RC_CHANCE_PCT) * RC_PER_RP_FACTOR * rp_spent;
291
292    if proc_chance(ctx.rng(), chance) {
293        ctx.apply_aura(AURA_RUNIC_CORRUPTION);
294    }
295}
296
297pub(crate) fn festering_strike_hook(ctx: &mut HookCtx<'_>) {
298    let scythe = ctx.is_aura_active(AURA_FESTERING_SCYTHE)
299        && !ctx.is_aura_active(AURA_FESTERING_SCYTHE_TT);
300
301    if scythe {
302        ctx.consume_aura(AURA_FESTERING_SCYTHE);
303        ctx.apply_aura(AURA_FESTERING_SCYTHE_TT);
304        let coef = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::SCYTHE_DAMAGE);
305
306        ctx.deal_damage_ap(
307            crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::FESTERING_SCYTHE_STRIKE,
308            coef,
309            DamageFlags::PHYSICAL,
310        );
311    } else {
312        ctx.apply_aura(AURA_FESTERING_SCYTHE);
313    }
314
315    let min = ctx.effect_base_points(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::FS_GHOULS_MIN);
316    let max = ctx.effect_base_points(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::FS_GHOULS_MAX) + 1.0;
317    let span = (max - min).max(0.0);
318    let readies = wowlab_types::numeric::f64_to_i32_saturating_trunc(min + ctx.rng()() * span);
319
320    ctx.apply_aura_n(AURA_LESSER_GHOUL, readies.max(1));
321
322    ctx.apply_aura(AURA_RUNE_MASTERY);
323    maybe_summon_random_rider(ctx);
324}
325
326pub(crate) fn scourge_strike_hook(ctx: &mut HookCtx<'_>) {
327    let erupt = ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::SS_ERUPT_PCT);
328
329    if ctx.is_aura_active(AURA_VIRULENT_PLAGUE) {
330        ctx.deal_profiled_damage_ap(
331            crate::generated::specs::unholy_death_knight::DAMAGE_REPORTED::VIRULENT_PLAGUE_ERUPT,
332            crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::VP_TICK.action(),
333            erupt * ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::VP_TICK),
334            DamageFlags::PERIODIC,
335        );
336    }
337
338    if ctx.is_aura_active(AURA_DREAD_PLAGUE) {
339        ctx.deal_profiled_damage_ap(
340            crate::generated::specs::unholy_death_knight::DAMAGE_REPORTED::DREAD_PLAGUE_ERUPT,
341            crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DP_TICK.action(),
342            erupt * ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DP_TICK),
343            DamageFlags::PERIODIC,
344        );
345    }
346
347    if ctx.consume_aura_stack(AURA_LESSER_GHOUL) {
348        summon_lesser_ghoul(ctx, false);
349    }
350
351    trigger_icy_fury(ctx);
352    trigger_whitemane_famine(ctx);
353    ctx.apply_aura(AURA_RUNE_MASTERY);
354    maybe_summon_random_rider(ctx);
355}
356
357pub(crate) fn death_coil_hook(ctx: &mut HookCtx<'_>) {
358    let free = ctx.is_aura_active(AURA_SUDDEN_DOOM);
359    let mut mult = 1.0;
360
361    if ctx.consume_aura(AURA_SUDDEN_DOOM) {
362        mult *= 1.0 + ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::SD_DC_PCT);
363    }
364
365    if ctx.is_aura_active(AURA_FORBIDDEN_KNOWLEDGE) {
366        let shadow = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::NECROTIC_SHADOW) * mult;
367        let strike = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::NECROTIC_STRIKE) * mult;
368
369        ctx.deal_damage_ap(
370            crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::NECROTIC_COIL,
371            shadow,
372            DamageFlags::empty(),
373        );
374        ctx.deal_damage_ap(
375            crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::NECROTIC_COIL_SHADOWSTRIKE,
376            strike,
377            DamageFlags::empty(),
378        );
379    } else {
380        let coef = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DC_DAMAGE) * mult;
381
382        ctx.deal_damage_ap(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DC_DAMAGE.action(), coef, DamageFlags::empty());
383    }
384
385    if free {
386        summon_lesser_ghoul(ctx, false);
387    } else {
388        runic_corruption(ctx, f64::from(DEATH_COIL_RP_COST));
389    }
390
391    empower_gargoyle(ctx, DEATH_COIL_RP_COST);
392    extend_riders_from_rp(
393        ctx,
394        (f64::from(DEATH_COIL_RP_COST) - ctx.last_secondary_spent()).max(0.0),
395    );
396    trigger_let_terror_reign(ctx);
397    ctx.apply_aura(AURA_ICY_TALONS);
398    extend_dark_transformation(ctx);
399}
400
401fn extend_dark_transformation(ctx: &mut HookCtx<'_>) {
402    let extension_ms = ctx
403        .spec_config::<UnholyConfig>()
404        .map_or(0, |config| config.eternal_agony_extension_ms);
405
406    if extension_ms > 0 && ctx.is_aura_active(AURA_DARK_TRANSFORMATION) {
407        ctx.extend_aura(AURA_DARK_TRANSFORMATION, extension_ms);
408    }
409}
410
411pub(crate) fn outbreak_hook(ctx: &mut HookCtx<'_>) {
412    ctx.apply_aura(AURA_RUNE_MASTERY);
413    maybe_summon_random_rider(ctx);
414}
415
416pub(crate) fn dark_transformation_hook(ctx: &mut HookCtx<'_>) {
417    let energy = ctx.effect_base_points(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DT_ENERGY_PCT);
418
419    if let Some(resource) = ctx.primary_pet_resource() {
420        ctx.gain_pet_resource(resource, energy);
421    }
422
423    ctx.apply_aura(AURA_COMMANDER_OF_THE_DEAD);
424    ctx.apply_aura(AURA_REAPING);
425    magus_package(ctx);
426}
427
428fn magus_package(ctx: &mut HookCtx<'_>) {
429    let mult = pet_package_mult(ctx);
430    let config = ctx
431        .spec_config::<UnholyConfig>()
432        .copied()
433        .unwrap_or_default();
434
435    if config.mid2_2pc {
436        let execute = config.mid2_4pc_threshold > 0.0
437            && ctx.target_health_fraction() <= config.mid2_4pc_threshold;
438        let direct_mult = config.lord_of_the_dead_direct_mult
439            * if execute { config.mid2_4pc_direct_mult } else { 1.0 };
440        let periodic_mult = config.lord_of_the_dead_periodic_mult
441            * if execute { config.mid2_4pc_periodic_mult } else { 1.0 };
442        let necrotic = ctx.effect_ap_coefficient(
443            crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::MAGUS_NECROTIC_BOLT,
444        ) * MAGUS_FROSTBOLTS * mult * direct_mult;
445        let withering = ctx.effect_ap_coefficient(
446            crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::MAGUS_WITHERING_GRASP,
447        ) * MAGUS_SHADOW_BOLTS * mult * periodic_mult;
448
449        ctx.deal_pet_damage_ap(
450            crate::generated::specs::unholy_death_knight::DAMAGE_REPORTED::NECROTIC_BOLT,
451            necrotic,
452        );
453        ctx.deal_pet_damage_ap(
454            crate::generated::specs::unholy_death_knight::DAMAGE_REPORTED::WITHERING_GRASP,
455            withering,
456        );
457
458        return;
459    }
460
461    let sb = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::MAGUS_SHADOW_BOLT)
462        * MAGUS_SHADOW_BOLTS
463        * mult
464        * config.lord_of_the_dead_periodic_mult;
465    let fb = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::MAGUS_FROSTBOLT)
466        * MAGUS_FROSTBOLTS
467        * mult
468        * config.lord_of_the_dead_direct_mult;
469
470    ctx.deal_pet_damage_ap(crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::SHADOW_BOLT, sb);
471    ctx.deal_pet_damage_ap(crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::FROSTBOLT, fb);
472}
473
474pub(crate) fn army_of_the_dead_hook(ctx: &mut HookCtx<'_>) {
475    maybe_summon_random_rider(ctx);
476
477    for _ in 0..ARMY_GHOULS {
478        summon_lesser_ghoul(ctx, true);
479    }
480
481    summon_apocalypse_riders(ctx);
482
483    if let Some(config) = ctx
484        .spec_config::<UnholyConfig>()
485        .and_then(|config| config.gargoyle)
486    {
487        ctx.schedule_hook(config.army_spawn_delay, summon_gargoyle);
488    }
489
490    magus_package(ctx);
491
492    ctx.apply_aura(AURA_FORBIDDEN_KNOWLEDGE);
493    ctx.apply_aura(AURA_RUNE_MASTERY);
494}
495
496pub(crate) fn soul_reaper_hook(ctx: &mut HookCtx<'_>) {
497    ctx.consume_aura_stack(AURA_REAPING);
498
499    let summons = wowlab_types::numeric::f64_to_usize_saturating_round(
500        ctx.effect_base_points(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::SR_GHOULS).max(0.0),
501    );
502
503    for _ in 0..summons {
504        if !ctx.consume_aura_stack(AURA_LESSER_GHOUL) {
505            break;
506        }
507
508        trigger_putrefy_damage(ctx, 1.0);
509    }
510
511    ctx.apply_aura(AURA_RUNE_MASTERY);
512    maybe_summon_random_rider(ctx);
513}
514
515pub(crate) fn putrefy_hook(ctx: &mut HookCtx<'_>) {
516    trigger_putrefy_damage(ctx, 1.0);
517
518    let reanimate_chance = ctx
519        .spec_config::<UnholyConfig>()
520        .map_or(0.0, |config| config.lord_of_the_dead_chance);
521
522    if reanimate_chance > 0.0 && proc_chance(ctx.rng(), reanimate_chance) {
523        magus_package(ctx);
524    }
525
526    if ctx.spell_current_charges(SPELL_PUTREFY) > 1
527        && ctx.spend_spell_charge(SPELL_PUTREFY)
528    {
529        trigger_putrefy_damage(ctx, 1.0);
530    }
531}
532
533pub(crate) fn epidemic_hook(ctx: &mut HookCtx<'_>) {
534    if !ctx.consume_aura(AURA_SUDDEN_DOOM) {
535        return;
536    }
537
538    let ghouls = ctx
539        .spec_runtime::<UnholyRuntime>()
540        .map_or(0, |runtime| runtime.active_army_ghouls);
541
542    if ghouls == 0 {
543        return;
544    }
545
546    let effect =
547        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::EPIDEMIC_ORDER_DAMAGE;
548    let coefficient = ctx.effect_ap_coefficient(effect) * f64::from(ghouls);
549
550    ctx.with_resolved_spell_effect_targets(effect, effect.action(), None, |ctx, targets| {
551        for target in targets.iter().copied() {
552            ctx.deal_damage_ap_to_target(
553                target,
554                effect.action(),
555                coefficient,
556                DamageFlags::PET | DamageFlags::AOE,
557            );
558        }
559    });
560}
561
562pub(super) fn forbidden_knowledge_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
563    let Some(config) = ctx
564        .spec_config::<UnholyConfig>()
565        .and_then(|config| config.forbidden_knowledge)
566    else {
567        return;
568    };
569
570    if proc_chance(ctx.rng(), config.proc_chance) {
571        trigger_putrefy_damage(ctx, config.putrefy_fraction);
572    }
573}
574
575fn trigger_putrefy_damage(ctx: &mut HookCtx<'_>, damage_fraction: f64) {
576    let devotion_duration_ms = ctx
577        .spec_config::<UnholyConfig>()
578        .map_or(0, |config| config.unholy_devotion_duration_ms);
579
580    if devotion_duration_ms > 0 {
581        let duration_ms = wowlab_types::numeric::f64_to_u32_saturating_round(
582            f64::from(devotion_duration_ms) * damage_fraction,
583        );
584
585        ctx.apply_pet_aura_with_duration(AURA_UNHOLY_DEVOTION, duration_ms.max(1));
586    }
587
588    let st = ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::PUTREFY_ST) * damage_fraction;
589
590    ctx.deal_damage_ap(
591        crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::PUTREFY_ST,
592        st,
593        DamageFlags::empty(),
594    );
595    ctx.deal_effect_damage_ap_with_geometry(
596        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::PUTREFY_AOE,
597        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::PUTREFY_AOE,
598        damage_fraction,
599        DamageFlags::empty(),
600    );
601    trigger_blightburst_disease(
602        ctx,
603        AURA_VIRULENT_PLAGUE,
604        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::VP_TICK,
605        crate::generated::specs::unholy_death_knight::DAMAGE_REPORTED::VIRULENT_PLAGUE_ERUPT,
606        damage_fraction,
607    );
608    trigger_blightburst_disease(
609        ctx,
610        AURA_DREAD_PLAGUE,
611        crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DP_TICK,
612        crate::generated::specs::unholy_death_knight::DAMAGE_REPORTED::DREAD_PLAGUE_ERUPT,
613        damage_fraction,
614    );
615    let ready_stacks = ctx
616        .spec_config::<UnholyConfig>()
617        .map_or(0, |config| config.forbidden_knowledge_ready_stacks);
618
619    if ready_stacks > 0 {
620        ctx.apply_aura_n(AURA_LESSER_GHOUL, ready_stacks);
621    }
622}
623
624fn trigger_blightburst_disease(
625    ctx: &mut HookCtx<'_>,
626    aura: wowlab_engine_combat::LocalAuraIdx,
627    periodic_effect: wowlab_engine_combat::DamageEffectId,
628    report_action: wowlab_engine_combat::DamageActionId,
629    source_fraction: f64,
630) {
631    let Some(config) = ctx
632        .spec_config::<UnholyConfig>()
633        .and_then(|config| config.blightburst)
634    else {
635        return;
636    };
637
638    if !ctx.is_aura_active(aura) {
639        ctx.apply_aura(aura);
640
641        return;
642    }
643
644    let base_period_ms = f64::from(ctx.effect_period_ms(periodic_effect).max(1));
645    let duration_ms = wowlab_types::numeric::f64_to_u32_saturating_round(
646        f64::from(config.duration_ms) * source_fraction,
647    );
648    let tick_count = f64::from(duration_ms) * ctx.haste_multiplier() / base_period_ms;
649    let coefficient =
650        ctx.effect_ap_coefficient(periodic_effect) * tick_count * config.damage_fraction;
651
652    ctx.deal_profiled_damage_ap(
653        report_action,
654        periodic_effect.action(),
655        coefficient,
656        DamageFlags::PERIODIC,
657    );
658    ctx.extend_aura(aura, duration_ms);
659}
660
661/// Brittle rolls spell 374504's proc chance on every disease tick.
662pub(crate) fn disease_tick_hook(ctx: &mut HookCtx<'_>) {
663    let chance = ctx
664        .spec_config::<UnholyConfig>()
665        .map_or(0.0, |config| config.brittle_proc_chance);
666
667    if proc_chance(ctx.rng(), chance) {
668        ctx.apply_aura(AURA_BRITTLE);
669    }
670}
671
672fn permanent_ghoul_sweeping_claws(ctx: &mut HookCtx<'_>) {
673    let coefficient = GHOUL_AP
674        * ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::SWEEPING_MAIN)
675        * CLAW_BASE_MULT
676        * (1.0 + ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DT_PET_PCT));
677
678    ctx.deal_pet_damage_ap(crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::SWEEPING_CLAWS, coefficient);
679    roll_infected_claws(ctx);
680}
681
682fn permanent_ghoul_claw(ctx: &mut HookCtx<'_>) {
683    let coefficient = GHOUL_AP * ctx.effect_ap_coefficient(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::CLAW) * CLAW_BASE_MULT;
684
685    ctx.deal_pet_damage_ap(crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::CLAW, coefficient);
686    roll_infected_claws(ctx);
687}
688
689const PERMANENT_GHOUL_ABILITIES: [PetAbility; 2] = [
690    PetAbility::new(
691        PERMANENT_GHOUL_ABILITY_COST,
692        46.0,
693        permanent_ghoul_sweeping_claws,
694    )
695    .gcd_ms(SWEEPING_CLAWS_GCD_MS)
696    .while_owner_aura_active(AURA_DARK_TRANSFORMATION),
697    PetAbility::new(PERMANENT_GHOUL_ABILITY_COST, 71.0, permanent_ghoul_claw)
698        .while_owner_aura_inactive(AURA_DARK_TRANSFORMATION),
699];
700
701pub(super) const PERMANENT_GHOUL_ACTION_BAR: PetActionBar = PetActionBar::new(
702    PetResourceModel::new(HUNDRED).regen_per_second(PERMANENT_GHOUL_ENERGY_REGEN),
703    PERMANENT_GHOUL_GCD_MS,
704    &PERMANENT_GHOUL_ABILITIES,
705)
706.inherit_owner_stats();
707
708/// Permanent-ghoul white-swing effects. Active abilities use the pet action bar.
709pub(crate) fn pet_swing(ctx: &mut HookCtx<'_>, event: SwingEvent) {
710    let hand = event.hand;
711
712    if hand == SwingHand::OffHand {
713        return;
714    }
715
716    let dt_up = ctx.is_aura_active(AURA_DARK_TRANSFORMATION);
717    let dt_mult = if dt_up {
718        1.0 + ctx.effect_percent(crate::generated::specs::unholy_death_knight::DAMAGE_EFFECT::DT_PET_PCT)
719    } else {
720        1.0
721    };
722
723    if dt_up {
724        // The definition's swing excludes Dark Transformation's bonus portion.
725        let bonus =
726            GHOUL_AP * wowlab_engine_domain::damage::pet_swing_ap(PET_SWING_MS) * (dt_mult - 1.0);
727
728        ctx.deal_pet_damage_ap(crate::generated::specs::unholy_death_knight::DAMAGE_SPELL::MELEE, bonus);
729    }
730
731    roll_infected_claws(ctx);
732}
733
734#[cfg(test)]
735mod tests {
736    use googletest::prelude::*;
737    use rstest::rstest;
738
739    use super::{compounded_crit_damage_bonus, lesser_ghoul_claw_at};
740
741    #[gtest]
742    #[rstest]
743    #[case::neither(0.0, 0.0, 0.0)]
744    #[case::dreadblade_only(40.0, 0.0, 40.0)]
745    #[case::grave_mastery_only(0.0, 15.0, 15.0)]
746    #[case::both_compound(40.0, 15.0, 61.0)]
747    fn companion_crit_bonuses_compound(
748        #[case] dreadblade_pct: f64,
749        #[case] grave_mastery_pct: f64,
750        #[case] expected: f64,
751    ) -> Result<()> {
752        verify_that!(
753            compounded_crit_damage_bonus(dreadblade_pct, grave_mastery_pct),
754            near(expected, 1e-9)
755        )
756    }
757
758    #[gtest]
759    #[rstest]
760    #[case::spawn(0, true)]
761    #[case::second_gcd(3, true)]
762    #[case::third_gcd(6, true)]
763    #[case::energy_starved(9, false)]
764    #[case::first_regenerated_claw(12, true)]
765    #[case::still_starved(18, false)]
766    #[case::second_regenerated_claw(21, true)]
767    fn lesser_ghoul_claws_follow_energy_and_gcd(
768        #[case] step: u16,
769        #[case] expected: bool,
770    ) -> Result<()> {
771        verify_that!(lesser_ghoul_claw_at(step), eq(expected))
772    }
773}
774
775#[cfg(test)]
776#[path = "hooks/icy_talons_tests.rs"]
777mod icy_talons_tests;
778
779#[cfg(test)]
780#[path = "hooks/outbreak_tests.rs"]
781mod outbreak_tests;