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