Skip to main content

wowlab_engine_content/hooks/devastation_evoker/
hooks.rs

1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::AuraOps as _;
4use wowlab_engine_combat::{DamageFlags, EmpowerReleaseAdjustment, HookCtx, MasteryCtx};
5use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
6use wowlab_engine_rng::proc_chance;
7use wowlab_types::{constants::HUNDRED};
8
9use super::config::{DevastationConfig, config};
10use crate::generated::specs::devastation_evoker::{
11    AURA_BOMBARDMENTS, AURA_BURNOUT, AURA_CHARGED_BLAST, AURA_DEEP_BREATH_TRIGGER, AURA_DRAGONRAGE,
12    AURA_ESSENCE_BURST, AURA_IMMINENT_DESTRUCTION, AURA_IRIDESCENCE_BLUE, AURA_IRIDESCENCE_RED,
13    AURA_MASS_DISINTEGRATE, AURA_MELT_ARMOR_DOT, AURA_POWER_SWELL, AURA_RISING_FURY,
14    AURA_STRAFING_RUN, EFFECT, REPORTED_SPELL, SPELL, SPELL_ETERNITY_SURGE, SPELL_FIRE_BREATH,
15    TALENT,
16};
17
18pub(crate) fn living_flame_hook(ctx: &mut HookCtx<'_>) {
19    let cfg = config(ctx);
20
21    if cfg.ruby_eb_chance > 0.0
22        && (ctx.is_aura_active(AURA_DRAGONRAGE.raw()) || proc_chance(ctx.rng(), cfg.ruby_eb_chance))
23    {
24        ctx.apply_aura(AURA_ESSENCE_BURST.raw());
25    }
26}
27
28pub(crate) fn azure_strike_hook(ctx: &mut HookCtx<'_>) {
29    let cfg = config(ctx);
30
31    if cfg.azure_eb_chance > 0.0
32        && (ctx.is_aura_active(AURA_DRAGONRAGE.raw())
33            || proc_chance(ctx.rng(), cfg.azure_eb_chance))
34    {
35        ctx.apply_aura(AURA_ESSENCE_BURST.raw());
36    }
37}
38
39pub(crate) fn disintegrate_hook(ctx: &mut HookCtx<'_>) {
40    let cfg = config(ctx);
41
42    if ctx.is_aura_active(AURA_ESSENCE_BURST.raw()) {
43        ctx.consume_aura_stack(AURA_ESSENCE_BURST.raw());
44
45        if cfg.titanic_wrath_mult > 1.0 {
46            ctx.multiply_active_channel_damage(cfg.titanic_wrath_mult);
47        }
48    }
49
50    if cfg.mass_disintegrate && ctx.is_aura_active(AURA_MASS_DISINTEGRATE.raw()) {
51        if cfg.bombardments {
52            ctx.apply_aura(AURA_BOMBARDMENTS.raw());
53        }
54
55        ctx.consume_aura_stack(AURA_MASS_DISINTEGRATE.raw());
56    }
57}
58
59pub(crate) fn disintegrate_tick_hook(ctx: &mut HookCtx<'_>) {
60    let cfg = config(ctx);
61
62    ctx.apply_aura(AURA_CHARGED_BLAST.raw());
63
64    if cfg.causality_tick_cdr_ms > 0 {
65        ctx.reduce_cooldown(SPELL_FIRE_BREATH.raw(), cfg.causality_tick_cdr_ms);
66        ctx.reduce_cooldown(SPELL_ETERNITY_SURGE.raw(), cfg.causality_tick_cdr_ms);
67    }
68
69    if cfg.scintillation_chance > 0.0 && proc_chance(ctx.rng(), cfg.scintillation_chance) {
70        ctx.deal_damage_sp(
71            TALENT::SCINTILLATION,
72            cfg.scintillation_coef,
73            DamageFlags::empty(),
74        );
75
76        if cfg.shattering_star_coef > 0.0 {
77            ctx.deal_damage_sp(
78                SPELL::SHATTERING_STAR,
79                cfg.shattering_star_coef,
80                DamageFlags::empty(),
81            );
82        }
83    }
84}
85
86fn empower_release(ctx: &mut HookCtx<'_>, cfg: &DevastationConfig, iridescence_aura: u8) {
87    ctx.apply_aura(AURA_POWER_SWELL.raw());
88
89    if cfg.iridescence {
90        ctx.apply_aura(iridescence_aura);
91    }
92
93    if cfg.mass_disintegrate {
94        ctx.apply_aura(AURA_MASS_DISINTEGRATE.raw());
95    }
96
97    if cfg.animosity_extend_ms > 0 && ctx.is_aura_active(AURA_DRAGONRAGE.raw()) {
98        ctx.extend_aura(AURA_DRAGONRAGE.raw(), cfg.animosity_extend_ms);
99    }
100}
101
102pub(crate) fn eternity_surge_hook(ctx: &mut HookCtx<'_>) {
103    let cfg = config(ctx);
104
105    empower_release(ctx, &cfg, AURA_IRIDESCENCE_BLUE.raw());
106
107    if cfg.shattering_star_coef > 0.0 {
108        ctx.deal_damage_sp(
109            SPELL::SHATTERING_STAR,
110            cfg.shattering_star_coef,
111            DamageFlags::empty(),
112        );
113    }
114    // Eye of Infinity: EB on ES crit, folded as an expected-value roll at the
115    // sheet crit chance (crit result is not visible to cast hooks).
116
117    if cfg.eye_of_infinity {
118        let crit = ctx.buf().player().crit / HUNDRED;
119
120        if proc_chance(ctx.rng(), crit) {
121            ctx.apply_aura(AURA_ESSENCE_BURST.raw());
122        }
123    }
124}
125
126pub(crate) fn fire_breath_hook(ctx: &mut HookCtx<'_>) {
127    let cfg = config(ctx);
128
129    empower_release(ctx, &cfg, AURA_IRIDESCENCE_RED.raw());
130
131    if cfg.burnout_chance > 0.0 && proc_chance(ctx.rng(), cfg.burnout_chance) {
132        ctx.apply_aura(AURA_BURNOUT.raw());
133    }
134}
135
136/// Apply Fire Breath's server-script tick transfer for the selected release rank.
137pub(crate) fn fire_breath_empower_release(ctx: &HookCtx<'_>, rank: u8) -> EmpowerReleaseAdjustment {
138    const BASE_TICKS: u16 = 10;
139    const TRANSFERRED_TICKS_PER_RANK: u16 = 3;
140
141    let cfg = config(ctx);
142    let total_ticks = BASE_TICKS + u16::from(cfg.blast_furnace_bonus_ticks);
143    let transferred_ticks = u16::from(rank.saturating_sub(1)) * TRANSFERRED_TICKS_PER_RANK;
144
145    EmpowerReleaseAdjustment::transfer_periodic_ticks(total_ticks, transferred_ticks)
146}
147
148pub(crate) fn pyre_hook(ctx: &mut HookCtx<'_>) {
149    if ctx.is_aura_active(AURA_ESSENCE_BURST.raw()) {
150        ctx.consume_aura_stack(AURA_ESSENCE_BURST.raw());
151    }
152
153    ctx.expire_aura(AURA_CHARGED_BLAST.raw());
154}
155
156pub(crate) fn dragonrage_hook(ctx: &mut HookCtx<'_>) {
157    let cfg = config(ctx);
158
159    if cfg.rising_fury {
160        ctx.apply_aura(AURA_RISING_FURY.raw());
161    }
162
163    let coef = ctx.game_data().effect_sp_coefficient(EFFECT::PYRE_DAMAGE);
164
165    ctx.deal_damage_sp(REPORTED_SPELL::PYRE_DAMAGE, coef, DamageFlags::empty());
166}
167
168pub(crate) fn dragonrage_expire_hook(ctx: &mut HookCtx<'_>) {
169    ctx.expire_aura(AURA_RISING_FURY.raw());
170}
171
172pub(crate) fn deep_breath_hook(ctx: &mut HookCtx<'_>) {
173    let cfg = config(ctx);
174    let targets = ctx.resolved_spell_effect_targets(
175        EFFECT::DEEP_BREATH_PATH,
176        EFFECT::DEEP_BREATH_PATH.0,
177        None,
178    );
179
180    for target in targets {
181        ctx.with_resolved_target(target, |ctx| {
182            ctx.apply_aura(AURA_DEEP_BREATH_TRIGGER.raw());
183        });
184    }
185
186    if cfg.melt_armor {
187        ctx.apply_aura(AURA_MELT_ARMOR_DOT.raw());
188    }
189
190    if cfg.imminent_destruction {
191        ctx.apply_aura(AURA_IMMINENT_DESTRUCTION.raw());
192    }
193
194    if cfg.strafing_run {
195        if ctx.is_aura_active(AURA_STRAFING_RUN.raw()) {
196            ctx.expire_aura(AURA_STRAFING_RUN.raw());
197        } else {
198            ctx.apply_aura(AURA_STRAFING_RUN.raw());
199        }
200    }
201}
202
203/// Returns Giantkiller mastery damage for spell 362980.
204#[must_use]
205pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
206    const GIANTKILLER_FLOOR: f64 = 0.3;
207    let health = ctx.target_health_pct().max(GIANTKILLER_FLOOR);
208
209    1.0 + ctx.points(1) / HUNDRED * health
210}