Skip to main content

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