Skip to main content

wowlab_engine_content/hooks/retribution_paladin/
hooks.rs

1//! Retribution per-spell cast hooks, the swing hook, and mastery.
2
3use wowlab_engine_combat::DamageOps as _;
4use wowlab_engine_combat::CooldownOps as _;
5use wowlab_engine_combat::ResourceOps as _;
6use wowlab_engine_combat::ProcOps as _;
7use wowlab_engine_combat::SchedulingOps as _;
8use wowlab_engine_combat::AuraOps as _;
9use wowlab_engine_combat::{DamageFlags, HookCtx, MasteryCtx, SwingEvent, SwingHand};
10use wowlab_engine_rng::proc_chance;
11use wowlab_types::{constants::HUNDRED};
12
13use super::{config::retribution_config, templar};
14use crate::generated::specs::retribution_paladin::{
15    AURA_ART_OF_WAR, AURA_AVENGING_WRATH, AURA_CONSECRATED_BLADE_LOCKOUT, AURA_DIVINE_PURPOSE,
16    AURA_DIVINE_ARBITER_DIVINE_STORM, AURA_DIVINE_ARBITER_HAMMER_OF_LIGHT,
17    AURA_DIVINE_ARBITER_VERDICT, AURA_DIVINE_POWER,
18    AURA_EMPYREAN_LEGACY, AURA_EMPYREAN_POWER, AURA_EXECUTION_SENTENCE_TRACKER, AURA_EXPURGATION,
19    AURA_HAMMER_OF_WRATH, AURA_JUDGMENT_DEBUFF, AURA_RIGHTEOUS_CAUSE, AURA_SANCTIFICATION,
20    AURA_SANCTIFY, EFFECT, SPELL_BLADE_OF_JUSTICE,
21};
22
23const NORMAL_SPENDER_HOLY_POWER: u8 = 3;
24const FREE_SPENDER_CRUSADE_STACKS: i32 = 3;
25const HAMMER_OF_LIGHT_CRUSADE_STACKS: i32 = 5;
26const BUGGED_HAMMER_OF_LIGHT_CRUSADE_STACKS: i32 = 10;
27const GREATER_JUDGMENT_DEBUFF_STACKS: i32 = 2;
28
29const CRUSADING_STRIKE_HIGHER_CALLING_MS: u32 = 500;
30// Spell 1306923 effect 2 is 2.70 AP versus effect 1's 4.05 AP in the 12.1 DBC.
31// The checked-in 12.1.0.69299 data predates that added row, so retain the exact ratio here.
32const DIVINE_ARBITER_SECONDARY_RATIO: f64 = 2.0 / 3.0;
33
34pub(crate) const fn blade_of_vengeance_trigger_disabled(_ctx: &HookCtx<'_>) -> bool {
35    false
36}
37
38/// Consumes the live Divine Purpose charge, then rolls a fresh one.
39pub(crate) fn divine_purpose_cycle(ctx: &mut HookCtx<'_>) -> bool {
40    let consumed = ctx.consume_aura(AURA_DIVINE_PURPOSE);
41
42    let chance = retribution_config(ctx).divine_purpose_chance;
43
44    if chance > 0.0 && proc_chance(ctx.rng(), chance) {
45        ctx.apply_aura(AURA_DIVINE_PURPOSE);
46    }
47
48    consumed
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub(super) enum SpenderKind {
53    DivineStorm,
54    HammerOfLight,
55    Other,
56    Verdict,
57}
58
59const fn divine_arbiter_marker(kind: SpenderKind) -> Option<wowlab_engine_combat::LocalAuraIdx> {
60    match kind {
61        SpenderKind::DivineStorm => Some(AURA_DIVINE_ARBITER_DIVINE_STORM),
62        SpenderKind::HammerOfLight => Some(AURA_DIVINE_ARBITER_HAMMER_OF_LIGHT),
63        SpenderKind::Verdict => Some(AURA_DIVINE_ARBITER_VERDICT),
64        SpenderKind::Other => None,
65    }
66}
67
68const DIVINE_ARBITER_MARKERS: [wowlab_engine_combat::LocalAuraIdx; 3] = [
69    AURA_DIVINE_ARBITER_VERDICT,
70    AURA_DIVINE_ARBITER_DIVINE_STORM,
71    AURA_DIVINE_ARBITER_HAMMER_OF_LIGHT,
72];
73
74pub(super) fn mid2_spender(
75    ctx: &mut HookCtx<'_>,
76    kind: SpenderKind,
77    consumed_divine_purpose: bool,
78) {
79    let cfg = retribution_config(ctx);
80
81    if consumed_divine_purpose && cfg.mid2_2pc {
82        ctx.apply_aura(AURA_DIVINE_POWER);
83    }
84
85    if !cfg.mid2_4pc {
86        return;
87    }
88
89    let active_marker = DIVINE_ARBITER_MARKERS
90        .into_iter()
91        .find(|marker| ctx.is_aura_active(*marker));
92
93    if consumed_divine_purpose
94        && active_marker.is_none()
95        && let Some(marker) = divine_arbiter_marker(kind)
96    {
97        ctx.apply_aura(marker);
98
99        return;
100    }
101
102    let Some(current_marker) = divine_arbiter_marker(kind) else {
103        return;
104    };
105    let Some(previous_marker) = active_marker.filter(|marker| *marker != current_marker) else {
106        return;
107    };
108
109    ctx.expire_aura(previous_marker);
110    trigger_divine_arbiter(ctx);
111}
112
113fn trigger_divine_arbiter(ctx: &mut HookCtx<'_>) {
114    let coefficient = ctx.effect_ap_coefficient(
115        crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_ARBITER_DAMAGE,
116    );
117
118    ctx.deal_damage_ap(
119        crate::generated::specs::retribution_paladin::DAMAGE_REPORTED::DIVINE_ARBITER,
120        coefficient,
121        DamageFlags::empty(),
122    );
123
124    let primary = ctx.target();
125
126    ctx.with_resolved_spell_effect_targets(
127        crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_STORM_DAMAGE,
128        crate::generated::specs::retribution_paladin::DAMAGE_REPORTED::DIVINE_ARBITER,
129        None,
130        |ctx, targets| {
131            for target in targets
132                .iter()
133                .copied()
134                .filter(|target| Some(*target) != primary)
135            {
136                ctx.deal_damage_ap_to_target(
137                    target,
138                    crate::generated::specs::retribution_paladin::DAMAGE_REPORTED::DIVINE_ARBITER,
139                    coefficient * DIVINE_ARBITER_SECONDARY_RATIO,
140                    DamageFlags::empty(),
141                );
142            }
143        },
144    );
145}
146
147/// Righteous Cause rolls once per Holy Power in a normal three-point spender.
148pub(crate) fn righteous_cause_roll(ctx: &mut HookCtx<'_>) {
149    let chance = retribution_config(ctx).righteous_cause_chance;
150
151    if chance <= 0.0 || !ctx.proc_category_ready(wowlab_engine_combat::generated_identities::proc_category::RIGHTEOUS_CAUSE) {
152        return;
153    }
154
155    for _ in 0..NORMAL_SPENDER_HOLY_POWER {
156        if proc_chance(ctx.rng(), chance) {
157            ctx.apply_aura(AURA_RIGHTEOUS_CAUSE);
158            ctx.reset_cooldown(SPELL_BLADE_OF_JUSTICE);
159            ctx.start_proc_category_cooldown(wowlab_engine_combat::generated_identities::proc_category::RIGHTEOUS_CAUSE);
160            break;
161        }
162    }
163}
164
165fn add_crusade_stacks(ctx: &mut HookCtx<'_>, count: i32) {
166    if !retribution_config(ctx).crusade || !ctx.is_aura_active(AURA_AVENGING_WRATH) {
167        return;
168    }
169
170    for _ in 0..count.max(0) {
171        ctx.add_aura_stack(AURA_AVENGING_WRATH);
172    }
173}
174
175pub(crate) fn crusade_spender_stacks(ctx: &mut HookCtx<'_>) {
176    let spent =
177        wowlab_types::numeric::f64_to_i32_saturating_trunc(ctx.last_primary_spent().floor());
178
179    add_crusade_stacks(
180        ctx,
181        if spent > 0 {
182            spent
183        } else {
184            FREE_SPENDER_CRUSADE_STACKS
185        },
186    );
187}
188
189pub(crate) fn crusade_hammer_of_light_stacks(ctx: &mut HookCtx<'_>) {
190    let count = if retribution_config(ctx).hammer_of_light_crusade_bug
191        && ctx.aura_stacks(AURA_AVENGING_WRATH) < HAMMER_OF_LIGHT_CRUSADE_STACKS
192    {
193        BUGGED_HAMMER_OF_LIGHT_CRUSADE_STACKS
194    } else {
195        HAMMER_OF_LIGHT_CRUSADE_STACKS
196    };
197
198    add_crusade_stacks(ctx, count);
199}
200
201/// Handles Crusading Strikes and Art of War on auto attacks.
202pub(crate) fn swing_hook(ctx: &mut HookCtx<'_>, event: SwingEvent) {
203    let hand = event.hand;
204    let is_crit = event.is_crit;
205
206    if hand == SwingHand::OffHand {
207        return;
208    }
209
210    let cfg = retribution_config(ctx);
211
212    if cfg.crusading_strikes {
213        let hp = ctx.effect_base_points(
214            crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::CRUSADING_STRIKES_HP,
215        );
216
217        ctx.gain_resource(hp);
218
219        if cfg.empyrean_power_chance > 0.0 && proc_chance(ctx.rng(), cfg.empyrean_power_chance) {
220            ctx.apply_aura(AURA_EMPYREAN_POWER);
221        }
222
223        if cfg.higher_calling_ms > 0 {
224            templar::higher_calling_extend(ctx, CRUSADING_STRIKE_HIGHER_CALLING_MS);
225        }
226    }
227
228    if cfg.art_of_war_chance <= 0.0 || !ctx.proc_category_ready(wowlab_engine_combat::generated_identities::proc_category::ART_OF_WAR) {
229        return;
230    }
231
232    let mut chance = cfg.art_of_war_chance;
233
234    if is_crit {
235        chance *= 1.0 + cfg.art_of_war_crit_bonus;
236    }
237
238    if proc_chance(ctx.rng(), chance) {
239        ctx.apply_aura(AURA_ART_OF_WAR);
240        ctx.reset_cooldown(SPELL_BLADE_OF_JUSTICE);
241        ctx.start_proc_category_cooldown(wowlab_engine_combat::generated_identities::proc_category::ART_OF_WAR);
242    }
243}
244
245fn judgment_family_execute(ctx: &mut HookCtx<'_>) {
246    let cfg = retribution_config(ctx);
247
248    if cfg.sanctification {
249        ctx.apply_aura(AURA_SANCTIFICATION);
250    }
251
252    templar::higher_calling_extend(ctx, cfg.higher_calling_ms);
253}
254
255const fn judgment_debuff_stacks(greater_judgment: bool) -> i32 {
256    if greater_judgment {
257        GREATER_JUDGMENT_DEBUFF_STACKS
258    } else {
259        1
260    }
261}
262
263pub(super) fn apply_judgment_debuff(ctx: &mut HookCtx<'_>) {
264    let stacks = judgment_debuff_stacks(retribution_config(ctx).greater_judgment);
265
266    ctx.apply_aura_n(AURA_JUDGMENT_DEBUFF, stacks);
267}
268
269/// Judgment on-cast hook: target debuff + judgment-family execute tail.
270pub(crate) fn judgment_hook(ctx: &mut HookCtx<'_>) {
271    apply_judgment_debuff(ctx);
272    judgment_family_execute(ctx);
273}
274
275fn vengeful_wrath_multiplier(max_bonus: f64, target_health_fraction: f64) -> f64 {
276    const FULL_EFFECT_HEALTH_FRACTION: f64 = 0.2;
277
278    let scaled = 1.0
279        - ((target_health_fraction - FULL_EFFECT_HEALTH_FRACTION).max(0.0)
280            / (1.0 - FULL_EFFECT_HEALTH_FRACTION))
281            .clamp(0.0, 1.0);
282
283    1.0 + max_bonus * scaled
284}
285
286/// Applies Hammer of Wrath's judgment-family riders.
287pub(crate) fn hammer_of_wrath_hook(ctx: &mut HookCtx<'_>) {
288    let coef = ctx.effect_ap_coefficient(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::HAMMER_OF_WRATH_DAMAGE);
289    let health_mult = vengeful_wrath_multiplier(
290        retribution_config(ctx).vengeful_wrath_max_bonus,
291        ctx.target_health_fraction(),
292    );
293
294    ctx.deal_damage_ap(
295        crate::generated::specs::retribution_paladin::DAMAGE_SPELL::HAMMER_OF_WRATH,
296        coef * health_mult,
297        DamageFlags::empty(),
298    );
299    apply_judgment_debuff(ctx);
300    judgment_family_execute(ctx);
301}
302
303const LIGHT_WITHIN_DELAY_MS: u32 = 350;
304
305fn light_within_fire_hook(ctx: &mut HookCtx<'_>) {
306    let coef = ctx.effect_ap_coefficient(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::LIGHT_WITHIN);
307    let mult = retribution_config(ctx).light_within_wave_mult.max(1.0);
308
309    ctx.deal_damage_ap(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::LIGHT_WITHIN.action(), coef * mult, DamageFlags::empty());
310}
311
312/// Handles Blade of Justice's reset buffs and Light Within nuke.
313pub(crate) fn blade_of_justice_hook(ctx: &mut HookCtx<'_>) {
314    ctx.apply_aura(AURA_EXPURGATION);
315    let cfg = retribution_config(ctx);
316
317    if cfg.consecrated_blade
318        && !ctx.is_aura_active(AURA_CONSECRATED_BLADE_LOCKOUT)
319        && cfg
320            .consecration_ground
321            .is_some_and(|effect| ctx.schedule_ground_effect(effect))
322    {
323        ctx.apply_aura(AURA_CONSECRATED_BLADE_LOCKOUT);
324    }
325
326    let art_of_war = ctx.consume_aura(AURA_ART_OF_WAR);
327    let righteous_cause = ctx.consume_aura(AURA_RIGHTEOUS_CAUSE);
328    let buff_up = art_of_war || righteous_cause;
329
330    if buff_up && cfg.light_within {
331        ctx.schedule_hook(LIGHT_WITHIN_DELAY_MS, light_within_fire_hook);
332    }
333}
334
335fn trigger_empyrean_legacy_divine_storm(
336    ctx: &mut HookCtx<'_>,
337    effectiveness: f64,
338) -> bool {
339    if !ctx.consume_aura(AURA_EMPYREAN_LEGACY) {
340        return false;
341    }
342
343    let coef = ctx.effect_ap_coefficient(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_STORM_DAMAGE);
344
345    ctx.deal_damage_ap(
346        crate::generated::specs::retribution_paladin::DAMAGE_SPELL::DIVINE_STORM,
347        coef * effectiveness,
348        DamageFlags::empty(),
349    );
350    mid2_spender(ctx, SpenderKind::DivineStorm, false);
351    ctx.consume_aura(AURA_EMPYREAN_POWER);
352
353    true
354}
355
356pub(crate) fn templars_verdict_hook(ctx: &mut HookCtx<'_>) {
357    ctx.consume_aura_stack(AURA_JUDGMENT_DEBUFF);
358
359    templar::holy_power_spender(ctx, SpenderKind::Verdict);
360    let cfg = retribution_config(ctx);
361
362    if cfg.empyrean_legacy {
363        trigger_empyrean_legacy_divine_storm(ctx, cfg.empyrean_legacy_effectiveness);
364    }
365}
366
367/// Handles Divine Storm's spender riders.
368pub(crate) fn divine_storm_hook(ctx: &mut HookCtx<'_>) {
369    ctx.consume_aura_stack(AURA_JUDGMENT_DEBUFF);
370    ctx.consume_aura(AURA_EMPYREAN_POWER);
371
372    if retribution_config(ctx).sanctify {
373        ctx.apply_aura(AURA_SANCTIFY);
374    }
375
376    templar::holy_power_spender(ctx, SpenderKind::DivineStorm);
377}
378
379pub(crate) fn execution_sentence_hook(ctx: &mut HookCtx<'_>) {
380    templar::holy_power_spender(ctx, SpenderKind::Other);
381    ctx.consume_aura_stack(AURA_JUDGMENT_DEBUFF);
382}
383
384pub(crate) fn execution_sentence_expire_hook(ctx: &mut HookCtx<'_>) {
385    let pct = ctx.effect_base_points(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::EXECUTION_SENTENCE_ACCUM_PCT)
386        / HUNDRED;
387    let accumulated = ctx.take_accumulated_damage(AURA_EXECUTION_SENTENCE_TRACKER);
388
389    ctx.deal_damage_flat(crate::generated::specs::retribution_paladin::DAMAGE_REPORTED::EXECUTION_SENTENCE, accumulated * pct);
390    ctx.expire_aura(AURA_EXECUTION_SENTENCE_TRACKER);
391}
392
393fn judgment_echo(ctx: &mut HookCtx<'_>, mult: f64, use_how: bool) {
394    if use_how {
395        let coef = ctx.effect_ap_coefficient(
396            crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_TOLL_HOW,
397        )
398            * mult
399            * vengeful_wrath_multiplier(
400                retribution_config(ctx).vengeful_wrath_max_bonus,
401                ctx.target_health_fraction(),
402            );
403
404        ctx.deal_damage_ap(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_TOLL_HOW.action(), coef, DamageFlags::empty());
405    } else {
406        let coef = ctx.effect_sp_coefficient(
407            crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_TOLL_JUDGMENT,
408        ) * mult;
409
410        ctx.deal_damage_sp(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::DIVINE_TOLL_JUDGMENT.action(), coef, DamageFlags::empty());
411    }
412
413    ctx.gain_resource(1.0);
414    apply_judgment_debuff(ctx);
415    judgment_family_execute(ctx);
416}
417
418const DIVINE_EXACTION_ECHO_DELAY_MS: u32 = 300;
419
420fn divine_exaction_judgment_fire_hook(ctx: &mut HookCtx<'_>) {
421    let mult = retribution_config(ctx).divine_exaction_mult;
422
423    judgment_echo(ctx, mult, false);
424}
425
426fn divine_exaction_how_fire_hook(ctx: &mut HookCtx<'_>) {
427    let mult = retribution_config(ctx).divine_exaction_mult;
428
429    judgment_echo(ctx, mult, true);
430}
431
432/// Handles Divine Toll's immediate and delayed echoes.
433pub(crate) fn divine_toll_hook(ctx: &mut HookCtx<'_>) {
434    let cfg = retribution_config(ctx);
435    let how_up = ctx.is_aura_active(AURA_HAMMER_OF_WRATH);
436
437    judgment_echo(ctx, cfg.divine_toll_echo_mult, how_up);
438    let fire = if how_up {
439        divine_exaction_how_fire_hook
440    } else {
441        divine_exaction_judgment_fire_hook
442    };
443
444    for i in 0..cfg.divine_exaction_count {
445        ctx.schedule_hook(DIVINE_EXACTION_ECHO_DELAY_MS * (i + 1), fire);
446    }
447
448    templar::start_divine_hammer(ctx);
449}
450
451/// Opens Avenging Wrath's Hammer of Wrath window.
452pub(crate) fn avenging_wrath_hook(ctx: &mut HookCtx<'_>) {
453    let cfg = retribution_config(ctx);
454
455    if cfg.hammer_of_wrath {
456        ctx.apply_aura(AURA_HAMMER_OF_WRATH);
457    }
458
459    if cfg.empyrean_legacy {
460        ctx.apply_aura(AURA_EMPYREAN_LEGACY);
461    }
462}
463
464/// Closes Avenging Wrath's Hammer of Wrath window.
465pub(crate) fn avenging_wrath_expire_hook(ctx: &mut HookCtx<'_>) {
466    ctx.expire_aura(AURA_HAMMER_OF_WRATH);
467}
468
469/// Applies Highlord's Judgment mastery (spell 267316).
470#[must_use]
471pub(crate) fn mastery(ctx: &MasteryCtx<'_>) -> f64 {
472    if ctx.affects_spell(EFFECT::HIGHLORDS_MASTERY_DAMAGE.1) {
473        ctx.bonus(EFFECT::HIGHLORDS_MASTERY_DAMAGE.1)
474    } else {
475        1.0
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use googletest::prelude::*;
482    use wowlab_engine_ports::{CombatStats};
483    use wowlab_engine_telemetry::{TelemetrySink};
484
485    use wowlab_types::sim::SimTime;
486
487    use super::*;
488    use crate::generated::specs::retribution_paladin::AURA;
489
490    /// Manifest aura order up to Divine Purpose; the local indices are generated from it.
491    const AURA_PREFIX: [(&str, u32); 4] = [
492        ("avenging_wrath", AURA::AVENGING_WRATH),
493        ("empyrean_power", AURA::EMPYREAN_POWER),
494        ("empyrean_legacy", AURA::EMPYREAN_LEGACY),
495        ("divine_purpose", AURA::DIVINE_PURPOSE),
496    ];
497
498    #[gtest]
499    fn a_holy_power_spender_consumes_divine_purpose_before_rerolling_it() -> Result<()> {
500        let mut builder = crate::test_combat_builder_for(wowlab_engine_combat::generated_identities::specs::retribution_paladin::PROOF, CombatStats::default());
501
502        for (name, aura_id) in AURA_PREFIX {
503            builder = builder.aura(name, aura_id, |a| Ok(a.duration_ms(12_000)));
504        }
505
506        let mut built = builder
507            .into_draft().expect("valid combat draft").finalize(&crate::test_support::wait_rotation())
508            .or_fail()?;
509
510        verify_that!(
511            built.program.diagnostic_aura_local(AURA::DIVINE_PURPOSE),
512            some(eq(AURA_DIVINE_PURPOSE))
513        )?;
514
515        let mut sink = TelemetrySink::default();
516        let mut rng = || 0.5;
517        let mut ctx = HookCtx::new(
518            wowlab_engine_combat::HookCtxServices {
519                program: &built.program,
520                instance: &mut built.instance,
521                sink: &mut sink,
522                rng: &mut rng,
523            },
524            wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
525        );
526
527        ctx.apply_aura(AURA_DIVINE_PURPOSE);
528
529        verify_true!(ctx.is_aura_active(AURA_DIVINE_PURPOSE))?;
530
531        // The default config leaves the re-roll chance at zero, so a live buff can only survive
532        // this call if the spender never consumed it.
533        divine_purpose_cycle(&mut ctx);
534
535        verify_false!(ctx.is_aura_active(AURA_DIVINE_PURPOSE))
536    }
537
538    #[gtest]
539    fn empyrean_legacy_divine_storm_consumes_the_trigger_and_free_spender_buffs() -> Result<()> {
540        let mut builder = crate::test_combat_builder_for(wowlab_engine_combat::generated_identities::specs::retribution_paladin::PROOF, CombatStats::default());
541
542        for (name, aura_id) in AURA_PREFIX {
543            builder = builder.aura(name, aura_id, |a| Ok(a.duration_ms(12_000)));
544        }
545
546        let mut built = builder
547            .into_draft().expect("valid combat draft").finalize(&crate::test_support::wait_rotation())
548            .or_fail()?;
549        let mut sink = TelemetrySink::default();
550        let mut rng = || 0.5;
551        let mut ctx = HookCtx::new(
552            wowlab_engine_combat::HookCtxServices {
553                program: &built.program,
554                instance: &mut built.instance,
555                sink: &mut sink,
556                rng: &mut rng,
557            },
558            wowlab_engine_combat::HookCtxRequest::targetless(SimTime::ZERO),
559        );
560
561        ctx.apply_aura(AURA_EMPYREAN_LEGACY);
562        ctx.apply_aura(AURA_EMPYREAN_POWER);
563        ctx.apply_aura(AURA_DIVINE_PURPOSE);
564
565        verify_true!(trigger_empyrean_legacy_divine_storm(&mut ctx, 1.25))?;
566        verify_false!(ctx.is_aura_active(AURA_EMPYREAN_LEGACY))?;
567        verify_false!(ctx.is_aura_active(AURA_EMPYREAN_POWER))?;
568        verify_true!(ctx.is_aura_active(AURA_DIVINE_PURPOSE))?;
569
570        verify_false!(trigger_empyrean_legacy_divine_storm(&mut ctx, 1.25))
571    }
572
573    #[gtest]
574    fn vengeful_wrath_scales_linearly_to_full_effect_at_twenty_percent_health() -> Result<()> {
575        verify_that!(vengeful_wrath_multiplier(0.5, 1.0), near(1.0, f64::EPSILON))?;
576        verify_that!(
577            vengeful_wrath_multiplier(0.5, 0.6),
578            near(1.25, f64::EPSILON)
579        )?;
580        verify_that!(vengeful_wrath_multiplier(0.5, 0.2), near(1.5, f64::EPSILON))?;
581
582        verify_that!(vengeful_wrath_multiplier(0.5, 0.0), near(1.5, f64::EPSILON))
583    }
584
585    #[gtest]
586    fn greater_judgment_applies_two_debuff_stacks_per_judgment_family_hit() -> Result<()> {
587        verify_that!(judgment_debuff_stacks(false), eq(1))?;
588
589        verify_that!(judgment_debuff_stacks(true), eq(2))
590    }
591}