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