mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
345 lines
12 KiB
Rust
345 lines
12 KiB
Rust
use crate::DamageSource;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use crate::{comp, consts::HP_PER_LEVEL};
|
|
use hashbrown::HashMap;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::convert::TryFrom;
|
|
|
|
use crate::{combat::DamageContributor, resources::Time};
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use specs::{Component, DerefFlaggedStorage};
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use specs_idvs::IdvStorage;
|
|
use std::ops::Mul;
|
|
|
|
/// Specifies what and how much changed current health
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
|
|
pub struct HealthChange {
|
|
/// The amount of the health change, negative is damage, positive is healing
|
|
pub amount: f32,
|
|
/// The individual or group who caused the health change (None if the
|
|
/// damage wasn't caused by an entity)
|
|
pub by: Option<DamageContributor>,
|
|
/// The category of action that resulted in the health change
|
|
pub cause: Option<DamageSource>,
|
|
/// The time that the health change occurred at
|
|
pub time: Time,
|
|
}
|
|
|
|
impl HealthChange {
|
|
pub fn damage_by(&self) -> Option<DamageContributor> {
|
|
self.cause.is_some().then_some(self.by).flatten()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
/// Health is represented by u32s within the module, but treated as a float by
|
|
/// the rest of the game.
|
|
// As a general rule, all input and output values to public functions should be
|
|
// floats rather than integers.
|
|
pub struct Health {
|
|
// Current and base_max are scaled by 256 within this module compared to what is visible to
|
|
// outside this module. The scaling is done to allow health to function as a fixed point while
|
|
// still having the advantages of being an integer. The scaling of 256 was chosen so that max
|
|
// health could be u16::MAX - 1, and then the scaled health could fit inside an f32 with no
|
|
// precision loss
|
|
/// Current health is how much health the entity currently has. Current
|
|
/// health *must* be lower than or equal to maximum health.
|
|
current: u32,
|
|
/// Base max is the amount of health the entity has without considering
|
|
/// temporary modifiers such as buffs
|
|
base_max: u32,
|
|
/// Maximum is the amount of health the entity has after temporary modifiers
|
|
/// are considered
|
|
maximum: u32,
|
|
/// The last change to health
|
|
pub last_change: HealthChange,
|
|
pub is_dead: bool,
|
|
|
|
/// Keeps track of damage per DamageContributor and the last time they
|
|
/// caused damage, used for EXP sharing
|
|
#[serde(skip)]
|
|
damage_contributors: HashMap<DamageContributor, (u64, Time)>,
|
|
}
|
|
|
|
impl Health {
|
|
/// Used when comparisons to health are needed outside this module.
|
|
// This value is chosen as anything smaller than this is more precise than our
|
|
// units of health.
|
|
pub const HEALTH_EPSILON: f32 = 0.5 / Self::MAX_SCALED_HEALTH as f32;
|
|
/// Maximum value allowed for health before scaling
|
|
const MAX_HEALTH: u16 = u16::MAX - 1;
|
|
/// The maximum value allowed for current and maximum health
|
|
/// Maximum value is (u16:MAX - 1) * 256, which only requires 24 bits. This
|
|
/// can fit into an f32 with no loss to precision
|
|
// Cast to u32 done as u32::from cannot be called inside constant
|
|
const MAX_SCALED_HEALTH: u32 = Self::MAX_HEALTH as u32 * Self::SCALING_FACTOR_INT;
|
|
/// The amount health is scaled by within this module
|
|
const SCALING_FACTOR_FLOAT: f32 = 256.;
|
|
const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
|
|
|
|
/// Returns the current value of health casted to a float
|
|
pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
|
|
|
|
/// Returns the base maximum value of health casted to a float
|
|
pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
|
|
|
|
/// Returns the maximum value of health casted to a float
|
|
pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
|
|
|
|
/// Returns the fraction of health an entity has remaining
|
|
pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
|
|
|
|
/// Calculates a new maximum value and returns it if the value differs from
|
|
/// the current maximum.
|
|
///
|
|
/// Note: The returned value uses an internal format so don't expect it to
|
|
/// be useful for anything other than a parameter to
|
|
/// [`Self::update_maximum`].
|
|
pub fn needs_maximum_update(&self, modifiers: comp::stats::StatsModifier) -> Option<u32> {
|
|
let maximum = modifiers
|
|
.compute_maximum(self.base_max())
|
|
.mul(Self::SCALING_FACTOR_FLOAT)
|
|
// NaN does not need to be handled here as rust will automatically change to 0 when casting to u32
|
|
.clamp(0.0, Self::MAX_SCALED_HEALTH as f32) as u32;
|
|
|
|
(maximum != self.maximum).then(|| maximum)
|
|
}
|
|
|
|
/// Updates the maximum value for health.
|
|
///
|
|
/// Note: The accepted `u32` value is in the internal format of this type.
|
|
/// So attempting to pass values that weren't returned from
|
|
/// [`Self::needs_maximum_update`] can produce strange or unexpected
|
|
/// results.
|
|
pub fn update_internal_integer_maximum(&mut self, maximum: u32) {
|
|
self.maximum = maximum;
|
|
// Clamp the current health to enforce the current <= maximum invariant.
|
|
self.current = self.current.min(self.maximum);
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn new(body: comp::Body, level: u16) -> Self {
|
|
let health = u32::from(
|
|
body.base_health()
|
|
.saturating_add(HP_PER_LEVEL.saturating_mul(level)),
|
|
) * Self::SCALING_FACTOR_INT;
|
|
Health {
|
|
current: health,
|
|
base_max: health,
|
|
maximum: health,
|
|
last_change: HealthChange {
|
|
amount: 0.0,
|
|
by: None,
|
|
cause: None,
|
|
time: Time(0.0),
|
|
},
|
|
is_dead: false,
|
|
damage_contributors: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
// TODO: Delete this once stat points will be a thing
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn update_max_hp(&mut self, body: comp::Body, level: u16) {
|
|
let old_max = self.base_max;
|
|
self.base_max = u32::from(
|
|
body.base_health()
|
|
.saturating_add(HP_PER_LEVEL.saturating_mul(level)),
|
|
) * Self::SCALING_FACTOR_INT;
|
|
self.current = (self.current + self.base_max - old_max).min(self.maximum);
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn change_by(&mut self, change: HealthChange) {
|
|
let prev_health = i64::from(self.current);
|
|
self.current = (((self.current() + change.amount).clamp(0.0, f32::from(Self::MAX_HEALTH))
|
|
* Self::SCALING_FACTOR_FLOAT) as u32)
|
|
.min(self.maximum);
|
|
let delta = i64::from(self.current) - prev_health;
|
|
|
|
self.last_change = change;
|
|
|
|
// If damage is applied by an entity, update the damage contributors
|
|
if delta < 0 {
|
|
if let Some(attacker) = change.by {
|
|
let entry = self
|
|
.damage_contributors
|
|
.entry(attacker)
|
|
.or_insert((0, change.time));
|
|
(*entry).0 += u64::try_from(-delta).unwrap_or(0);
|
|
(*entry).1 = change.time
|
|
}
|
|
|
|
// Prune any damage contributors who haven't contributed damage for over the
|
|
// threshold - this enforces a maximum period that an entity will receive EXP
|
|
// for a kill after they last damaged the killed entity.
|
|
const DAMAGE_CONTRIB_PRUNE_SECS: f64 = 600.0;
|
|
self.damage_contributors.retain(|_, (_, last_damage_time)| {
|
|
(change.time.0 - last_damage_time.0) < DAMAGE_CONTRIB_PRUNE_SECS
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn damage_contributions(&self) -> impl Iterator<Item = (&DamageContributor, &u64)> {
|
|
self.damage_contributors
|
|
.iter()
|
|
.map(|(damage_contrib, (damage, _))| (damage_contrib, damage))
|
|
}
|
|
|
|
pub fn should_die(&self) -> bool { self.current == 0 }
|
|
|
|
pub fn kill(&mut self) {
|
|
self.current = 0;
|
|
self.is_dead = true;
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
pub fn revive(&mut self) {
|
|
self.current = self.maximum;
|
|
self.is_dead = false;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub fn empty() -> Self {
|
|
Health {
|
|
current: 0,
|
|
base_max: 0,
|
|
maximum: 0,
|
|
last_change: HealthChange {
|
|
amount: 0.0,
|
|
by: None,
|
|
cause: None,
|
|
time: Time(0.0),
|
|
},
|
|
is_dead: false,
|
|
damage_contributors: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
impl Component for Health {
|
|
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{
|
|
combat::DamageContributor,
|
|
comp::{Health, HealthChange},
|
|
resources::Time,
|
|
uid::Uid,
|
|
};
|
|
|
|
#[test]
|
|
fn test_change_by_negative_health_change_adds_to_damage_contributors() {
|
|
let mut health = Health::empty();
|
|
health.current = 100 * Health::SCALING_FACTOR_INT;
|
|
health.maximum = health.current;
|
|
|
|
let damage_contrib = DamageContributor::Solo(Uid(0));
|
|
let health_change = HealthChange {
|
|
amount: -5.0,
|
|
time: Time(123.0),
|
|
by: Some(damage_contrib),
|
|
cause: None,
|
|
};
|
|
|
|
health.change_by(health_change);
|
|
|
|
let (damage, time) = health.damage_contributors.get(&damage_contrib).unwrap();
|
|
|
|
assert_eq!(
|
|
health_change.amount.abs() as u64 * Health::SCALING_FACTOR_INT as u64,
|
|
*damage
|
|
);
|
|
assert_eq!(health_change.time, *time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_change_by_positive_health_change_does_not_add_damage_contributor() {
|
|
let mut health = Health::empty();
|
|
health.maximum = 100 * Health::SCALING_FACTOR_INT;
|
|
health.current = (health.maximum as f32 * 0.5) as u32;
|
|
|
|
let damage_contrib = DamageContributor::Solo(Uid(0));
|
|
let health_change = HealthChange {
|
|
amount: 20.0,
|
|
time: Time(123.0),
|
|
by: Some(damage_contrib),
|
|
cause: None,
|
|
};
|
|
|
|
health.change_by(health_change);
|
|
|
|
assert!(health.damage_contributors.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_change_by_multiple_damage_from_same_damage_contributor() {
|
|
let mut health = Health::empty();
|
|
health.current = 100 * Health::SCALING_FACTOR_INT;
|
|
health.maximum = health.current;
|
|
|
|
let damage_contrib = DamageContributor::Solo(Uid(0));
|
|
let health_change = HealthChange {
|
|
amount: -5.0,
|
|
time: Time(123.0),
|
|
by: Some(damage_contrib),
|
|
cause: None,
|
|
};
|
|
health.change_by(health_change);
|
|
health.change_by(health_change);
|
|
|
|
let (damage, _) = health.damage_contributors.get(&damage_contrib).unwrap();
|
|
|
|
assert_eq!(
|
|
(health_change.amount.abs() * 2.0) as u64 * Health::SCALING_FACTOR_INT as u64,
|
|
*damage
|
|
);
|
|
assert_eq!(1, health.damage_contributors.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_change_by_damage_contributor_pruning() {
|
|
let mut health = Health::empty();
|
|
health.current = 100 * Health::SCALING_FACTOR_INT;
|
|
health.maximum = health.current;
|
|
|
|
let damage_contrib1 = DamageContributor::Solo(Uid(0));
|
|
let health_change = HealthChange {
|
|
amount: -5.0,
|
|
time: Time(10.0),
|
|
by: Some(damage_contrib1),
|
|
cause: None,
|
|
};
|
|
health.change_by(health_change);
|
|
|
|
let damage_contrib2 = DamageContributor::Solo(Uid(1));
|
|
let health_change = HealthChange {
|
|
amount: -5.0,
|
|
time: Time(100.0),
|
|
by: Some(damage_contrib2),
|
|
cause: None,
|
|
};
|
|
health.change_by(health_change);
|
|
|
|
assert!(health.damage_contributors.contains_key(&damage_contrib1));
|
|
assert!(health.damage_contributors.contains_key(&damage_contrib2));
|
|
|
|
// Apply damage 610 seconds after the damage from damage_contrib1 - this should
|
|
// result in the damage from damage_contrib1 being pruned.
|
|
let health_change = HealthChange {
|
|
amount: -5.0,
|
|
time: Time(620.0),
|
|
by: Some(damage_contrib2),
|
|
cause: None,
|
|
};
|
|
health.change_by(health_change);
|
|
|
|
assert!(!health.damage_contributors.contains_key(&damage_contrib1));
|
|
assert!(health.damage_contributors.contains_key(&damage_contrib2));
|
|
}
|
|
}
|