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