wowlab_engine_content/hooks/devastation_evoker/
hooks.rs1use 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 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
154pub(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
205pub(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#[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}