Skip to main content

wowlab_engine_content/hooks/demonology_warlock/
config.rs

1use wowlab_engine_combat::{BuffEffect, DriverSpellModifier, DriverSpellModifierResult};
2use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
3use wowlab_types::constants::HUNDRED;
4#[cfg(test)]
5use wowlab_types::sim::SpellIdx;
6
7use crate::generated::specs::demonology_warlock::{
8    AURA_DOMINION_OF_ARGUS, EFFECT, SET_BONUS, SPELL, TALENT,
9};
10
11use super::hooks::{
12    DEMONIC_CALLING_BASE_CAST_MS, DEMONIC_CALLING_BASE_COST, DEMONIC_CALLING_CAST_REDUCTION_MS,
13    DOMINION_BASE_DURATION_MS, DOMINION_RANK_2_EXTENSION_MS, FLAMETOUCHED_CRIT_PCT,
14    FLAMETOUCHED_CRIT_SPELLS, REIGN_OF_TYRANNY_EXTENSION_MS, TYRANT_LIFETIME_MS,
15};
16
17static MID2_IMPLOSION_SPELLS: [u32; 3] = [196_277, 196_278, 1_306_077];
18const POWER_SIPHON_COOLDOWN_MS: u32 = 30_000;
19const POWER_SIPHON_GCD_MS: u32 = 1_500;
20
21crate::hooks::define_spec_config! {
22    pub(super) struct DemonologyConfig {
23        dominion_rank_1: bool { provenance: "Dominion of Argus rank one enables the cast counter.", gate: params.talent_ranks(TALENT::DOMINION_OF_ARGUS) > 0, source: true, transform: |value| value },
24        dominion_rank_2: u8 { provenance: "Dominion of Argus rank two extends Dominion duration.", gate: true, source: params.talent_ranks(TALENT::DOMINION_OF_ARGUS_2), transform: |value| value },
25        dominion_rank_3: bool { provenance: "Dominion of Argus rank three extends guardian duration.", gate: params.talent_ranks(TALENT::DOMINION_OF_ARGUS_3) > 0, source: true, transform: |value| value },
26        demonic_brutality: bool { provenance: "Demonic Brutality enables its guardian damage rider.", gate: params.talent_ranks(TALENT::DEMONIC_BRUTALITY) > 0, source: true, transform: |value| value },
27        demonic_core_imp_chance: f64 { provenance: "Demonic Core 267102 e1 supplies the Wild Imp fade chance when Demoniac is selected.", gate: params.talent_picked(TALENT::DEMONIAC), source: params.game_data.effect_base_points(EFFECT::DEMONIC_CORE_IMP_CHANCE), transform: |value| value / HUNDRED },
28        demonic_knowledge_rank: u8 { provenance: "Demonic Knowledge selected rank controls its proc chance.", gate: true, source: params.talent_ranks(TALENT::DEMONIC_KNOWLEDGE), transform: |value| value },
29        blighted_maw: bool { provenance: "Blighted Maw enables its Dreadstalker rider.", gate: params.talent_ranks(TALENT::BLIGHTED_MAW) > 0, source: true, transform: |value| value },
30        dreadlash: bool { provenance: "Dreadlash enables its Dreadstalker rider.", gate: params.talent_ranks(TALENT::DREADLASH) > 0, source: true, transform: |value| value },
31        flametouched: bool { provenance: "Flametouched enables its guardian critical-strike aura.", gate: params.talent_ranks(TALENT::FLAMETOUCHED) > 0, source: true, transform: |value| value },
32        gloom_of_nathreza: bool { provenance: "Gloom of Nathreza enables its shard-spend scaling.", gate: params.talent_ranks(TALENT::GLOOM_OF_NATHREZA) > 0, source: true, transform: |value| value },
33        hellbent_commander: bool { provenance: "Hellbent Commander scales mastery with active demons.", gate: params.talent_ranks(TALENT::HELLBENT_COMMANDER) > 0, source: true, transform: |value| value },
34        imp_gang_boss: bool { provenance: "Imp Gang Boss enables wild-imp promotions.", gate: params.talent_ranks(TALENT::IMP_GANG_BOSS) > 0, source: true, transform: |value| value },
35        infernal_rapidity: bool { provenance: "Infernal Rapidity enables its guardian action proc.", gate: params.talent_ranks(TALENT::INFERNAL_RAPIDITY) > 0, source: true, transform: |value| value },
36        reign_of_tyranny: bool { provenance: "Reign of Tyranny extends the Demonic Tyrant window.", gate: params.talent_ranks(TALENT::REIGN_OF_TYRANNY) > 0, source: true, transform: |value| value },
37        mark_of_shatug: bool { provenance: "Mark of Shatug selects its Vilefiend variant.", gate: params.talent_ranks(TALENT::MARK_OF_SHATUG) > 0, source: true, transform: |value| value },
38        mid2_imp_mult: f64 { provenance: "MID2 Demonology two-piece Wild Imp damage from 1296573 e1.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_DEMONOLOGY_2PC), source: params.game_data.effect_base_points(EFFECT::MID2_DEMONOLOGY_2PC_IMP), transform: |value| 1.0 + value / HUNDRED, fallback: 1.0 },
39        mid2_implosion_mult: f64 { provenance: "MID2 Demonology two-piece Implosion damage from 1296573 e2.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_DEMONOLOGY_2PC), source: params.game_data.effect_base_points(EFFECT::MID2_DEMONOLOGY_2PC_IMPLOSION), transform: |value| 1.0 + value / HUNDRED, fallback: 1.0 },
40        mid2_isolated_chance: f64 { provenance: "MID2 Demonology four-piece isolated Implosion chance from 1296574 e3.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_DEMONOLOGY_4PC), source: params.game_data.effect_base_points(EFFECT::MID2_DEMONOLOGY_4PC_CHANCE), transform: |value| value / HUNDRED },
41        mid2_isolated_main_mult: f64 { provenance: "MID2 Demonology four-piece primary Implosion multiplier from 1296574 e1.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_DEMONOLOGY_4PC), source: params.game_data.effect_base_points(EFFECT::MID2_DEMONOLOGY_4PC_MAIN), transform: |value| value / HUNDRED },
42        mid2_isolated_secondary_mult: f64 { provenance: "MID2 Demonology four-piece secondary Implosion multiplier from 1296574 e2.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_DEMONOLOGY_4PC), source: params.game_data.effect_base_points(EFFECT::MID2_DEMONOLOGY_4PC_SECONDARY), transform: |value| value / HUNDRED },
43        spiteful_reconstitution: bool { provenance: "Spiteful Reconstitution enables its guardian proc.", gate: params.talent_ranks(TALENT::SPITEFUL_RECONSTITUTION) > 0, source: true, transform: |value| value },
44        to_hell_and_back: bool { provenance: "To Hell and Back enables gang-boss conversion.", gate: params.talent_ranks(TALENT::TO_HELL_AND_BACK) > 0, source: true, transform: |value| value },
45        tyrants_oblation: bool { provenance: "Tyrant's Oblation enables its tyrant rider.", gate: params.talent_ranks(TALENT::TYRANTS_OBLATION) > 0, source: true, transform: |value| value },
46        summon_vilefiend: bool { provenance: "Summon Vilefiend enables the Vilefiend guardian.", gate: params.talent_ranks(TALENT::SUMMON_VILEFIEND) > 0, source: true, transform: |value| value },
47    }
48    accessor pub(super) fn demonology_config(ctx);
49    register pub(super) fn register_demonology_config(built, params) -> ();
50    prepare {}
51    auras {}
52    finish |cfg| {
53        if let Some(local) = built.spell_local(SPELL::IMPLOSION) {
54            built.patch_spell(local, |spell| {
55                // Wild Imp availability is owned by guardian state, not the stale DBC aura.
56                spell.requirements.caster_aura_spell = 0;
57            });
58        }
59        if let Some(local) = built.spell_local(SPELL::POWER_SIPHON) {
60            built.patch_spell(local, |spell| {
61                spell.cooldown.cooldown_duration_ms = POWER_SIPHON_COOLDOWN_MS;
62                spell.gcd_ms = POWER_SIPHON_GCD_MS;
63            });
64        }
65        if cfg.mid2_implosion_mult > 1.0 {
66            built.register_driver_spell_modifier(DriverSpellModifier::new(
67                SET_BONUS::MID2_DEMONOLOGY_2PC,
68                &MID2_IMPLOSION_SPELLS,
69                mid2_implosion_modifier,
70            ));
71        }
72        if cfg.dominion_rank_2 > 0 {
73            built.patch_aura(AURA_DOMINION_OF_ARGUS, |aura| {
74                aura.base_duration_ms = DOMINION_BASE_DURATION_MS + u32::from(cfg.dominion_rank_2) * DOMINION_RANK_2_EXTENSION_MS;
75            });
76        }
77        if cfg.reign_of_tyranny {
78            built.patch_aura(crate::generated::specs::demonology_warlock::AURA_TYRANTS_OBLATION, |aura| {
79                aura.base_duration_ms = TYRANT_LIFETIME_MS + REIGN_OF_TYRANNY_EXTENSION_MS;
80            });
81        }
82        if cfg.flametouched {
83            built.set_aura_effect(
84                crate::generated::specs::demonology_warlock::AURA_FLAMETOUCHED_TALENT,
85                BuffEffect::CritChanceSpells(FLAMETOUCHED_CRIT_PCT, &FLAMETOUCHED_CRIT_SPELLS),
86            );
87        }
88        built.set_spec_config(cfg);
89    }
90}
91
92#[derive(Clone, Copy, Debug, Default)]
93pub(super) struct DemonologyRuntime {
94    pub(super) isolated_implosion_attempts: u32,
95}
96
97fn mid2_implosion_modifier(view: wowlab_engine_combat::HookView<'_>) -> DriverSpellModifierResult {
98    DriverSpellModifierResult::damage_mult(
99        view.program()
100            .spec_config::<DemonologyConfig>()
101            .map_or(1.0, |cfg| cfg.mid2_implosion_mult),
102    )
103}
104
105pub(super) fn apply_demonic_calling(
106    built: &mut wowlab_engine_combat::CombatSystemDraft,
107    ranks: u8,
108) {
109    if ranks == 0 {
110        return;
111    }
112
113    let Some(local) = built.spell_local(SPELL::CALL_DREADSTALKERS) else {
114        return;
115    };
116    let cost = (DEMONIC_CALLING_BASE_COST - f64::from(ranks)).max(0.0);
117    let cast_time_ms = DEMONIC_CALLING_BASE_CAST_MS
118        .saturating_sub(u32::from(ranks) * DEMONIC_CALLING_CAST_REDUCTION_MS);
119
120    built.patch_spell(local, |spell| {
121        spell.cost.resource_cost = cost;
122        spell.cast_time_ms = cast_time_ms;
123    });
124}
125
126#[cfg(test)]
127mod tests {
128    use googletest::prelude::*;
129    use wowlab_engine_gamedata::ResolvedGameData;
130    use wowlab_engine_ports::{CombatStats, ConsumableFlags, TalentSelection};
131    use wowlab_types::game::RaceId;
132
133    use super::*;
134
135    const UNUSUAL_IMP_CHANCE_PERCENT: f64 = 37.0;
136
137    fn registered_config(demoniac_selected: bool) -> Result<DemonologyConfig> {
138        let encounter = wowlab_engine_ports::test_support::introspection_fixture(60.0).or_fail()?;
139        let environment = wowlab_engine_ports::test_support::introspection_game_data(&encounter);
140        let mut data = ResolvedGameData::builder();
141
142        data.set_armor_constant(environment.armor_constant());
143        data.set_creature_armor(environment.creature_armor());
144        data.set_armor_constant_mod(environment.armor_constant_mod());
145        data.insert_base_points(
146            SpellIdx::from_raw(EFFECT::DEMONIC_CORE_IMP_CHANCE.0),
147            EFFECT::DEMONIC_CORE_IMP_CHANCE.1,
148            UNUSUAL_IMP_CHANCE_PERCENT,
149        );
150        let data = data.build();
151        let stats = CombatStats::default();
152        let demoniac = [TalentSelection {
153            spell_id: TALENT::DEMONIAC,
154            override_spell_id: 0,
155            replaces_spell_id: 0,
156            ranks: 1,
157            precombat_aura: false,
158            hero_tree: None,
159            effect_overrides: Vec::new(),
160        }];
161        let talents = if demoniac_selected {
162            demoniac.as_slice()
163        } else {
164            &[]
165        };
166        let rotation = crate::test_support::wait_rotation();
167        let params =
168            crate::test_support::handler_params(crate::test_support::HandlerParamsFixture {
169                game_data: data.clone(),
170                rotation: &rotation,
171                stats: &stats,
172                talent_selections: talents,
173                encounter: &encounter,
174                consumables: ConsumableFlags::default(),
175                race: RaceId::Human,
176            })
177            .or_fail()?;
178        let mut draft = crate::test_combat_builder(CombatStats::default())
179            .game_data(data)
180            .into_draft()
181            .or_fail()?;
182
183        register_demonology_config(&mut draft, &params);
184        let built = draft
185            .finalize(&crate::test_support::wait_rotation())
186            .or_fail()?;
187
188        built
189            .program
190            .spec_config::<DemonologyConfig>()
191            .copied()
192            .or_fail()
193    }
194
195    #[gtest]
196    fn imp_fade_chance_uses_unusual_demonic_core_effect_value() -> Result<()> {
197        let config = registered_config(true)?;
198
199        verify_that!(
200            config.demonic_core_imp_chance,
201            near(UNUSUAL_IMP_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
202        )
203    }
204
205    #[gtest]
206    fn imp_fade_chance_is_zero_without_demoniac() -> Result<()> {
207        let config = registered_config(false)?;
208
209        verify_that!(config.demonic_core_imp_chance, eq(0.0))
210    }
211}