Skip to main content

wowlab_engine_content/hooks/shared/
warlock.rs

1use wowlab_engine_combat::{
2    AuraOps as _, DamageFlags, DamageOps as _, GuardianAbility, GuardianEvent, GuardianOps as _,
3    GuardianSpec, HookCtx, LocalAuraIdx,
4};
5use wowlab_types::constants::MS_PER_SECOND;
6
7use crate::generated::{
8    shared::warlock::diabolist::{
9        DAMAGE_EFFECT as DIABOLIC_DAMAGE_EFFECT, DAMAGE_REPORTED as DIABOLIC_DAMAGE_REPORTED,
10    },
11    specs::{
12        demonology_warlock::{
13            AURA_DEMONIC_ART_MOTHER as DEMO_ART_MOTHER,
14            AURA_DEMONIC_ART_OVERLORD as DEMO_ART_OVERLORD,
15            AURA_DEMONIC_ART_PIT_LORD as DEMO_ART_PIT_LORD, AURA_DEMONIC_OCULI as DEMO_OCULI,
16            AURA_DIABOLIC_OCULI_TALENT as DEMO_OCULI_TALENT,
17            AURA_INFERNAL_BOLT as DEMO_INFERNAL_BOLT,
18            AURA_LOOKS_THAT_KILL_TALENT as DEMO_LOOKS_THAT_KILL,
19            AURA_MINDS_EYES as DEMO_MINDS_EYES, AURA_MINDS_EYES_TALENT as DEMO_MINDS_EYES_TALENT,
20            AURA_RUINATION as DEMO_RUINATION,
21        },
22        destruction_warlock::{
23            AURA_DEMONIC_ART_MOTHER as DESTRO_ART_MOTHER,
24            AURA_DEMONIC_ART_OVERLORD as DESTRO_ART_OVERLORD,
25            AURA_DEMONIC_ART_PIT_LORD as DESTRO_ART_PIT_LORD, AURA_DEMONIC_OCULI as DESTRO_OCULI,
26            AURA_DIABOLIC_OCULI_TALENT as DESTRO_OCULI_TALENT,
27            AURA_INFERNAL_BOLT as DESTRO_INFERNAL_BOLT,
28            AURA_LOOKS_THAT_KILL_TALENT as DESTRO_LOOKS_THAT_KILL,
29            AURA_MINDS_EYES as DESTRO_MINDS_EYES,
30            AURA_MINDS_EYES_TALENT as DESTRO_MINDS_EYES_TALENT, AURA_RUINATION as DESTRO_RUINATION,
31        },
32    },
33};
34
35// Synthetic guardian tags must remain outside the game spell-ID range.
36const DEMONOLOGY_TAG_BASE: u32 = 4_285_140;
37const DESTRUCTION_TAG_BASE: u32 = 4_285_150;
38const DEMONOLOGY_DAMAGE_MULT: f64 = 0.80;
39const DESTRUCTION_DAMAGE_MULT: f64 = 1.15;
40const GUARDIAN_LIFETIME_MS: u32 = 8_000;
41const OVERLORD_ACTION_DELAY_MS: u32 = 1_000;
42const OVERLORD_TAG_OFFSET: u32 = 1;
43const OVERLORD_MAX_ACTIONS: u16 = 1;
44const OVERLORD_AP_FROM_SP: f64 = 1.0;
45const MOTHER_TAG_OFFSET: u32 = 2;
46const PIT_LORD_TAG_OFFSET: u32 = 3;
47const PIT_LORD_ACTION_DELAY_MS: u32 = 500;
48const PIT_LORD_MAX_ACTIONS: u16 = 4;
49
50const RITUAL_VARIANT_COUNT: usize = 3;
51const RITUAL_FIRST_THRESHOLD: f64 = 1.0 / 3.0;
52const RITUAL_SECOND_THRESHOLD: f64 = 2.0 / 3.0;
53const OCULI_GAZE_ONE_STACK: i32 = 1;
54const OCULI_GAZE_TWO_STACKS: i32 = 2;
55const OCULI_GAZE_THREE_STACKS: i32 = 3;
56const OCULI_CONTROLLER_TAG_OFFSET: u32 = 4;
57
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59#[non_exhaustive]
60pub(crate) enum WarlockSpec {
61    Demonology,
62    Destruction,
63}
64
65#[derive(Clone, Copy, Debug)]
66enum DiabolicDemon {
67    Overlord,
68    Mother,
69    PitLord,
70}
71
72#[derive(Clone, Copy, Debug)]
73pub(crate) struct DiabolicRitual {
74    pub talent: LocalAuraIdx,
75    pub art: [LocalAuraIdx; RITUAL_VARIANT_COUNT],
76    pub ritual: [LocalAuraIdx; RITUAL_VARIANT_COUNT],
77}
78
79impl DiabolicRitual {
80    #[must_use]
81    pub(crate) fn active(self, ctx: &HookCtx<'_>) -> bool {
82        self.ritual.into_iter().any(|aura| ctx.is_aura_active(aura))
83    }
84
85    #[must_use]
86    pub(crate) fn next_from_art(self, ctx: &HookCtx<'_>) -> Option<LocalAuraIdx> {
87        let [overlord_art, mother_art, pit_lord_art] = self.art;
88        let [overlord_ritual, mother_ritual, pit_lord_ritual] = self.ritual;
89
90        if ctx.is_aura_active(overlord_art) {
91            Some(mother_ritual)
92        } else if ctx.is_aura_active(mother_art) {
93            Some(pit_lord_ritual)
94        } else if ctx.is_aura_active(pit_lord_art) {
95            Some(overlord_ritual)
96        } else {
97            None
98        }
99    }
100
101    pub(crate) fn consume_art(self, ctx: &mut HookCtx<'_>) -> Option<LocalAuraIdx> {
102        let next = self.next_from_art(ctx);
103
104        for aura in self.art {
105            if ctx.consume_aura(aura) {
106                return next;
107            }
108        }
109
110        None
111    }
112
113    pub(crate) fn advance(
114        self,
115        ctx: &mut HookCtx<'_>,
116        next: Option<LocalAuraIdx>,
117        reduction_ms: u32,
118    ) {
119        if !ctx.is_aura_active(self.talent) {
120            return;
121        }
122
123        for aura in self.ritual {
124            if ctx.is_aura_active(aura) {
125                ctx.reduce_aura(aura, reduction_ms);
126
127                return;
128            }
129        }
130
131        let aura = next.unwrap_or_else(|| {
132            let roll = ctx.rng()();
133            let [overlord, mother, pit_lord] = self.ritual;
134
135            if roll < RITUAL_FIRST_THRESHOLD {
136                overlord
137            } else if roll < RITUAL_SECOND_THRESHOLD {
138                mother
139            } else {
140                pit_lord
141            }
142        });
143
144        ctx.apply_aura(aura);
145        ctx.reduce_aura(aura, reduction_ms);
146    }
147}
148
149const fn damage_mult(tag: u32) -> f64 {
150    if tag >= DESTRUCTION_TAG_BASE {
151        DESTRUCTION_DAMAGE_MULT
152    } else {
153        DEMONOLOGY_DAMAGE_MULT
154    }
155}
156
157fn overlord_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
158    let coefficient = ctx.effect_ap_coefficient(DIABOLIC_DAMAGE_EFFECT::WICKED_CLEAVE_DAMAGE);
159
160    ctx.deal_damage_sp(
161        DIABOLIC_DAMAGE_REPORTED::WICKED_CLEAVE,
162        coefficient * OVERLORD_AP_FROM_SP * damage_mult(event.tag),
163        DamageFlags::PET | DamageFlags::IGNORE_CRIT_DAMAGE_BONUSES,
164    );
165}
166
167fn mother_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
168    let coefficient = ctx.effect_sp_coefficient(DIABOLIC_DAMAGE_EFFECT::CHAOS_SALVO_DAMAGE);
169
170    ctx.deal_damage_sp(
171        DIABOLIC_DAMAGE_REPORTED::CHAOS_SALVO,
172        coefficient * damage_mult(event.tag),
173        DamageFlags::PET,
174    );
175}
176
177fn pit_lord_action(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
178    let coefficient = ctx.effect_sp_coefficient(DIABOLIC_DAMAGE_EFFECT::FELSEEKER_DAMAGE);
179
180    ctx.deal_damage_sp(
181        DIABOLIC_DAMAGE_REPORTED::FELSEEKER,
182        coefficient * damage_mult(event.tag),
183        DamageFlags::PET,
184    );
185}
186
187const fn tag(base: u32, spec: WarlockSpec) -> u32 {
188    match spec {
189        WarlockSpec::Demonology => DEMONOLOGY_TAG_BASE + base,
190        WarlockSpec::Destruction => DESTRUCTION_TAG_BASE + base,
191    }
192}
193
194fn summon_overlord(ctx: &mut HookCtx<'_>, spec: WarlockSpec) {
195    let _ = ctx.summon_guardian(
196        GuardianSpec::new_inheriting_owner(tag(OVERLORD_TAG_OFFSET, spec), GUARDIAN_LIFETIME_MS)
197            .ability(
198                GuardianAbility::new(OVERLORD_ACTION_DELAY_MS, overlord_action)
199                    .first_action_delay(OVERLORD_ACTION_DELAY_MS)
200                    .max_actions(OVERLORD_MAX_ACTIONS)
201                    .ends_guardian(),
202            ),
203    );
204}
205
206// Bounding by the channel window preserves haste-scaled tick counts.
207const CHAOS_SALVO_CHANNEL_MS: u32 = 4_000;
208const CHAOS_SALVO_TICK_MS: u32 = 200;
209
210fn summon_mother(ctx: &mut HookCtx<'_>, spec: WarlockSpec) {
211    let _ = ctx.summon_guardian(
212        GuardianSpec::new_inheriting_owner(
213            tag(MOTHER_TAG_OFFSET, spec),
214            CHAOS_SALVO_CHANNEL_MS + CHAOS_SALVO_TICK_MS,
215        )
216        .ability(
217            GuardianAbility::new(CHAOS_SALVO_TICK_MS, mother_action)
218                .first_action_delay(CHAOS_SALVO_TICK_MS)
219                .hasted_actions(),
220        ),
221    );
222}
223
224fn summon_pit_lord(ctx: &mut HookCtx<'_>, spec: WarlockSpec) {
225    let _ = ctx.summon_guardian(
226        GuardianSpec::new_inheriting_owner(tag(PIT_LORD_TAG_OFFSET, spec), GUARDIAN_LIFETIME_MS)
227            .ability(
228                GuardianAbility::new(PIT_LORD_ACTION_DELAY_MS, pit_lord_action)
229                    .first_action_delay(PIT_LORD_ACTION_DELAY_MS)
230                    .hasted_actions()
231                    .max_actions(PIT_LORD_MAX_ACTIONS)
232                    .ends_guardian(),
233            ),
234    );
235}
236
237const fn art_aura(spec: WarlockSpec, demon: DiabolicDemon) -> LocalAuraIdx {
238    match (spec, demon) {
239        (WarlockSpec::Demonology, DiabolicDemon::Overlord) => DEMO_ART_OVERLORD,
240        (WarlockSpec::Demonology, DiabolicDemon::Mother) => DEMO_ART_MOTHER,
241        (WarlockSpec::Demonology, DiabolicDemon::PitLord) => DEMO_ART_PIT_LORD,
242        (WarlockSpec::Destruction, DiabolicDemon::Overlord) => DESTRO_ART_OVERLORD,
243        (WarlockSpec::Destruction, DiabolicDemon::Mother) => DESTRO_ART_MOTHER,
244        (WarlockSpec::Destruction, DiabolicDemon::PitLord) => DESTRO_ART_PIT_LORD,
245    }
246}
247
248fn diabolic_ritual_expire(ctx: &mut HookCtx<'_>, spec: WarlockSpec, demon: DiabolicDemon) {
249    ctx.apply_aura(art_aura(spec, demon));
250}
251
252fn demonic_art_expire(ctx: &mut HookCtx<'_>, spec: WarlockSpec, demon: DiabolicDemon) {
253    match demon {
254        DiabolicDemon::Overlord => summon_overlord(ctx, spec),
255        DiabolicDemon::Mother => {
256            summon_mother(ctx, spec);
257            let aura = match spec {
258                WarlockSpec::Demonology => DEMO_INFERNAL_BOLT,
259                WarlockSpec::Destruction => DESTRO_INFERNAL_BOLT,
260            };
261
262            ctx.apply_aura(aura);
263        }
264        DiabolicDemon::PitLord => {
265            summon_pit_lord(ctx, spec);
266            let aura = match spec {
267                WarlockSpec::Demonology => DEMO_RUINATION,
268                WarlockSpec::Destruction => DESTRO_RUINATION,
269            };
270
271            ctx.apply_aura(aura);
272        }
273    }
274}
275
276pub(crate) fn demonology_diabolic_ritual_overlord_expire_hook(ctx: &mut HookCtx<'_>) {
277    diabolic_ritual_expire(ctx, WarlockSpec::Demonology, DiabolicDemon::Overlord);
278}
279
280pub(crate) fn demonology_diabolic_ritual_mother_expire_hook(ctx: &mut HookCtx<'_>) {
281    diabolic_ritual_expire(ctx, WarlockSpec::Demonology, DiabolicDemon::Mother);
282}
283
284pub(crate) fn demonology_diabolic_ritual_pit_lord_expire_hook(ctx: &mut HookCtx<'_>) {
285    diabolic_ritual_expire(ctx, WarlockSpec::Demonology, DiabolicDemon::PitLord);
286}
287
288pub(crate) fn demonology_demonic_art_overlord_expire_hook(ctx: &mut HookCtx<'_>) {
289    demonic_art_expire(ctx, WarlockSpec::Demonology, DiabolicDemon::Overlord);
290}
291
292pub(crate) fn demonology_demonic_art_mother_expire_hook(ctx: &mut HookCtx<'_>) {
293    demonic_art_expire(ctx, WarlockSpec::Demonology, DiabolicDemon::Mother);
294}
295
296pub(crate) fn demonology_demonic_art_pit_lord_expire_hook(ctx: &mut HookCtx<'_>) {
297    demonic_art_expire(ctx, WarlockSpec::Demonology, DiabolicDemon::PitLord);
298}
299
300pub(crate) fn destruction_diabolic_ritual_overlord_expire_hook(ctx: &mut HookCtx<'_>) {
301    diabolic_ritual_expire(ctx, WarlockSpec::Destruction, DiabolicDemon::Overlord);
302}
303
304pub(crate) fn destruction_diabolic_ritual_mother_expire_hook(ctx: &mut HookCtx<'_>) {
305    diabolic_ritual_expire(ctx, WarlockSpec::Destruction, DiabolicDemon::Mother);
306}
307
308pub(crate) fn destruction_diabolic_ritual_pit_lord_expire_hook(ctx: &mut HookCtx<'_>) {
309    diabolic_ritual_expire(ctx, WarlockSpec::Destruction, DiabolicDemon::PitLord);
310}
311
312pub(crate) fn destruction_demonic_art_overlord_expire_hook(ctx: &mut HookCtx<'_>) {
313    demonic_art_expire(ctx, WarlockSpec::Destruction, DiabolicDemon::Overlord);
314}
315
316pub(crate) fn destruction_demonic_art_mother_expire_hook(ctx: &mut HookCtx<'_>) {
317    demonic_art_expire(ctx, WarlockSpec::Destruction, DiabolicDemon::Mother);
318}
319
320pub(crate) fn destruction_demonic_art_pit_lord_expire_hook(ctx: &mut HookCtx<'_>) {
321    demonic_art_expire(ctx, WarlockSpec::Destruction, DiabolicDemon::PitLord);
322}
323
324#[derive(Clone, Copy)]
325struct OculiIds {
326    talent: LocalAuraIdx,
327    oculi: LocalAuraIdx,
328    looks_that_kill: LocalAuraIdx,
329    minds_eyes_talent: LocalAuraIdx,
330    minds_eyes: LocalAuraIdx,
331}
332
333const fn oculi_ids(spec: WarlockSpec) -> OculiIds {
334    match spec {
335        WarlockSpec::Demonology => OculiIds {
336            talent: DEMO_OCULI_TALENT,
337            oculi: DEMO_OCULI,
338            looks_that_kill: DEMO_LOOKS_THAT_KILL,
339            minds_eyes_talent: DEMO_MINDS_EYES_TALENT,
340            minds_eyes: DEMO_MINDS_EYES,
341        },
342        WarlockSpec::Destruction => OculiIds {
343            talent: DESTRO_OCULI_TALENT,
344            oculi: DESTRO_OCULI,
345            looks_that_kill: DESTRO_LOOKS_THAT_KILL,
346            minds_eyes_talent: DESTRO_MINDS_EYES_TALENT,
347            minds_eyes: DESTRO_MINDS_EYES,
348        },
349    }
350}
351
352fn oculi_tick(ctx: &mut HookCtx<'_>, _event: GuardianEvent, spec: WarlockSpec) {
353    let ids = oculi_ids(spec);
354
355    if !ctx.is_aura_active(ids.oculi) || !ctx.is_aura_active(ids.looks_that_kill) {
356        return;
357    }
358
359    let (spell_id, effect) = match ctx.aura_stacks(ids.oculi) {
360        OCULI_GAZE_ONE_STACK => (
361            DIABOLIC_DAMAGE_REPORTED::DIABOLIC_GAZE_1,
362            DIABOLIC_DAMAGE_EFFECT::DIABOLIC_GAZE_1_DAMAGE,
363        ),
364        OCULI_GAZE_TWO_STACKS => (
365            DIABOLIC_DAMAGE_REPORTED::DIABOLIC_GAZE_2,
366            DIABOLIC_DAMAGE_EFFECT::DIABOLIC_GAZE_2_DAMAGE,
367        ),
368        OCULI_GAZE_THREE_STACKS => (
369            DIABOLIC_DAMAGE_REPORTED::DIABOLIC_GAZE_3,
370            DIABOLIC_DAMAGE_EFFECT::DIABOLIC_GAZE_3_DAMAGE,
371        ),
372        _ => return,
373    };
374    let coefficient = ctx.effect_sp_coefficient(effect);
375
376    ctx.deal_damage_sp(spell_id, coefficient, DamageFlags::empty());
377}
378
379fn demonology_oculi_tick(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
380    oculi_tick(ctx, event, WarlockSpec::Demonology);
381}
382
383fn destruction_oculi_tick(ctx: &mut HookCtx<'_>, event: GuardianEvent) {
384    oculi_tick(ctx, event, WarlockSpec::Destruction);
385}
386
387pub(crate) fn trigger_oculi(ctx: &mut HookCtx<'_>, spec: WarlockSpec) {
388    let ids = oculi_ids(spec);
389
390    if !ctx.is_aura_active(ids.talent) {
391        return;
392    }
393
394    ctx.apply_aura(ids.oculi);
395    let controller_tag = tag(OCULI_CONTROLLER_TAG_OFFSET, spec);
396
397    if ctx.active_guardian_count(controller_tag) > 0 {
398        return;
399    }
400
401    let fight_end_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(
402        ctx.program().encounter().fight_duration_secs() * MS_PER_SECOND,
403    );
404    let duration_ms = fight_end_ms
405        .saturating_sub(ctx.now().as_millis())
406        .saturating_add(1);
407    let action = match spec {
408        WarlockSpec::Demonology => demonology_oculi_tick,
409        WarlockSpec::Destruction => destruction_oculi_tick,
410    };
411    let oculi_action = match spec {
412        WarlockSpec::Demonology => {
413            crate::generated::specs::demonology_warlock::DAMAGE_AURA::DEMONIC_OCULI
414        }
415        WarlockSpec::Destruction => {
416            crate::generated::specs::destruction_warlock::DAMAGE_AURA::DEMONIC_OCULI
417        }
418    };
419    let tick_ms = ctx.action_aura_tick_ms(oculi_action);
420    let _ = ctx.summon_guardian(
421        GuardianSpec::new_inheriting_owner(controller_tag, duration_ms)
422            .ability(GuardianAbility::new(tick_ms, action).first_action_delay(tick_ms)),
423    );
424}
425
426pub(crate) fn explode_oculi(ctx: &mut HookCtx<'_>, spec: WarlockSpec) {
427    let ids = oculi_ids(spec);
428    let stacks = ctx.aura_stacks(ids.oculi);
429
430    if stacks <= 0 {
431        return;
432    }
433
434    if ctx.is_aura_active(ids.minds_eyes_talent) {
435        ctx.apply_aura_n(ids.minds_eyes, stacks);
436    }
437
438    let coefficient = ctx.effect_sp_coefficient(DIABOLIC_DAMAGE_EFFECT::EYE_EXPLOSION_DAMAGE);
439
440    ctx.deal_damage_sp(
441        DIABOLIC_DAMAGE_REPORTED::EYE_EXPLOSION,
442        coefficient * f64::from(stacks),
443        DamageFlags::empty(),
444    );
445    ctx.expire_aura(ids.oculi);
446}