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
31const 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(¶ms.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(¶ms.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
320pub(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 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}