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
25const 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(¶ms.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(¶ms.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
310pub(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 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}