wowlab_engine_content/hooks/affliction_warlock/
hooks.rs1use 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
50pub(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
93pub(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
180pub(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
209pub(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
216pub(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#[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}