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