Skip to main content

wowlab_engine_content/hooks/demonology_warlock/
config.rs

1use wowlab_engine_combat::BuffEffect;
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
17crate::hooks::define_spec_config! {
18    pub(super) struct DemonologyConfig {
19        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 },
20        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 },
21        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 },
22        demonic_brutality: bool { provenance: "Demonic Brutality enables its guardian damage rider.", gate: params.talent_ranks(TALENT::DEMONIC_BRUTALITY) > 0, source: true, transform: |value| value },
23        demonic_core_imp_chance: f64 { provenance: "Demonic Core 267102 e1 supplies the Wild Imp fade chance when Demoniac is selected (`SimC` `sc_warlock_init.cpp:1265`).", gate: params.talent_picked(TALENT::DEMONIAC), source: params.game_data.effect_base_points(EFFECT::DEMONIC_CORE_IMP_CHANCE), transform: |value| value / HUNDRED },
24        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 },
25        blighted_maw: bool { provenance: "Blighted Maw enables its Dreadstalker rider.", gate: params.talent_ranks(TALENT::BLIGHTED_MAW) > 0, source: true, transform: |value| value },
26        dreadlash: bool { provenance: "Dreadlash enables its Dreadstalker rider.", gate: params.talent_ranks(TALENT::DREADLASH) > 0, source: true, transform: |value| value },
27        flametouched: bool { provenance: "Flametouched enables its guardian critical-strike aura.", gate: params.talent_ranks(TALENT::FLAMETOUCHED) > 0, source: true, transform: |value| value },
28        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 },
29        hellbent_commander: bool { provenance: "Hellbent Commander scales mastery with active demons.", gate: params.talent_ranks(TALENT::HELLBENT_COMMANDER) > 0, source: true, transform: |value| value },
30        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 },
31        infernal_rapidity: bool { provenance: "Infernal Rapidity enables its guardian action proc.", gate: params.talent_ranks(TALENT::INFERNAL_RAPIDITY) > 0, source: true, transform: |value| value },
32        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 },
33        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 },
34        mid1_4pc: bool { provenance: "MID1 Demonology four-piece enables its tier rider.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID1_DEMONOLOGY_4PC), source: true, transform: |value| value },
35        spiteful_reconstitution: bool { provenance: "Spiteful Reconstitution enables its guardian proc.", gate: params.talent_ranks(TALENT::SPITEFUL_RECONSTITUTION) > 0, source: true, transform: |value| value },
36        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 },
37        tyrants_oblation: bool { provenance: "Tyrant's Oblation enables its tyrant rider.", gate: params.talent_ranks(TALENT::TYRANTS_OBLATION) > 0, source: true, transform: |value| value },
38        summon_vilefiend: bool { provenance: "Summon Vilefiend enables the Vilefiend guardian.", gate: params.talent_ranks(TALENT::SUMMON_VILEFIEND) > 0, source: true, transform: |value| value },
39    }
40    accessor pub(super) fn demonology_config(ctx);
41    register pub(super) fn register_demonology_config(built, params) -> ();
42    prepare {}
43    auras {}
44    finish |cfg| {
45        if cfg.dominion_rank_2 > 0 {
46            built.patch_aura(AURA_DOMINION_OF_ARGUS, |aura| {
47                aura.base_duration_ms = DOMINION_BASE_DURATION_MS + u32::from(cfg.dominion_rank_2) * DOMINION_RANK_2_EXTENSION_MS;
48            });
49        }
50        if cfg.reign_of_tyranny {
51            built.patch_aura(crate::generated::specs::demonology_warlock::AURA_TYRANTS_OBLATION, |aura| {
52                aura.base_duration_ms = TYRANT_LIFETIME_MS + REIGN_OF_TYRANNY_EXTENSION_MS;
53            });
54        }
55        if cfg.flametouched {
56            built.set_aura_effect(
57                crate::generated::specs::demonology_warlock::AURA_FLAMETOUCHED_TALENT,
58                BuffEffect::CritChanceSpells(FLAMETOUCHED_CRIT_PCT, &FLAMETOUCHED_CRIT_SPELLS),
59            );
60        }
61        built.state.set_spec_config(cfg);
62    }
63}
64
65pub(super) fn apply_demonic_calling(
66    built: &mut wowlab_engine_combat::BuiltCombatSystem,
67    ranks: u8,
68) {
69    if ranks == 0 {
70        return;
71    }
72
73    let Some(local) = built.state.spell_local(SPELL::CALL_DREADSTALKERS) else {
74        return;
75    };
76    let cost = (DEMONIC_CALLING_BASE_COST - f64::from(ranks)).max(0.0);
77    let cast_time_ms = DEMONIC_CALLING_BASE_CAST_MS
78        .saturating_sub(u32::from(ranks) * DEMONIC_CALLING_CAST_REDUCTION_MS);
79
80    built.patch_spell(local, |spell| {
81        spell.cost.resource_cost = cost;
82        spell.cast_time_ms = cast_time_ms;
83    });
84}
85
86#[cfg(test)]
87mod tests {
88    use googletest::prelude::*;
89    use wowlab_engine_ports::{CombatStats, ConsumableFlags, TalentSelection};
90use wowlab_engine_gamedata::{ResolvedGameData};
91    use wowlab_types::game::RaceId;
92
93    use super::*;
94
95    const UNUSUAL_IMP_CHANCE_PERCENT: f64 = 37.0;
96
97    fn registered_config(demoniac_selected: bool) -> Result<DemonologyConfig> {
98        let encounter = wowlab_engine_ports::test_support::introspection_fixture(60.0).or_fail()?;
99        let environment = wowlab_engine_ports::test_support::introspection_game_data(&encounter);
100        let mut data = ResolvedGameData::builder();
101
102        data.set_armor_constant(environment.armor_constant());
103        data.set_creature_armor(environment.creature_armor());
104        data.set_armor_constant_mod(environment.armor_constant_mod());
105        data.insert_base_points(
106            SpellIdx::from_raw(EFFECT::DEMONIC_CORE_IMP_CHANCE.0),
107            EFFECT::DEMONIC_CORE_IMP_CHANCE.1,
108            UNUSUAL_IMP_CHANCE_PERCENT,
109        );
110        let data = data.build();
111        let stats = CombatStats::default();
112        let demoniac = [TalentSelection {
113            spell_id: TALENT::DEMONIAC,
114            override_spell_id: 0,
115            replaces_spell_id: 0,
116            ranks: 1,
117            precombat_aura: false,
118            hero_tree: None,
119            effect_overrides: Vec::new(),
120        }];
121        let talents = if demoniac_selected {
122            demoniac.as_slice()
123        } else {
124            &[]
125        };
126        let rotation = crate::test_support::wait_rotation();
127        let params =
128            crate::test_support::handler_params(crate::test_support::HandlerParamsFixture {
129                game_data: data.clone(),
130                rotation: &rotation,
131                stats: &stats,
132                talent_selections: talents,
133                encounter: &encounter,
134                consumables: ConsumableFlags::default(),
135                race: RaceId::Human,
136            })
137            .or_fail()?;
138        let mut built = crate::test_combat_builder(CombatStats::default())
139            .game_data(data)
140            .build(crate::test_support::wait_rotation())
141            .or_fail()?;
142
143        register_demonology_config(&mut built, &params);
144
145        built
146            .state
147            .spec_config::<DemonologyConfig>()
148            .copied()
149            .or_fail()
150    }
151
152    #[gtest]
153    fn imp_fade_chance_uses_unusual_demonic_core_effect_value() -> Result<()> {
154        let config = registered_config(true)?;
155
156        verify_that!(
157            config.demonic_core_imp_chance,
158            near(UNUSUAL_IMP_CHANCE_PERCENT / HUNDRED, f64::EPSILON)
159        )
160    }
161
162    #[gtest]
163    fn imp_fade_chance_is_zero_without_demoniac() -> Result<()> {
164        let config = registered_config(false)?;
165
166        verify_that!(config.demonic_core_imp_chance, eq(0.0))
167    }
168}