Skip to main content

wowlab_engine_content/hooks/demonology_warlock/
hooks.rs

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