1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::ResourceOps as _;
3use wowlab_engine_combat::GuardianOps as _;
4use wowlab_engine_combat::AuraOps as _;
5use wowlab_engine_combat::{
6 DamageFlags, GuardianAbility, GuardianActionFn, GuardianEvent, GuardianSpec, HookCtx,
7 MasteryCtx,
8};
9use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
10use wowlab_engine_rng::proc_chance;
11use wowlab_types::constants::HUNDRED;
12
13use super::config::{DemonologyConfig, demonology_config};
14
15use crate::generated::specs::demonology_warlock::{
16 AURA, AURA_ABYSSAL_DOMINION, AURA_ABYSSAL_DOMINION_TALENT, AURA_CARNIVOROUS_STALKERS_TALENT,
17 AURA_DEMONIC_ART_MOTHER, AURA_DEMONIC_ART_OVERLORD, AURA_DEMONIC_ART_PIT_LORD,
18 AURA_DEMONIC_CORE, AURA_DIABOLIC_RITUAL_MOTHER, AURA_DIABOLIC_RITUAL_OVERLORD,
19 AURA_DIABOLIC_RITUAL_PIT_LORD, AURA_DIABOLIC_RITUAL_TALENT, AURA_DOMINION_OF_ARGUS,
20 AURA_DREADSTALKERS, AURA_POWER_SIPHON, AURA_WILD_IMPS, EFFECT, HERO, REPORTED_SPELL, TALENT,
21};
22use crate::hooks::shared::warlock::{DiabolicRitual, WarlockSpec};
23
24const DIABOLIC_RITUAL: DiabolicRitual = DiabolicRitual {
25 talent: AURA_DIABOLIC_RITUAL_TALENT,
26 art: [
27 AURA_DEMONIC_ART_OVERLORD,
28 AURA_DEMONIC_ART_MOTHER,
29 AURA_DEMONIC_ART_PIT_LORD,
30 ],
31 ritual: [
32 AURA_DIABOLIC_RITUAL_OVERLORD,
33 AURA_DIABOLIC_RITUAL_MOTHER,
34 AURA_DIABOLIC_RITUAL_PIT_LORD,
35 ],
36};
37
38const MAX_SHARDS_PER_HAND_OF_GULDAN: u32 = 3;
39const DOMINION_ROLL_QUARTER: f64 = 0.25;
40const DOMINION_ROLL_HALF: f64 = 0.50;
41const DOMINION_ROLL_THREE_QUARTERS: f64 = 0.75;
42const VILEFIEND_HEADBUTT_PERIOD: u16 = 3;
43const DEMONIC_KNOWLEDGE_RANK_1_CHANCE: f64 = 0.08;
44const DEMONIC_KNOWLEDGE_RANK_2_CHANCE: f64 = 0.15;
45const SPITEFUL_RECONSTITUTION_CHANCE: f64 = 0.10;
46const POWER_SIPHON_MAX_IMPS: usize = 2;
47pub(super) const DEMONIC_CALLING_BASE_COST: f64 = 2.0;
48pub(super) const DEMONIC_CALLING_BASE_CAST_MS: u32 = 2_000;
49pub(super) const DEMONIC_CALLING_CAST_REDUCTION_MS: u32 = 1_000;
50const CARNIVOROUS_STALKERS_CHANCE: f64 = 0.20;
51
52const FIREBOLTS_PER_IMP: u16 = 6;
54const WILD_IMP_LIFETIME_MS: u32 = 40_000;
55const WILD_IMP_CAST_MS: u32 = 2_000;
56const IMP_GANG_BOSS_DAMAGE_MULT: f64 = 2.0;
58const UNSTABLE_SOUL_DAMAGE_MULT: f64 = 1.5;
60const TO_HELL_AND_BACK_IMPS_PER_BOSS: usize = 2;
62const INFERNAL_RAPIDITY_EFFECTIVE_CHANCE: f64 = 0.048;
63const INFERNAL_RAPIDITY_DAMAGE_MULT: f64 = 2.0;
64pub(super) const INNER_DEMONS_PERIOD_MS: u32 = 12_000;
65const DOMINION_TRIGGER_CASTS: i32 = 2;
66pub(super) const DOMINION_BASE_DURATION_MS: u32 = 15_000;
67pub(super) const DOMINION_RANK_2_EXTENSION_MS: u32 = 5_000;
68const DOMINION_GUARDIAN_BASE_DURATION_MS: u32 = 10_000;
69const DOMINION_GUARDIAN_RANK_3_EXTENSION_MS: u32 = 4_000;
70const DOMINION_HAND_OF_GULDAN_PER_RANK: f64 = 0.10;
71const GLOOM_OF_NATHREZA_PER_SHARD: f64 = 0.15;
72const DOMINION_SHARD_REFUND: f64 = 1.0;
73const DOMINION_ACTION_MS: u32 = 2_000;
74const VILEFIEND_LIFETIME_MS: u32 = 15_000;
75const VILEFIEND_ACTION_MS: u32 = 2_000;
76const VILEFIEND_SP_FROM_SP: f64 = 1.95;
77const VILEFIEND_AP_FROM_SP: f64 = 0.45;
78const VILEFIEND_MELEE_WM: f64 = 2.0;
79const VILEFIEND_MELEE_SP: f64 = VILEFIEND_AP_FROM_SP * PET_SWING_AP * VILEFIEND_MELEE_WM;
80const BILE_SPIT_TICKS: u8 = 5;
81const BLIGHTED_MAW_FRACTION: f64 = 0.50;
82const DREADLASH_DAMAGE_MULT: f64 = 1.10;
83pub(super) const FELGUARD_CONTROLLER_TICK_MS: u32 = 1_000;
84const FELGUARD_AP_FROM_SP: f64 = 1.5201;
85const LEGION_STRIKE_PERIOD_TICKS: u16 = 7;
86const FELSTORM_PERIOD_TICKS: u16 = 30;
87const FELSTORM_TICKS: u8 = 6;
89
90const DREADSTALKER_AP_FROM_SP: f64 = 0.825;
92const PET_SWING_AP: f64 = 2.0 / 6.0;
94const DREADSTALKER_MELEE_WM: f64 = 0.96;
95const FLAMETOUCHED_ATTACK_SPEED_MULT: f64 = 1.10;
97pub(super) const FLAMETOUCHED_CRIT_PCT: f64 = 15.0;
99#[rustfmt::skip]
101pub(super) const FLAMETOUCHED_CRIT_SPELLS: [u32; 2] = [
102 REPORTED_SPELL::DREADSTALKER_MELEE,
103 REPORTED_SPELL::DREADBITE,
104];
105const DREADSTALKER_LIFETIME_MS: u32 = 12_000;
107const MID1_DEMONOLOGY_4PC_DURATION_MS: u32 = 3_000;
108const MID1_DEMONOLOGY_4PC_DAMAGE_MULT: f64 = 1.10;
109const DREADSTALKER_SWING_MS: u32 = 2_000;
110const DREADSTALKERS_PER_CALL: u32 = 2;
111const TYRANT_TAG_BASE: u32 = 2_650_000;
112pub(super) const TYRANT_LIFETIME_MS: u32 = 15_000;
113pub(super) const REIGN_OF_TYRANNY_EXTENSION_MS: u32 = 5_000;
114const REIGN_DREADSTALKER_EXTENSION_MS: u32 = 15_000;
115const TYRANT_ACTION_MS: u32 = 2_000;
116const DEMONIC_POWER_PER_DEMON: f64 = 0.10;
117
118const DIABOLIC_RITUAL_REDUCTION_PER_SHARD_MS: u32 = 1_000;
119const RUINATION_FOLLOWUP_DELAY_MS: u32 = 1_000;
120
121fn wild_imp_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
122 let imp_mult = match event.tag {
123 TALENT::IMP_GANG_BOSS => IMP_GANG_BOSS_DAMAGE_MULT,
124 REPORTED_SPELL::UNSTABLE_GANG_BOSS => IMP_GANG_BOSS_DAMAGE_MULT * UNSTABLE_SOUL_DAMAGE_MULT,
125 _ => 1.0,
126 };
127 let coefficient = ctx.effect_sp_coefficient(EFFECT::FEL_FIREBOLT_DAMAGE);
128
129 ctx.deal_damage_sp(
130 REPORTED_SPELL::FEL_FIREBOLT,
131 coefficient * imp_mult,
132 DamageFlags::PET,
133 );
134 let infernal_rapidity = demonology_config(ctx).infernal_rapidity;
135
136 if infernal_rapidity && proc_chance(ctx.rng(), INFERNAL_RAPIDITY_EFFECTIVE_CHANCE) {
137 ctx.deal_damage_sp(
138 REPORTED_SPELL::FEL_FIREBOLT,
139 coefficient * imp_mult * INFERNAL_RAPIDITY_DAMAGE_MULT,
140 DamageFlags::PET,
141 );
142 }
143}
144
145fn wild_imp_demise(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
146 ctx.consume_owner_aura_stack(AURA_WILD_IMPS.raw());
147
148 let chance = demonology_config(ctx).demonic_core_imp_chance;
149
150 if chance > 0.0 && proc_chance(ctx.rng(), chance) {
151 ctx.apply_owner_aura(AURA_DEMONIC_CORE.raw());
152 }
153}
154
155fn summon_imp_with_tag(ctx: &mut HookCtx<'_>, tag: u32) {
156 ctx.apply_owner_aura(AURA_WILD_IMPS.raw());
157 let _ = ctx.summon_guardian(
158 GuardianSpec::new_inheriting_owner(tag, WILD_IMP_LIFETIME_MS)
159 .ability(
160 GuardianAbility::new(WILD_IMP_CAST_MS, wild_imp_action)
161 .hasted_actions()
162 .max_actions(FIREBOLTS_PER_IMP)
163 .ends_guardian(),
164 )
165 .on_demise(wild_imp_demise),
166 );
167}
168
169fn summon_wild_imp(ctx: &mut HookCtx<'_>, gang_boss: bool) {
170 summon_imp_with_tag(
171 ctx,
172 if gang_boss {
173 TALENT::IMP_GANG_BOSS
174 } else {
175 AURA::WILD_IMPS
176 },
177 );
178}
179
180fn shadow_nova_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
181 let coefficient = ctx.effect_sp_coefficient(EFFECT::SHADOW_NOVA_DAMAGE);
182
183 ctx.deal_damage_sp(REPORTED_SPELL::SHADOW_NOVA, coefficient, DamageFlags::PET);
184}
185
186fn blaze_missile_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
187 let coefficient = ctx.effect_sp_coefficient(EFFECT::BLAZE_MISSILE_DAMAGE);
188
189 ctx.deal_damage_sp(REPORTED_SPELL::BLAZE_MISSILE, coefficient, DamageFlags::PET);
190}
191
192fn mind_sear_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
193 let coefficient = ctx.effect_sp_coefficient(EFFECT::MIND_SEAR_DAMAGE);
194
195 ctx.deal_damage_sp(
196 REPORTED_SPELL::MIND_SEAR_CHANNEL,
197 coefficient,
198 DamageFlags::PET,
199 );
200}
201
202fn soul_barrage_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
203 let coefficient = ctx.effect_sp_coefficient(EFFECT::SOUL_BARRAGE_DAMAGE);
204
205 ctx.deal_damage_sp(
206 REPORTED_SPELL::SOUL_BARRAGE_CAST,
207 coefficient,
208 DamageFlags::PET,
209 );
210}
211
212fn summon_dominion_guardian(ctx: &mut HookCtx<'_>) {
213 let rank_3 = ctx
214 .spec_config::<DemonologyConfig>()
215 .is_some_and(|cfg| cfg.dominion_rank_3);
216 let duration_ms = DOMINION_GUARDIAN_BASE_DURATION_MS
217 + u32::from(rank_3) * DOMINION_GUARDIAN_RANK_3_EXTENSION_MS;
218 let roll = ctx.rng()();
219 let (tag, action): (u32, GuardianActionFn) = if roll < DOMINION_ROLL_QUARTER {
220 (REPORTED_SPELL::ANTORAN_JAILER, soul_barrage_action)
221 } else if roll < DOMINION_ROLL_HALF {
222 (REPORTED_SPELL::ANTORAN_INQUISITOR, mind_sear_action)
223 } else if roll < DOMINION_ROLL_THREE_QUARTERS
224 && ctx.active_guardian_count(REPORTED_SPELL::GRAND_WARLOCK_ALYTHESS) == 0
225 {
226 (REPORTED_SPELL::GRAND_WARLOCK_ALYTHESS, blaze_missile_action)
227 } else if roll < DOMINION_ROLL_THREE_QUARTERS {
228 (REPORTED_SPELL::ANTORAN_JAILER, soul_barrage_action)
229 } else if ctx.active_guardian_count(REPORTED_SPELL::LADY_SACROLASH) == 0 {
230 (REPORTED_SPELL::LADY_SACROLASH, shadow_nova_action)
231 } else {
232 (REPORTED_SPELL::ANTORAN_INQUISITOR, mind_sear_action)
233 };
234 let _ = ctx.summon_guardian(
235 GuardianSpec::new_inheriting_owner(tag, duration_ms).ability(
236 GuardianAbility::new(DOMINION_ACTION_MS, action)
237 .first_action_delay(DOMINION_ACTION_MS)
238 .hasted_actions(),
239 ),
240 );
241}
242
243fn trigger_dominion(ctx: &mut HookCtx<'_>) {
244 let Some(cfg) = ctx.spec_config::<DemonologyConfig>() else {
245 return;
246 };
247
248 if !cfg.dominion_rank_1 || !ctx.is_aura_active(AURA_DOMINION_OF_ARGUS.raw()) {
249 return;
250 }
251
252 let refund = cfg.dominion_rank_3;
253
254 ctx.add_aura_stack(AURA_DOMINION_OF_ARGUS.raw());
255
256 if ctx.aura_stacks(AURA_DOMINION_OF_ARGUS.raw()) > DOMINION_TRIGGER_CASTS {
257 ctx.consume_aura_stack(AURA_DOMINION_OF_ARGUS.raw());
258 ctx.consume_aura_stack(AURA_DOMINION_OF_ARGUS.raw());
259 summon_dominion_guardian(ctx);
260 }
261
262 if refund {
263 ctx.gain_resource(DOMINION_SHARD_REFUND);
264 }
265}
266
267pub(super) fn inner_demons_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
268 summon_wild_imp(ctx, false);
269}
270
271pub(super) fn felguard_controller_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
272 let tick = event.action_index + 1;
273
274 if tick % LEGION_STRIKE_PERIOD_TICKS == 0 {
275 ctx.deal_damage_sp(
276 REPORTED_SPELL::LEGION_STRIKE,
277 ctx.effect_ap_coefficient(EFFECT::LEGION_STRIKE_DAMAGE) * FELGUARD_AP_FROM_SP,
278 DamageFlags::PHYSICAL | DamageFlags::PET,
279 );
280 }
281
282 if tick % FELSTORM_PERIOD_TICKS == 0 {
283 for _ in 0..FELSTORM_TICKS {
284 ctx.deal_damage_sp(
285 REPORTED_SPELL::FELSTORM,
286 ctx.effect_ap_coefficient(EFFECT::FELSTORM_DAMAGE) * FELGUARD_AP_FROM_SP,
287 DamageFlags::PHYSICAL | DamageFlags::PET,
288 );
289 }
290 }
291}
292
293fn ruination_followup(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
294 let (dominion_mult, gloom_mult) =
295 ctx.spec_config::<DemonologyConfig>()
296 .map_or((1.0, 1.0), |cfg| {
297 (
298 1.0 + DOMINION_HAND_OF_GULDAN_PER_RANK * f64::from(cfg.dominion_rank_2),
299 if cfg.gloom_of_nathreza {
300 1.0 + GLOOM_OF_NATHREZA_PER_SHARD * f64::from(MAX_SHARDS_PER_HAND_OF_GULDAN)
301 } else {
302 1.0
303 },
304 )
305 });
306
307 ctx.deal_damage_sp(
308 REPORTED_SPELL::HAND_OF_GULDAN_IMPACT,
309 ctx.effect_sp_coefficient(EFFECT::HAND_OF_GULDAN_IMPACT_DAMAGE)
310 * f64::from(MAX_SHARDS_PER_HAND_OF_GULDAN)
311 * dominion_mult
312 * gloom_mult,
313 DamageFlags::empty(),
314 );
315 let gang_boss = ctx
316 .spec_config::<DemonologyConfig>()
317 .is_some_and(|cfg| cfg.imp_gang_boss);
318
319 for index in 0..MAX_SHARDS_PER_HAND_OF_GULDAN {
320 summon_wild_imp(ctx, gang_boss && index == 0);
321 }
322}
323
324pub(crate) fn ruination_hook(ctx: &mut HookCtx<'_>) {
325 if DIABOLIC_RITUAL.consume_art(ctx).is_some() {
326 crate::hooks::shared::warlock::explode_oculi(ctx, WarlockSpec::Demonology);
327 }
328
329 trigger_dominion(ctx);
330 let _ = ctx.summon_guardian(
331 GuardianSpec::new_inheriting_owner(
332 HERO::DIABOLIST::SPELL::RUINATION_IMPACT,
333 RUINATION_FOLLOWUP_DELAY_MS + 1,
334 )
335 .ability(
336 GuardianAbility::new(RUINATION_FOLLOWUP_DELAY_MS, ruination_followup)
337 .first_action_delay(RUINATION_FOLLOWUP_DELAY_MS)
338 .max_actions(1)
339 .ends_guardian(),
340 ),
341 );
342}
343
344fn dreadstalker_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
345 let Some((dreadlash_selected, blighted_maw, mid1_4pc)) = ctx
346 .spec_config::<DemonologyConfig>()
347 .map(|cfg| (cfg.dreadlash, cfg.blighted_maw, cfg.mid1_4pc))
348 else {
349 return;
350 };
351 let set_mult = if mid1_4pc {
352 MID1_DEMONOLOGY_4PC_DAMAGE_MULT
353 } else {
354 1.0
355 };
356 let swing = DREADSTALKER_AP_FROM_SP * PET_SWING_AP * DREADSTALKER_MELEE_WM * set_mult;
358
359 ctx.deal_damage_sp(
360 REPORTED_SPELL::DREADSTALKER_MELEE,
361 swing,
362 DamageFlags::PHYSICAL | DamageFlags::PET,
363 );
364 let extra_bite = ctx.is_aura_active(AURA_CARNIVOROUS_STALKERS_TALENT.raw())
365 && proc_chance(ctx.rng(), CARNIVOROUS_STALKERS_CHANCE);
366
367 if event.action_index == 0 || extra_bite {
368 let dreadlash = if dreadlash_selected {
369 DREADLASH_DAMAGE_MULT
370 } else {
371 1.0
372 };
373 let dreadbite = ctx.effect_ap_coefficient(EFFECT::DREADBITE_DAMAGE)
374 * DREADSTALKER_AP_FROM_SP
375 * set_mult
376 * dreadlash;
377
378 ctx.deal_damage_sp(
379 REPORTED_SPELL::DREADBITE,
380 dreadbite,
381 DamageFlags::PHYSICAL | DamageFlags::PET,
382 );
383
384 if blighted_maw {
385 ctx.deal_damage_sp(
386 REPORTED_SPELL::BLIGHTED_MAW,
387 dreadbite * BLIGHTED_MAW_FRACTION,
388 DamageFlags::PET,
389 );
390 }
391 }
392}
393
394fn vilefiend_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
395 if event.action_index == 0 {
396 ctx.deal_damage_sp(
397 REPORTED_SPELL::BILE_SPIT,
398 ctx.effect_sp_coefficient(EFFECT::BILE_SPIT_DIRECT_DAMAGE) * VILEFIEND_SP_FROM_SP,
399 DamageFlags::PET,
400 );
401
402 for _ in 0..BILE_SPIT_TICKS {
403 ctx.deal_damage_sp(
404 REPORTED_SPELL::BILE_SPIT,
405 ctx.effect_sp_coefficient(EFFECT::BILE_SPIT_TICK_DAMAGE) * VILEFIEND_SP_FROM_SP,
406 DamageFlags::PET | DamageFlags::NO_CRIT,
407 );
408 }
409
410 return;
411 }
412
413 let mark_of_shatug = ctx
414 .spec_config::<DemonologyConfig>()
415 .is_some_and(|cfg| cfg.mark_of_shatug);
416
417 ctx.deal_damage_sp(
418 1,
419 VILEFIEND_MELEE_SP,
420 DamageFlags::PHYSICAL | DamageFlags::PET,
421 );
422
423 if mark_of_shatug {
424 ctx.deal_damage_sp(
425 REPORTED_SPELL::GLOOM_SLASH,
426 ctx.effect_sp_coefficient(EFFECT::GLOOM_SLASH_DAMAGE) * VILEFIEND_SP_FROM_SP,
427 DamageFlags::PET,
428 );
429 }
430
431 if (event.action_index - 1) % VILEFIEND_HEADBUTT_PERIOD == 0 {
432 ctx.deal_damage_sp(
433 REPORTED_SPELL::HEADBUTT,
434 ctx.effect_ap_coefficient(EFFECT::HEADBUTT_DAMAGE) * VILEFIEND_AP_FROM_SP,
435 DamageFlags::PHYSICAL | DamageFlags::PET,
436 );
437
438 if mark_of_shatug {
439 ctx.deal_damage_sp(
440 REPORTED_SPELL::GLOOM_SLASH,
441 ctx.effect_sp_coefficient(EFFECT::GLOOM_SLASH_DAMAGE) * VILEFIEND_SP_FROM_SP,
442 DamageFlags::PET,
443 );
444 }
445 }
446}
447
448fn summon_vilefiend(ctx: &mut HookCtx<'_>) {
449 let _ = ctx.summon_guardian(
450 GuardianSpec::new_inheriting_owner(REPORTED_SPELL::VILEFIEND, VILEFIEND_LIFETIME_MS)
451 .ability(
452 GuardianAbility::new(VILEFIEND_ACTION_MS, vilefiend_action)
453 .first_action_delay(VILEFIEND_ACTION_MS)
454 .hasted_actions(),
455 ),
456 );
457}
458
459fn dreadstalker_demise(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
460 ctx.consume_owner_aura_stack(AURA_DREADSTALKERS.raw());
461 ctx.apply_owner_aura(AURA_DEMONIC_CORE.raw());
462}
463
464fn demonic_tyrant_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
465 let static_demons = event.tag.saturating_sub(TYRANT_TAG_BASE);
467 let live_demons = ctx.active_guardian_count(AURA::WILD_IMPS)
468 + ctx.active_guardian_count(TALENT::IMP_GANG_BOSS)
469 + ctx.active_guardian_count(AURA::DREADSTALKERS);
470 let demonic_power = 1.0
471 + DEMONIC_POWER_PER_DEMON
472 * f64::from(
473 static_demons + wowlab_types::numeric::usize_to_u32_saturating(live_demons),
474 );
475 let coefficient = ctx
477 .game_data()
478 .effect_sp_coefficient(EFFECT::BURNING_CLEAVE);
479
480 ctx.deal_damage_sp(
481 EFFECT::BURNING_CLEAVE.0,
482 coefficient * demonic_power,
483 DamageFlags::PET,
484 );
485}
486
487pub(crate) fn hand_of_guldan_hook(ctx: &mut HookCtx<'_>) {
488 let ritual_was_active = DIABOLIC_RITUAL.active(ctx);
489 let next_ritual = DIABOLIC_RITUAL.consume_art(ctx);
490
491 if next_ritual.is_some() {
492 crate::hooks::shared::warlock::explode_oculi(ctx, WarlockSpec::Demonology);
493 }
494
495 DIABOLIC_RITUAL.advance(
496 ctx,
497 next_ritual,
498 MAX_SHARDS_PER_HAND_OF_GULDAN * DIABOLIC_RITUAL_REDUCTION_PER_SHARD_MS,
499 );
500
501 if ritual_was_active {
502 crate::hooks::shared::warlock::trigger_oculi(ctx, WarlockSpec::Demonology);
503 }
504
505 let dominion_mult = ctx.spec_config::<DemonologyConfig>().map_or(1.0, |cfg| {
506 1.0 + DOMINION_HAND_OF_GULDAN_PER_RANK * f64::from(cfg.dominion_rank_2)
507 });
508 let gloom_mult = if ctx
509 .spec_config::<DemonologyConfig>()
510 .is_some_and(|cfg| cfg.gloom_of_nathreza)
511 {
512 1.0 + GLOOM_OF_NATHREZA_PER_SHARD * f64::from(MAX_SHARDS_PER_HAND_OF_GULDAN)
513 } else {
514 1.0
515 };
516
517 ctx.deal_damage_sp(
518 REPORTED_SPELL::HAND_OF_GULDAN_IMPACT,
519 ctx.effect_sp_coefficient(EFFECT::HAND_OF_GULDAN_IMPACT_DAMAGE)
520 * f64::from(MAX_SHARDS_PER_HAND_OF_GULDAN)
521 * dominion_mult
522 * gloom_mult,
523 DamageFlags::NO_CRIT,
524 );
525
526 let gang_boss = ctx
527 .spec_config::<DemonologyConfig>()
528 .is_some_and(|cfg| cfg.imp_gang_boss);
529
530 for index in 0..MAX_SHARDS_PER_HAND_OF_GULDAN {
531 summon_wild_imp(ctx, gang_boss && index == 0);
532 }
533
534 let demonic_knowledge_rank = ctx
535 .spec_config::<DemonologyConfig>()
536 .map_or(0, |cfg| cfg.demonic_knowledge_rank);
537 let demonic_knowledge_chance = match demonic_knowledge_rank {
538 0 => 0.0,
539 1 => DEMONIC_KNOWLEDGE_RANK_1_CHANCE,
540 _ => DEMONIC_KNOWLEDGE_RANK_2_CHANCE,
541 };
542
543 if proc_chance(ctx.rng(), demonic_knowledge_chance) {
544 ctx.apply_aura(AURA_DEMONIC_CORE.raw());
545 }
546
547 trigger_dominion(ctx);
548}
549
550pub(crate) fn call_dreadstalkers_hook(ctx: &mut HookCtx<'_>) {
551 let next_ritual = DIABOLIC_RITUAL.next_from_art(ctx);
552
553 DIABOLIC_RITUAL.advance(ctx, next_ritual, 0);
554 let dreadstalker_lifetime_ms = if ctx
555 .spec_config::<DemonologyConfig>()
556 .is_some_and(|cfg| cfg.mid1_4pc)
557 {
558 DREADSTALKER_LIFETIME_MS + MID1_DEMONOLOGY_4PC_DURATION_MS
559 } else {
560 DREADSTALKER_LIFETIME_MS
561 };
562 let swing_ms = if ctx
563 .spec_config::<DemonologyConfig>()
564 .is_some_and(|cfg| cfg.flametouched)
565 {
566 wowlab_types::numeric::f64_to_u32_saturating_trunc(
567 f64::from(DREADSTALKER_SWING_MS) / FLAMETOUCHED_ATTACK_SPEED_MULT,
568 )
569 } else {
570 DREADSTALKER_SWING_MS
571 };
572
573 for _ in 0..DREADSTALKERS_PER_CALL {
574 ctx.apply_aura(AURA_DREADSTALKERS.raw());
575 let _ = ctx.summon_guardian(
576 GuardianSpec::new_inheriting_owner(AURA::DREADSTALKERS, dreadstalker_lifetime_ms)
577 .ability(
578 GuardianAbility::new(swing_ms, dreadstalker_action)
579 .first_action_delay(swing_ms)
580 .hasted_actions(),
581 )
582 .on_demise(dreadstalker_demise),
583 );
584 }
585
586 if ctx
587 .spec_config::<DemonologyConfig>()
588 .is_some_and(|cfg| cfg.summon_vilefiend)
589 {
590 summon_vilefiend(ctx);
591 }
592}
593
594pub(crate) fn demonbolt_hook(ctx: &mut HookCtx<'_>) {
595 ctx.consume_aura_stack(AURA_DEMONIC_CORE.raw());
596 ctx.consume_aura_stack(AURA_POWER_SIPHON.raw());
597 let spiteful_reconstitution = ctx
598 .spec_config::<DemonologyConfig>()
599 .is_some_and(|cfg| cfg.spiteful_reconstitution);
600
601 if spiteful_reconstitution && proc_chance(ctx.rng(), SPITEFUL_RECONSTITUTION_CHANCE) {
602 summon_wild_imp(ctx, false);
603 }
604}
605
606pub(crate) fn power_siphon_hook(ctx: &mut HookCtx<'_>) {
607 let normal = ctx.dismiss_guardians(AURA::WILD_IMPS, POWER_SIPHON_MAX_IMPS);
608 let sacrificed =
609 normal + ctx.dismiss_guardians(TALENT::IMP_GANG_BOSS, POWER_SIPHON_MAX_IMPS - normal);
610
611 for _ in 0..sacrificed {
612 ctx.consume_aura_stack(AURA_WILD_IMPS.raw());
613 ctx.apply_aura(AURA_DEMONIC_CORE.raw());
614 ctx.apply_aura(AURA_POWER_SIPHON.raw());
615 }
616
617 if ctx
618 .spec_config::<DemonologyConfig>()
619 .is_some_and(|cfg| cfg.to_hell_and_back)
620 {
621 for _ in 0..(sacrificed / TO_HELL_AND_BACK_IMPS_PER_BOSS) {
622 summon_imp_with_tag(ctx, REPORTED_SPELL::UNSTABLE_GANG_BOSS);
623 }
624 }
625}
626
627pub(crate) fn summon_demonic_tyrant_hook(ctx: &mut HookCtx<'_>) {
628 if ctx.is_aura_active(AURA_ABYSSAL_DOMINION_TALENT.raw()) {
629 ctx.apply_aura(AURA_ABYSSAL_DOMINION.raw());
630 }
631
632 if ctx
633 .spec_config::<DemonologyConfig>()
634 .is_some_and(|cfg| cfg.dominion_rank_1)
635 {
636 ctx.apply_aura(AURA_DOMINION_OF_ARGUS.raw());
637 }
638
639 if ctx
640 .spec_config::<DemonologyConfig>()
641 .is_some_and(|cfg| cfg.tyrants_oblation)
642 {
643 ctx.apply_aura(crate::generated::specs::demonology_warlock::AURA_TYRANTS_OBLATION.raw());
644 }
645
646 let reign_of_tyranny = ctx
647 .spec_config::<DemonologyConfig>()
648 .is_some_and(|cfg| cfg.reign_of_tyranny);
649
650 if reign_of_tyranny {
651 ctx.extend_guardians(AURA::DREADSTALKERS, REIGN_DREADSTALKER_EXTENSION_MS);
652 ctx.extend_aura(AURA_DREADSTALKERS.raw(), REIGN_DREADSTALKER_EXTENSION_MS);
653 }
654 let static_demons = ctx.active_guardian_count(REPORTED_SPELL::VILEFIEND) + 1;
657 let tyrant_duration_ms =
658 TYRANT_LIFETIME_MS + u32::from(reign_of_tyranny) * REIGN_OF_TYRANNY_EXTENSION_MS;
659 let _ = ctx.summon_guardian(
660 GuardianSpec::new_inheriting_owner(
661 TYRANT_TAG_BASE + wowlab_types::numeric::usize_to_u32_saturating(static_demons),
662 tyrant_duration_ms,
663 )
664 .ability(
665 GuardianAbility::new(TYRANT_ACTION_MS, demonic_tyrant_action)
666 .first_action_delay(TYRANT_ACTION_MS)
667 .hasted_actions(),
668 ),
669 );
670}
671
672const DEMONIC_BRUTALITY_PET_CRIT_BONUS_PTS: f64 = 60.0;
674
675#[must_use]
676pub(crate) fn crit_damage(ctx: &MasteryCtx<'_>) -> f64 {
677 let brutality = ctx
678 .state
679 .spec_config::<DemonologyConfig>()
680 .is_some_and(|cfg| cfg.demonic_brutality);
681
682 if !brutality || !ctx.is_pet {
683 return 0.0;
684 }
685
686 match ctx.spell_id {
687 1
688 | REPORTED_SPELL::FEL_FIREBOLT
689 | REPORTED_SPELL::DREADSTALKER_MELEE
690 | REPORTED_SPELL::DREADBITE
691 | REPORTED_SPELL::LEGION_STRIKE
692 | REPORTED_SPELL::FELSTORM => DEMONIC_BRUTALITY_PET_CRIT_BONUS_PTS,
693 id if id == EFFECT::BURNING_CLEAVE.0 => DEMONIC_BRUTALITY_PET_CRIT_BONUS_PTS,
694 _ => 0.0,
695 }
696}
697
698#[must_use]
700pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
701 let mastery = ctx.pet_bonus(1);
702 let hellbent = ctx
703 .state
704 .spec_config::<DemonologyConfig>()
705 .is_some_and(|cfg| cfg.hellbent_commander);
706
707 if ctx.is_pet && hellbent {
708 mastery
709 * (1.0 + wowlab_types::numeric::usize_to_f64(ctx.active_guardian_count() + 1) / HUNDRED)
710 } else {
711 mastery
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use googletest::prelude::*;
718 use wowlab_engine_combat::GuardianHandle;
719 use wowlab_engine_ports::{CombatStats};
720 use wowlab_engine_telemetry::{TelemetrySink};
721
722 use wowlab_types::sim::SimTime;
723
724 use super::*;
725
726 const UNUSUAL_IMP_CHANCE: f64 = 0.37;
727
728 fn imp_demise_outcome(chance: f64, roll: f64) -> Result<(usize, bool)> {
729 let mut builder = crate::test_combat_builder(CombatStats::default());
730
731 for local in 0..=AURA_WILD_IMPS.raw() {
732 let aura_id = match local {
733 value if value == AURA_DEMONIC_CORE.raw() => AURA::DEMONIC_CORE,
734 value if value == AURA_WILD_IMPS.raw() => AURA::WILD_IMPS,
735 _ => 900_000 + u32::from(local),
736 };
737
738 builder = builder.aura(&format!("demonology_test_aura_{local}"), aura_id, |aura| {
739 Ok(aura.on_player().max_stacks(20))
740 });
741 }
742
743 let mut built = builder.build(crate::test_support::wait_rotation()).or_fail()?;
744
745 built.state.set_spec_config(DemonologyConfig {
746 demonic_core_imp_chance: chance,
747 ..DemonologyConfig::default()
748 });
749 let mut telemetry = TelemetrySink::default();
750 let mut draws = 0;
751 let mut rng = || {
752 draws += 1;
753
754 roll
755 };
756 let core_active;
757
758 {
759 let mut ctx = HookCtx::new(
760 wowlab_engine_combat::HookCtxServices {
761 state: &mut built.state,
762 buf: &mut built.buffer,
763 sink: &mut telemetry,
764 rng: &mut rng,
765 },
766 wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
767 );
768
769 ctx.apply_aura(AURA_WILD_IMPS.raw());
770 wild_imp_demise(
771 &mut ctx,
772 GuardianEvent {
773 handle: GuardianHandle::new(0, 1),
774 tag: AURA::WILD_IMPS,
775 ability_index: 0,
776 action_index: 0,
777 spawned_at: SimTime::ZERO,
778 expires_at: SimTime::ZERO,
779 },
780 );
781 core_active = ctx.is_aura_active(AURA_DEMONIC_CORE.raw());
782 };
783
784 Ok((draws, core_active))
785 }
786
787 #[gtest]
788 fn imp_demise_skips_zero_chance_rng_and_uses_configured_threshold() -> Result<()> {
789 verify_that!(imp_demise_outcome(0.0, 0.0)?, eq((0, false)))?;
790 verify_that!(imp_demise_outcome(UNUSUAL_IMP_CHANCE, 0.36)?, eq((1, true)))?;
791
792 verify_that!(
793 imp_demise_outcome(UNUSUAL_IMP_CHANCE, 0.38)?,
794 eq((1, false))
795 )
796 }
797}