veloren_common/comp/
health.rs

1use crate::{DamageSource, combat::DamageContributor, comp, resources::Time, uid::Uid};
2use hashbrown::HashMap;
3use serde::{Deserialize, Serialize};
4use specs::{Component, DerefFlaggedStorage};
5use std::{convert::TryFrom, ops::Mul};
6
7/// Specifies what and how much changed current health
8#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
9#[serde(deny_unknown_fields)]
10pub struct HealthChange {
11    /// The amount of the health change, negative is damage, positive is healing
12    pub amount: f32,
13    /// The individual or group who caused the health change (None if the
14    /// damage wasn't caused by an entity)
15    pub by: Option<DamageContributor>,
16    /// The category of action that resulted in the health change
17    pub cause: Option<DamageSource>,
18    /// The time that the health change occurred at
19    pub time: Time,
20    /// A boolean that tells you if the change was a precsie hit
21    pub precise: bool,
22    /// A random ID, used to group up health changes from the same attack
23    pub instance: u64,
24}
25
26impl HealthChange {
27    pub fn damage_by(&self) -> Option<DamageContributor> {
28        self.cause.is_some().then_some(self.by).flatten()
29    }
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize)]
33/// Health is represented by u32s within the module, but treated as a float by
34/// the rest of the game.
35// As a general rule, all input and output values to public functions should be
36// floats rather than integers.
37pub struct Health {
38    // Current and base_max are scaled by 256 within this module compared to what is visible to
39    // outside this module. The scaling is done to allow health to function as a fixed point while
40    // still having the advantages of being an integer. The scaling of 256 was chosen so that max
41    // health could be u16::MAX - 1, and then the scaled health could fit inside an f32 with no
42    // precision loss
43    /// Current health is how much health the entity currently has. Current
44    /// health *must* be lower than or equal to maximum health.
45    current: u32,
46    /// Base max is the amount of health the entity has without considering
47    /// temporary modifiers such as buffs
48    base_max: u32,
49    /// Maximum is the amount of health the entity has after temporary modifiers
50    /// are considered
51    maximum: u32,
52    /// The last change to health
53    pub last_change: HealthChange,
54    pub is_dead: bool,
55    /// If this entity supports having death protection.
56    pub can_have_death_protection: bool,
57    /// If death protection is true, any damage that would kill instead leaves
58    /// the entity at 1 health.
59    pub death_protection: bool,
60
61    /// Keeps track of damage per DamageContributor and the last time they
62    /// caused damage, used for EXP sharing
63    #[serde(skip)]
64    damage_contributors: HashMap<DamageContributor, (u64, Time)>,
65}
66
67impl Health {
68    /// Used when comparisons to health are needed outside this module.
69    // This value is chosen as anything smaller than this is more precise than our
70    // units of health.
71    pub const HEALTH_EPSILON: f32 = 0.5 / Self::MAX_SCALED_HEALTH as f32;
72    /// Maximum value allowed for health before scaling
73    const MAX_HEALTH: u16 = u16::MAX - 1;
74    /// The maximum value allowed for current and maximum health
75    /// Maximum value is (u16:MAX - 1) * 256, which only requires 24 bits. This
76    /// can fit into an f32 with no loss to precision
77    // Cast to u32 done as u32::from cannot be called inside constant
78    const MAX_SCALED_HEALTH: u32 = Self::MAX_HEALTH as u32 * Self::SCALING_FACTOR_INT;
79    /// The amount health is scaled by within this module
80    const SCALING_FACTOR_FLOAT: f32 = 256.;
81    const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
82
83    /// Returns the current value of health casted to a float
84    pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
85
86    /// Returns the base maximum value of health casted to a float
87    pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
88
89    /// Returns the maximum value of health casted to a float
90    pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
91
92    /// Returns the fraction of health an entity has remaining
93    pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
94
95    /// Instantly set the health fraction.
96    pub fn set_fraction(&mut self, fraction: f32) {
97        self.current =
98            (self.maximum() * fraction.clamp(0.0, 1.0) * Self::SCALING_FACTOR_FLOAT).ceil() as u32;
99    }
100
101    pub fn set_amount(&mut self, amount: f32) {
102        self.current = (amount * Self::SCALING_FACTOR_FLOAT)
103            .clamp(0.0, self.maximum())
104            .ceil() as u32;
105    }
106
107    /// Calculates a new maximum value and returns it if the value differs from
108    /// the current maximum.
109    ///
110    /// Note: The returned value uses an internal format so don't expect it to
111    /// be useful for anything other than a parameter to
112    /// [`Self::update_internal_integer_maximum`].
113    pub fn needs_maximum_update(&self, modifiers: comp::stats::StatsModifier) -> Option<u32> {
114        let maximum = modifiers
115            .compute_maximum(self.base_max())
116            .mul(Self::SCALING_FACTOR_FLOAT)
117            // NaN does not need to be handled here as rust will automatically change to 0 when casting to u32
118            .clamp(0.0, Self::MAX_SCALED_HEALTH as f32) as u32;
119
120        (maximum != self.maximum).then_some(maximum)
121    }
122
123    /// Updates the maximum value for health.
124    ///
125    /// Note: The accepted `u32` value is in the internal format of this type.
126    /// So attempting to pass values that weren't returned from
127    /// [`Self::needs_maximum_update`] can produce strange or unexpected
128    /// results.
129    pub fn update_internal_integer_maximum(&mut self, maximum: u32) {
130        self.maximum = maximum;
131        // Clamp the current health to enforce the current <= maximum invariant.
132        self.current = self.current.min(self.maximum);
133    }
134
135    pub fn new(body: comp::Body) -> Self {
136        let health = u32::from(body.base_health()) * Self::SCALING_FACTOR_INT;
137        let death_protection = body.has_death_protection();
138        Health {
139            current: health,
140            base_max: health,
141            maximum: health,
142            last_change: HealthChange {
143                amount: 0.0,
144                by: None,
145                cause: None,
146                precise: false,
147                time: Time(0.0),
148                instance: rand::random(),
149            },
150            is_dead: false,
151            can_have_death_protection: death_protection,
152            death_protection,
153            damage_contributors: HashMap::new(),
154        }
155    }
156
157    /// Returns a boolean if the delta was not zero.
158    pub fn change_by(&mut self, change: HealthChange) -> bool {
159        let prev_health = i64::from(self.current);
160        self.current = (((self.current() + change.amount).clamp(0.0, f32::from(Self::MAX_HEALTH))
161            * Self::SCALING_FACTOR_FLOAT) as u32)
162            .min(self.maximum);
163        let delta = i64::from(self.current) - prev_health;
164
165        self.last_change = change;
166
167        // If damage is applied by an entity, update the damage contributors
168        if delta < 0 {
169            if let Some(attacker) = change.by {
170                let entry = self
171                    .damage_contributors
172                    .entry(attacker)
173                    .or_insert((0, change.time));
174                entry.0 += u64::try_from(-delta).unwrap_or(0);
175                entry.1 = change.time
176            }
177
178            // Prune any damage contributors who haven't contributed damage for over the
179            // threshold - this enforces a maximum period that an entity will receive EXP
180            // for a kill after they last damaged the killed entity.
181            const DAMAGE_CONTRIB_PRUNE_SECS: f64 = 600.0;
182            self.damage_contributors.retain(|_, (_, last_damage_time)| {
183                (change.time.0 - last_damage_time.0) < DAMAGE_CONTRIB_PRUNE_SECS
184            });
185        }
186        delta != 0
187    }
188
189    pub fn damage_contributions(&self) -> impl Iterator<Item = (&DamageContributor, &u64)> {
190        self.damage_contributors
191            .iter()
192            .map(|(damage_contrib, (damage, _))| (damage_contrib, damage))
193    }
194
195    pub fn recent_damagers(&self) -> impl Iterator<Item = (Uid, Time)> + '_ {
196        self.damage_contributors
197            .iter()
198            .map(|(contrib, (_, time))| (contrib.uid(), *time))
199    }
200
201    pub fn should_die(&self) -> bool { self.current == 0 }
202
203    pub fn kill(&mut self) {
204        self.current = 0;
205        self.death_protection = false;
206    }
207
208    pub fn revive(&mut self) {
209        self.current = self.maximum;
210        self.is_dead = false;
211        self.death_protection = self.can_have_death_protection;
212    }
213
214    pub fn consume_death_protection(&mut self) {
215        if self.death_protection {
216            self.death_protection = false;
217            if self.current() < 1.0 {
218                self.set_amount(1.0);
219            }
220        }
221    }
222
223    pub fn refresh_death_protection(&mut self) {
224        if self.can_have_death_protection {
225            self.death_protection = true;
226        }
227    }
228
229    pub fn has_consumed_death_protection(&self) -> bool {
230        self.can_have_death_protection && !self.death_protection
231    }
232
233    #[cfg(test)]
234    pub fn empty() -> Self {
235        Health {
236            current: 0,
237            base_max: 0,
238            maximum: 0,
239            last_change: HealthChange {
240                amount: 0.0,
241                by: None,
242                cause: None,
243                precise: false,
244                time: Time(0.0),
245                instance: rand::random(),
246            },
247            is_dead: false,
248            can_have_death_protection: false,
249            death_protection: false,
250            damage_contributors: HashMap::new(),
251        }
252    }
253}
254
255/// Returns true if an entity is downed, their character state is `Crawl` and
256/// their death protection has been consumed.
257pub fn is_downed(health: Option<&Health>, character_state: Option<&super::CharacterState>) -> bool {
258    health.is_some_and(|health| !health.is_dead && health.has_consumed_death_protection())
259        && matches!(character_state, Some(super::CharacterState::Crawl))
260}
261
262pub fn is_downed_or_dead(
263    health: Option<&Health>,
264    character_state: Option<&super::CharacterState>,
265) -> bool {
266    health.is_some_and(|health| health.is_dead) || is_downed(health, character_state)
267}
268
269impl Component for Health {
270    type Storage = DerefFlaggedStorage<Self, specs::VecStorage<Self>>;
271}
272
273#[cfg(test)]
274mod tests {
275    use crate::{
276        combat::DamageContributor,
277        comp::{Health, HealthChange},
278        resources::Time,
279        uid::Uid,
280    };
281    use std::num::NonZeroU64;
282
283    #[test]
284    fn test_change_by_negative_health_change_adds_to_damage_contributors() {
285        let mut health = Health::empty();
286        health.current = 100 * Health::SCALING_FACTOR_INT;
287        health.maximum = health.current;
288
289        let damage_contrib = DamageContributor::Solo(Uid(NonZeroU64::new(1).unwrap()));
290        let health_change = HealthChange {
291            amount: -5.0,
292            time: Time(123.0),
293            by: Some(damage_contrib),
294            cause: None,
295            precise: false,
296            instance: rand::random(),
297        };
298
299        health.change_by(health_change);
300
301        let (damage, time) = health.damage_contributors.get(&damage_contrib).unwrap();
302
303        assert_eq!(
304            health_change.amount.abs() as u64 * Health::SCALING_FACTOR_INT as u64,
305            *damage
306        );
307        assert_eq!(health_change.time, *time);
308    }
309
310    #[test]
311    fn test_change_by_positive_health_change_does_not_add_damage_contributor() {
312        let mut health = Health::empty();
313        health.maximum = 100 * Health::SCALING_FACTOR_INT;
314        health.current = (health.maximum as f32 * 0.5) as u32;
315
316        let damage_contrib = DamageContributor::Solo(Uid(NonZeroU64::new(1).unwrap()));
317        let health_change = HealthChange {
318            amount: 20.0,
319            time: Time(123.0),
320            by: Some(damage_contrib),
321            cause: None,
322            precise: false,
323            instance: rand::random(),
324        };
325
326        health.change_by(health_change);
327
328        assert!(health.damage_contributors.is_empty());
329    }
330
331    #[test]
332    fn test_change_by_multiple_damage_from_same_damage_contributor() {
333        let mut health = Health::empty();
334        health.current = 100 * Health::SCALING_FACTOR_INT;
335        health.maximum = health.current;
336
337        let damage_contrib = DamageContributor::Solo(Uid(NonZeroU64::new(1).unwrap()));
338        let health_change = HealthChange {
339            amount: -5.0,
340            time: Time(123.0),
341            by: Some(damage_contrib),
342            cause: None,
343            precise: false,
344            instance: rand::random(),
345        };
346        health.change_by(health_change);
347        health.change_by(health_change);
348
349        let (damage, _) = health.damage_contributors.get(&damage_contrib).unwrap();
350
351        assert_eq!(
352            (health_change.amount.abs() * 2.0) as u64 * Health::SCALING_FACTOR_INT as u64,
353            *damage
354        );
355        assert_eq!(1, health.damage_contributors.len());
356    }
357
358    #[test]
359    fn test_change_by_damage_contributor_pruning() {
360        let mut health = Health::empty();
361        health.current = 100 * Health::SCALING_FACTOR_INT;
362        health.maximum = health.current;
363
364        let damage_contrib1 = DamageContributor::Solo(Uid(NonZeroU64::new(1).unwrap()));
365        let health_change = HealthChange {
366            amount: -5.0,
367            time: Time(10.0),
368            by: Some(damage_contrib1),
369            cause: None,
370            precise: false,
371            instance: rand::random(),
372        };
373        health.change_by(health_change);
374
375        let damage_contrib2 = DamageContributor::Solo(Uid(NonZeroU64::new(2).unwrap()));
376        let health_change = HealthChange {
377            amount: -5.0,
378            time: Time(100.0),
379            by: Some(damage_contrib2),
380            cause: None,
381            precise: false,
382            instance: rand::random(),
383        };
384        health.change_by(health_change);
385
386        assert!(health.damage_contributors.contains_key(&damage_contrib1));
387        assert!(health.damage_contributors.contains_key(&damage_contrib2));
388
389        // Apply damage 610 seconds after the damage from damage_contrib1 - this should
390        // result in the damage from damage_contrib1 being pruned.
391        let health_change = HealthChange {
392            amount: -5.0,
393            time: Time(620.0),
394            by: Some(damage_contrib2),
395            cause: None,
396            precise: false,
397            instance: rand::random(),
398        };
399        health.change_by(health_change);
400
401        assert!(!health.damage_contributors.contains_key(&damage_contrib1));
402        assert!(health.damage_contributors.contains_key(&damage_contrib2));
403    }
404}