Skip to main content

wowlab_engine_content/hooks/destruction_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::SchedulingOps as _;
6use wowlab_engine_combat::{
7    DamageFlags, GuardianAbility, GuardianEvent, GuardianSpec, HookCtx, MasteryCtx,
8};
9use wowlab_engine_domain::damage::pet_swing_power_coefficient;
10use wowlab_engine_rng::proc_chance;
11use wowlab_types::{combat::DamageSchool, constants::HUNDRED};
12
13use crate::generated::specs::destruction_warlock::{
14    AURA, AURA_AVATAR_OF_DESTRUCTION_TALENT, AURA_BACKDRAFT, AURA_CHAOTIC_INFERNO,
15    AURA_CHAOTIC_INFERNO_TALENT, AURA_CONFLAGRATION_OF_CHAOS_TALENT, AURA_DARK_TITANS_MARK,
16    AURA_DEMONIC_ART_MOTHER, AURA_DEMONIC_ART_OVERLORD, AURA_DEMONIC_ART_PIT_LORD,
17    AURA_DIABOLIC_RITUAL_MOTHER, AURA_DIABOLIC_RITUAL_OVERLORD, AURA_DIABOLIC_RITUAL_PIT_LORD,
18    AURA_DIABOLIC_RITUAL_TALENT, AURA_EMBERS_OF_NIHILAM_1_TALENT, AURA_EMBERS_OF_NIHILAM_2_TALENT,
19    AURA_EMBERS_OF_NIHILAM_3_TALENT, AURA_FIENDISH_CRUELTY, AURA_FIENDISH_CRUELTY_TALENT,
20    AURA_INFERNAL_EMBERS, AURA_VISION_OF_NIHILAM, REPORTED_SPELL, SPELL,
21};
22use crate::hooks::shared::warlock::{DiabolicRitual, WarlockSpec};
23
24const DIABOLIC_RITUAL: DiabolicRitual = DiabolicRitual {
25    talent: AURA_DIABOLIC_RITUAL_TALENT,
26    art: [
27        AURA_DEMONIC_ART_OVERLORD,
28        AURA_DEMONIC_ART_MOTHER,
29        AURA_DEMONIC_ART_PIT_LORD,
30    ],
31    ritual: [
32        AURA_DIABOLIC_RITUAL_OVERLORD,
33        AURA_DIABOLIC_RITUAL_MOTHER,
34        AURA_DIABOLIC_RITUAL_PIT_LORD,
35    ],
36};
37
38/// Half of the Chaotic Energies bonus is guaranteed; the rest is a uniform roll in `[0, half]`.
39const CHAOTIC_ENERGIES_MEAN_ROLL: f64 = 0.75;
40
41/// Infernal pet AP/SP is 2.2275 times owner SP.
42const INFERNAL_COEF_FROM_SP: f64 = 2.2275;
43const INFERNAL_DURATION_MS: u32 = 30_000;
44const INFERNAL_ACTION_MS: u32 = 2_000;
45const DIABOLIC_RITUAL_REDUCTION_MS: u32 = 1_000;
46const OVERFIEND_DURATION_MS: u32 = 8_000;
47const OVERFIEND_CAST_MS: u32 = 3_000;
48const OVERFIEND_CHAOS_BOLT_EFFICIENCY: f64 = 0.60;
49const OVERFIEND_SHARDS: f64 = 0.8;
50pub(super) const SAYAAD_ACTION_MS: u32 = 6_000;
51const SAYAAD_DAMAGE_MULT: f64 = 1.15;
52const CHAOTIC_INFERNO_CHANCE: f64 = 0.25;
53const CHAOS_BOLT_RITUAL_SHARDS: u32 = 2;
54const FIENDISH_CRUELTY_BASE_CHANCE: f64 = 0.10;
55const OVERFIEND_BOLTS_PER_ACTION: f64 = 2.0;
56const OVERFIEND_MAX_ACTIONS: u16 = 3;
57const CONFLAGRATION_OF_CHAOS_MULT: f64 = 2.0;
58
59const fn pet_inherits_chaotic_energies(is_pet: bool, spell_id: u32) -> bool {
60    !is_pet || spell_id == SPELL::CHAOS_BOLT
61}
62
63pub(super) fn sayaad_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
64    let coefficient = ctx.effect_sp_coefficient(
65        crate::generated::specs::destruction_warlock::DAMAGE_EFFECT::LASH_OF_PAIN_DAMAGE,
66    );
67
68    ctx.deal_damage_sp(
69        crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::LASH_OF_PAIN,
70        coefficient * SAYAAD_DAMAGE_MULT,
71        DamageFlags::PET,
72    );
73}
74
75fn trigger_echo_of_sargeras(
76    ctx: &mut HookCtx<'_>,
77    report_action: wowlab_engine_combat::DamageActionId,
78) {
79    let coefficient = ctx.effect_sp_coefficient(
80        crate::generated::specs::destruction_warlock::DAMAGE_EFFECT::ECHO_OF_SARGERAS_DAMAGE,
81    );
82
83    ctx.deal_profiled_damage_sp(
84        report_action,
85        crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::ECHO_OF_SARGERAS,
86        coefficient,
87        DamageFlags::empty(),
88    );
89
90    if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_2_TALENT) {
91        ctx.apply_aura(AURA_VISION_OF_NIHILAM);
92    }
93
94    if ctx
95        .program()
96        .spec_config::<super::handler::DestructionConfig>()
97        .is_some_and(|cfg| cfg.mid2_4pc)
98    {
99        ctx.apply_aura(AURA_DARK_TITANS_MARK);
100    }
101}
102
103fn maybe_trigger_base_echo(ctx: &mut HookCtx<'_>) {
104    let chance = ctx
105        .program()
106        .spec_config::<super::handler::DestructionConfig>()
107        .map_or(super::handler::BASE_ECHO_CHANCE, |cfg| cfg.echo_chance);
108
109    if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_1_TALENT) && proc_chance(ctx.rng(), chance) {
110        trigger_echo_of_sargeras(
111            ctx,
112            crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::ECHO_OF_SARGERAS,
113        );
114    }
115}
116
117fn diabolist_spender(ctx: &mut HookCtx<'_>, reduction_ms: u32) {
118    let ritual_was_active = DIABOLIC_RITUAL.active(ctx);
119    let next_ritual = DIABOLIC_RITUAL.consume_art(ctx);
120
121    if next_ritual.is_some() {
122        crate::hooks::shared::warlock::explode_oculi(ctx, WarlockSpec::Destruction);
123    }
124
125    DIABOLIC_RITUAL.advance(ctx, next_ritual, reduction_ms);
126
127    if ritual_was_active {
128        crate::hooks::shared::warlock::trigger_oculi(ctx, WarlockSpec::Destruction);
129    }
130}
131
132fn maybe_trigger_fiendish_cruelty(ctx: &mut HookCtx<'_>, chance: f64) {
133    if ctx.is_aura_active(AURA_FIENDISH_CRUELTY_TALENT) && proc_chance(ctx.rng(), chance) {
134        ctx.apply_aura(AURA_FIENDISH_CRUELTY);
135    }
136}
137
138pub(crate) fn chaos_bolt_hook(ctx: &mut HookCtx<'_>) {
139    ctx.consume_aura_stack(AURA_BACKDRAFT);
140
141    if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_3_TALENT) {
142        trigger_echo_of_sargeras(
143            ctx,
144            crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::ECHO_OF_SARGERAS_CB,
145        );
146    }
147
148    if ctx.is_aura_active(AURA_CHAOTIC_INFERNO_TALENT)
149        && proc_chance(ctx.rng(), CHAOTIC_INFERNO_CHANCE)
150    {
151        ctx.apply_aura(AURA_CHAOTIC_INFERNO);
152    }
153
154    diabolist_spender(ctx, DIABOLIC_RITUAL_REDUCTION_MS * CHAOS_BOLT_RITUAL_SHARDS);
155    maybe_trigger_fiendish_cruelty(ctx, FIENDISH_CRUELTY_BASE_CHANCE);
156}
157
158pub(crate) fn conflagrate_hook(ctx: &mut HookCtx<'_>) {
159    ctx.apply_aura(AURA_BACKDRAFT);
160    maybe_trigger_fiendish_cruelty(ctx, FIENDISH_CRUELTY_BASE_CHANCE);
161}
162
163pub(crate) fn incinerate_hook(ctx: &mut HookCtx<'_>) {
164    let chaotic_inferno = ctx.is_aura_active(AURA_CHAOTIC_INFERNO);
165
166    if !chaotic_inferno {
167        ctx.consume_aura_stack(AURA_BACKDRAFT);
168    }
169
170    ctx.consume_aura(AURA_CHAOTIC_INFERNO);
171    maybe_trigger_base_echo(ctx);
172    let buffs = ctx.buff_totals();
173    let crit_chance = ((ctx.buf().player().crit + buffs.crit_pct) / HUNDRED).clamp(0.0, 1.0);
174
175    maybe_trigger_fiendish_cruelty(ctx, FIENDISH_CRUELTY_BASE_CHANCE * crit_chance);
176}
177
178fn overfiend_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
179    let coefficient = ctx.effect_sp_coefficient(
180        crate::generated::specs::destruction_warlock::DAMAGE_EFFECT::CHAOS_BOLT_DAMAGE,
181    );
182
183    ctx.deal_damage_sp(
184        crate::generated::specs::destruction_warlock::DAMAGE_SPELL::CHAOS_BOLT,
185        coefficient * OVERFIEND_CHAOS_BOLT_EFFICIENCY * OVERFIEND_BOLTS_PER_ACTION,
186        DamageFlags::PET | DamageFlags::NO_CRIT,
187    );
188}
189
190pub(crate) fn soul_fire_hook(ctx: &mut HookCtx<'_>) {
191    ctx.consume_aura_stack(AURA_BACKDRAFT);
192
193    if !ctx.is_aura_active(AURA_AVATAR_OF_DESTRUCTION_TALENT) {
194        return;
195    }
196
197    ctx.gain_resource(OVERFIEND_SHARDS);
198    let _ = ctx.summon_guardian(
199        GuardianSpec::new_inheriting_owner(AURA::OVERFIEND, OVERFIEND_DURATION_MS).ability(
200            GuardianAbility::new(OVERFIEND_CAST_MS, overfiend_action)
201                .first_action_delay(OVERFIEND_CAST_MS)
202                .hasted_actions()
203                .max_actions(OVERFIEND_MAX_ACTIONS)
204                .ends_guardian(),
205        ),
206    );
207}
208
209pub(crate) fn infernal_bolt_hook(ctx: &mut HookCtx<'_>) {
210    ctx.consume_aura_stack(AURA_BACKDRAFT);
211    ctx.consume_aura(AURA_CHAOTIC_INFERNO);
212    maybe_trigger_base_echo(ctx);
213}
214
215pub(crate) fn rain_of_fire_hook(ctx: &mut HookCtx<'_>) {
216    if let Some(cfg) = ctx
217        .program()
218        .spec_config::<super::handler::DestructionConfig>()
219        && let Some(effect) = cfg.rain_of_fire_ground
220    {
221        ctx.schedule_ground_effect(effect);
222    }
223
224    diabolist_spender(ctx, DIABOLIC_RITUAL_REDUCTION_MS);
225}
226
227pub(crate) fn shadowburn_hook(ctx: &mut HookCtx<'_>) {
228    if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_3_TALENT) {
229        trigger_echo_of_sargeras(
230            ctx,
231            crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::ECHO_OF_SARGERAS_SB,
232        );
233    }
234
235    diabolist_spender(ctx, DIABOLIC_RITUAL_REDUCTION_MS);
236    ctx.consume_aura(AURA_FIENDISH_CRUELTY);
237}
238
239pub(crate) fn ruination_hook(ctx: &mut HookCtx<'_>) {
240    if DIABOLIC_RITUAL.consume_art(ctx).is_some() {
241        crate::hooks::shared::warlock::explode_oculi(ctx, WarlockSpec::Destruction);
242    }
243}
244
245fn infernal_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
246    let swing = INFERNAL_COEF_FROM_SP * pet_swing_power_coefficient(1.0, INFERNAL_ACTION_MS, 1.0);
247
248    ctx.deal_damage_sp(
249        crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::INFERNAL_MELEE,
250        swing,
251        DamageFlags::PHYSICAL | DamageFlags::PET,
252    );
253    let immolation = ctx.effect_sp_coefficient(
254        crate::generated::specs::destruction_warlock::DAMAGE_EFFECT::IMMOLATION_DAMAGE,
255    ) * INFERNAL_COEF_FROM_SP;
256
257    ctx.deal_damage_sp(
258        crate::generated::specs::destruction_warlock::DAMAGE_REPORTED::IMMOLATION,
259        immolation,
260        DamageFlags::PET,
261    );
262}
263
264/// Summons a 30-second Infernal and starts its Embers fragment stream.
265pub(crate) fn summon_infernal_hook(ctx: &mut HookCtx<'_>) {
266    let _ = ctx.summon_guardian(
267        GuardianSpec::new_inheriting_owner(REPORTED_SPELL::INFERNAL_MELEE, INFERNAL_DURATION_MS)
268            .ability(
269                GuardianAbility::new(INFERNAL_ACTION_MS, infernal_action)
270                    .first_action_delay(INFERNAL_ACTION_MS)
271                    .hasted_actions(),
272            ),
273    );
274
275    ctx.apply_aura(AURA_INFERNAL_EMBERS);
276}
277
278/// Returns Chaotic Energies mastery damage.
279#[must_use]
280pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
281    // Ordinary pet spells do not inherit Chaotic Energies. Destruction rift
282    // and Overfiend Chaos Bolts opt in explicitly.
283    if !pet_inherits_chaotic_energies(ctx.is_pet, ctx.spell_id)
284        || matches!(
285            ctx.spell_id,
286            REPORTED_SPELL::DIABOLIC_GAZE_1
287                | REPORTED_SPELL::DIABOLIC_GAZE_2
288                | REPORTED_SPELL::DIABOLIC_GAZE_3
289        )
290    {
291        return 1.0;
292    }
293
294    let mastery = match ctx.school {
295        DamageSchool::Fire | DamageSchool::Chaos => {
296            1.0 + CHAOTIC_ENERGIES_MEAN_ROLL * ctx.points(1) / HUNDRED
297        }
298        _ => 1.0,
299    };
300
301    if ctx.spell_id == SPELL::CHAOS_BOLT {
302        let buffs = wowlab_engine_combat::buff_totals(ctx.program, ctx.instance, ctx.target);
303        let crit_fraction = (ctx.instance.player_crit() + buffs.crit_pct) / HUNDRED;
304
305        mastery * (1.0 + crit_fraction)
306    } else if matches!(ctx.spell_id, SPELL::CONFLAGRATE | SPELL::SHADOWBURN)
307        && ctx.is_aura_active(AURA_CONFLAGRATION_OF_CHAOS_TALENT)
308    {
309        mastery * CONFLAGRATION_OF_CHAOS_MULT
310    } else {
311        mastery
312    }
313}
314
315#[cfg(test)]
316mod tests {
317    use googletest::prelude::*;
318
319    use super::*;
320
321    #[gtest]
322    fn only_explicit_pet_chaos_bolts_inherit_chaotic_energies() -> Result<()> {
323        verify_false!(pet_inherits_chaotic_energies(
324            true,
325            REPORTED_SPELL::CHAOS_SALVO
326        ))?;
327        verify_false!(pet_inherits_chaotic_energies(
328            true,
329            REPORTED_SPELL::IMMOLATION
330        ))?;
331        verify_true!(pet_inherits_chaotic_energies(true, SPELL::CHAOS_BOLT))?;
332
333        verify_true!(pet_inherits_chaotic_energies(false, SPELL::CONFLAGRATE))
334    }
335}