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
30const DEATH_COIL_RP_COST: i32 = 30;
32const RC_PER_RP_FACTOR: f64 = 0.0001;
33
34const GHOUL_AP: f64 = 0.474;
36const LESSER_GHOUL_NPC_ID: u32 = 237_409;
37
38const 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
59const 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
66const MAGUS_SHADOW_BOLTS: f64 = 20.0;
68const MAGUS_FROSTBOLTS: f64 = 9.0;
69
70const 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
661pub(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
708pub(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 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;