use crate::{combat::DamageContributor, comp, resources::Time, uid::Uid, DamageSource};
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage};
use std::{convert::TryFrom, ops::Mul};
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct HealthChange {
pub amount: f32,
pub by: Option<DamageContributor>,
pub cause: Option<DamageSource>,
pub time: Time,
pub precise: bool,
pub instance: u64,
}
impl HealthChange {
pub fn damage_by(&self) -> Option<DamageContributor> {
self.cause.is_some().then_some(self.by).flatten()
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Health {
current: u32,
base_max: u32,
maximum: u32,
pub last_change: HealthChange,
pub is_dead: bool,
#[serde(skip)]
damage_contributors: HashMap<DamageContributor, (u64, Time)>,
}
impl Health {
pub const HEALTH_EPSILON: f32 = 0.5 / Self::MAX_SCALED_HEALTH as f32;
const MAX_HEALTH: u16 = u16::MAX - 1;
const MAX_SCALED_HEALTH: u32 = Self::MAX_HEALTH as u32 * Self::SCALING_FACTOR_INT;
const SCALING_FACTOR_FLOAT: f32 = 256.;
const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
pub fn set_fraction(&mut self, fraction: f32) {
self.current = (self.maximum() * fraction * Self::SCALING_FACTOR_FLOAT) as u32;
self.is_dead = self.current == 0;
}
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)
.clamp(0.0, Self::MAX_SCALED_HEALTH as f32) as u32;
(maximum != self.maximum).then_some(maximum)
}
pub fn update_internal_integer_maximum(&mut self, maximum: u32) {
self.maximum = maximum;
self.current = self.current.min(self.maximum);
}
pub fn new(body: comp::Body) -> Self {
let health = u32::from(body.base_health()) * Self::SCALING_FACTOR_INT;
Health {
current: health,
base_max: health,
maximum: health,
last_change: HealthChange {
amount: 0.0,
by: None,
cause: None,
precise: false,
time: Time(0.0),
instance: rand::random(),
},
is_dead: false,
damage_contributors: HashMap::new(),
}
}
pub fn change_by(&mut self, change: HealthChange) -> bool {
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 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
}
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
});
}
delta != 0
}
pub fn damage_contributions(&self) -> impl Iterator<Item = (&DamageContributor, &u64)> {
self.damage_contributors
.iter()
.map(|(damage_contrib, (damage, _))| (damage_contrib, damage))
}
pub fn recent_damagers(&self) -> impl Iterator<Item = (Uid, Time)> + '_ {
self.damage_contributors
.iter()
.map(|(contrib, (_, time))| (contrib.uid(), *time))
}
pub fn should_die(&self) -> bool { self.current == 0 }
pub fn kill(&mut self) { self.current = 0; }
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,
precise: false,
time: Time(0.0),
instance: rand::random(),
},
is_dead: false,
damage_contributors: HashMap::new(),
}
}
}
impl Component for Health {
type Storage = DerefFlaggedStorage<Self, specs::VecStorage<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,
precise: false,
instance: rand::random(),
};
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,
precise: false,
instance: rand::random(),
};
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,
precise: false,
instance: rand::random(),
};
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,
precise: false,
instance: rand::random(),
};
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,
precise: false,
instance: rand::random(),
};
health.change_by(health_change);
assert!(health.damage_contributors.contains_key(&damage_contrib1));
assert!(health.damage_contributors.contains_key(&damage_contrib2));
let health_change = HealthChange {
amount: -5.0,
time: Time(620.0),
by: Some(damage_contrib2),
cause: None,
precise: false,
instance: rand::random(),
};
health.change_by(health_change);
assert!(!health.damage_contributors.contains_key(&damage_contrib1));
assert!(health.damage_contributors.contains_key(&damage_contrib2));
}
}