Skip to main content

wowlab_engine_content/hooks/affliction_warlock/
hooks.rs

1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::ResourceOps as _;
3use wowlab_engine_combat::ProcOps as _;
4use wowlab_engine_combat::GuardianOps as _;
5use wowlab_engine_combat::AuraOps as _;
6use wowlab_engine_combat::{
7    DamageFlags, GuardianAbility, GuardianEvent, GuardianSpec, HookCtx, MasteryCtx,
8    ResourceGainEvent,
9};
10use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
11use wowlab_engine_rng::{prd_constant, proc_chance};
12use wowlab_types::{constants::DEFAULT_GCD_MS, sim::SpellIdx};
13
14use super::config::{AfflictionConfig, AfflictionRuntime};
15use crate::generated::specs::affliction_warlock::{
16    AURA_AGONY, AURA_CORRUPTION, AURA_SOUL_ANATHEMA, AURA_SUCCULENT_SOUL, AURA_UNSTABLE_AFFLICTION,
17    AURA_WITHER, EFFECT, REPORTED_SPELL, TALENT,
18};
19
20const DARKGLARE_DURATION_MS: u32 = 20_000;
21const MANIFESTED_AVARICE_RATE: f64 = 0.10;
22const SOUL_SWIPE_INTERVAL_MS: u32 = 1_000;
23const DESPERATE_SOUL_DURATION_MS: u32 = 1_000;
24const DESPERATE_SOUL_ACTION_DELAY_MS: u32 = 1;
25const MANIFESTED_SOUL_DURATION_MS: u32 = 14_000;
26
27fn darkglare_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
28    let dots = [
29        AURA_AGONY,
30        AURA_CORRUPTION,
31        AURA_UNSTABLE_AFFLICTION,
32        AURA_WITHER,
33    ]
34    .iter()
35    .filter(|a| ctx.is_aura_active(a.raw()))
36    .count();
37    let coefficient = ctx
38        .game_data()
39        .effect_sp_coefficient(EFFECT::EYE_BEAM_DAMAGE);
40    let per_dot = ctx.effect_percent(EFFECT::EYE_BEAM_PER_DOT);
41    let mult = 1.0 + per_dot * wowlab_types::numeric::usize_to_f64(dots);
42
43    ctx.deal_damage_sp(
44        REPORTED_SPELL::EYE_BEAM,
45        coefficient * mult,
46        DamageFlags::PET,
47    );
48}
49
50/// Summons a 20-second Darkglare whose Eye Beams read the live target `DoT` count.
51pub(crate) fn summon_darkglare_hook(ctx: &mut HookCtx<'_>) {
52    let action_ms = ctx
53        .game_data()
54        .gcd_ms(SpellIdx::from_raw(REPORTED_SPELL::EYE_BEAM))
55        .unwrap_or(DEFAULT_GCD_MS);
56    let _ = ctx.summon_guardian(
57        GuardianSpec::new_inheriting_owner(TALENT::SUMMON_DARKGLARE, DARKGLARE_DURATION_MS)
58            .ability(
59                GuardianAbility::new(action_ms, darkglare_action)
60                    .first_action_delay(action_ms)
61                    .hasted_actions(),
62            ),
63    );
64}
65
66pub(super) fn succulent_soul_proc(ctx: &mut HookCtx<'_>, _event: ResourceGainEvent) {
67    ctx.apply_aura(AURA_SUCCULENT_SOUL.raw());
68}
69
70fn manifested_soul_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
71    ctx.deal_damage_sp(
72        REPORTED_SPELL::SOUL_SWIPE,
73        ctx.effect_sp_coefficient(EFFECT::SOUL_SWIPE_DAMAGE),
74        DamageFlags::PET,
75    );
76    ctx.deal_effect_damage_sp_with_geometry(
77        EFFECT::SOUL_SWIPE_AOE_DAMAGE,
78        EFFECT::SOUL_SWIPE_AOE_DAMAGE,
79        1.0,
80        DamageFlags::PET,
81    );
82}
83
84fn wrath_of_nathreza_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
85    ctx.deal_effect_damage_sp_with_geometry(
86        EFFECT::WRATH_OF_NATHREZA_DAMAGE,
87        EFFECT::WRATH_OF_NATHREZA_DAMAGE,
88        1.0,
89        DamageFlags::PET,
90    );
91}
92
93/// Haunt periodic: Shadow of Nathreza damage and rank-three desperate-soul RPPM.
94pub(crate) fn haunt_tick_hook(ctx: &mut HookCtx<'_>) {
95    const SHADOW_OF_NATHREZA_MAX_RANK: u8 = 3;
96
97    let config = ctx
98        .spec_config::<AfflictionConfig>()
99        .copied()
100        .unwrap_or_default();
101
102    if config.shadow_of_nathreza_rank == 0 {
103        return;
104    }
105
106    ctx.deal_damage_sp(
107        REPORTED_SPELL::SHADOW_OF_NATHREZA,
108        ctx.effect_sp_coefficient(EFFECT::SHADOW_OF_NATHREZA_DAMAGE),
109        DamageFlags::empty(),
110    );
111
112    if config.shadow_of_nathreza_rank < SHADOW_OF_NATHREZA_MAX_RANK {
113        return;
114    }
115
116    if ctx.roll_system_rppm(TALENT::SHADOW_OF_NATHREZA_3) {
117        let _ = ctx.summon_guardian(
118            GuardianSpec::new_inheriting_owner(
119                TALENT::SHADOW_OF_NATHREZA_3,
120                DESPERATE_SOUL_DURATION_MS,
121            )
122            .ability(
123                GuardianAbility::new(DESPERATE_SOUL_ACTION_DELAY_MS, wrath_of_nathreza_action)
124                    .max_actions(1)
125                    .ends_guardian(),
126            ),
127        );
128    }
129}
130
131fn trigger_manifested_soul(ctx: &mut HookCtx<'_>) {
132    let Some(config) = ctx.spec_config::<AfflictionConfig>() else {
133        return;
134    };
135
136    tracing::trace!(
137        enabled = config.manifested_avarice,
138        attempts = ctx
139            .spec_runtime::<AfflictionRuntime>()
140            .map_or(0, |runtime| runtime.manifested_avarice_attempts),
141        "MANIFESTED_AVARICE_CHECK"
142    );
143
144    if !config.manifested_avarice {
145        return;
146    }
147
148    let attempts = ctx
149        .spec_runtime::<AfflictionRuntime>()
150        .map_or(1, |runtime| {
151            runtime.manifested_avarice_attempts.saturating_add(1)
152        });
153    let success = proc_chance(
154        ctx.rng(),
155        prd_constant(MANIFESTED_AVARICE_RATE) * f64::from(attempts),
156    );
157
158    if let Some(runtime) = ctx.spec_runtime_mut::<AfflictionRuntime>() {
159        runtime.manifested_avarice_attempts = if success { 0 } else { attempts };
160    }
161
162    if !success {
163        return;
164    }
165
166    let duration_ms = ctx
167        .game_data()
168        .aura_duration_ms(SpellIdx::from_raw(
169            crate::generated::specs::affliction_warlock::AURA::MANIFESTED_DEMONIC_SOUL,
170        ))
171        .unwrap_or(MANIFESTED_SOUL_DURATION_MS);
172    let _ = ctx.summon_guardian(
173        GuardianSpec::new_inheriting_owner(TALENT::MANIFESTED_AVARICE, duration_ms).ability(
174            GuardianAbility::new(SOUL_SWIPE_INTERVAL_MS, manifested_soul_action)
175                .first_action_delay(SOUL_SWIPE_INTERVAL_MS),
176        ),
177    );
178}
179
180/// Consumes Succulent Soul to release Demonic Soul and possibly manifest its guardian.
181pub(crate) fn unstable_affliction_hook(ctx: &mut HookCtx<'_>) {
182    if !ctx.consume_aura_stack(AURA_SUCCULENT_SOUL.raw()) {
183        return;
184    }
185
186    let config = ctx
187        .spec_config::<AfflictionConfig>()
188        .copied()
189        .unwrap_or_default();
190    let fervor = if config.demoniacs_fervor && ctx.is_aura_active(AURA_UNSTABLE_AFFLICTION.raw()) {
191        1.0 + ctx.effect_percent(EFFECT::DEMONIACS_FERVOR_DAMAGE)
192    } else {
193        1.0
194    };
195
196    ctx.deal_damage_sp(
197        REPORTED_SPELL::DEMONIC_SOUL,
198        ctx.effect_sp_coefficient(EFFECT::DEMONIC_SOUL_DAMAGE) * fervor,
199        DamageFlags::empty(),
200    );
201
202    if config.soul_anathema {
203        ctx.apply_aura(AURA_SOUL_ANATHEMA.raw());
204    }
205
206    trigger_manifested_soul(ctx);
207}
208
209/// Resets Shadow of Death's per-channel tick position.
210pub(crate) fn dark_harvest_hook(ctx: &mut HookCtx<'_>) {
211    if let Some(runtime) = ctx.spec_runtime_mut::<AfflictionRuntime>() {
212        runtime.dark_harvest_ticks = 0;
213    }
214}
215
216/// Dark Harvest grants Shadow of Death shards and guaranteed Succulent Souls after tick one.
217pub(crate) fn dark_harvest_tick_hook(ctx: &mut HookCtx<'_>) {
218    let enabled = ctx
219        .spec_config::<AfflictionConfig>()
220        .is_some_and(|config| config.shadow_of_death);
221    let tick = ctx
222        .spec_runtime_mut::<AfflictionRuntime>()
223        .map_or(0, |runtime| {
224            runtime.dark_harvest_ticks = runtime.dark_harvest_ticks.saturating_add(1);
225
226            runtime.dark_harvest_ticks
227        });
228
229    if !enabled || tick <= 1 {
230        return;
231    }
232
233    let shards = ctx.effect_base_points(EFFECT::SHADOW_OF_DEATH_SHARDS);
234
235    ctx.gain_resource(shards);
236    ctx.apply_aura_n(
237        AURA_SUCCULENT_SOUL.raw(),
238        wowlab_types::numeric::f64_to_i32_saturating_round(shards.max(0.0)),
239    );
240}
241
242/// Returns Potent Afflictions mastery damage (77215): e1 periodic, e2 direct, e3 periodic by label.
243///
244/// `SimC` reaches all three effects generically through `parse_effects( potent_afflictions )`.
245/// See `warlock/sc_warlock.cpp:1053` and `warlock/sc_warlock_actions.cpp:198`.
246/// The DBC affect lists are therefore the whole rule, and the direct half carries Malefic Rapture.
247#[must_use]
248pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
249    ctx.direct_or_periodic_affected_bonus_any(
250        &[EFFECT::POTENT_AFFLICTIONS_DIRECT.1],
251        &[
252            EFFECT::POTENT_AFFLICTIONS_DAMAGE.1,
253            EFFECT::POTENT_AFFLICTIONS_LABEL_DAMAGE.1,
254        ],
255    )
256}