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 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, ¶ms);
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}