Skip to main content

wowlab_engine_content/hooks/havoc_demon_hunter/
config.rs

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    SPELL_THE_HUNT, TALENT,
11};
12
13/// Burning Blades triggers, excluding Demon Blades and including Furious Throws 393035.
14#[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.raw(), 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    // Soulscar ticks are chaos and benefit Demonic Presence (185164 e2 lists
55    // 390181) while the physical glaive trigger does not; the drain deals
56    // resolved flat amounts, so the mastery bonus folds in at trigger time.
57    let mastery_bonus = 1.0 + ctx.effect_points(EFFECT::MASTERY_PERIODIC_BONUS) / HUNDRED;
58
59    ctx.add_residual_damage(AURA_SOULSCAR.raw(), impact.amount * fraction * mastery_bonus);
60}
61
62/// Fury energize base values carry the power-type scale of 10 (e.g. 203550 e3 base 150 = 15 fury/s).
63const FURY_POWER_DIVISOR: f64 = 10.0;
64/// Eye Beam channels 10 ticks over 2s (198013 e1 period 200ms).
65const 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    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 },
94    serrated_glaive: bool { provenance: "Serrated Glaive 390154 selection.", gate: serrated, source: true, transform: |value| value },
95    furious_throws: bool { provenance: "Furious Throws 393029 selection.", gate: params.talent_ranks(TALENT::FURIOUS_THROWS) > 0, source: true, transform: |value| value },
96    screaming_brutality: bool { provenance: "Screaming Brutality 1220506 selection.", gate: screaming, source: true, transform: |value| value },
97    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 },
98    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 },
99    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 },
100    soulscar_fraction: f64 { provenance: "Soulscar 388106 e1 residual fraction.", gate: soulscar, source: params.effect_base_points(EFFECT::SOULSCAR_FRACTION), transform: |value| value / HUNDRED },
101    demonic: bool { provenance: "Demonic 213410 selection.", gate: demonic, source: true, transform: |value| value },
102    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 },
103    chaotic_transformation: bool { provenance: "Chaotic Transformation 388112 selection.", gate: params.talent_ranks(TALENT::CHAOTIC_TRANSFORMATION) > 0, source: true, transform: |value| value },
104    cycle_of_hatred: bool { provenance: "Cycle of Hatred 258887 selection.", gate: cycle, source: true, transform: |value| value },
105    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 },
106    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 },
107    furious_gaze: bool { provenance: "Furious Gaze 343311 selection.", gate: params.talent_ranks(TALENT::FURIOUS_GAZE) > 0, source: true, transform: |value| value },
108    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 },
109    demonic_intensity: bool { provenance: "Demonic Intensity 452415 selection.", gate: params.talent_ranks(HERO::FEL_SCARRED::SPELL::DEMONIC_INTENSITY) > 0, source: true, transform: |value| value },
110    eternal_hunt: bool { provenance: "Eternal Hunt 1270898 selection.", gate: params.talent_ranks(TALENT::ETERNAL_HUNT) > 0, source: true, transform: |value| value },
111    demonsurge: bool { provenance: "Demonsurge 452402 selection.", gate: demonsurge, source: true, transform: |value| value },
112    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 },
113}
114accessor pub(crate) fn havoc_config(ctx);
115register pub(crate) fn register_havoc(built, params) -> HavocConfig;
116prepare {
117    let serrated = params.talent_ranks(TALENT::SERRATED_GLAIVE) > 0;
118    let screaming = params.talent_ranks(TALENT::SCREAMING_BRUTALITY) > 0;
119    let demonic = params.talent_ranks(TALENT::DEMONIC) > 0;
120    let cycle = params.talent_ranks(TALENT::CYCLE_OF_HATRED) > 0;
121    let demonsurge = params.talent_ranks(HERO::FEL_SCARRED::SPELL::DEMONSURGE) > 0;
122    let soulscar = params.talent_ranks(TALENT::SOULSCAR) > 0;
123    let burning_blades = params.talent_ranks(HERO::FEL_SCARRED::SPELL::BURNING_BLADES) > 0;
124}
125auras {}
126wiring {
127    { provenance: "Eternal Hunt direct, periodic, and cooldown modifiers.", gate: params.talent_ranks(TALENT::ETERNAL_HUNT_RANK_2) > 0, apply: {
128        let direct_mult = 1.0 + params.effect_base_points(EFFECT::ETERNAL_HUNT_DAMAGE_PCT) / HUNDRED;
129        let periodic_mult = 1.0 + params.effect_base_points(EFFECT::ETERNAL_HUNT_PERIODIC_PCT) / HUNDRED;
130        built.patch_spell(SPELL_THE_HUNT, |spell| {
131            spell.damage = spell.damage.scaled(direct_mult);
132            // The modifier is stored as -15000ms in DBC.
133            let cooldown_delta = params.effect_base_points(EFFECT::ETERNAL_HUNT_COOLDOWN_MS);
134            let adjusted = (f64::from(spell.cooldown.cooldown_duration_ms) + cooldown_delta).max(0.0);
135            spell.cooldown.cooldown_duration_ms = wowlab_types::numeric::f64_to_u32_saturating_trunc(adjusted);
136        });
137        built.patch_aura(AURA_THE_HUNT_DOT, |aura| {
138            if let Some(periodic) = aura.periodic.as_mut() {
139                periodic.effect = match periodic.effect {
140                    PeriodicKind::DamageAp { coef, is_physical } => PeriodicKind::DamageAp {
141                        coef: coef * periodic_mult,
142                        is_physical,
143                    },
144                    other => other,
145                };
146            }
147        });
148    } },
149    { provenance: "Soulscar Throw Glaive residual proc.", gate: soulscar, apply: {
150        built.register_impact_proc(
151            ImpactProc::new(soulscar_impact)
152                .with_spell_filter(soulscar_trigger_filter)
153                .skip_periodic(),
154        );
155    } },
156    { provenance: "Burning Blades residual proc.", gate: burning_blades, apply: {
157        built.register_impact_proc(
158            ImpactProc::new(burning_blades_impact)
159                .with_spell_filter(burning_blades_trigger_filter)
160                .skip_periodic(),
161        );
162    } },
163}
164finish |cfg| {
165    built.state.set_spec_config(cfg);
166    built.state.set_spec_runtime(HavocRuntime::default());
167    cfg
168}
169}