wowlab_engine_combat/systems/
healing_pipeline.rs1use 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#[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#[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#[must_use]
39pub(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;