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, CombatSystemDraft, DamageFlags, HookCtx, ImpactActorFilter,
6    ImpactEvent, ImpactProc, ProcDriver, ProcTriggerSide,
7};
8#[cfg(test)]
9use wowlab_engine_combat::CombatProgram;
10use wowlab_engine_domain::dbc::ResolvedGameDataEffectExt as _;
11#[cfg(test)]
12use wowlab_engine_domain::dbc::is_physical;
13use wowlab_engine_ports::HandlerParams;
14use wowlab_engine_gamedata::{ResolvedGameData};
15use wowlab_engine_rng::proc_chance;
16use wowlab_types::{
17    constants::{HUNDRED, MS_PER_SECOND},
18    sim::SpellIdx,
19};
20
21use super::{
22    bugs::{BUG_BURN_TO_ASH_CONSECRATION, BUG_HAMMER_OF_LIGHT_CRUSADE},
23    templar,
24};
25use crate::generated::specs::retribution_paladin::{
26    AURA, AURA_AVENGING_WRATH, AURA_CRUSADING_STRIKES, AURA_EXECUTION_SENTENCE_GATHER,
27    AURA_EXECUTION_SENTENCE_TRACKER, AURA_RUSH_OF_LIGHT, AURA_TRUTHS_WAKE, AURA_ZEALOTS_FERVOR,
28    EFFECT, HERO, REPORTED_SPELL, SET_BONUS, SPELL, SPELL_BLADE_OF_JUSTICE, TALENT,
29};
30
31/// Light's Judicator proc chance (spell 1261525, 50%).
32const LIGHTS_JUDICATOR_PROC_CHANCE: f64 = 0.5;
33
34fn execution_sentence_accumulate(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
35    if ctx.is_owner_aura_active(AURA_EXECUTION_SENTENCE_TRACKER)
36        && (ctx.is_owner_aura_active(AURA_EXECUTION_SENTENCE_GATHER)
37            || impact.spell_id == REPORTED_SPELL::EXECUTION_SENTENCE_INIT)
38        && !ctx.impact_is_physical(impact)
39    {
40        ctx.accumulate_owner_damage(AURA_EXECUTION_SENTENCE_TRACKER, impact.amount);
41    }
42}
43
44#[cfg(test)]
45fn execution_sentence_accepts(data: &ResolvedGameData, spell_id: u32) -> bool {
46    is_physical(data, SpellIdx::from_raw(spell_id)) == Some(false)
47}
48
49fn execution_sentence_periodic_snapshot_mult(data: &ResolvedGameData) -> f64 {
50    1.0 + data.effect_base_points(EFFECT::AVENGING_WRATH_PERIODIC_DAMAGE) / HUNDRED
51}
52
53const fn truths_wake_filter(spell_id: u32) -> bool {
54    spell_id == AURA::TRUTHS_WAKE
55}
56
57fn burn_to_ash_extend(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
58    let extension_ms = retribution_config(ctx).burn_to_ash_extend_ms;
59
60    if extension_ms > 0
61        && ctx
62            .aura_remaining_ms(AURA_TRUTHS_WAKE)
63            .is_some_and(|remaining_ms| remaining_ms > 0)
64    {
65        ctx.extend_aura(AURA_TRUTHS_WAKE, extension_ms);
66    }
67}
68
69#[derive(Clone, Copy, Debug)]
70struct RetributionWiring {
71    templar_selected: bool,
72    lights_deliverance: bool,
73}
74
75fn wire_retribution(
76    built: &mut CombatSystemDraft,
77    params: &HandlerParams<'_>,
78    wiring: RetributionWiring,
79) {
80    if wiring.templar_selected {
81        register_templar_proc(built, params, wiring.lights_deliverance);
82    }
83}
84
85crate::hooks::define_spec_config! {
86pub(super) struct RetributionConfig {
87    consecration_ground: Option<wowlab_engine_combat::GroundEffectId> { provenance: "Finalized Consecration ground-effect plan.", gate: !params.game_data.is_empty(), source: Some(built.register_ground_effect(wowlab_engine_combat::GroundEffectDefinition::from_driver(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::CONSECRATION_TIMING.action(), crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::CONSECRATION_TIMING, crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::CONSECRATION_GEOMETRY, crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::CONSECRATION_DAMAGE).flags(DamageFlags::PERIODIC))?), transform: |value| value },
88    crusade: bool { provenance: "Crusade selection.", gate: crusade, source: true, transform: |value| value },
89    hammer_of_light_crusade_bug: bool { provenance: "Frozen live-bug selection for Hammer of Light's doubled Crusade stacks.", gate: params.bugs.enabled(&BUG_HAMMER_OF_LIGHT_CRUSADE), source: true, transform: |value| value },
90    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 },
91    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 },
92    light_within: bool { provenance: "Light Within selection.", gate: light_within, source: true, transform: |value| value },
93    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 },
94    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 },
95    mid2_2pc: bool { provenance: "MID2 Retribution two-piece enables Divine Power after Divine Purpose consumption.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_RETRIBUTION_2PC), source: true, transform: |value| value },
96    mid2_4pc: bool { provenance: "MID2 Retribution four-piece enables Divine Arbiter spender sequencing.", gate: params.set_bonus_auras.contains(&SET_BONUS::MID2_RETRIBUTION_4PC), source: true, transform: |value| value },
97    crusading_strikes: bool { provenance: "Crusading Strikes selection.", gate: crusading_strikes, source: true, transform: |value| value },
98    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 },
99    divine_purpose_chance: f64 { provenance: "Divine Purpose proc chance plus the MID2 two-piece's additional chance.", gate: params.talent_picked(TALENT::DIVINE_PURPOSE), source: params.effect_base_points(EFFECT::DIVINE_PURPOSE_CHANCE) + if params.set_bonus_auras.contains(&SET_BONUS::MID2_RETRIBUTION_2PC) { params.effect_base_points(EFFECT::MID2_DIVINE_PURPOSE_CHANCE) } else { 0.0 }, transform: |value| value / HUNDRED },
100    empyrean_legacy: bool { provenance: "Empyrean Legacy selection.", gate: params.talent_picked(TALENT::EMPYREAN_LEGACY), source: true, transform: |value| value },
101    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 },
102    templar: bool { provenance: "Light's Guidance Templar selection.", gate: templar_selected, source: true, transform: |value| value },
103    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 },
104    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 },
105    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 },
106    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 },
107    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 },
108    sanctification: bool { provenance: "Sanctification selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::SANCTIFICATION_TALENT), source: true, transform: |value| value },
109    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) },
110    hammerfall: bool { provenance: "Hammerfall selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::HAMMERFALL), source: true, transform: |value| value },
111    lights_deliverance: bool { provenance: "Light's Deliverance selection.", gate: lights_deliverance, source: true, transform: |value| value },
112    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 },
113    undisputed_ruling: bool { provenance: "Undisputed Ruling selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::UNDISPUTED_RULING_TALENT), source: true, transform: |value| value },
114    divine_hammer: bool { provenance: "Divine Hammer selection.", gate: templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::DIVINE_HAMMER_TALENT), source: true, transform: |value| value },
115    greater_judgment: bool { provenance: "Greater Judgment selection.", gate: params.talent_picked(TALENT::GREATER_JUDGMENT), source: true, transform: |value| value },
116    sanctify: bool { provenance: "Sanctify selection.", gate: params.talent_picked(TALENT::SANCTIFY), source: true, transform: |value| value },
117    hammer_of_wrath: bool { provenance: "Hammer of Wrath talent selection.", gate: params.talent_picked(TALENT::HAMMER_OF_WRATH_TALENT), source: true, transform: |value| value },
118    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 },
119    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 },
120    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 },
121    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 },
122    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) },
123    consecrated_blade: bool { provenance: "Consecrated Blade baseline specialization selection.", gate: params.talent_selected(TALENT::CONSECRATED_BLADE), source: true, transform: |value| value },
124}
125accessor pub(crate) fn retribution_config(ctx);
126register pub(crate) fn register_retribution(built, params) -> Result<RetributionConfig, wowlab_engine_ports::EngineError>;
127prepare {
128    let crusade = params.talent_picked(TALENT::CRUSADE);
129    let light_within = params.talent_picked(TALENT::LIGHT_WITHIN);
130    let crusading_strikes = params.talent_picked(TALENT::CRUSADING_STRIKES);
131    let art_of_war = params.talent_picked(TALENT::ART_OF_WAR);
132    let righteous_cause = params.talent_picked(TALENT::RIGHTEOUS_CAUSE);
133    let templar_selected = params.talent_picked(HERO::TEMPLAR::SPELL::LIGHTS_GUIDANCE);
134    let lights_deliverance = templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::LIGHTS_DELIVERANCE_TALENT);
135    let divine_exaction = templar_selected && params.talent_picked(HERO::TEMPLAR::SPELL::DIVINE_EXACTION);
136    let burn_to_ash = params.talent_picked(TALENT::BURN_TO_ASH);
137}
138auras {
139    { 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) },
140    { 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) },
141}
142wiring {
143    { 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: {
144        built.mask_aura_dbc_effect(
145            AURA_AVENGING_WRATH,
146            EFFECT::AVENGING_WRATH_CRIT.0,
147            EFFECT::AVENGING_WRATH_CRIT.1,
148        );
149        let haste_per_stack = params.effect_base_points(EFFECT::CRUSADE_HASTE_PER_STACK);
150        let haste_cap = params.effect_base_points(EFFECT::CRUSADE_HASTE_CAP);
151        let max_stacks = wowlab_types::numeric::f64_to_u8_saturating_trunc(
152            haste_cap / haste_per_stack.max(f64::EPSILON),
153        ).max(1);
154
155        built.patch_aura(AURA_AVENGING_WRATH, |aura| {
156            aura.max_stacks = max_stacks;
157        });
158        built.set_aura_effect(
159            AURA_AVENGING_WRATH,
160            BuffEffect::CritActive(params.effect_base_points(EFFECT::AVENGING_WRATH_CRIT)),
161        );
162    } },
163    { 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.bug_enabled(&BUG_BURN_TO_ASH_CONSECRATION), apply: {
164        let duplicated = built.duplicate_aura_dbc_effect(
165            AURA_TRUTHS_WAKE,
166            EFFECT::TRUTHS_WAKE_BURN_EFFECT_4.0,
167            EFFECT::TRUTHS_WAKE_BURN_EFFECT_4.1,
168        );
169        debug_assert_eq!(duplicated, 1);
170    } },
171    { provenance: "Crusading Strikes precombat aura; generic OverridesSpellID handling disables Crusader Strike.", gate: crusading_strikes, apply: {
172        built.push_precombat_aura(AURA_CRUSADING_STRIKES);
173    } },
174    { provenance: "Zealot's Fervor precombat aura.", gate: params.talent_picked(TALENT::ZEALOTS_FERVOR), apply: {
175        built.push_precombat_aura(AURA_ZEALOTS_FERVOR);
176    } },
177    { provenance: "Execution Sentence damage accumulator proc.", gate: params.talent_picked(TALENT::EXECUTION_SENTENCE), apply: {
178        built.register_impact_proc(
179            ImpactProc::new(execution_sentence_accumulate)
180                .with_actor_filter(ImpactActorFilter::Any),
181        );
182    } },
183    { provenance: "Burn to Ash extends Truth's Wake after each non-final critical periodic tick.", gate: burn_to_ash, apply: {
184        built.register_impact_proc(
185            ImpactProc::new(burn_to_ash_extend)
186                .with_spell_filter(truths_wake_filter)
187                .periodic_only()
188                .crit_only(),
189        );
190    } },
191    { 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: {
192        built.register_impact_proc(rush_of_light_proc(&params.game_data));
193    } },
194    { provenance: "Holy Blade flat Blade of Justice energize modifier.", gate: params.talent_picked(TALENT::HOLY_BLADE), apply: {
195        let extra = params.effect_base_points(EFFECT::HOLY_BLADE_ENERGIZE);
196        built.patch_spell(SPELL_BLADE_OF_JUSTICE, |spell| {
197            spell.cost.resource_gain += extra;
198        });
199    } },
200}
201finish |cfg| {
202    wire_retribution(
203        built,
204        params,
205        RetributionWiring {
206            templar_selected,
207            lights_deliverance,
208        },
209    );
210    built.set_spec_config(cfg);
211    Ok(cfg)
212}
213}
214
215fn rush_of_light_fire(ctx: &mut HookCtx<'_>, impact: ImpactEvent) {
216    if !ctx.impact_generates_primary_resource(impact)
217        || !ctx.proc_category_ready(wowlab_engine_combat::generated_identities::proc_category::RUSH_OF_LIGHT)
218    {
219        return;
220    }
221
222    ctx.apply_aura(AURA_RUSH_OF_LIGHT);
223    ctx.start_proc_category_cooldown(wowlab_engine_combat::generated_identities::proc_category::RUSH_OF_LIGHT);
224}
225
226#[cfg(test)]
227fn rush_of_light_accepts(program: &CombatProgram, spell_id: u32) -> bool {
228    program
229        .diagnostic_spell_data(spell_id)
230        .is_some_and(|(_, spell)| spell.cost.resource_gain > 0.0)
231}
232
233fn rush_of_light_proc(data: &ResolvedGameData) -> ImpactProc {
234    ImpactProc::new(rush_of_light_fire)
235        .with_driver(ProcDriver::from_spell(
236            data,
237            TALENT::RUSH_OF_LIGHT,
238            ProcTriggerSide::Caster,
239        ))
240        .skip_periodic()
241        .crit_only()
242}
243
244fn register_templar_proc(
245    built: &mut CombatSystemDraft,
246    params: &HandlerParams<'_>,
247    lights_deliverance: bool,
248) {
249    let enabled =
250        lights_deliverance && params.talent_picked(HERO::TEMPLAR::SPELL::LIGHTS_JUDICATOR);
251
252    if enabled {
253        built.register_impact_proc(
254            ImpactProc::new(lights_judicator_fire)
255                .with_chance(LIGHTS_JUDICATOR_PROC_CHANCE)
256                .with_spell_filter(is_empyrean_hammer)
257                .skip_periodic()
258                .crit_only(),
259        );
260    }
261}
262
263const LIGHT_WITHIN_VERDICT_SPELLS: [u32; 2] = [SPELL::FINAL_VERDICT, SPELL::DIVINE_STORM];
264const EXECUTION_SENTENCE_INIT_SPELLS: [u32; 1] = [REPORTED_SPELL::EXECUTION_SENTENCE_INIT];
265const fn is_empyrean_hammer(spell_id: u32) -> bool {
266    spell_id == SPELL::EMPYREAN_HAMMER
267}
268
269fn lights_judicator_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
270    templar::gain_lights_deliverance(ctx);
271}
272
273const fn highlords_judgment_filter(spell_id: u32) -> bool {
274    matches!(
275        spell_id,
276        SPELL::JUDGMENT
277            | SPELL::HAMMER_OF_WRATH
278            | SPELL::JUDGMENT_DIVINE_TOLL
279            | SPELL::HAMMER_OF_WRATH_DIVINE_TOLL
280    )
281}
282
283fn highlords_judgment_chance(ctx: &HookCtx<'_>) -> f64 {
284    ctx.effect_points(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::HIGHLORDS_MASTERY_FACTOR) / HUNDRED
285}
286
287fn fire_highlords_judgment(ctx: &mut HookCtx<'_>, chance: f64) {
288    if !proc_chance(ctx.rng(), chance) {
289        return;
290    }
291
292    let coef = ctx.effect_ap_coefficient(crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::HIGHLORDS_JUDGMENT);
293
294    ctx.deal_damage_ap(crate::generated::specs::retribution_paladin::DAMAGE_SPELL::HIGHLORDS_JUDGMENT, coef, DamageFlags::empty());
295}
296
297fn highlords_judgment_fire(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
298    let chance = highlords_judgment_chance(ctx);
299
300    fire_highlords_judgment(ctx, chance);
301}
302
303fn highlords_judgment_fire_wrath(ctx: &mut HookCtx<'_>, _impact: ImpactEvent) {
304    let divisor = ctx.effect_base_points(
305        crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::HIGHLORDS_WRATH_DIVISOR,
306    );
307    let percent = ctx.effect_percent(
308        crate::generated::specs::retribution_paladin::DAMAGE_EFFECT::HIGHLORDS_WRATH,
309    );
310    let mult = if divisor > 0.0 {
311        1.0 + percent / divisor
312    } else {
313        1.0
314    };
315    let chance = highlords_judgment_chance(ctx) * mult;
316
317    fire_highlords_judgment(ctx, chance);
318}
319
320/// Register the Highlord's Judgment mastery proc.
321pub(crate) fn register_highlords_judgment(
322    built: &mut CombatSystemDraft,
323    params: &HandlerParams<'_>,
324) {
325    if params
326        .game_data
327        .effect_sp_coefficient(EFFECT::HIGHLORDS_MASTERY_FACTOR)
328        <= 0.0
329    {
330        return;
331    }
332
333    let wrath_talented = params.talent_picked(TALENT::HIGHLORDS_WRATH);
334    let fire = if wrath_talented {
335        highlords_judgment_fire_wrath
336    } else {
337        highlords_judgment_fire
338    };
339
340    // The mastery-scaled roll happens inside `fire`.
341
342    built.register_impact_proc(
343        ImpactProc::new(fire)
344            .with_spell_filter(highlords_judgment_filter)
345            .skip_periodic(),
346    );
347}
348
349#[cfg(test)]
350mod tests {
351    use googletest::prelude::*;
352    use rstest::rstest;
353    use wowlab_engine_domain::dbc::{SpellAttributeKind, SpellSchoolMask};
354    use wowlab_engine_ports::{CombatStats};
355use wowlab_engine_gamedata::{ResolvedGameData, SpellProps};
356
357    use super::*;
358
359    #[gtest]
360    fn execution_sentence_banks_only_resolved_nonphysical_damage() -> Result<()> {
361        let physical = SpellIdx::from_raw(6603);
362        let holy = SpellIdx::from_raw(SPELL::FINAL_VERDICT);
363        let unknown = SpellIdx::from_raw(853);
364        let mut builder = ResolvedGameData::builder();
365
366        builder.insert_school_mask(physical, i32::from(SpellSchoolMask::PHYSICAL.bits()));
367        builder.insert_school_mask(holy, i32::from(SpellSchoolMask::HOLY.bits()));
368
369        let data = builder.build();
370
371        wowlab_types::bench_metrics::reset();
372        verify_false!(execution_sentence_accepts(&data, physical.0))?;
373        verify_true!(execution_sentence_accepts(&data, holy.0))?;
374        verify_false!(execution_sentence_accepts(&data, unknown.0))?;
375
376        #[cfg(feature = "bench-metrics")]
377        verify_that!(
378            wowlab_types::bench_metrics::snapshot().game_data_hot_reads,
379            eq(3)
380        )?;
381
382        Ok(())
383    }
384
385    #[gtest]
386    fn execution_sentence_initial_snapshot_uses_avenging_wrath_periodic_effect() -> Result<()> {
387        let mut builder = ResolvedGameData::builder();
388
389        builder.insert_base_points(
390            SpellIdx::from_raw(EFFECT::AVENGING_WRATH_PERIODIC_DAMAGE.0),
391            EFFECT::AVENGING_WRATH_PERIODIC_DAMAGE.1,
392            20.0,
393        );
394        let data = builder.build();
395
396        verify_that!(
397            execution_sentence_periodic_snapshot_mult(&data),
398            near(1.2, f64::EPSILON),
399        )
400    }
401
402    #[gtest]
403    #[rstest]
404    #[case::judgment(SPELL::JUDGMENT, true)]
405    #[case::hammer_of_wrath(SPELL::HAMMER_OF_WRATH, true)]
406    #[case::divine_toll_judgment(SPELL::JUDGMENT_DIVINE_TOLL, true)]
407    #[case::divine_toll_hammer_of_wrath(SPELL::HAMMER_OF_WRATH_DIVINE_TOLL, true)]
408    #[case::highlords_judgment(SPELL::HIGHLORDS_JUDGMENT, false)]
409    #[case::arbitrary(853, false)]
410    fn highlords_judgment_filter_allows_only_judgment_family_impacts(
411        #[case] spell_id: u32,
412        #[case] expected: bool,
413    ) -> Result<()> {
414        verify_that!(highlords_judgment_filter(spell_id), eq(expected))
415    }
416
417    #[gtest]
418    #[rstest]
419    #[case::empyrean_hammer(SPELL::EMPYREAN_HAMMER, true)]
420    #[case::judgment(SPELL::JUDGMENT, false)]
421    #[case::arbitrary(853, false)]
422    fn lights_judicator_filter_allows_only_empyrean_hammer_impacts(
423        #[case] spell_id: u32,
424        #[case] expected: bool,
425    ) -> Result<()> {
426        verify_that!(is_empyrean_hammer(spell_id), eq(expected))
427    }
428
429    #[gtest]
430    fn rush_of_light_accepts_only_lowered_spells_with_primary_resource_gain() -> Result<()> {
431        const GENERATOR: u32 = 100;
432        const NON_GENERATOR: u32 = 200;
433
434        let built = crate::test_combat_builder(CombatStats::default())
435            .spell("generator", GENERATOR, |spell| Ok(spell.gain(1.0)))
436            .spell("non_generator", NON_GENERATOR, Ok)
437            .into_draft().expect("valid combat draft").finalize(&
438                crate::test_support::wait_rotation(),
439            )
440            .or_fail()?;
441
442        wowlab_types::bench_metrics::reset();
443        verify_true!(rush_of_light_accepts(&built.program, GENERATOR))?;
444        verify_false!(rush_of_light_accepts(&built.program, NON_GENERATOR))?;
445        verify_false!(rush_of_light_accepts(&built.program, 300))?;
446
447        #[cfg(feature = "bench-metrics")]
448        verify_that!(
449            wowlab_types::bench_metrics::snapshot().keyed_lookups,
450            eq(3)
451        )?;
452
453        Ok(())
454    }
455
456    #[gtest]
457    fn rush_of_light_registration_uses_caster_driver_and_critical_direct_player_impacts()
458    -> Result<()> {
459        let attribute = SpellAttributeKind::CanProcFromProcs;
460        let mut attributes = vec![0; 14];
461
462        attributes[attribute.block()] |= attribute.mask();
463
464        let mut builder = ResolvedGameData::builder();
465
466        builder.insert_spell_props(
467            SpellIdx::from_raw(TALENT::RUSH_OF_LIGHT),
468            SpellProps {
469                attributes,
470                proc_category_recovery_ms: 500,
471                ..SpellProps::default()
472            },
473        );
474
475        let data = builder.build();
476        let proc = rush_of_light_proc(&data);
477
478        verify_that!(proc.actor_filter, eq(ImpactActorFilter::Player))?;
479        verify_that!(proc.chance, near(1.0, f64::EPSILON))?;
480        verify_that!(proc.driver.trigger_side, eq(ProcTriggerSide::Caster))?;
481        verify_true!(proc.driver.policy.can_proc_from_procs)?;
482        verify_true!(proc.spell_filter.is_none())?;
483        verify_false!(proc.periodic_only)?;
484        verify_true!(proc.skip_periodic)?;
485        verify_true!(proc.crit_only)?;
486
487        verify_that!(
488            data.proc_category_recovery_ms(SpellIdx::from_raw(TALENT::RUSH_OF_LIGHT)),
489            some(eq(500))
490        )
491    }
492}