Skip to main content

wowlab_engine_content/hooks/elemental_shaman/
hooks.rs

1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::CooldownOps as _;
3use wowlab_engine_combat::ResourceOps as _;
4use wowlab_engine_combat::ProcOps as _;
5use wowlab_engine_combat::SchedulingOps as _;
6use wowlab_engine_combat::GuardianOps as _;
7use wowlab_engine_combat::AuraOps as _;
8use wowlab_engine_combat::{
9    BuffEffect, BuiltCombatSystem, DamageFlags, HookCtx, ImpactEvent, ImpactProc, MasteryCtx,
10    PassiveDriverEffect, RuntimeDamageDef, try_push_effect,
11};
12use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
13use wowlab_engine_ports::HandlerParams;
14use wowlab_engine_rng::proc_chance;
15use wowlab_types::{constants::HUNDRED, sim::SpellIdx};
16
17use crate::generated::specs::elemental_shaman::{
18    AURA, AURA_ANCESTRAL_SWIFTNESS, AURA_ASCENDANCE, AURA_ELEMENTAL_BLAST_CRIT,
19    AURA_ELEMENTAL_BLAST_HASTE, AURA_ELEMENTAL_BLAST_MASTERY, AURA_FIRE_ELEMENTAL,
20    AURA_FLAME_SHOCK, AURA_LAVA_SURGE, AURA_LIGHTNING_ROD, AURA_LIGHTNING_SHIELD,
21    AURA_MASTER_OF_THE_ELEMENTS, AURA_POWER_OF_THE_MAELSTROM, AURA_STORM_ELEMENTAL,
22    AURA_STORMKEEPER, AURA_STORMS_EYE, AURA_TEMPEST, AURA_THUNDEROUS_VELOCITY,
23    AURA_UNLIMITED_POWER, AURA_WIND_GUST, EFFECT, SET_BONUS, SPELL, SPELL_ELEMENTAL_BLAST,
24    SPELL_LAVA_BURST, TALENT,
25};
26
27/// Server-side Maelstrom amounts absent from DBC.
28const LAVA_BURST_MAELSTROM: f64 = 8.0;
29const LIGHTNING_BOLT_OVERLOAD_MAELSTROM: f64 = 2.0;
30const CHAIN_LIGHTNING_OVERLOAD_MAELSTROM: f64 = 1.0;
31const LAVA_BURST_OVERLOAD_MAELSTROM: f64 = 3.0;
32
33const CHAIN_OVERLOAD_DIVISOR: f64 = 3.0;
34
35/// Stormkeeper's normal cast grants two charges (191634 e5).
36const STORMKEEPER_CAST_CHARGES: u32 = 2;
37/// Arc Discharge adds one Stormkeeper charge (455096 e1).
38const ARC_DISCHARGE_CHARGES: u32 = 1;
39
40/// Lava Surge chance is `max(0, 0.6 - 1.16^(-2 * (attempts - 5)))`.
41const LAVA_SURGE_CEILING: f64 = 0.6;
42const LAVA_SURGE_DECAY_BASE: f64 = 1.16;
43const LAVA_SURGE_MIDPOINT: f64 = 5.0;
44const LAVA_SURGE_EXPONENT_SCALE: f64 = -2.0;
45
46/// 188592 e4/e5 affect mask (family flag 103), player half.
47#[rustfmt::skip]
48static EU_FIRE_PLAYER_SPELLS: [u32; 3] = [
49    SPELL::ELEMENTAL_BLAST,
50    EFFECT::ELEMENTAL_BLAST_OVERLOAD.0,
51    SPELL::FLAME_SHOCK,
52];
53/// 188592 e4/e5 affect mask (family flag 103), Fire Elemental actions.
54static EU_FIRE_PET_SPELLS: [u32; 3] = [SPELL::METEOR, SPELL::IMMOLATE, SPELL::FIRE_BLAST];
55/// Family-flag-126 class mask shared by Storm Elemental unity (157299 e3/e4) and Lightning Capacitor (462862 e1/e2), player spells.
56#[rustfmt::skip]
57static FLAG_126_PLAYER_SPELLS: [u32; 11] = [
58    SPELL::LIGHTNING_BOLT,
59    SPELL::CHAIN_LIGHTNING,
60    SPELL::ELEMENTAL_BLAST,
61    SPELL::TEMPEST,
62    SPELL::EARTH_SHOCK,
63    SPELL::FLAME_SHOCK,
64    SPELL::STORMS_EYE,
65    EFFECT::LIGHTNING_BOLT_OVERLOAD.0,
66    EFFECT::CHAIN_LIGHTNING_OVERLOAD.0,
67    EFFECT::ELEMENTAL_BLAST_OVERLOAD.0,
68    EFFECT::TEMPEST_OVERLOAD.0,
69];
70/// 157299 e3/e4 affect mask (family flag 126), Storm Elemental actions.
71static EU_STORM_PET_SPELLS: [u32; 3] = [SPELL::WIND_GUST, SPELL::CALL_LIGHTNING, SPELL::STORMFURY];
72/// Elemental Unity rows the summon auras declare in data but the game scripts on only with the talent.
73static EU_FIRE_MASKED_EFFECTS: [(u32, u8); 2] = [
74    EFFECT::FIRE_ELEMENTAL_UNITY,
75    EFFECT::FIRE_ELEMENTAL_UNITY_PERIODIC,
76];
77static EU_STORM_MASKED_EFFECTS: [(u32, u8); 2] = [
78    EFFECT::STORM_ELEMENTAL_UNITY,
79    EFFECT::STORM_ELEMENTAL_UNITY_PET,
80];
81/// 382028 e1 affect masks (family flags 28/44/86/98): the fire half.
82#[rustfmt::skip]
83static IFW_SPELLS: [u32; 5] = [
84    SPELL::FLAME_SHOCK,
85    SPELL::LAVA_BURST,
86    SPELL::ELEMENTAL_BLAST,
87    EFFECT::LAVA_BURST_OVERLOAD.0,
88    EFFECT::ELEMENTAL_BLAST_OVERLOAD.0,
89];
90
91pub(crate) fn earthquake_hook(ctx: &mut HookCtx<'_>) {
92    ctx.schedule_ground_effect_from_data(
93        SPELL::EARTHQUAKE,
94        EFFECT::EARTHQUAKE_TIMING.1,
95        EFFECT::EARTHQUAKE_DAMAGE,
96        EFFECT::EARTHQUAKE_DAMAGE,
97        DamageFlags::PERIODIC,
98    );
99}
100
101const STORMS_EYE_CHARGES: i32 = 2;
102
103const RANDOM_STAT_ROLL_THIRD: f64 = 1.0 / 3.0;
104const RANDOM_STAT_ROLL_TWO_THIRDS: f64 = 2.0 / 3.0;
105
106use super::{
107    bugs::BUG_STORMWELL_TEMPEST_MAELSTROM,
108    config::{ELEMENTAL_BLAST_COST, elemental_config, register_elemental_config},
109    elementals::{STORM_ELEMENTAL_GUARDIAN_TAG, summon_fire_elemental, summon_storm_elemental},
110};
111
112fn trigger_elemental_blast_stat(ctx: &mut HookCtx<'_>) {
113    let aura = match ctx.rng()() {
114        roll if roll < RANDOM_STAT_ROLL_THIRD => AURA_ELEMENTAL_BLAST_CRIT,
115        roll if roll < RANDOM_STAT_ROLL_TWO_THIRDS => AURA_ELEMENTAL_BLAST_HASTE,
116        _ => AURA_ELEMENTAL_BLAST_MASTERY,
117    };
118
119    ctx.apply_aura(aura.raw());
120}
121
122/// Overload proc chance: mastery points x 168534 e1 coefficient (1.875 %/point).
123fn overload_chance(ctx: &HookCtx<'_>) -> f64 {
124    ctx.effect_points(EFFECT::OVERLOAD_CHANCE) / HUNDRED
125}
126
127/// Overload damage penalty from spell 168534 e2.
128fn overload_penalty(ctx: &HookCtx<'_>) -> f64 {
129    ctx.game_data().effect_base_points(EFFECT::OVERLOAD_PENALTY) / HUNDRED
130}
131
132fn fire_overload(ctx: &mut HookCtx<'_>, effect: (u32, u8), maelstrom: f64) {
133    fire_overload_scaled(ctx, effect, maelstrom, 1.0);
134    let feedback_chance = elemental_config(ctx).feedback_loop_chance;
135
136    if feedback_chance > 0.0 && proc_chance(ctx.rng(), feedback_chance) {
137        fire_overload_scaled(ctx, effect, maelstrom, 1.0);
138    }
139}
140
141fn fire_overload_scaled(
142    ctx: &mut HookCtx<'_>,
143    effect: (u32, u8),
144    maelstrom: f64,
145    damage_mult: f64,
146) {
147    let coef = ctx
148        .game_data()
149        .sp_coef(SpellIdx::from_raw(effect.0), effect.1)
150        * overload_penalty(ctx)
151        * damage_mult;
152
153    ctx.deal_damage_sp(effect.0, coef, DamageFlags::empty());
154
155    if maelstrom > 0.0 {
156        ctx.gain_resource(maelstrom);
157    }
158}
159
160// Elemental Blast never rolls Supercharge but retains mastery and Ascendance overloads.
161fn fire_elemental_blast_overloads(ctx: &mut HookCtx<'_>, damage_mult: f64) {
162    let cfg = elemental_config(ctx);
163    let fire = |ctx: &mut HookCtx<'_>| {
164        trigger_elemental_blast_stat(ctx);
165        fire_overload_scaled(ctx, EFFECT::ELEMENTAL_BLAST_OVERLOAD, 0.0, damage_mult);
166
167        if cfg.feedback_loop_chance > 0.0 && proc_chance(ctx.rng(), cfg.feedback_loop_chance) {
168            trigger_elemental_blast_stat(ctx);
169            fire_overload_scaled(ctx, EFFECT::ELEMENTAL_BLAST_OVERLOAD, 0.0, damage_mult);
170        }
171    };
172
173    let chance = overload_chance(ctx);
174
175    if proc_chance(ctx.rng(), chance) {
176        fire(ctx);
177    }
178
179    if ctx.is_aura_active(AURA_ASCENDANCE.raw()) {
180        fire(ctx);
181    }
182}
183
184fn fire_elemental_blast(ctx: &mut HookCtx<'_>, damage_mult: f64) {
185    trigger_elemental_blast_stat(ctx);
186    let coefficient = ctx
187        .game_data()
188        .effect_sp_coefficient(EFFECT::ELEMENTAL_BLAST_DAMAGE)
189        * damage_mult;
190
191    ctx.deal_damage_sp(SPELL::ELEMENTAL_BLAST, coefficient, DamageFlags::empty());
192    let cfg = elemental_config(ctx);
193
194    if cfg.eb_overload {
195        fire_elemental_blast_overloads(ctx, cfg.eb_overload_mult * damage_mult);
196    }
197}
198
199fn spend_stormkeeper_charge(ctx: &mut HookCtx<'_>) {
200    if ctx.consume_proc_counter(SPELL::STORMKEEPER, 1) == 0 {
201        ctx.expire_aura(AURA_STORMKEEPER.raw());
202    }
203}
204
205#[derive(Clone, Copy)]
206struct EmpoweredLightningOptions {
207    chance_divisor: f64,
208    stormkeeper_overload: bool,
209    consume_stormkeeper: bool,
210}
211
212const LIGHTNING_BOLT_OPTIONS: EmpoweredLightningOptions = EmpoweredLightningOptions {
213    chance_divisor: 1.0,
214    stormkeeper_overload: true,
215    consume_stormkeeper: true,
216};
217const TEMPEST_OPTIONS: EmpoweredLightningOptions = EmpoweredLightningOptions {
218    chance_divisor: 1.0,
219    stormkeeper_overload: false,
220    consume_stormkeeper: false,
221};
222const CHAIN_LIGHTNING_OPTIONS: EmpoweredLightningOptions = EmpoweredLightningOptions {
223    chance_divisor: CHAIN_OVERLOAD_DIVISOR,
224    stormkeeper_overload: true,
225    consume_stormkeeper: true,
226};
227
228fn empowered_lightning_cast(
229    ctx: &mut HookCtx<'_>,
230    overload: (u32, u8),
231    overload_maelstrom: f64,
232    options: EmpoweredLightningOptions,
233) {
234    let stormkeeper = ctx.is_aura_active(AURA_STORMKEEPER.raw());
235    let cfg = elemental_config(ctx);
236
237    let chance = overload_chance(ctx) / options.chance_divisor;
238
239    if (stormkeeper && options.stormkeeper_overload) || proc_chance(ctx.rng(), chance) {
240        fire_overload(ctx, overload, overload_maelstrom);
241    }
242
243    if stormkeeper && options.consume_stormkeeper {
244        spend_stormkeeper_charge(ctx);
245    }
246
247    if ctx.consume_aura(AURA_POWER_OF_THE_MAELSTROM.raw()) {
248        fire_overload(ctx, overload, overload_maelstrom);
249
250        if cfg.fusion_of_elements_mult > 0.0 {
251            fire_elemental_blast(ctx, cfg.fusion_of_elements_mult);
252        }
253    }
254
255    if cfg.supercharge_chance > 0.0 && proc_chance(ctx.rng(), cfg.supercharge_chance) {
256        fire_overload(ctx, overload, overload_maelstrom);
257    }
258    // Ascendance adds one overload without duplicating Supercharge.
259
260    if ctx.is_aura_active(AURA_ASCENDANCE.raw()) {
261        fire_overload(ctx, overload, overload_maelstrom);
262    }
263
264    if ctx.active_guardian_count(STORM_ELEMENTAL_GUARDIAN_TAG) > 0 {
265        ctx.apply_aura(AURA_WIND_GUST.raw());
266    }
267
268    ctx.consume_aura(AURA_MASTER_OF_THE_ELEMENTS.raw());
269}
270
271pub(crate) fn lightning_bolt_hook(ctx: &mut HookCtx<'_>) {
272    empowered_lightning_cast(
273        ctx,
274        EFFECT::LIGHTNING_BOLT_OVERLOAD,
275        LIGHTNING_BOLT_OVERLOAD_MAELSTROM,
276        LIGHTNING_BOLT_OPTIONS,
277    );
278    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS.raw());
279}
280
281pub(crate) fn tempest_hook(ctx: &mut HookCtx<'_>) {
282    let stormwell_maelstrom = elemental_config(ctx).stormwell_tempest_maelstrom;
283
284    if stormwell_maelstrom > 0.0
285        && ctx.state().bug_enabled(&BUG_STORMWELL_TEMPEST_MAELSTROM)
286        && !ctx.is_aura_active(AURA_STORMKEEPER.raw())
287    {
288        ctx.gain_resource(stormwell_maelstrom);
289    }
290
291    // sc_shaman.cpp:9693 — every Tempest execute decrements the buff.
292
293    ctx.consume_aura(AURA_TEMPEST.raw());
294
295    if elemental_config(ctx).tww3_stormbringer_4pc && ctx.consume_aura_stack(AURA_STORMS_EYE.raw())
296    {
297        let storms_eye = ctx
298            .game_data()
299            .effect_sp_coefficient(EFFECT::STORMS_EYE_DAMAGE);
300
301        ctx.deal_damage_sp(SPELL::STORMS_EYE, storms_eye, DamageFlags::empty());
302    }
303
304    empowered_lightning_cast(ctx, EFFECT::TEMPEST_OVERLOAD, 0.0, TEMPEST_OPTIONS);
305
306    if elemental_config(ctx).arc_discharge {
307        ctx.apply_aura(AURA_STORMKEEPER.raw());
308        ctx.add_proc_counter(SPELL::STORMKEEPER, ARC_DISCHARGE_CHARGES);
309    }
310}
311
312pub(crate) fn chain_lightning_hook(ctx: &mut HookCtx<'_>) {
313    empowered_lightning_cast(
314        ctx,
315        EFFECT::CHAIN_LIGHTNING_OVERLOAD,
316        CHAIN_LIGHTNING_OVERLOAD_MAELSTROM,
317        CHAIN_LIGHTNING_OPTIONS,
318    );
319    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS.raw());
320}
321
322pub(crate) fn lava_burst_hook(ctx: &mut HookCtx<'_>) {
323    let cfg = elemental_config(ctx);
324
325    if cfg.master_of_the_elements {
326        ctx.apply_aura(AURA_MASTER_OF_THE_ELEMENTS.raw());
327    }
328
329    if cfg.potm_chance > 0.0 && proc_chance(ctx.rng(), cfg.potm_chance) {
330        ctx.apply_aura(AURA_POWER_OF_THE_MAELSTROM.raw());
331    }
332
333    ctx.gain_resource(LAVA_BURST_MAELSTROM);
334    ctx.consume_aura(AURA_LAVA_SURGE.raw());
335
336    let chance = overload_chance(ctx);
337
338    if proc_chance(ctx.rng(), chance) {
339        fire_overload(
340            ctx,
341            EFFECT::LAVA_BURST_OVERLOAD,
342            LAVA_BURST_OVERLOAD_MAELSTROM,
343        );
344    }
345
346    if ctx.is_aura_active(AURA_ASCENDANCE.raw()) {
347        fire_overload(
348            ctx,
349            EFFECT::LAVA_BURST_OVERLOAD,
350            LAVA_BURST_OVERLOAD_MAELSTROM,
351        );
352    }
353
354    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS.raw());
355}
356
357pub(crate) fn elemental_blast_hook(ctx: &mut HookCtx<'_>) {
358    fire_elemental_blast(ctx, 1.0);
359
360    if elemental_config(ctx).unlimited_power {
361        ctx.apply_aura(AURA_UNLIMITED_POWER.raw());
362    }
363
364    ctx.apply_aura(AURA_LIGHTNING_ROD.raw());
365    ctx.consume_aura(AURA_MASTER_OF_THE_ELEMENTS.raw());
366    let cfg = elemental_config(ctx);
367
368    if cfg.aftershock_chance > 0.0 && proc_chance(ctx.rng(), cfg.aftershock_chance) {
369        ctx.gain_resource(ELEMENTAL_BLAST_COST);
370    }
371
372    if cfg.tempest_chance_on_blast > 0.0 && proc_chance(ctx.rng(), cfg.tempest_chance_on_blast) {
373        ctx.apply_aura(AURA_TEMPEST.raw());
374    }
375
376    ctx.consume_aura(AURA_ANCESTRAL_SWIFTNESS.raw());
377}
378
379pub(crate) fn stormkeeper_hook(ctx: &mut HookCtx<'_>) {
380    let cfg = elemental_config(ctx);
381
382    ctx.reset_proc_counter(SPELL::STORMKEEPER);
383    let charges = STORMKEEPER_CAST_CHARGES + u32::from(cfg.mid1_4pc);
384
385    ctx.add_proc_counter(SPELL::STORMKEEPER, charges);
386
387    if cfg.mid1_2pc {
388        ctx.apply_aura(AURA_THUNDEROUS_VELOCITY.raw());
389    }
390
391    if cfg.summon_storm_elemental {
392        summon_storm_elemental(ctx);
393    }
394}
395
396pub(crate) fn ascendance_hook(ctx: &mut HookCtx<'_>) {
397    let cfg = elemental_config(ctx);
398
399    if cfg.tww3_stormbringer_2pc {
400        ctx.apply_aura(AURA_TEMPEST.raw());
401    }
402
403    if cfg.tww3_stormbringer_4pc {
404        ctx.apply_aura_n(AURA_STORMS_EYE.raw(), STORMS_EYE_CHARGES);
405    }
406
407    ctx.reset_cooldown(SPELL_LAVA_BURST.raw());
408    ctx.apply_aura(AURA_FLAME_SHOCK.raw());
409    let lava_burst = ctx
410        .game_data()
411        .effect_sp_coefficient(EFFECT::LAVA_BURST_DAMAGE);
412
413    ctx.deal_damage_sp(SPELL::LAVA_BURST, lava_burst, DamageFlags::empty());
414    fire_overload(
415        ctx,
416        EFFECT::LAVA_BURST_OVERLOAD,
417        LAVA_BURST_OVERLOAD_MAELSTROM,
418    );
419
420    let chance = overload_chance(ctx);
421
422    if proc_chance(ctx.rng(), chance) {
423        fire_overload(
424            ctx,
425            EFFECT::LAVA_BURST_OVERLOAD,
426            LAVA_BURST_OVERLOAD_MAELSTROM,
427        );
428    }
429
430    if cfg.summon_fire_elemental {
431        summon_fire_elemental(ctx);
432    }
433}
434
435/// Returns Elemental Overload mastery damage from spell 168534 e3/e4.
436#[must_use]
437pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
438    if ctx.is_pet {
439        return ctx.bonus(EFFECT::MASTERY_PET_DAMAGE.1);
440    }
441
442    let effect = if ctx.is_periodic {
443        EFFECT::MASTERY_PERIODIC_DAMAGE.1
444    } else {
445        EFFECT::MASTERY_DIRECT_DAMAGE.1
446    };
447    let mut mult = if ctx.affects_spell(effect) {
448        ctx.bonus(effect)
449    } else {
450        1.0
451    };
452
453    if matches!(ctx.spell_id, SPELL::LAVA_BURST | SPELL::LAVA_BURST_OVERLOAD)
454        && ctx.is_aura_active_id(AURA::FLAME_SHOCK)
455    {
456        mult *= 1.0 + ctx.buf.player().crit / HUNDRED;
457    }
458
459    mult
460}
461
462const fn flame_shock_filter(spell_id: u32) -> bool {
463    spell_id == SPELL::FLAME_SHOCK
464}
465
466const fn lightning_rod_filter(spell_id: u32) -> bool {
467    matches!(
468        spell_id,
469        SPELL::LIGHTNING_BOLT
470            | SPELL::CHAIN_LIGHTNING
471            | SPELL::TEMPEST
472            | SPELL::LIGHTNING_BOLT_OVERLOAD
473            | SPELL::CHAIN_LIGHTNING_OVERLOAD
474            | SPELL::TEMPEST_OVERLOAD
475    )
476}
477
478fn lightning_rod_fire(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
479    let cfg = elemental_config(ctx);
480
481    if cfg.lightning_rod_mult > 0.0 && ctx.is_aura_active(AURA_LIGHTNING_ROD.raw()) {
482        ctx.deal_resolved_damage(
483            SPELL::LIGHTNING_ROD_DAMAGE,
484            impact.amount * cfg.lightning_rod_mult,
485        );
486    }
487}
488
489fn lava_surge_fire(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
490    if ctx.roll_attempt_proc(impact.spell_id, |attempt| {
491        let exponent = LAVA_SURGE_EXPONENT_SCALE * (f64::from(attempt) - LAVA_SURGE_MIDPOINT);
492
493        (LAVA_SURGE_CEILING - LAVA_SURGE_DECAY_BASE.powf(exponent)).max(0.0)
494    }) {
495        ctx.reset_cooldown(SPELL_LAVA_BURST.raw());
496        ctx.apply_aura(AURA_LAVA_SURGE.raw());
497    }
498}
499
500/// `SimC` hand-flags only Lightning Bolt Overload (`sc_shaman.cpp:7097`).
501static STORMKEEPER_OVERLOAD_SPELLS: [u32; 1] = [EFFECT::LIGHTNING_BOLT_OVERLOAD.0];
502
503/// The MID1 4pc tier multiplier skips background actions, so overloads are excluded.
504static MID1_STORMKEEPER_DAMAGE_SPELLS: [u32; 2] = [SPELL::LIGHTNING_BOLT, SPELL::CHAIN_LIGHTNING];
505
506fn register_conditional_multipliers(built: &mut BuiltCombatSystem, params: &HandlerParams<'_>) {
507    let data = &params.game_data;
508    let mult_from = |effect: (u32, u8)| {
509        1.0 + data.base_points(SpellIdx::from_raw(effect.0), effect.1) / HUNDRED
510    };
511
512    // The elemental summon auras carry their Elemental Unity modifiers as ordinary DBC effects, but
513    // the game only scripts them on when Elemental Unity is picked, and swaps them for the talent's
514    // own effects under Primal Elementalist (sc_shaman.cpp:2754-2775). Mask the data-derived rows so
515    // the talent state below is the only thing that applies them.
516
517    for effect in EU_FIRE_MASKED_EFFECTS {
518        built.mask_aura_dbc_effect(AURA_FIRE_ELEMENTAL, effect.0, effect.1);
519    }
520
521    for effect in EU_STORM_MASKED_EFFECTS {
522        built.mask_aura_dbc_effect(AURA_STORM_ELEMENTAL, effect.0, effect.1);
523    }
524
525    if params.talent_picked(TALENT::ELEMENTAL_UNITY) {
526        let pe = params.talent_picked(TALENT::PRIMAL_ELEMENTALIST);
527        let fire = if pe {
528            EFFECT::ELEMENTAL_UNITY_FIRE
529        } else {
530            EFFECT::FIRE_ELEMENTAL_UNITY
531        };
532        let storm = if pe {
533            EFFECT::ELEMENTAL_UNITY_STORM
534        } else {
535            EFFECT::STORM_ELEMENTAL_UNITY_PET
536        };
537
538        let fire_mult = mult_from(fire);
539        let storm_mult = mult_from(storm);
540
541        for spells in [
542            EU_FIRE_PLAYER_SPELLS.as_slice(),
543            EU_FIRE_PET_SPELLS.as_slice(),
544        ] {
545            built.set_aura_effect(
546                AURA_FIRE_ELEMENTAL,
547                BuffEffect::DamageMultSpells(fire_mult, spells),
548            );
549        }
550
551        for spells in [
552            FLAG_126_PLAYER_SPELLS.as_slice(),
553            EU_STORM_PET_SPELLS.as_slice(),
554        ] {
555            built.set_aura_effect(
556                AURA_STORM_ELEMENTAL,
557                BuffEffect::DamageMultSpells(storm_mult, spells),
558            );
559        }
560    }
561
562    if params.talent_picked(TALENT::LIGHTNING_CAPACITOR) {
563        built.set_aura_effect(
564            AURA_LIGHTNING_SHIELD,
565            BuffEffect::DamageMultSpells(
566                mult_from(EFFECT::LIGHTNING_CAPACITOR_DAMAGE),
567                &FLAG_126_PLAYER_SPELLS,
568            ),
569        );
570    }
571
572    if params.talent_picked(TALENT::FLAMETONGUE_WEAPON) {
573        let effect = EFFECT::IMPROVED_FLAMETONGUE_DAMAGE;
574        let pct = data.base_points(SpellIdx::from_raw(effect.0), effect.1);
575
576        built.register_passive_driver_effect(PassiveDriverEffect::new(
577            TALENT::FLAMETONGUE_WEAPON,
578            BuffEffect::DamageMultSpells(1.0 + pct / HUNDRED, &IFW_SPELLS),
579        ));
580    }
581}
582
583/// Replaces the generic `e4` row — the tier skips background overloads (`sc_shaman.cpp:3204`).
584fn register_stormkeeper_damage(built: &mut BuiltCombatSystem, params: &HandlerParams<'_>) {
585    let data = &params.game_data;
586    let stormkeeper_mult = 1.0 + data.effect_base_points(EFFECT::STORMKEEPER_DAMAGE) / HUNDRED;
587
588    built.patch_aura(AURA_STORMKEEPER, |aura| {
589        for slot in &mut aura.effects {
590            let is_tier_row = matches!(
591                slot,
592                Some(BuffEffect::SpellDamageFromEffect {
593                    source_spell_id,
594                    effect_index,
595                    ..
596                }) if *source_spell_id == EFFECT::STORMKEEPER_TIER_DAMAGE.0
597                    && *effect_index == EFFECT::STORMKEEPER_TIER_DAMAGE.1
598            );
599
600            if is_tier_row {
601                *slot = None;
602            }
603        }
604
605        try_push_effect(
606            &mut aura.effects,
607            BuffEffect::DamageMultSpells(stormkeeper_mult, &STORMKEEPER_OVERLOAD_SPELLS),
608        )
609        .expect("Stormkeeper aura has room for its overload damage modifier");
610
611        if params
612            .set_bonus_auras
613            .contains(&SET_BONUS::MID1_ELEMENTAL_4PC)
614        {
615            let tier_bonus = data.effect_base_points(EFFECT::MID1_STORMKEEPER_BONUS) / HUNDRED;
616            // Separate multiplier, not folded into the SK value.
617            let tier_mult = 1.0 + tier_bonus;
618
619            try_push_effect(
620                &mut aura.effects,
621                BuffEffect::DamageMultSpells(tier_mult, &MID1_STORMKEEPER_DAMAGE_SPELLS),
622            )
623            .expect("Stormkeeper aura has room for the MID1 4pc damage modifier");
624        }
625    });
626}
627
628pub(super) fn wire_elemental(built: &mut BuiltCombatSystem, params: &HandlerParams<'_>) {
629    if params.talent_picked(TALENT::ELEMENTAL_BLAST) {
630        built.disable_spell(SPELL::EARTH_SHOCK);
631
632        built.patch_spell(SPELL_ELEMENTAL_BLAST, |spell| {
633            spell.damage = RuntimeDamageDef::None;
634        });
635    }
636
637    register_stormkeeper_damage(built, params);
638    register_conditional_multipliers(built, params);
639
640    if params.talent_picked(TALENT::FLAMETONGUE_WEAPON) {
641        built.register_weapon_imbue(SPELL::FLAMETONGUE_WEAPON, "flametongue_weapon", true);
642    }
643
644    built.register_impact_proc(
645        ImpactProc::new(lava_surge_fire)
646            .with_spell_filter(flame_shock_filter)
647            .periodic_only(),
648    );
649
650    built.register_impact_proc(
651        ImpactProc::new(lightning_rod_fire)
652            .with_spell_filter(lightning_rod_filter)
653            .skip_periodic(),
654    );
655
656    register_elemental_config(built, params);
657}