wowlab_engine_content/hooks/destruction_warlock/
hooks.rs1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::ResourceOps as _;
3use wowlab_engine_combat::SchedulingOps 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};
9use wowlab_engine_domain::{
10 damage::pet_swing_power_coefficient,
11 dbc::ResolvedGameDataEffectExt as _,
12};
13use wowlab_engine_rng::proc_chance;
14use wowlab_types::{combat::DamageSchool, constants::HUNDRED};
15
16use crate::generated::specs::destruction_warlock::{
17 AURA, AURA_AVATAR_OF_DESTRUCTION_TALENT, AURA_BACKDRAFT, AURA_CHAOTIC_INFERNO,
18 AURA_CHAOTIC_INFERNO_TALENT, AURA_DEMONIC_ART_MOTHER, AURA_DEMONIC_ART_OVERLORD,
19 AURA_DEMONIC_ART_PIT_LORD, AURA_DIABOLIC_RITUAL_MOTHER, AURA_DIABOLIC_RITUAL_OVERLORD,
20 AURA_DIABOLIC_RITUAL_PIT_LORD, AURA_DIABOLIC_RITUAL_TALENT, AURA_EMBERS_OF_NIHILAM_1_TALENT,
21 AURA_EMBERS_OF_NIHILAM_2_TALENT, AURA_EMBERS_OF_NIHILAM_3_TALENT, AURA_FIENDISH_CRUELTY,
22 AURA_FIENDISH_CRUELTY_TALENT, AURA_INFERNAL_EMBERS, AURA_VISION_OF_NIHILAM, EFFECT,
23 REPORTED_SPELL, SPELL,
24};
25use crate::hooks::shared::warlock::{DiabolicRitual, WarlockSpec};
26
27const DIABOLIC_RITUAL: DiabolicRitual = DiabolicRitual {
28 talent: AURA_DIABOLIC_RITUAL_TALENT,
29 art: [
30 AURA_DEMONIC_ART_OVERLORD,
31 AURA_DEMONIC_ART_MOTHER,
32 AURA_DEMONIC_ART_PIT_LORD,
33 ],
34 ritual: [
35 AURA_DIABOLIC_RITUAL_OVERLORD,
36 AURA_DIABOLIC_RITUAL_MOTHER,
37 AURA_DIABOLIC_RITUAL_PIT_LORD,
38 ],
39};
40
41const CHAOTIC_ENERGIES_MEAN_ROLL: f64 = 0.75;
43
44const INFERNAL_COEF_FROM_SP: f64 = 2.2275;
46const INFERNAL_DURATION_MS: u32 = 30_000;
47const INFERNAL_ACTION_MS: u32 = 2_000;
48const DIABOLIC_RITUAL_REDUCTION_MS: u32 = 1_000;
49const OVERFIEND_DURATION_MS: u32 = 8_000;
50const OVERFIEND_CAST_MS: u32 = 3_000;
51const OVERFIEND_CHAOS_BOLT_EFFICIENCY: f64 = 0.60;
52const OVERFIEND_SHARDS: f64 = 0.8;
53const ECHO_OF_SARGERAS_BASE_CHANCE: f64 = 0.10;
54pub(super) const SAYAAD_ACTION_MS: u32 = 6_000;
55const SAYAAD_DAMAGE_MULT: f64 = 1.15;
56const CHAOTIC_INFERNO_CHANCE: f64 = 0.25;
57const CHAOS_BOLT_RITUAL_SHARDS: u32 = 2;
58const FIENDISH_CRUELTY_BASE_CHANCE: f64 = 0.10;
59const OVERFIEND_BOLTS_PER_ACTION: f64 = 2.0;
60const OVERFIEND_MAX_ACTIONS: u16 = 3;
61const CONFLAGRATION_OF_CHAOS_MULT: f64 = 2.0;
62
63const fn pet_inherits_chaotic_energies(is_pet: bool, spell_id: u32) -> bool {
64 !is_pet || spell_id == SPELL::CHAOS_BOLT
65}
66
67pub(super) fn sayaad_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
68 let coefficient = ctx
69 .game_data()
70 .effect_sp_coefficient(EFFECT::LASH_OF_PAIN_DAMAGE);
71
72 ctx.deal_damage_sp(
73 REPORTED_SPELL::LASH_OF_PAIN,
74 coefficient * SAYAAD_DAMAGE_MULT,
75 DamageFlags::PET,
76 );
77}
78
79fn trigger_echo_of_sargeras(ctx: &mut HookCtx<'_>, report_spell_id: u32) {
80 let coefficient = ctx
81 .game_data()
82 .effect_sp_coefficient(EFFECT::ECHO_OF_SARGERAS_DAMAGE);
83
84 ctx.deal_profiled_damage_sp(
85 report_spell_id,
86 REPORTED_SPELL::ECHO_OF_SARGERAS,
87 coefficient,
88 DamageFlags::empty(),
89 );
90
91 if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_2_TALENT.raw()) {
92 ctx.apply_aura(AURA_VISION_OF_NIHILAM.raw());
93 }
94}
95
96fn maybe_trigger_base_echo(ctx: &mut HookCtx<'_>) {
97 if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_1_TALENT.raw())
98 && proc_chance(ctx.rng(), ECHO_OF_SARGERAS_BASE_CHANCE)
99 {
100 trigger_echo_of_sargeras(ctx, REPORTED_SPELL::ECHO_OF_SARGERAS);
101 }
102}
103
104fn diabolist_spender(ctx: &mut HookCtx<'_>, reduction_ms: u32) {
105 let ritual_was_active = DIABOLIC_RITUAL.active(ctx);
106 let next_ritual = DIABOLIC_RITUAL.consume_art(ctx);
107
108 if next_ritual.is_some() {
109 crate::hooks::shared::warlock::explode_oculi(ctx, WarlockSpec::Destruction);
110 }
111
112 DIABOLIC_RITUAL.advance(ctx, next_ritual, reduction_ms);
113
114 if ritual_was_active {
115 crate::hooks::shared::warlock::trigger_oculi(ctx, WarlockSpec::Destruction);
116 }
117}
118
119fn maybe_trigger_fiendish_cruelty(ctx: &mut HookCtx<'_>, chance: f64) {
120 if ctx.is_aura_active(AURA_FIENDISH_CRUELTY_TALENT.raw()) && proc_chance(ctx.rng(), chance) {
121 ctx.apply_aura(AURA_FIENDISH_CRUELTY.raw());
122 }
123}
124
125pub(crate) fn chaos_bolt_hook(ctx: &mut HookCtx<'_>) {
126 ctx.consume_aura_stack(AURA_BACKDRAFT.raw());
127
128 if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_3_TALENT.raw()) {
129 trigger_echo_of_sargeras(ctx, REPORTED_SPELL::ECHO_OF_SARGERAS_CB);
130 }
131
132 if ctx.is_aura_active(AURA_CHAOTIC_INFERNO_TALENT.raw())
133 && proc_chance(ctx.rng(), CHAOTIC_INFERNO_CHANCE)
134 {
135 ctx.apply_aura(AURA_CHAOTIC_INFERNO.raw());
136 }
137
138 diabolist_spender(ctx, DIABOLIC_RITUAL_REDUCTION_MS * CHAOS_BOLT_RITUAL_SHARDS);
139 maybe_trigger_fiendish_cruelty(ctx, FIENDISH_CRUELTY_BASE_CHANCE);
140}
141
142pub(crate) fn conflagrate_hook(ctx: &mut HookCtx<'_>) {
143 ctx.apply_aura(AURA_BACKDRAFT.raw());
144 maybe_trigger_fiendish_cruelty(ctx, FIENDISH_CRUELTY_BASE_CHANCE);
145}
146
147pub(crate) fn incinerate_hook(ctx: &mut HookCtx<'_>) {
148 let chaotic_inferno = ctx.is_aura_active(AURA_CHAOTIC_INFERNO.raw());
149
150 if !chaotic_inferno {
151 ctx.consume_aura_stack(AURA_BACKDRAFT.raw());
152 }
153
154 ctx.consume_aura(AURA_CHAOTIC_INFERNO.raw());
155 maybe_trigger_base_echo(ctx);
156 let buffs = wowlab_engine_combat::accumulate_buffs(ctx.state(), ctx.buf(), ctx.target());
157 let crit_chance = ((ctx.buf().player().crit + buffs.crit_pct) / HUNDRED).clamp(0.0, 1.0);
158
159 maybe_trigger_fiendish_cruelty(ctx, FIENDISH_CRUELTY_BASE_CHANCE * crit_chance);
160}
161
162fn overfiend_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
163 let coefficient = ctx
164 .game_data()
165 .effect_sp_coefficient(EFFECT::CHAOS_BOLT_DAMAGE);
166
167 ctx.deal_damage_sp(
168 SPELL::CHAOS_BOLT,
169 coefficient * OVERFIEND_CHAOS_BOLT_EFFICIENCY * OVERFIEND_BOLTS_PER_ACTION,
170 DamageFlags::PET | DamageFlags::NO_CRIT,
171 );
172}
173
174pub(crate) fn soul_fire_hook(ctx: &mut HookCtx<'_>) {
175 ctx.consume_aura_stack(AURA_BACKDRAFT.raw());
176
177 if !ctx.is_aura_active(AURA_AVATAR_OF_DESTRUCTION_TALENT.raw()) {
178 return;
179 }
180
181 ctx.gain_resource(OVERFIEND_SHARDS);
182 let _ = ctx.summon_guardian(
183 GuardianSpec::new_inheriting_owner(AURA::OVERFIEND, OVERFIEND_DURATION_MS).ability(
184 GuardianAbility::new(OVERFIEND_CAST_MS, overfiend_action)
185 .first_action_delay(OVERFIEND_CAST_MS)
186 .hasted_actions()
187 .max_actions(OVERFIEND_MAX_ACTIONS)
188 .ends_guardian(),
189 ),
190 );
191}
192
193pub(crate) fn infernal_bolt_hook(ctx: &mut HookCtx<'_>) {
194 ctx.consume_aura_stack(AURA_BACKDRAFT.raw());
195 ctx.consume_aura(AURA_CHAOTIC_INFERNO.raw());
196 maybe_trigger_base_echo(ctx);
197}
198
199pub(crate) fn rain_of_fire_hook(ctx: &mut HookCtx<'_>) {
200 ctx.schedule_ground_effect_from_data(
201 SPELL::RAIN_OF_FIRE,
202 EFFECT::RAIN_OF_FIRE_TIMING.1,
203 (SPELL::RAIN_OF_FIRE, 1),
204 EFFECT::RAIN_OF_FIRE_DAMAGE,
205 DamageFlags::PERIODIC,
206 );
207 diabolist_spender(ctx, DIABOLIC_RITUAL_REDUCTION_MS);
208}
209
210pub(crate) fn shadowburn_hook(ctx: &mut HookCtx<'_>) {
211 if ctx.is_aura_active(AURA_EMBERS_OF_NIHILAM_3_TALENT.raw()) {
212 trigger_echo_of_sargeras(ctx, REPORTED_SPELL::ECHO_OF_SARGERAS_SB);
213 }
214
215 diabolist_spender(ctx, DIABOLIC_RITUAL_REDUCTION_MS);
216 ctx.consume_aura(AURA_FIENDISH_CRUELTY.raw());
217}
218
219pub(crate) fn ruination_hook(ctx: &mut HookCtx<'_>) {
220 if DIABOLIC_RITUAL.consume_art(ctx).is_some() {
221 crate::hooks::shared::warlock::explode_oculi(ctx, WarlockSpec::Destruction);
222 }
223}
224
225fn infernal_action(ctx: &mut HookCtx<'_>, _event: GuardianEvent) {
226 let swing = INFERNAL_COEF_FROM_SP * pet_swing_power_coefficient(1.0, INFERNAL_ACTION_MS, 1.0);
227
228 ctx.deal_damage_sp(
229 REPORTED_SPELL::INFERNAL_MELEE,
230 swing,
231 DamageFlags::PHYSICAL | DamageFlags::PET,
232 );
233 let immolation = ctx
234 .game_data()
235 .effect_sp_coefficient(EFFECT::IMMOLATION_DAMAGE)
236 * INFERNAL_COEF_FROM_SP;
237
238 ctx.deal_damage_sp(REPORTED_SPELL::IMMOLATION, immolation, DamageFlags::PET);
239}
240
241pub(crate) fn summon_infernal_hook(ctx: &mut HookCtx<'_>) {
243 let _ = ctx.summon_guardian(
244 GuardianSpec::new_inheriting_owner(REPORTED_SPELL::INFERNAL_MELEE, INFERNAL_DURATION_MS)
245 .ability(
246 GuardianAbility::new(INFERNAL_ACTION_MS, infernal_action)
247 .first_action_delay(INFERNAL_ACTION_MS)
248 .hasted_actions(),
249 ),
250 );
251
252 ctx.apply_aura(AURA_INFERNAL_EMBERS.raw());
253}
254
255#[must_use]
257pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
258 if !pet_inherits_chaotic_energies(ctx.is_pet, ctx.spell_id)
261 || matches!(
262 ctx.spell_id,
263 REPORTED_SPELL::DIABOLIC_GAZE_1
264 | REPORTED_SPELL::DIABOLIC_GAZE_2
265 | REPORTED_SPELL::DIABOLIC_GAZE_3
266 )
267 {
268 return 1.0;
269 }
270
271 let mastery = match ctx.school {
272 DamageSchool::Fire | DamageSchool::Chaos => {
273 1.0 + CHAOTIC_ENERGIES_MEAN_ROLL * ctx.points(1) / HUNDRED
274 }
275 _ => 1.0,
276 };
277
278 if ctx.spell_id == SPELL::CHAOS_BOLT {
279 let buffs = wowlab_engine_combat::accumulate_buffs(ctx.state, ctx.buf, ctx.target);
280 let crit_fraction = (ctx.buf.player().crit + buffs.crit_pct) / HUNDRED;
281
282 mastery * (1.0 + crit_fraction)
283 } else if matches!(ctx.spell_id, SPELL::CONFLAGRATE | SPELL::SHADOWBURN)
284 && ctx.is_aura_active_id(AURA::CONFLAGRATION_OF_CHAOS_TALENT)
285 {
286 mastery * CONFLAGRATION_OF_CHAOS_MULT
287 } else {
288 mastery
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use googletest::prelude::*;
295
296 use super::*;
297
298 #[gtest]
299 fn only_explicit_pet_chaos_bolts_inherit_chaotic_energies() -> Result<()> {
300 verify_false!(pet_inherits_chaotic_energies(
301 true,
302 REPORTED_SPELL::CHAOS_SALVO
303 ))?;
304 verify_false!(pet_inherits_chaotic_energies(
305 true,
306 REPORTED_SPELL::IMMOLATION
307 ))?;
308 verify_true!(pet_inherits_chaotic_energies(true, SPELL::CHAOS_BOLT))?;
309
310 verify_true!(pet_inherits_chaotic_energies(false, SPELL::CONFLAGRATE))
311 }
312}