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::{
5    DamageFlags, EmpowerReleaseAdjustment, HookCtx, LocalAuraIdx, MasteryCtx,
6};
7use wowlab_engine_rng::proc_chance;
8use wowlab_types::{constants::HUNDRED};
9
10use super::config::{DevastationConfig, config};
11use crate::generated::specs::devastation_evoker::{
12    AURA_BOMBARDMENTS, AURA_BURNOUT, AURA_CHARGED_BLAST, AURA_DEEP_BREATH_TRIGGER, AURA_DRAGONRAGE,
13    AURA_ESSENCE_BURST, AURA_IMMINENT_DESTRUCTION, AURA_IRIDESCENCE_BLUE, AURA_IRIDESCENCE_RED,
14    AURA_MASS_DISINTEGRATE, AURA_MELT_ARMOR_DOT, AURA_POWER_SWELL, AURA_RISING_FURY,
15    AURA_STRAFING_RUN, AURA_UNBOUND_FLAME, SPELL_ETERNITY_SURGE, SPELL_FIRE_BREATH,
16};
17
18const MAX_EMPOWER_RANK: u8 = 4;
19const UNBOUND_FLAME_REDUCED_AOE_TARGETS: usize = 5;
20
21fn shattering_star_multiplier(ctx: &HookCtx<'_>, cfg: &DevastationConfig) -> f64 {
22    let rank = if cfg.mid2_max_shattering_star {
23        MAX_EMPOWER_RANK
24    } else {
25        ctx.empower_rank().unwrap_or(1)
26    };
27
28    cfg.mid2_shattering_star_mult
29        * (1.0 + f64::from(rank.saturating_sub(1)) * cfg.shattering_star_per_rank)
30}
31
32pub(crate) fn living_flame_hook(ctx: &mut HookCtx<'_>) {
33    let cfg = config(ctx);
34
35    if cfg.ruby_eb_chance > 0.0
36        && (ctx.is_aura_active(AURA_DRAGONRAGE) || proc_chance(ctx.rng(), cfg.ruby_eb_chance))
37    {
38        ctx.apply_aura(AURA_ESSENCE_BURST);
39    }
40}
41
42pub(crate) fn azure_strike_hook(ctx: &mut HookCtx<'_>) {
43    let cfg = config(ctx);
44
45    if cfg.azure_eb_chance > 0.0
46        && (ctx.is_aura_active(AURA_DRAGONRAGE)
47            || proc_chance(ctx.rng(), cfg.azure_eb_chance))
48    {
49        ctx.apply_aura(AURA_ESSENCE_BURST);
50    }
51}
52
53pub(crate) fn disintegrate_hook(ctx: &mut HookCtx<'_>) {
54    let cfg = config(ctx);
55
56    if ctx.is_aura_active(AURA_ESSENCE_BURST) {
57        ctx.consume_aura_stack(AURA_ESSENCE_BURST);
58
59        if cfg.titanic_wrath_mult > 1.0 {
60            ctx.multiply_active_channel_damage(cfg.titanic_wrath_mult);
61        }
62    }
63
64    if cfg.mass_disintegrate && ctx.is_aura_active(AURA_MASS_DISINTEGRATE) {
65        if cfg.bombardments {
66            ctx.apply_aura(AURA_BOMBARDMENTS);
67        }
68
69        ctx.consume_aura_stack(AURA_MASS_DISINTEGRATE);
70    }
71}
72
73pub(crate) fn disintegrate_tick_hook(ctx: &mut HookCtx<'_>) {
74    let cfg = config(ctx);
75
76    ctx.apply_aura(AURA_CHARGED_BLAST);
77
78    if cfg.causality_tick_cdr_ms > 0 {
79        ctx.reduce_cooldown(SPELL_FIRE_BREATH, cfg.causality_tick_cdr_ms);
80        ctx.reduce_cooldown(SPELL_ETERNITY_SURGE, cfg.causality_tick_cdr_ms);
81    }
82
83    if cfg.scintillation_chance > 0.0 && proc_chance(ctx.rng(), cfg.scintillation_chance) {
84        ctx.deal_damage_sp(
85            crate::generated::specs::devastation_evoker::DAMAGE_TALENT::SCINTILLATION,
86            cfg.scintillation_coef,
87            DamageFlags::empty(),
88        );
89
90        if cfg.shattering_star_coef > 0.0 {
91            ctx.deal_damage_sp(
92                crate::generated::specs::devastation_evoker::DAMAGE_SPELL::SHATTERING_STAR,
93                cfg.shattering_star_coef * shattering_star_multiplier(ctx, &cfg),
94                DamageFlags::empty(),
95            );
96        }
97    }
98}
99
100fn empower_release(
101    ctx: &mut HookCtx<'_>,
102    cfg: &DevastationConfig,
103    iridescence_aura: LocalAuraIdx,
104) {
105    ctx.apply_aura(AURA_POWER_SWELL);
106
107    if cfg.iridescence {
108        ctx.apply_aura(iridescence_aura);
109    }
110
111    if cfg.mass_disintegrate {
112        ctx.apply_aura(AURA_MASS_DISINTEGRATE);
113    }
114
115    if cfg.animosity_extend_ms > 0 && ctx.is_aura_active(AURA_DRAGONRAGE) {
116        ctx.extend_aura(AURA_DRAGONRAGE, cfg.animosity_extend_ms);
117    }
118}
119
120pub(crate) fn eternity_surge_hook(ctx: &mut HookCtx<'_>) {
121    let cfg = config(ctx);
122
123    empower_release(ctx, &cfg, AURA_IRIDESCENCE_BLUE);
124
125    if cfg.shattering_star_coef > 0.0 {
126        ctx.deal_damage_sp(
127            crate::generated::specs::devastation_evoker::DAMAGE_SPELL::SHATTERING_STAR,
128            cfg.shattering_star_coef * shattering_star_multiplier(ctx, &cfg),
129            DamageFlags::empty(),
130        );
131    }
132    // Eye of Infinity: EB on ES crit, folded as an expected-value roll at the
133    // sheet crit chance (crit result is not visible to cast hooks).
134
135    if cfg.eye_of_infinity {
136        let crit = ctx.buf().player().crit / HUNDRED;
137
138        if proc_chance(ctx.rng(), crit) {
139            ctx.apply_aura(AURA_ESSENCE_BURST);
140        }
141    }
142}
143
144pub(crate) fn fire_breath_hook(ctx: &mut HookCtx<'_>) {
145    let cfg = config(ctx);
146
147    empower_release(ctx, &cfg, AURA_IRIDESCENCE_RED);
148
149    if cfg.burnout_chance > 0.0 && proc_chance(ctx.rng(), cfg.burnout_chance) {
150        ctx.apply_aura(AURA_BURNOUT);
151    }
152}
153
154/// Apply Fire Breath's server-script tick transfer for the selected release rank.
155pub(crate) fn fire_breath_empower_release(ctx: &HookCtx<'_>, rank: u8) -> EmpowerReleaseAdjustment {
156    const BASE_TICKS: u16 = 10;
157    const TRANSFERRED_TICKS_PER_RANK: u16 = 3;
158
159    let cfg = config(ctx);
160    let total_ticks = BASE_TICKS + u16::from(cfg.blast_furnace_bonus_ticks);
161    let transferred_ticks = u16::from(rank.saturating_sub(1)) * TRANSFERRED_TICKS_PER_RANK;
162
163    EmpowerReleaseAdjustment::transfer_periodic_ticks(total_ticks, transferred_ticks)
164}
165
166pub(crate) fn pyre_hook(ctx: &mut HookCtx<'_>) {
167    if ctx.is_aura_active(AURA_ESSENCE_BURST) {
168        ctx.consume_aura_stack(AURA_ESSENCE_BURST);
169    }
170
171    ctx.expire_aura(AURA_CHARGED_BLAST);
172}
173
174pub(crate) fn dragonrage_hook(ctx: &mut HookCtx<'_>) {
175    let cfg = config(ctx);
176
177    if cfg.rising_fury {
178        ctx.apply_aura(AURA_RISING_FURY);
179    }
180
181    let coef = ctx.effect_sp_coefficient(crate::generated::specs::devastation_evoker::DAMAGE_EFFECT::PYRE_DAMAGE);
182
183    ctx.deal_damage_sp(crate::generated::specs::devastation_evoker::DAMAGE_REPORTED::PYRE_DAMAGE, coef, DamageFlags::empty());
184}
185
186pub(crate) fn dragonrage_expire_hook(ctx: &mut HookCtx<'_>) {
187    let cfg = config(ctx);
188    let stacks = ctx.aura_stacks(AURA_RISING_FURY).max(0);
189
190    ctx.expire_aura(AURA_RISING_FURY);
191
192    if cfg.rising_fury_3 {
193        let duration = u32::try_from(stacks)
194            .unwrap_or(u32::MAX)
195            .saturating_mul(cfg.rising_fury_persist_ms);
196
197        for _ in 0..stacks {
198            ctx.apply_aura_with_duration(AURA_RISING_FURY, duration);
199        }
200
201        ctx.apply_aura(AURA_UNBOUND_FLAME);
202    }
203}
204
205/// Deals Unbound Flame while its guaranteed-crit aura is live, consumes the charge, and grants Essence Burst.
206pub(crate) fn unbound_flame_hook(ctx: &mut HookCtx<'_>) {
207    let effect = crate::generated::specs::devastation_evoker::DAMAGE_EFFECT::UNBOUND_FLAME_DAMAGE;
208    let coef = ctx.effect_sp_coefficient(effect);
209
210    ctx.with_resolved_spell_effect_targets(
211        effect,
212        crate::generated::specs::devastation_evoker::DAMAGE_REPORTED::UNBOUND_FLAME_DAMAGE,
213        None,
214        |ctx, targets| {
215            let reduced_aoe = if targets.len() > UNBOUND_FLAME_REDUCED_AOE_TARGETS {
216                (wowlab_types::numeric::usize_to_f64(UNBOUND_FLAME_REDUCED_AOE_TARGETS)
217                    / wowlab_types::numeric::usize_to_f64(targets.len()))
218                .sqrt()
219            } else {
220                1.0
221            };
222
223            for target in targets.iter().copied() {
224                ctx.deal_damage_sp_to_target(
225                    target,
226                    crate::generated::specs::devastation_evoker::DAMAGE_REPORTED::UNBOUND_FLAME_DAMAGE,
227                    coef * reduced_aoe,
228                    DamageFlags::AOE,
229                );
230            }
231        },
232    );
233    ctx.consume_aura_stack(AURA_UNBOUND_FLAME);
234    ctx.apply_aura(AURA_ESSENCE_BURST);
235}
236
237pub(crate) fn deep_breath_hook(ctx: &mut HookCtx<'_>) {
238    let cfg = config(ctx);
239
240    ctx.with_resolved_spell_effect_targets(
241        crate::generated::specs::devastation_evoker::DAMAGE_EFFECT::DEEP_BREATH_PATH,
242        crate::generated::specs::devastation_evoker::DAMAGE_EFFECT::DEEP_BREATH_PATH.action(),
243        None,
244        |ctx, targets| {
245            for target in targets.iter().copied() {
246                ctx.with_resolved_target(target, |ctx| {
247                    ctx.apply_aura(AURA_DEEP_BREATH_TRIGGER);
248                });
249            }
250        },
251    );
252
253    if cfg.melt_armor {
254        ctx.apply_aura(AURA_MELT_ARMOR_DOT);
255    }
256
257    if cfg.imminent_destruction {
258        ctx.apply_aura(AURA_IMMINENT_DESTRUCTION);
259    }
260
261    if cfg.strafing_run {
262        if ctx.is_aura_active(AURA_STRAFING_RUN) {
263            ctx.expire_aura(AURA_STRAFING_RUN);
264        } else {
265            ctx.apply_aura(AURA_STRAFING_RUN);
266        }
267    }
268}
269
270/// Returns Giantkiller mastery damage for spell 362980.
271#[must_use]
272pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
273    const GIANTKILLER_FLOOR: f64 = 0.3;
274    let health = ctx.target_health_pct().max(GIANTKILLER_FLOOR);
275
276    1.0 + ctx.points(1) / HUNDRED * health
277}