Skip to main content

wowlab_engine_combat/systems/
healing_pipeline.rs

1//! Healing pipeline mirroring damage setup, crit/versatility, target modifiers, and application.
2
3use wowlab_engine_domain::dbc::SpellSchoolMask;
4use wowlab_engine_rng::proc_chance;
5use wowlab_types::{
6    combat::DamageSchool,
7    constants::HUNDRED,
8    sim::{ActorId, SimTime},
9};
10
11use crate::state::{AbsorbKind, BuffEffect, CombatInstance, EffectFamily};
12
13/// Typed heal request before source/target modifiers and healing absorbs.
14#[derive(Clone, Copy, Debug)]
15pub(crate) struct HealRequest {
16    pub source: ActorId,
17    pub target: ActorId,
18    pub base: f64,
19    pub percent_of_max: f64,
20    pub school: DamageSchool,
21    pub periodic: bool,
22    pub may_crit: bool,
23    pub at: SimTime,
24}
25
26/// Result of one healing event.
27#[derive(Clone, Copy, Debug, Default, PartialEq)]
28pub(crate) struct HealResult {
29    pub requested: f64,
30    pub modified: f64,
31    pub absorbed: f64,
32    pub effective: f64,
33    pub overheal: f64,
34    pub is_crit: bool,
35}
36
37/// Resolves one direct, periodic, percentage, or leech heal.
38#[must_use]
39// #t(fn: rust_cyclomatic_complexity) ordered source, target, caster-scoped, crit, and absorb stages are intentionally explicit
40pub(crate) fn deal_heal(
41    program: &crate::state::CombatProgram,
42    state: &mut CombatInstance,
43    rng: &mut dyn FnMut() -> f64,
44    heal: HealRequest,
45) -> HealResult {
46    if !state.is_friendly_actor_alive(heal.target) {
47        return HealResult::default();
48    }
49
50    let combat_stats = &program.config.base_stats.stats;
51    let maximum = state.friendly_actor_max_health(heal.target).unwrap_or(0.0);
52    let mut requested = heal.base.max(0.0) + maximum * heal.percent_of_max;
53    let schools = SpellSchoolMask::from(heal.school);
54    let mut done_multiplier = wowlab_engine_domain::stats::versatility_multiplier(combat_stats);
55    let mut received_multiplier = 1.0;
56    let mut crit_chance = combat_stats.crit_chance;
57
58    super::buffs::for_each_planned_actor_effect(
59        crate::context::ActorView::new(program, state, heal.source),
60        EffectFamily::HEALING | EffectFamily::STATS,
61        |stacks, effect| match effect {
62            BuffEffect::HealingDoneFlat {
63                amount,
64                schools: effect_schools,
65            } if effect_schools.is_empty() || effect_schools.intersects(schools) => {
66                requested += amount * stacks;
67            }
68            BuffEffect::HealingDonePercent(percent) | BuffEffect::Versatility(percent) => {
69                done_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
70            }
71            BuffEffect::Crit(percent) | BuffEffect::CritFromEffect { percent, .. } => {
72                crit_chance += percent * stacks;
73            }
74            BuffEffect::CritActive(percent) => crit_chance += percent,
75            _ => {}
76        },
77    );
78
79    super::buffs::for_each_planned_actor_effect(
80        crate::context::ActorView::new(program, state, heal.target),
81        EffectFamily::HEALING,
82        |stacks, effect| match effect {
83            BuffEffect::HealingReceivedPercent(percent) => {
84                received_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
85            }
86            BuffEffect::PeriodicHealingPercent(percent) if heal.periodic => {
87                received_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
88            }
89            _ => {}
90        },
91    );
92
93    super::buffs::for_each_planned_actor_aura_effect(
94        crate::context::ActorView::new(program, state, heal.target),
95        EffectFamily::HEALING,
96        |key, _local, stacks, effect| {
97            if key.source() == heal.source
98                && let BuffEffect::HealingReceivedFromCasterPercent(percent) = effect
99            {
100                received_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
101            }
102        },
103    );
104
105    let is_crit = heal.may_crit && proc_chance(rng, (crit_chance / HUNDRED).clamp(0.0, 1.0));
106    let crit_multiplier = if is_crit {
107        wowlab_engine_domain::stats::crit_multiplier(combat_stats)
108    } else {
109        1.0
110    };
111    let modified = requested * done_multiplier * received_multiplier * crit_multiplier;
112    let absorbed =
113        super::absorbs::consume_absorbs(state, heal.target, AbsorbKind::Healing, schools, modified);
114    let available = (modified - absorbed).max(0.0);
115    let before = state
116        .actor_health_fraction(program, heal.target, heal.at)
117        .unwrap_or(1.0);
118    let effective = state
119        .apply_friendly_actor_heal(heal.target, available)
120        .unwrap_or(0.0);
121    let after = state
122        .actor_health_fraction(program, heal.target, heal.at)
123        .unwrap_or(before);
124
125    super::process_actor_health_change(program, state, heal.target, before, after, heal.at);
126
127    HealResult {
128        requested,
129        modified,
130        absorbed,
131        effective,
132        overheal: (available - effective).max(0.0),
133        is_crit,
134    }
135}
136
137#[cfg(test)]
138mod tests;