wowlab_engine_content/hooks/demonology_warlock/
config.rs1use 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, ¶ms);
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}