Skip to main content

wowlab_engine_content/hooks/demonology_warlock/
hooks.rs

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
52/// Bolts per imp: 100 energy at 16 cost (334591 e1) yields six before the imp dies.
53const FIREBOLTS_PER_IMP: u16 = 6;
54const WILD_IMP_LIFETIME_MS: u32 = 40_000;
55const WILD_IMP_CAST_MS: u32 = 2_000;
56/// Imp Gang Boss buff 1250772 e2: +100% damage.
57const IMP_GANG_BOSS_DAMAGE_MULT: f64 = 2.0;
58/// To Hell and Back gang boss also carries Unstable Soul 1281512 e1 (+50%).
59const UNSTABLE_SOUL_DAMAGE_MULT: f64 = 1.5;
60/// To Hell and Back 1281511: e1 = 1 Imp Gang Boss per e2 = 2 sacrificed imps.
61const 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;
87/// Felstorm 89751: six ticks over a five-second channel including tick zero.
88const FELSTORM_TICKS: u8 = 6;
89
90/// Dreadstalker pet AP is 0.825 times owner SP.
91const DREADSTALKER_AP_FROM_SP: f64 = 0.825;
92/// Pet swing coefficient: 2s swing / weapon-power coefficient 6; npc weapon dps (~13) unmodeled.
93const PET_SWING_AP: f64 = 2.0 / 6.0;
94const DREADSTALKER_MELEE_WM: f64 = 0.96;
95/// Flametouched 453699 e1: +10% Dreadstalker attack speed.
96const FLAMETOUCHED_ATTACK_SPEED_MULT: f64 = 1.10;
97/// Flametouched 453699 e2 (buff 453704 e2): +15% Dreadstalker crit chance.
98pub(super) const FLAMETOUCHED_CRIT_PCT: f64 = 15.0;
99/// Dreadstalker attacks that Flametouched's crit bonus applies to.
100#[rustfmt::skip]
101pub(super) const FLAMETOUCHED_CRIT_SPELLS: [u32; 2] = [
102    REPORTED_SPELL::DREADSTALKER_MELEE,
103    REPORTED_SPELL::DREADBITE,
104];
105/// 12s summon (193332) with a 2s base swing.
106const 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    // Flametouched critical strike chance is already carried by its aura.
357    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    // Vilefiend and Felguard Demonic Power stacks never decrement and ride in the tag.
466    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    // Abyssal Dominion 429581 e1 is already folded into spell 1264093.
476    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    // Only never-decremented Vilefiend and Felguard stacks ride in the tag.
655
656    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
672// Demonic Brutality 453908 e1/e2 yields 260% pet crits; Vilefiend's shared report ID causes a known melee-row overshoot.
673const 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/// Returns Master Demonologist pet damage.
699#[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}