1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::SchedulingOps as _;
5use wowlab_engine_combat::AuraOps as _;
6use wowlab_types::{constants::HUNDRED, constants::MS_PER_SECOND};
7
8use wowlab_engine_combat::{DamageFlags, HookCtx, MasteryCtx, SwingEvent};
9use wowlab_engine_rng::proc_chance;
10
11use crate::generated::specs::havoc_demon_hunter::{
12 AURA_CYCLE_OF_HATRED, AURA_DEMONIC_INTENSITY, AURA_EMPOWERED_EYE_BEAM, AURA_FURIOUS_GAZE,
13 AURA_METAMORPHOSIS, AURA_SERRATED_GLAIVE, AURA_STUDENT_OF_SUFFERING, EFFECT,
14 SPELL_BLADE_DANCE, SPELL_ESSENCE_BREAK, SPELL_EYE_BEAM, SPELL_THROW_GLAIVE,
15};
16
17use super::config::{DemonsurgeAbilities, HavocConfig, HavocRuntime, havoc_config};
18
19const META_BASE_MS: u32 = 20_000;
21
22const BLADE_DANCE_SLASHES: usize = 3;
24
25const DEMONSURGE_DELAY_MS: u32 = 450;
26const DEMONSURGE_DEATH_SWEEP_DELAY_MS: u32 = 700;
27const DEMONSURGE_MAX_STACKS: u8 = 4;
29const CYCLE_CROSS_REDUCTION_MAX_STACKS: u32 = 4;
31
32#[must_use]
36pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
37 ctx.direct_or_periodic_affected_bonus_any(
38 &[EFFECT::MASTERY_DIRECT_BONUS.1],
39 &[EFFECT::MASTERY_PERIODIC_BONUS.1],
40 )
41}
42
43fn apply_serrated_glaive(ctx: &mut HookCtx<'_>) {
44 if havoc_config(ctx).serrated_glaive {
45 ctx.apply_aura(AURA_SERRATED_GLAIVE);
46 }
47}
48
49pub(crate) fn chaos_strike_hook(ctx: &mut HookCtx<'_>) {
50 apply_serrated_glaive(ctx);
51 let refund_chance = ctx.action_proc_chance(
52 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::CHAOS_STRIKE_REFUND_PROC
53 .action(),
54 );
55
56 if proc_chance(ctx.rng(), refund_chance) {
57 let refund = ctx.effect_base_points(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::CHAOS_STRIKE_FURY_REFUND);
58
59 ctx.gain_resource(refund);
60 }
61}
62
63fn demonsurge_damage_hook(ctx: &mut HookCtx<'_>) {
64 let cfg = havoc_config(ctx);
65 let mult = ctx
66 .spec_runtime_mut::<HavocRuntime>()
67 .map_or(1.0, |runtime| {
68 let mult = 1.0 + cfg.demonsurge_stack_pct * f64::from(runtime.demonsurge_stacks);
69
70 runtime.demonsurge_stacks = runtime
71 .demonsurge_stacks
72 .saturating_add(1)
73 .min(DEMONSURGE_MAX_STACKS);
74
75 mult
76 });
77
78 if cfg.demonsurge {
79 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::DEMONSURGE_DAMAGE, mult, DamageFlags::empty());
80 }
81}
82
83fn trigger_demonsurge(ctx: &mut HookCtx<'_>, ability: DemonsurgeAbilities, delay_ms: u32) {
84 if !havoc_config(ctx).demonsurge {
85 return;
86 }
87
88 let available = ctx
89 .spec_runtime_mut::<HavocRuntime>()
90 .is_some_and(|runtime| {
91 if !runtime.demonsurge_available.contains(ability) {
92 return false;
93 }
94
95 runtime.demonsurge_available.remove(ability);
96
97 true
98 });
99
100 if available {
101 ctx.schedule_hook(delay_ms, demonsurge_damage_hook);
102 }
103}
104
105pub(crate) fn annihilation_hook(ctx: &mut HookCtx<'_>) {
107 chaos_strike_hook(ctx);
108 trigger_demonsurge(ctx, DemonsurgeAbilities::ANNIHILATION, DEMONSURGE_DELAY_MS);
109}
110
111pub(crate) fn throw_glaive_hook(ctx: &mut HookCtx<'_>) {
112 apply_serrated_glaive(ctx);
113
114 if havoc_config(ctx).furious_throws {
115 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FURIOUS_THROWS_DAMAGE, 1.0, DamageFlags::PHYSICAL);
116 }
117}
118
119fn throw_glaive_cd_ready(ctx: &HookCtx<'_>) -> bool {
120 ctx.spell_ready(SPELL_THROW_GLAIVE)
121}
122
123fn consume_throw_glaive_cd(ctx: &mut HookCtx<'_>) {
125 ctx.start_cooldown(SPELL_THROW_GLAIVE);
126}
127
128fn screaming_brutality_on_blade_dance(ctx: &mut HookCtx<'_>, cfg: &HavocConfig) {
129 if !cfg.screaming_brutality {
130 return;
131 }
132
133 if throw_glaive_cd_ready(ctx) {
134 consume_throw_glaive_cd(ctx);
135 apply_serrated_glaive(ctx);
136 ctx.deal_effect_damage_ap(
137 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::THROW_GLAIVE_DAMAGE,
138 cfg.sb_full_pct,
139 DamageFlags::PHYSICAL,
140 );
141
142 if cfg.furious_throws {
143 ctx.deal_effect_damage_ap(
144 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FURIOUS_THROWS_DAMAGE,
145 cfg.sb_full_pct,
146 DamageFlags::PHYSICAL,
147 );
148 }
149 }
150
151 for _ in 0..=BLADE_DANCE_SLASHES {
152 if proc_chance(ctx.rng(), cfg.sb_slash_chance) {
153 apply_serrated_glaive(ctx);
154 ctx.deal_effect_damage_ap(
155 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::THROW_GLAIVE_DAMAGE,
156 cfg.sb_slash_pct,
157 DamageFlags::PHYSICAL,
158 );
159
160 if cfg.furious_throws {
161 ctx.deal_effect_damage_ap(
162 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FURIOUS_THROWS_DAMAGE,
163 cfg.sb_slash_pct,
164 DamageFlags::PHYSICAL,
165 );
166 }
167 }
168 }
169}
170
171pub(crate) fn blade_dance_hook(ctx: &mut HookCtx<'_>) {
172 let cfg = havoc_config(ctx);
173
174 screaming_brutality_on_blade_dance(ctx, &cfg);
175
176 if cfg.first_blood {
177 let mult = 1.0 + ctx.effect_base_points(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FIRST_BLOOD_MULT) / HUNDRED;
178
179 for _ in 0..BLADE_DANCE_SLASHES {
180 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FIRST_BLOOD_HIT, mult, DamageFlags::empty());
181 }
182
183 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FIRST_BLOOD_FINAL, mult, DamageFlags::empty());
184 } else {
185 for _ in 0..BLADE_DANCE_SLASHES {
186 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::BLADE_DANCE_HIT, 1.0, DamageFlags::PHYSICAL);
187 }
188
189 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::BLADE_DANCE_FINAL, 1.0, DamageFlags::PHYSICAL);
190 }
191
192 trigger_trail_of_ruin(ctx, &cfg);
193}
194
195pub(crate) fn death_sweep_hook(ctx: &mut HookCtx<'_>) {
196 let cfg = havoc_config(ctx);
197
198 screaming_brutality_on_blade_dance(ctx, &cfg);
199
200 if cfg.first_blood {
201 let mult = 1.0 + ctx.effect_base_points(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::FIRST_BLOOD_MULT) / HUNDRED;
202
203 for _ in 0..BLADE_DANCE_SLASHES {
204 ctx.deal_effect_damage_ap(
205 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::DEATH_SWEEP_FIRST_BLOOD_HIT,
206 mult,
207 DamageFlags::empty(),
208 );
209 }
210
211 ctx.deal_effect_damage_ap(
212 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::DEATH_SWEEP_FIRST_BLOOD_FINAL,
213 mult,
214 DamageFlags::empty(),
215 );
216 }
217
218 trigger_trail_of_ruin(ctx, &cfg);
219
220 trigger_demonsurge(
221 ctx,
222 DemonsurgeAbilities::DEATH_SWEEP,
223 DEMONSURGE_DEATH_SWEEP_DELAY_MS,
224 );
225}
226
227fn trigger_trail_of_ruin(ctx: &mut HookCtx<'_>, cfg: &HavocConfig) {
228 if cfg.trail_of_ruin {
229 ctx.deal_effect_damage_ap(
230 crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::TRAIL_OF_RUIN_DAMAGE,
231 1.0,
232 DamageFlags::empty(),
233 );
234 }
235}
236
237fn eye_beam_channel_ms(ctx: &HookCtx<'_>) -> u32 {
238 let remaining_s = (ctx.buf().player().channel_end - ctx.now().as_secs_f64()).max(0.0);
239
240 wowlab_types::numeric::f64_to_u32_saturating_round(remaining_s * MS_PER_SECOND)
241}
242
243fn trigger_demonic(ctx: &mut HookCtx<'_>, cfg: &HavocConfig) {
244 if !cfg.demonic {
245 return;
246 }
247
248 if cfg.demonsurge
249 && let Some(runtime) = ctx.spec_runtime_mut::<HavocRuntime>() {
250 runtime
251 .demonsurge_available
252 .insert(DemonsurgeAbilities::DEMONIC);
253 }
254
255 let total_ms = cfg.demonic_extension_ms + eye_beam_channel_ms(ctx);
256
257 if ctx.is_aura_active(AURA_METAMORPHOSIS) {
258 ctx.extend_aura(AURA_METAMORPHOSIS, total_ms);
259 } else {
260 ctx.apply_aura(AURA_METAMORPHOSIS);
261 ctx.reduce_aura(
262 AURA_METAMORPHOSIS,
263 META_BASE_MS.saturating_sub(total_ms),
264 );
265 }
266}
267
268fn furious_gaze_apply_hook(ctx: &mut HookCtx<'_>) {
269 ctx.apply_aura(AURA_FURIOUS_GAZE);
270}
271
272fn empowered_eye_beam_expire_hook(ctx: &mut HookCtx<'_>) {
273 ctx.expire_aura(AURA_EMPOWERED_EYE_BEAM);
274}
275
276pub(crate) fn eye_beam_hook(ctx: &mut HookCtx<'_>) {
277 let cfg = havoc_config(ctx);
278
279 if cfg.student_of_suffering {
280 ctx.apply_aura(AURA_STUDENT_OF_SUFFERING);
281 }
282
283 trigger_demonic(ctx, &cfg);
284
285 if cfg.cycle_of_hatred {
286 let stacks = ctx.aura_stacks(AURA_CYCLE_OF_HATRED).max(0);
288
289 if stacks > 0 {
290 let stacks = wowlab_types::numeric::i32_to_u32_nonnegative(stacks);
291
292 ctx.reduce_cooldown(
293 SPELL_EYE_BEAM,
294 cfg.cycle_of_hatred_cdr_ms * stacks,
295 );
296
297 if cfg.mid2_4pc && stacks < CYCLE_CROSS_REDUCTION_MAX_STACKS {
298 ctx.reduce_cooldown(SPELL_ESSENCE_BREAK, cfg.cycle_of_hatred_cdr_ms);
299 }
300 }
301
302 ctx.apply_aura(AURA_CYCLE_OF_HATRED);
303 }
304
305 if cfg.furious_gaze {
306 let channel_ms = eye_beam_channel_ms(ctx);
307
308 ctx.schedule_hook(channel_ms, furious_gaze_apply_hook);
309 }
310
311 if cfg.eternal_hunt && ctx.is_aura_active(AURA_EMPOWERED_EYE_BEAM) {
312 ctx.schedule_hook(
315 eye_beam_channel_ms(ctx).saturating_add(1),
316 empowered_eye_beam_expire_hook,
317 );
318 }
319}
320
321pub(crate) fn essence_break_hook(ctx: &mut HookCtx<'_>) {
322 let cfg = havoc_config(ctx);
323
324 if !cfg.mid2_4pc || !cfg.cycle_of_hatred {
325 return;
326 }
327
328 let stacks = wowlab_types::numeric::i32_to_u32_nonnegative(
332 ctx.aura_stacks(AURA_CYCLE_OF_HATRED).max(0),
333 );
334
335 if stacks > 0 {
336 ctx.reduce_cooldown(
337 SPELL_ESSENCE_BREAK,
338 cfg.cycle_of_hatred_cdr_ms * stacks,
339 );
340
341 if stacks < CYCLE_CROSS_REDUCTION_MAX_STACKS {
342 ctx.reduce_cooldown(SPELL_EYE_BEAM, cfg.cycle_of_hatred_cdr_ms);
343 }
344 }
345
346 ctx.apply_aura(AURA_CYCLE_OF_HATRED);
347}
348
349pub(crate) fn abyssal_gaze_hook(ctx: &mut HookCtx<'_>) {
350 eye_beam_hook(ctx);
351 trigger_demonsurge(ctx, DemonsurgeAbilities::ABYSSAL_GAZE, DEMONSURGE_DELAY_MS);
352}
353
354pub(crate) fn eye_beam_tick_hook(ctx: &mut HookCtx<'_>) {
355 let per_tick = havoc_config(ctx).blind_fury_per_tick;
356
357 if per_tick > 0.0 {
358 ctx.gain_resource(per_tick);
359 }
360
361 if ctx.is_aura_active(AURA_EMPOWERED_EYE_BEAM) {
362 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::EMPOWERED_EYE_BEAM_DAMAGE, 1.0, DamageFlags::empty());
364 } else {
365 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::EYE_BEAM_DAMAGE, 1.0, DamageFlags::empty());
366 }
367}
368
369pub(crate) fn the_hunt_hook(ctx: &mut HookCtx<'_>) {
370 if havoc_config(ctx).eternal_hunt {
371 ctx.apply_aura(AURA_EMPOWERED_EYE_BEAM);
372 }
373}
374
375pub(crate) fn metamorphosis_hook(ctx: &mut HookCtx<'_>) {
376 let cfg = havoc_config(ctx);
377
378 if cfg.demonsurge
379 && let Some(runtime) = ctx.spec_runtime_mut::<HavocRuntime>() {
380 runtime.demonsurge_available = DemonsurgeAbilities::ALL_HARDCAST;
381 runtime.demonsurge_stacks = 0;
382 }
383
384 if cfg.demonic_intensity {
385 ctx.apply_aura(AURA_DEMONIC_INTENSITY);
386 }
387
388 if cfg.chaotic_transformation {
389 ctx.reset_cooldown(SPELL_EYE_BEAM);
390 ctx.reset_cooldown(SPELL_BLADE_DANCE);
391 }
392}
393
394pub(crate) fn consuming_fire_hook(ctx: &mut HookCtx<'_>) {
395 trigger_demonsurge(
396 ctx,
397 DemonsurgeAbilities::CONSUMING_FIRE,
398 DEMONSURGE_DELAY_MS,
399 );
400}
401
402pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, _event: SwingEvent) {
403 ctx.deal_effect_damage_ap(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::DEMON_BLADES_DAMAGE, 1.0, DamageFlags::empty());
404 let fury = ctx.effect_base_points(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::DEMON_BLADES_FURY);
405
406 ctx.gain_resource(fury);
407}
408
409#[cfg(test)]
410mod demonic_presence_mastery_tests {
411 use googletest::prelude::*;
412
413 use super::*;
414 use wowlab_engine_ports::{CombatStats};
415use wowlab_engine_gamedata::{SpellProps};
416 use wowlab_types::sim::{EnemyIdx, SimTime, SpellIdx};
417
418 use crate::generated::specs::havoc_demon_hunter::{REPORTED_SPELL, SPELL};
419
420 const DEMON_HUNTER_SPELL_FAMILY: i32 = 107;
421 const DEMONIC_PRESENCE_PCT: f64 = 34.07;
422 const MASTERY_TEST_ROTATION: &str = r#"{"version":1,"name":"presence","variables":{},"actions":[{"type":"wait","seconds":1.0}],"lists":{}}"#;
423
424 #[gtest]
427 fn demonic_presence_applies_only_to_its_dbc_affect_list() -> Result<()> {
428 let presence = SpellIdx::from_raw(EFFECT::MASTERY_DIRECT_BONUS.0);
429 let direct = EFFECT::MASTERY_DIRECT_BONUS.1;
430 let periodic = EFFECT::MASTERY_PERIODIC_BONUS.1;
431 let mut data = wowlab_engine_gamedata::ResolvedGameData::builder();
432
433 data.insert_spell_props(
434 presence,
435 SpellProps {
436 mastery_affects_points: true,
437 ..SpellProps::default()
438 },
439 );
440 data.insert_sp_coef(presence, direct, 1.0);
441 data.insert_sp_coef(presence, periodic, 1.0);
442 data.insert_spell_class_flags(presence, DEMON_HUNTER_SPELL_FAMILY, [0, 0, 0, 0]);
443 data.insert_effect_class_mask(presence, direct, [1, 0, 0, 0]);
444 data.insert_effect_class_mask(presence, periodic, [1, 0, 0, 0]);
445
446 for (spells, mask) in [
447 (
448 [
449 REPORTED_SPELL::CHAOS_STRIKE_DAMAGE_1,
450 REPORTED_SPELL::SOULSCAR,
451 ],
452 [1, 0, 0, 0],
453 ),
454 (
455 [
456 REPORTED_SPELL::FELBLADE_DAMAGE,
457 REPORTED_SPELL::IMMOLATION_AURA_DAMAGE,
458 ],
459 [2, 0, 0, 0],
460 ),
461 ] {
462 for spell in spells {
463 data.insert_spell_class_flags(
464 SpellIdx::from_raw(spell),
465 DEMON_HUNTER_SPELL_FAMILY,
466 mask,
467 );
468 }
469 }
470
471 let built = crate::test_combat_builder_for(wowlab_engine_combat::generated_identities::specs::havoc_demon_hunter::PROOF, CombatStats::default())
472 .game_data(data.build())
473 .effect_identities(wowlab_engine_combat::generated_identities::specs::havoc_demon_hunter::damage::ALL)
474 .mastery_spell(EFFECT::MASTERY_DIRECT_BONUS.0)
475 .mastery_hook(mastery)
476 .spell("felblade", SPELL::FELBLADE, Ok)
477 .into_draft().expect("valid combat draft").finalize(&crate::test_support::rotation_from_json(MASTERY_TEST_ROTATION))
478 .or_fail()?;
479 let bonus = |action, is_periodic: bool| {
480 mastery(
481 &MasteryCtx::new(
482 &built.program,
483 &built.instance,
484 wowlab_engine_combat::MasteryEvent {
485 action,
486 school: wowlab_types::combat::DamageSchool::Chaos,
487 mastery: DEMONIC_PRESENCE_PCT,
488 player_crit_pct: 0.0,
489 mastery_action: crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::MASTERY_DIRECT_BONUS.action(),
490 is_periodic,
491 is_pet: false,
492 source: wowlab_types::sim::ActorId::Player,
493 target: Some(EnemyIdx::PRIMARY),
494 now: SimTime::ZERO,
495 },
496 )
497 .expect("mastery fixture coordinates are finalized"),
498 )
499 };
500 let listed = 1.0 + DEMONIC_PRESENCE_PCT / HUNDRED;
501
502 verify_that!(
503 bonus(
504 crate::generated::specs::havoc_demon_hunter::DAMAGE_REPORTED::CHAOS_STRIKE_DAMAGE_1,
505 false,
506 ),
507 near(listed, 1e-12)
508 )?;
509 verify_that!(
510 bonus(
511 crate::generated::specs::havoc_demon_hunter::DAMAGE_REPORTED::SOULSCAR,
512 true,
513 ),
514 near(listed, 1e-12)
515 )?;
516
517 for action in [
518 crate::generated::specs::havoc_demon_hunter::DAMAGE_REPORTED::FELBLADE_DAMAGE,
519 crate::generated::specs::havoc_demon_hunter::DAMAGE_REPORTED::IMMOLATION_AURA_DAMAGE,
520 ] {
521 verify_that!(bonus(action, false), near(1.0, 1e-12))?;
522 }
523
524 Ok(())
525 }
526}
527
528#[cfg(test)]
529#[path = "hooks/serrated_glaive_tests.rs"]
530mod serrated_glaive_tests;