1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::AuraOps as _;
3use wowlab_engine_combat::{
4 HookCtx, ImpactEvent, ImpactProc, PeriodicKind,
5};
6use wowlab_types::constants::HUNDRED;
7
8use crate::generated::specs::havoc_demon_hunter::{
9 AURA_BURNING_BLADES, AURA_SOULSCAR, AURA_THE_HUNT_DOT, EFFECT, HERO, REPORTED_SPELL,
10 SET_BONUS, SPELL_THE_HUNT, TALENT,
11};
12
13#[rustfmt::skip]
15static BURNING_BLADES_TRIGGERS: &[u32] = &[
16 REPORTED_SPELL::AUTO_ATTACK,
17 REPORTED_SPELL::CHAOS_STRIKE_DAMAGE_2,
18 REPORTED_SPELL::ANNIHILATION_DAMAGE_2,
19 REPORTED_SPELL::CHAOS_STRIKE_DAMAGE_1,
20 REPORTED_SPELL::ANNIHILATION_DAMAGE_1,
21 REPORTED_SPELL::THROW_GLAIVE_DAMAGE,
22 REPORTED_SPELL::BLADE_DANCE_FIRST_BLOOD,
23 REPORTED_SPELL::BLADE_DANCE_FIRST_BLOOD_4,
24 REPORTED_SPELL::THROW_GLAIVE_FURIOUS_THROWS,
25 REPORTED_SPELL::DEATH_SWEEP_FIRST_BLOOD_4,
26 REPORTED_SPELL::DEATH_SWEEP_FIRST_BLOOD,
27];
28
29fn burning_blades_trigger_filter(spell_id: u32) -> bool {
30 BURNING_BLADES_TRIGGERS.contains(&spell_id)
31}
32
33fn burning_blades_impact(
34 ctx: &mut HookCtx<'_>,
35 impact: ImpactEvent,
36) {
37 let fraction = havoc_config(ctx).burning_blades_fraction;
38
39 ctx.add_residual_damage(AURA_BURNING_BLADES, impact.amount * fraction);
40}
41
42#[rustfmt::skip]
43static SOULSCAR_TRIGGERS: &[u32] = &[
44 REPORTED_SPELL::THROW_GLAIVE_DAMAGE,
45 REPORTED_SPELL::THROW_GLAIVE_FURIOUS_THROWS,
46];
47
48fn soulscar_trigger_filter(spell_id: u32) -> bool {
49 SOULSCAR_TRIGGERS.contains(&spell_id)
50}
51
52fn soulscar_impact(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
53 let fraction = havoc_config(ctx).soulscar_fraction;
54 let mastery_bonus = 1.0 + ctx.effect_points(crate::generated::specs::havoc_demon_hunter::DAMAGE_EFFECT::MASTERY_PERIODIC_BONUS) / HUNDRED;
58
59 ctx.add_residual_damage(AURA_SOULSCAR, impact.amount * fraction * mastery_bonus);
60}
61
62const FURY_POWER_DIVISOR: f64 = 10.0;
64const CHANNEL_TICKS_PER_SECOND: f64 = 5.0;
66
67bitflags::bitflags! {
68 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
69 pub(crate) struct DemonsurgeAbilities: u8 {
70 const ABYSSAL_GAZE = 1 << 0;
71 const ANNIHILATION = 1 << 1;
72 const DEATH_SWEEP = 1 << 2;
73 const CONSUMING_FIRE = 1 << 3;
74
75 const ALL_HARDCAST = Self::ABYSSAL_GAZE.bits()
76 | Self::ANNIHILATION.bits()
77 | Self::DEATH_SWEEP.bits()
78 | Self::CONSUMING_FIRE.bits();
79 const DEMONIC = Self::ANNIHILATION.bits()
80 | Self::DEATH_SWEEP.bits();
81 }
82}
83
84#[derive(Clone, Copy, Debug, Default)]
85pub(crate) struct HavocRuntime {
86 pub(crate) demonsurge_available: DemonsurgeAbilities,
87 pub(crate) demonsurge_stacks: u8,
88}
89
90crate::hooks::define_spec_config! {
91pub(super) struct HavocConfig {
92 first_blood: bool { provenance: "First Blood 206416 selection.", gate: params.talent_ranks(TALENT::FIRST_BLOOD) > 0, source: true, transform: |value| value },
93 trail_of_ruin: bool { provenance: "Trail of Ruin 258881 selection.", gate: params.talent_ranks(TALENT::TRAIL_OF_RUIN) > 0, source: true, transform: |value| value },
94 burning_blades_fraction: f64 { provenance: "Burning Blades 452408 e1 residual fraction with Havoc aura scaling.", gate: burning_blades, source: params.effect_base_points(EFFECT::BURNING_BLADES_FRACTION) * (1.0 + params.effect_base_points(EFFECT::HAVOC_AURA_DIRECT) / HUNDRED), transform: |value| value / HUNDRED },
95 serrated_glaive: bool { provenance: "Serrated Glaive 390154 selection.", gate: serrated, source: true, transform: |value| value },
96 furious_throws: bool { provenance: "Furious Throws 393029 selection.", gate: params.talent_ranks(TALENT::FURIOUS_THROWS) > 0, source: true, transform: |value| value },
97 screaming_brutality: bool { provenance: "Screaming Brutality 1220506 selection.", gate: screaming, source: true, transform: |value| value },
98 sb_slash_pct: f64 { provenance: "Screaming Brutality 1220506 e1 slash multiplier.", gate: screaming, source: params.effect_base_points(EFFECT::SB_SLASH_PCT), transform: |value| value / HUNDRED },
99 sb_slash_chance: f64 { provenance: "Screaming Brutality 1220506 e2 slash chance.", gate: screaming, source: params.effect_base_points(EFFECT::SB_SLASH_CHANCE), transform: |value| value / HUNDRED },
100 sb_full_pct: f64 { provenance: "Screaming Brutality 1220506 e3 full multiplier.", gate: screaming, source: params.effect_base_points(EFFECT::SB_FULL_PCT), transform: |value| value / HUNDRED },
101 soulscar_fraction: f64 { provenance: "Soulscar 388106 e1 residual fraction.", gate: soulscar, source: params.effect_base_points(EFFECT::SOULSCAR_FRACTION), transform: |value| value / HUNDRED },
102 demonic: bool { provenance: "Demonic 213410 selection.", gate: demonic, source: true, transform: |value| value },
103 demonic_extension_ms: u32 { provenance: "Demonic 213410 e1 extension milliseconds.", gate: demonic, source: params.effect_base_points(EFFECT::DEMONIC_EXTENSION), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
104 chaotic_transformation: bool { provenance: "Chaotic Transformation 388112 selection.", gate: params.talent_ranks(TALENT::CHAOTIC_TRANSFORMATION) > 0, source: true, transform: |value| value },
105 cycle_of_hatred: bool { provenance: "Cycle of Hatred 258887 selection.", gate: cycle, source: true, transform: |value| value },
106 cycle_of_hatred_cdr_ms: u32 { provenance: "Cycle of Hatred 258887 e1 CDR milliseconds.", gate: cycle, source: params.effect_base_points(EFFECT::CYCLE_OF_HATRED_CDR), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
107 mid2_4pc: bool { provenance: "MID2 Havoc 4pc 1296612 enables Cycle of Hatred cross-cooldown reduction.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_HAVOC_4PC), source: true, transform: |value| value },
108 blind_fury_per_tick: f64 { provenance: "Blind Fury 203550 e3 fury divided by power scale and channel ticks.", gate: params.talent_ranks(TALENT::BLIND_FURY) > 0, source: params.effect_base_points(EFFECT::BLIND_FURY_FURY), transform: |value| value / FURY_POWER_DIVISOR / CHANNEL_TICKS_PER_SECOND },
109 furious_gaze: bool { provenance: "Furious Gaze 343311 selection.", gate: params.talent_ranks(TALENT::FURIOUS_GAZE) > 0, source: true, transform: |value| value },
110 student_of_suffering: bool { provenance: "Student of Suffering 452412 selection.", gate: params.talent_ranks(HERO::FEL_SCARRED::SPELL::STUDENT_OF_SUFFERING) > 0, source: true, transform: |value| value },
111 demonic_intensity: bool { provenance: "Demonic Intensity 452415 selection.", gate: params.talent_ranks(HERO::FEL_SCARRED::SPELL::DEMONIC_INTENSITY) > 0, source: true, transform: |value| value },
112 eternal_hunt: bool { provenance: "Eternal Hunt 1270898 selection.", gate: params.talent_ranks(TALENT::ETERNAL_HUNT) > 0, source: true, transform: |value| value },
113 demonsurge: bool { provenance: "Demonsurge 452402 selection.", gate: demonsurge, source: true, transform: |value| value },
114 demonsurge_stack_pct: f64 { provenance: "Demonsurge 452416 e2 per-stack increase.", gate: demonsurge, source: params.effect_base_points(EFFECT::DEMONSURGE_STACK_PCT), transform: |value| value / HUNDRED },
115}
116accessor pub(crate) fn havoc_config(ctx);
117register pub(crate) fn register_havoc(built, params) -> HavocConfig;
118prepare {
119 let serrated = params.talent_ranks(TALENT::SERRATED_GLAIVE) > 0;
120 let screaming = params.talent_ranks(TALENT::SCREAMING_BRUTALITY) > 0;
121 let demonic = params.talent_ranks(TALENT::DEMONIC) > 0;
122 let cycle = params.talent_ranks(TALENT::CYCLE_OF_HATRED) > 0;
123 let demonsurge = params.talent_ranks(HERO::FEL_SCARRED::SPELL::DEMONSURGE) > 0;
124 let soulscar = params.talent_ranks(TALENT::SOULSCAR) > 0;
125 let burning_blades = params.talent_ranks(HERO::FEL_SCARRED::SPELL::BURNING_BLADES) > 0;
126}
127auras {}
128wiring {
129 { provenance: "Eternal Hunt direct, periodic, and cooldown modifiers.", gate: params.talent_ranks(TALENT::ETERNAL_HUNT_RANK_2) > 0, apply: {
130 let direct_mult = 1.0 + params.effect_base_points(EFFECT::ETERNAL_HUNT_DAMAGE_PCT) / HUNDRED;
131 let periodic_mult = 1.0 + params.effect_base_points(EFFECT::ETERNAL_HUNT_PERIODIC_PCT) / HUNDRED;
132 built.patch_spell(SPELL_THE_HUNT, |spell| {
133 spell.damage = spell.damage.scaled(direct_mult);
134 let cooldown_delta = params.effect_base_points(EFFECT::ETERNAL_HUNT_COOLDOWN_MS);
136 let adjusted = (f64::from(spell.cooldown.cooldown_duration_ms) + cooldown_delta).max(0.0);
137 spell.cooldown.cooldown_duration_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(adjusted);
138 });
139 built.patch_aura(AURA_THE_HUNT_DOT, |aura| {
140 if let Some(periodic) = aura.periodic.as_mut() {
141 periodic.effect = match periodic.effect {
142 PeriodicKind::DamageAp { coef, is_physical } => PeriodicKind::DamageAp {
143 coef: coef * periodic_mult,
144 is_physical,
145 },
146 other => other,
147 };
148 }
149 });
150 } },
151 { provenance: "Soulscar Throw Glaive residual proc.", gate: soulscar, apply: {
152 built.register_impact_proc(
153 ImpactProc::new(soulscar_impact)
154 .with_spell_filter(soulscar_trigger_filter)
155 .skip_periodic(),
156 );
157 } },
158 { provenance: "Burning Blades residual proc.", gate: burning_blades, apply: {
159 built.register_impact_proc(
160 ImpactProc::new(burning_blades_impact)
161 .with_spell_filter(burning_blades_trigger_filter)
162 .skip_periodic(),
163 );
164 } },
165}
166finish |cfg| {
167 built.set_spec_config(cfg);
168 built.set_spec_runtime(HavocRuntime::default());
169 cfg
170}
171}