wowlab_engine_combat/systems/
healing_pipeline.rs1use wowlab_engine_domain::{dbc::SpellSchoolMask, rotation::DenseBuffer};
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, CombatState};
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 state: &mut CombatState,
42 buf: &DenseBuffer,
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 = &state.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_active_actor_aura_effect(
59 crate::context::ActorView::new(state, buf, heal.source),
60 |stacks, effect| match effect {
61 BuffEffect::HealingDoneFlat {
62 amount,
63 schools: effect_schools,
64 } if effect_schools.is_empty() || effect_schools.intersects(schools) => {
65 requested += amount * stacks;
66 }
67 BuffEffect::HealingDonePercent(percent) | BuffEffect::Versatility(percent) => {
68 done_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
69 }
70 BuffEffect::Crit(percent) | BuffEffect::CritFromEffect { percent, .. } => {
71 crit_chance += percent * stacks;
72 }
73 BuffEffect::CritActive(percent) => crit_chance += percent,
74 _ => {}
75 },
76 );
77
78 super::buffs::for_each_active_actor_aura_effect(
79 crate::context::ActorView::new(state, buf, heal.target),
80 |stacks, effect| match effect {
81 BuffEffect::HealingReceivedPercent(percent) => {
82 received_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
83 }
84 BuffEffect::PeriodicHealingPercent(percent) if heal.periodic => {
85 received_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
86 }
87 _ => {}
88 },
89 );
90
91 for key in buf.aura_keys().filter(|key| {
92 key.affected() == heal.target
93 && key.source() == heal.source
94 && buf
95 .aura(*key)
96 .is_some_and(wowlab_engine_domain::rotation::AuraSlot::is_occupied)
97 }) {
98 let Some(local) = state.aura_local_for_key(key) else {
99 continue;
100 };
101 let stacks = buf.aura(key).map_or(1.0, |slot| f64::from(slot.stacks));
102
103 let Some(aura) = state.defs.auras.get(local.as_usize()) else {
105 continue;
106 };
107
108 for effect in aura.effects.iter().flatten() {
109 if let BuffEffect::HealingReceivedFromCasterPercent(percent) = effect {
110 received_multiplier *= (1.0 + percent / HUNDRED).max(0.0).powf(stacks);
111 }
112 }
113 }
114
115 let is_crit = heal.may_crit && proc_chance(rng, (crit_chance / HUNDRED).clamp(0.0, 1.0));
116 let crit_multiplier = if is_crit {
117 wowlab_engine_domain::stats::crit_multiplier(combat_stats)
118 } else {
119 1.0
120 };
121 let modified = requested * done_multiplier * received_multiplier * crit_multiplier;
122 let absorbed =
123 super::absorbs::consume_absorbs(state, heal.target, AbsorbKind::Healing, schools, modified);
124 let available = (modified - absorbed).max(0.0);
125 let before = state
126 .actor_health_fraction(heal.target, heal.at)
127 .unwrap_or(1.0);
128 let effective = state
129 .apply_friendly_actor_heal(heal.target, available)
130 .unwrap_or(0.0);
131 let after = state
132 .actor_health_fraction(heal.target, heal.at)
133 .unwrap_or(before);
134
135 super::process_actor_health_change(state, buf, heal.target, before, after, heal.at);
136
137 HealResult {
138 requested,
139 modified,
140 absorbed,
141 effective,
142 overheal: (available - effective).max(0.0),
143 is_crit,
144 }
145}
146
147#[cfg(test)]
148mod tests;