Skip to main content

wowlab_engine_content/hooks/retribution_paladin/
config.rs

1use wowlab_engine_combat::DamageOps as _;
2use wowlab_engine_combat::ProcOps as _;
3use wowlab_engine_combat::AuraOps as _;
4use wowlab_engine_combat::{
5    BuffEffect, BuiltCombatSystem, CombatState, DamageFlags, HookCtx, ImpactActorFilter,
6    ImpactEvent, ImpactProc, ProcDriver, ProcTriggerSide,
7};
8use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
9use wowlab_engine_domain::dbc::is_physical;
10use wowlab_engine_ports::HandlerParams;
11use wowlab_engine_gamedata::{ResolvedGameData};
12use wowlab_engine_rng::proc_chance;
13use wowlab_types::{
14    constants::{HUNDRED, MS_PER_SECOND},
15    sim::SpellIdx,
16};
17
18use super::{bugs::BUG_BURN_TO_ASH_CONSECRATION, templar};
19use crate::generated::specs::retribution_paladin::{
20    AURA, AURA_AVENGING_WRATH, AURA_CRUSADING_STRIKES, AURA_EXECUTION_SENTENCE_GATHER,
21    AURA_EXECUTION_SENTENCE_TRACKER, AURA_RUSH_OF_LIGHT, AURA_TRUTHS_WAKE, AURA_ZEALOTS_FERVOR,
22    EFFECT, HERO, REPORTED_SPELL, SET_BONUS, SPELL, SPELL_BLADE_OF_JUSTICE, TALENT,
23};
24
25/// Light's Judicator proc chance (spell 1261525, 50%).
26const LIGHTS_JUDICATOR_PROC_CHANCE: f64 = 0.5;
27
28fn execution_sentence_accumulate(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
29    if ctx.is_owner_aura_active(AURA_EXECUTION_SENTENCE_TRACKER.raw())
30        && (ctx.is_owner_aura_active(AURA_EXECUTION_SENTENCE_GATHER.raw())
31            || impact.spell_id == REPORTED_SPELL::EXECUTION_SENTENCE_INIT)
32        && execution_sentence_accepts(ctx.game_data(), impact.spell_id)
33    {
34        ctx.accumulate_owner_damage(AURA_EXECUTION_SENTENCE_TRACKER.raw(), impact.amount);
35    }
36}
37
38fn execution_sentence_accepts(data: &ResolvedGameData, spell_id: u32) -> bool {
39    is_physical(data, SpellIdx::from_raw(spell_id)) == Some(false)
40}
41
42fn execution_sentence_periodic_snapshot_mult(data: &ResolvedGameData) -> f64 {
43    1.0 + data.effect_base_points(EFFECT::AVENGING_WRATH_PERIODIC_DAMAGE) / HUNDRED
44}
45
46const fn truths_wake_filter(spell_id: u32) -> bool {
47    spell_id == AURA::TRUTHS_WAKE
48}
49
50fn burn_to_ash_extend(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
51    let extension_ms = retribution_config(ctx).burn_to_ash_extend_ms;
52
53    if extension_ms > 0
54        && ctx
55            .aura_remaining_ms(AURA_TRUTHS_WAKE.raw())
56            .is_some_and(|remaining_ms| remaining_ms > 0)
57    {
58        ctx.extend_aura(AURA_TRUTHS_WAKE.raw(), extension_ms);
59    }
60}
61
62#[derive(Clone, Copy, Debug)]
63struct RetributionWiring {
64    templar_selected: bool,
65    lights_deliverance: bool,
66}
67
68fn wire_retribution(
69    built: &mut BuiltCombatSystem,
70    params: &HandlerParams<'_>,
71    wiring: RetributionWiring,
72) {
73    if wiring.templar_selected {
74        register_templar_proc(built, params, wiring.lights_deliverance);
75    }
76}
77
78crate::hooks::define_spec_config! {
79pub(super) struct RetributionConfig {
80    crusade: bool { provenance: "Crusade selection.", gate: crusade, source: true, transform: |value| value },
81    art_of_war_chance: f64 { provenance: "Art of War e1 proc chance.", gate: art_of_war, source: params.effect_base_points(EFFECT::ART_OF_WAR_CHANCE), transform: |value| value / HUNDRED },
82    art_of_war_crit_bonus: f64 { provenance: "Art of War critical bonus.", gate: art_of_war, source: params.effect_base_points(EFFECT::ART_OF_WAR_CRIT_BONUS), transform: |value| value / HUNDRED },
83    light_within: bool { provenance: "Light Within selection.", gate: light_within, source: true, transform: |value| value },
84    light_within_wave_mult: f64 { provenance: "Light Within Wave damage multiplier.", gate: params.talent_picked(TALENT::LIGHT_WITHIN_WAVE), source: params.effect_base_points(EFFECT::LIGHT_WITHIN_WAVE_BONUS), transform: |value| 1.0 + value / HUNDRED },
85    righteous_cause_chance: f64 { provenance: "Righteous Cause proc chance.", gate: righteous_cause, source: params.effect_base_points(EFFECT::RIGHTEOUS_CAUSE_CHANCE), transform: |value| value / HUNDRED },
86    mid1_expurgation_final_verdict: f64 { provenance: "MID1 4pc Final Verdict Expurgation effectiveness.", gate: mid1_4pc, source: params.effect_base_points(EFFECT::MID1_4PC_FINAL_VERDICT), transform: |value| value / HUNDRED },
87    mid1_expurgation_divine_storm: f64 { provenance: "MID1 4pc Divine Storm Expurgation effectiveness.", gate: mid1_4pc, source: params.effect_base_points(EFFECT::MID1_4PC_DIVINE_STORM), transform: |value| value / HUNDRED },
88    crusading_strikes: bool { provenance: "Crusading Strikes selection.", gate: crusading_strikes, source: true, transform: |value| value },
89    empyrean_power_chance: f64 { provenance: "Empyrean Power proc chance.", gate: params.talent_picked(TALENT::EMPYREAN_POWER), source: params.effect_base_points(EFFECT::EMPYREAN_POWER_CHANCE), transform: |value| value / HUNDRED },
90    divine_purpose_chance: f64 { provenance: "Divine Purpose proc chance.", gate: params.talent_picked(TALENT::DIVINE_PURPOSE), source: params.effect_base_points(EFFECT::DIVINE_PURPOSE_CHANCE), transform: |value| value / HUNDRED },
91    empyrean_legacy: bool { provenance: "Empyrean Legacy selection.", gate: params.talent_picked(TALENT::EMPYREAN_LEGACY), source: true, transform: |value| value },
92    empyrean_legacy_effectiveness: f64 { provenance: "Empyrean Legacy e2 Divine Storm effectiveness.", gate: params.talent_picked(TALENT::EMPYREAN_LEGACY), source: params.effect_base_points(EFFECT::EMPYREAN_LEGACY_EFFECTIVENESS), transform: |value| 1.0 + value / HUNDRED },
93    templar: bool { provenance: "Light's Guidance Templar selection.", gate: templar_selected, source: true, transform: |value| value },
94    lights_guidance_count: u32 { provenance: "Light's Guidance 427445 e2 hammer count.", gate: templar_selected, source: params.effect_base_points(EFFECT::LIGHTS_GUIDANCE_COUNT), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
95    lights_guidance_delay_ms: u32 { provenance: "Light's Guidance 427445 e4 initial delay.", gate: templar_selected, source: params.effect_base_points(EFFECT::LIGHTS_GUIDANCE_DELAY), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
96    empyrean_hammer_stagger_ms: u32 { provenance: "Light's Guidance 427445 e3 stagger.", gate: templar_selected, source: params.effect_base_points(EFFECT::LIGHTS_GUIDANCE_STAGGER), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
97    zealous_vindication_count: u32 { provenance: "Zealous Vindication 431463 e1 hammer count.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::ZEALOUS_VINDICATION), source: params.effect_base_points(EFFECT::ZEALOUS_VINDICATION_COUNT), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
98    shake_the_heavens: bool { provenance: "Shake the Heavens selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::SHAKE_THE_HEAVENS_TALENT), source: true, transform: |value| value },
99    sanctification: bool { provenance: "Sanctification selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::SANCTIFICATION_TALENT), source: true, transform: |value| value },
100    higher_calling_ms: u32 { provenance: "Higher Calling e1 seconds converted to milliseconds.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::HIGHER_CALLING), source: params.effect_base_points(EFFECT::HIGHER_CALLING_EXTEND), transform: |value| wowlab_types::numeric::f64_to_u32_saturating_trunc(value * MS_PER_SECOND) },
101    hammerfall: bool { provenance: "Hammerfall selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::HAMMERFALL), source: true, transform: |value| value },
102    lights_deliverance: bool { provenance: "Light's Deliverance selection.", gate: lights_deliverance, source: true, transform: |value| value },
103    lights_deliverance_max_stacks: i32 { provenance: "Light's Deliverance passively-modified maximum stacks.", gate: lights_deliverance, source: params.game_data.aura_max_stacks(SpellIdx::from_raw(AURA::LIGHTS_DELIVERANCE)).unwrap_or_default(), transform: i32::from },
104    undisputed_ruling: bool { provenance: "Undisputed Ruling selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::UNDISPUTED_RULING_TALENT), source: true, transform: |value| value },
105    divine_hammer: bool { provenance: "Divine Hammer selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::DIVINE_HAMMER_TALENT), source: true, transform: |value| value },
106    greater_judgment: bool { provenance: "Greater Judgment selection.", gate: params.talent_picked(TALENT::GREATER_JUDGMENT), source: true, transform: |value| value },
107    sanctify: bool { provenance: "Sanctify selection.", gate: params.talent_picked(TALENT::SANCTIFY), source: true, transform: |value| value },
108    hammer_of_wrath: bool { provenance: "Hammer of Wrath talent selection.", gate: params.talent_picked(TALENT::HAMMER_OF_WRATH_TALENT), source: true, transform: |value| value },
109    vengeful_wrath_max_bonus: f64 { provenance: "Vengeful Wrath e1 maximum Hammer of Wrath damage bonus.", gate: params.talent_picked(TALENT::VENGEFUL_WRATH), source: params.effect_base_points(EFFECT::VENGEFUL_WRATH_MAX_BONUS), transform: |value| value / HUNDRED },
110    divine_toll_echo_mult: f64 { provenance: "Divine Toll echo effectiveness modifier.", gate: true, source: params.effect_base_points(EFFECT::DIVINE_TOLL_ECHO_MULT), transform: |value| 1.0 + value / HUNDRED },
111    divine_exaction_count: u32 { provenance: "Divine Exaction 1260429 e1 echo count.", gate: divine_exaction, source: params.effect_base_points(EFFECT::DIVINE_EXACTION_COUNT), transform: wowlab_types::numeric::f64_to_u32_saturating_trunc },
112    divine_exaction_mult: f64 { provenance: "Divine Exaction 1260429 e2 effectiveness plus base damage.", gate: divine_exaction, source: params.effect_base_points(EFFECT::DIVINE_EXACTION_MULT), transform: |value| 1.0 + value / HUNDRED },
113    burn_to_ash_extend_ms: u32 { provenance: "Burn to Ash 446663 e1 Truth's Wake extension in seconds.", gate: burn_to_ash, source: params.effect_base_points(EFFECT::BURN_TO_ASH_EXTEND), transform: |value| wowlab_types::numeric::f64_to_u32_saturating_trunc(value * MS_PER_SECOND) },
114    consecrated_blade: bool { provenance: "Consecrated Blade baseline specialization selection.", gate: params.talent_selected(TALENT::CONSECRATED_BLADE), source: true, transform: |value| value },
115}
116accessor pub(crate) fn retribution_config(ctx);
117register pub(crate) fn register_retribution(built, params) -> RetributionConfig;
118prepare {
119    let crusade = params.talent_picked(TALENT::CRUSADE);
120    let light_within = params.talent_picked(TALENT::LIGHT_WITHIN);
121    let crusading_strikes = params.talent_picked(TALENT::CRUSADING_STRIKES);
122    let art_of_war = params.talent_picked(TALENT::ART_OF_WAR);
123    let righteous_cause = params.talent_picked(TALENT::RIGHTEOUS_CAUSE);
124    let mid1_4pc = params.set_bonus_auras.contains(&SET_BONUS::MID1_RETRIBUTION_4PC);
125    let templar_selected = params.talent_picked(HERO::TEMPLAR::SPELL::LIGHTS_GUIDANCE);
126    let lights_deliverance = templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::LIGHTS_DELIVERANCE_TALENT);
127    let divine_exaction = templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::DIVINE_EXACTION);
128    let burn_to_ash = params.talent_picked(TALENT::BURN_TO_ASH);
129}
130auras {
131    { provenance: "Light Within Verdict damage is constant while the stackable Avenging Wrath aura is active.", gate: params.talent_picked(TALENT::LIGHT_WITHIN_VERDICT), aura: AURA_AVENGING_WRATH, effect: BuffEffect::DamageMultSpellsActive(1.0 + params.effect_base_points(EFFECT::LIGHT_WITHIN_VERDICT_BONUS) / HUNDRED, &LIGHT_WITHIN_VERDICT_SPELLS) },
132    { provenance: "Execution Sentence's initial payload snapshots Avenging Wrath e2's periodic multiplier in addition to its ordinary direct multiplier.", gate: params.talent_picked(TALENT::EXECUTION_SENTENCE), aura: AURA_AVENGING_WRATH, effect: BuffEffect::DamageMultSpellsActive(execution_sentence_periodic_snapshot_mult(&params.game_data), &EXECUTION_SENTENCE_INIT_SPELLS) },
133}
134wiring {
135    { provenance: "Crusade's passive e1/e2 derive Avenging Wrath's stack cap; the manifest gate retains e11 as the data-owned per-stack Haste payload.", gate: crusade, apply: {
136        built.mask_aura_dbc_effect(
137            AURA_AVENGING_WRATH,
138            EFFECT::AVENGING_WRATH_CRIT.0,
139            EFFECT::AVENGING_WRATH_CRIT.1,
140        );
141        let haste_per_stack = params.effect_base_points(EFFECT::CRUSADE_HASTE_PER_STACK);
142        let haste_cap = params.effect_base_points(EFFECT::CRUSADE_HASTE_CAP);
143        let max_stacks = wowlab_types::numeric::f64_to_u8_saturating_trunc(
144            haste_cap / haste_per_stack.max(f64::EPSILON),
145        ).max(1);
146
147        built.patch_aura(AURA_AVENGING_WRATH, |aura| {
148            aura.max_stacks = max_stacks;
149        });
150        built.set_aura_effect(
151            AURA_AVENGING_WRATH,
152            BuffEffect::CritActive(params.effect_base_points(EFFECT::AVENGING_WRATH_CRIT)),
153        );
154    } },
155    { provenance: "The live Burn to Ash bug applies Truth's Wake e4 twice; duplicate the resolved DBC row without reconstructing its magnitude or affected-spell list.", gate: burn_to_ash && built.state.bug_enabled(&BUG_BURN_TO_ASH_CONSECRATION), apply: {
156        let duplicated = built.duplicate_aura_dbc_effect(
157            AURA_TRUTHS_WAKE,
158            EFFECT::TRUTHS_WAKE_BURN_EFFECT_4.0,
159            EFFECT::TRUTHS_WAKE_BURN_EFFECT_4.1,
160        );
161        debug_assert_eq!(duplicated, 1);
162    } },
163    { provenance: "Crusading Strikes precombat aura; generic OverridesSpellID handling disables Crusader Strike.", gate: crusading_strikes, apply: {
164        built.push_precombat_aura(AURA_CRUSADING_STRIKES);
165    } },
166    { provenance: "Zealot's Fervor precombat aura.", gate: params.talent_picked(TALENT::ZEALOTS_FERVOR), apply: {
167        built.push_precombat_aura(AURA_ZEALOTS_FERVOR);
168    } },
169    { provenance: "Execution Sentence damage accumulator proc.", gate: params.talent_picked(TALENT::EXECUTION_SENTENCE), apply: {
170        built.register_impact_proc(
171            ImpactProc::new(execution_sentence_accumulate)
172                .with_actor_filter(ImpactActorFilter::Any),
173        );
174    } },
175    { provenance: "Burn to Ash extends Truth's Wake after each non-final critical periodic tick.", gate: burn_to_ash, apply: {
176        built.register_impact_proc(
177            ImpactProc::new(burn_to_ash_extend)
178                .with_spell_filter(truths_wake_filter)
179                .periodic_only()
180                .crit_only(),
181        );
182    } },
183    { provenance: "Rush of Light procs from critical non-periodic impacts of lowered spells that generate Holy Power.", gate: params.talent_picked(TALENT::RUSH_OF_LIGHT), apply: {
184        built.register_impact_proc(rush_of_light_proc(&params.game_data));
185    } },
186    { provenance: "Holy Blade flat Blade of Justice energize modifier.", gate: params.talent_picked(TALENT::HOLY_BLADE), apply: {
187        let extra = params.effect_base_points(EFFECT::HOLY_BLADE_ENERGIZE);
188        built.patch_spell(SPELL_BLADE_OF_JUSTICE, |spell| {
189            spell.cost.resource_gain += extra;
190        });
191    } },
192}
193finish |cfg| {
194    wire_retribution(
195        built,
196        params,
197        RetributionWiring {
198            templar_selected,
199            lights_deliverance,
200        },
201    );
202    built.state.set_spec_config(cfg);
203    cfg
204}
205}
206
207fn rush_of_light_accepts(state: &CombatState, spell_id: u32) -> bool {
208    state
209        .spell_data(spell_id)
210        .is_some_and(|(_, spell)| spell.cost.resource_gain > 0.0)
211}
212
213fn rush_of_light_fire(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
214    if !rush_of_light_accepts(ctx.state(), impact.spell_id)
215        || !ctx.proc_category_ready(TALENT::RUSH_OF_LIGHT)
216    {
217        return;
218    }
219
220    ctx.apply_aura(AURA_RUSH_OF_LIGHT.raw());
221    ctx.start_proc_category_cooldown(TALENT::RUSH_OF_LIGHT);
222}
223
224fn rush_of_light_proc(data: &ResolvedGameData) -> ImpactProc {
225    ImpactProc::new(rush_of_light_fire)
226        .with_driver(ProcDriver::from_spell(
227            data,
228            TALENT::RUSH_OF_LIGHT,
229            ProcTriggerSide::Caster,
230        ))
231        .skip_periodic()
232        .crit_only()
233}
234
235fn register_templar_proc(
236    built: &mut BuiltCombatSystem,
237    params: &HandlerParams<'_>,
238    lights_deliverance: bool,
239) {
240    let enabled =
241        lights_deliverance && params.talent_picked(HERO::TEMPLAR::SPELL::LIGHTS_JUDICATOR);
242
243    if enabled {
244        built.register_impact_proc(
245            ImpactProc::new(lights_judicator_fire)
246                .with_chance(LIGHTS_JUDICATOR_PROC_CHANCE)
247                .with_spell_filter(is_empyrean_hammer)
248                .skip_periodic()
249                .crit_only(),
250        );
251    }
252}
253
254const LIGHT_WITHIN_VERDICT_SPELLS: [u32; 2] = [SPELL::FINAL_VERDICT, SPELL::DIVINE_STORM];
255const EXECUTION_SENTENCE_INIT_SPELLS: [u32; 1] = [REPORTED_SPELL::EXECUTION_SENTENCE_INIT];
256const fn is_empyrean_hammer(spell_id: u32) -> bool {
257    spell_id == SPELL::EMPYREAN_HAMMER
258}
259
260fn lights_judicator_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
261    templar::gain_lights_deliverance(ctx);
262}
263
264const fn highlords_judgment_filter(spell_id: u32) -> bool {
265    matches!(
266        spell_id,
267        SPELL::JUDGMENT
268            | SPELL::HAMMER_OF_WRATH
269            | SPELL::JUDGMENT_DIVINE_TOLL
270            | SPELL::HAMMER_OF_WRATH_DIVINE_TOLL
271    )
272}
273
274fn highlords_judgment_chance(ctx: &HookCtx<'_>) -> f64 {
275    ctx.effect_points(EFFECT::HIGHLORDS_MASTERY_FACTOR) / HUNDRED
276}
277
278fn fire_highlords_judgment(ctx: &mut HookCtx<'_>, chance: f64) {
279    if !proc_chance(ctx.rng(), chance) {
280        return;
281    }
282
283    let coef = ctx
284        .game_data()
285        .effect_ap_coefficient(EFFECT::HIGHLORDS_JUDGMENT);
286
287    ctx.deal_damage_ap(SPELL::HIGHLORDS_JUDGMENT, coef, DamageFlags::empty());
288}
289
290fn highlords_judgment_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
291    let chance = highlords_judgment_chance(ctx);
292
293    fire_highlords_judgment(ctx, chance);
294}
295
296fn highlords_judgment_fire_wrath(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
297    let data = ctx.game_data();
298    let divisor = data.effect_base_points(EFFECT::HIGHLORDS_WRATH_DIVISOR);
299    let percent = data.effect_base_points(EFFECT::HIGHLORDS_WRATH) / HUNDRED;
300    let mult = if divisor > 0.0 {
301        1.0 + percent / divisor
302    } else {
303        1.0
304    };
305    let chance = highlords_judgment_chance(ctx) * mult;
306
307    fire_highlords_judgment(ctx, chance);
308}
309
310/// Register the Highlord's Judgment mastery proc.
311pub(crate) fn register_highlords_judgment(
312    built: &mut BuiltCombatSystem,
313    params: &HandlerParams<'_>,
314) {
315    if params
316        .game_data
317        .effect_sp_coefficient(EFFECT::HIGHLORDS_MASTERY_FACTOR)
318        <= 0.0
319    {
320        return;
321    }
322
323    let wrath_talented = params.talent_picked(TALENT::HIGHLORDS_WRATH);
324    let fire = if wrath_talented {
325        highlords_judgment_fire_wrath
326    } else {
327        highlords_judgment_fire
328    };
329
330    // The mastery-scaled roll happens inside `fire`.
331
332    built.register_impact_proc(
333        ImpactProc::new(fire)
334            .with_spell_filter(highlords_judgment_filter)
335            .skip_periodic(),
336    );
337}
338
339#[cfg(test)]
340mod tests {
341    use googletest::prelude::*;
342    use rstest::rstest;
343    use wowlab_engine_domain::dbc::{SpellAttributeKind, SpellSchoolMask};
344    use wowlab_engine_ports::{CombatStats};
345use wowlab_engine_gamedata::{ResolvedGameData, SpellProps};
346
347    use super::*;
348
349    #[gtest]
350    fn execution_sentence_banks_only_resolved_nonphysical_damage() -> Result<()> {
351        let physical = SpellIdx::from_raw(6603);
352        let holy = SpellIdx::from_raw(SPELL::FINAL_VERDICT);
353        let unknown = SpellIdx::from_raw(853);
354        let mut builder = ResolvedGameData::builder();
355
356        builder.insert_school_mask(physical, i32::from(SpellSchoolMask::PHYSICAL.bits()));
357        builder.insert_school_mask(holy, i32::from(SpellSchoolMask::HOLY.bits()));
358
359        let data = builder.build();
360
361        verify_false!(execution_sentence_accepts(&data, physical.0))?;
362        verify_true!(execution_sentence_accepts(&data, holy.0))?;
363
364        verify_false!(execution_sentence_accepts(&data, unknown.0))
365    }
366
367    #[gtest]
368    fn execution_sentence_initial_snapshot_uses_avenging_wrath_periodic_effect() -> Result<()> {
369        let mut builder = ResolvedGameData::builder();
370
371        builder.insert_base_points(
372            SpellIdx::from_raw(EFFECT::AVENGING_WRATH_PERIODIC_DAMAGE.0),
373            EFFECT::AVENGING_WRATH_PERIODIC_DAMAGE.1,
374            20.0,
375        );
376        let data = builder.build();
377
378        verify_that!(
379            execution_sentence_periodic_snapshot_mult(&data),
380            near(1.2, f64::EPSILON),
381        )
382    }
383
384    #[gtest]
385    #[rstest]
386    #[case::judgment(SPELL::JUDGMENT, true)]
387    #[case::hammer_of_wrath(SPELL::HAMMER_OF_WRATH, true)]
388    #[case::divine_toll_judgment(SPELL::JUDGMENT_DIVINE_TOLL, true)]
389    #[case::divine_toll_hammer_of_wrath(SPELL::HAMMER_OF_WRATH_DIVINE_TOLL, true)]
390    #[case::highlords_judgment(SPELL::HIGHLORDS_JUDGMENT, false)]
391    #[case::arbitrary(853, false)]
392    fn highlords_judgment_filter_allows_only_judgment_family_impacts(
393        #[case] spell_id: u32,
394        #[case] expected: bool,
395    ) -> Result<()> {
396        verify_that!(highlords_judgment_filter(spell_id), eq(expected))
397    }
398
399    #[gtest]
400    #[rstest]
401    #[case::empyrean_hammer(SPELL::EMPYREAN_HAMMER, true)]
402    #[case::judgment(SPELL::JUDGMENT, false)]
403    #[case::arbitrary(853, false)]
404    fn lights_judicator_filter_allows_only_empyrean_hammer_impacts(
405        #[case] spell_id: u32,
406        #[case] expected: bool,
407    ) -> Result<()> {
408        verify_that!(is_empyrean_hammer(spell_id), eq(expected))
409    }
410
411    #[gtest]
412    fn rush_of_light_accepts_only_lowered_spells_with_primary_resource_gain() -> Result<()> {
413        const GENERATOR: u32 = 100;
414        const NON_GENERATOR: u32 = 200;
415
416        let built = crate::test_combat_builder(CombatStats::default())
417            .spell("generator", GENERATOR, |spell| Ok(spell.gain(1.0)))
418            .spell("non_generator", NON_GENERATOR, Ok)
419            .build(
420                crate::test_support::wait_rotation(),
421            )
422            .or_fail()?;
423
424        verify_true!(rush_of_light_accepts(&built.state, GENERATOR))?;
425        verify_false!(rush_of_light_accepts(&built.state, NON_GENERATOR))?;
426
427        verify_false!(rush_of_light_accepts(&built.state, 300))
428    }
429
430    #[gtest]
431    fn rush_of_light_registration_uses_caster_driver_and_critical_direct_player_impacts()
432    -> Result<()> {
433        let attribute = SpellAttributeKind::CanProcFromProcs;
434        let mut attributes = vec![0; 14];
435
436        attributes[attribute.block()] |= attribute.mask();
437
438        let mut builder = ResolvedGameData::builder();
439
440        builder.insert_spell_props(
441            SpellIdx::from_raw(TALENT::RUSH_OF_LIGHT),
442            SpellProps {
443                attributes,
444                proc_category_recovery_ms: 500,
445                ..SpellProps::default()
446            },
447        );
448
449        let data = builder.build();
450        let proc = rush_of_light_proc(&data);
451
452        verify_that!(proc.actor_filter, eq(ImpactActorFilter::Player))?;
453        verify_that!(proc.chance, near(1.0, f64::EPSILON))?;
454        verify_that!(proc.driver.trigger_side, eq(ProcTriggerSide::Caster))?;
455        verify_true!(proc.driver.policy.can_proc_from_procs)?;
456        verify_true!(proc.spell_filter.is_none())?;
457        verify_false!(proc.periodic_only)?;
458        verify_true!(proc.skip_periodic)?;
459        verify_true!(proc.crit_only)?;
460
461        verify_that!(
462            data.proc_category_recovery_ms(SpellIdx::from_raw(TALENT::RUSH_OF_LIGHT)),
463            some(eq(500))
464        )
465    }
466}