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#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
9#[serde(deny_unknown_fields)]
10pub struct HealthChange {
11 pub amount: f32,
13 pub by: Option<DamageContributor>,
16 pub cause: Option<DamageSource>,
18 pub time: Time,
20 pub precise: bool,
22 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)]
33pub struct Health {
38 current: u32,
46 base_max: u32,
49 maximum: u32,
52 pub last_change: HealthChange,
54 pub is_dead: bool,
55 pub can_have_death_protection: bool,
57 pub death_protection: bool,
60
61 #[serde(skip)]
64 damage_contributors: HashMap<DamageContributor, (u64, Time)>,
65}
66
67impl Health {
68 pub const HEALTH_EPSILON: f32 = 0.5 / Self::MAX_SCALED_HEALTH as f32;
72 const MAX_HEALTH: u16 = u16::MAX - 1;
74 const MAX_SCALED_HEALTH: u32 = Self::MAX_HEALTH as u32 * Self::SCALING_FACTOR_INT;
79 const SCALING_FACTOR_FLOAT: f32 = 256.;
81 const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
82
83 pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
85
86 pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
88
89 pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
91
92 pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
94
95 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 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 .clamp(0.0, Self::MAX_SCALED_HEALTH as f32) as u32;
119
120 (maximum != self.maximum).then_some(maximum)
121 }
122
123 pub fn update_internal_integer_maximum(&mut self, maximum: u32) {
130 self.maximum = maximum;
131 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 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 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 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
255pub 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
282 #[test]
283 fn test_change_by_negative_health_change_adds_to_damage_contributors() {
284 let mut health = Health::empty();
285 health.current = 100 * Health::SCALING_FACTOR_INT;
286 health.maximum = health.current;
287
288 let damage_contrib = DamageContributor::Solo(Uid(0));
289 let health_change = HealthChange {
290 amount: -5.0,
291 time: Time(123.0),
292 by: Some(damage_contrib),
293 cause: None,
294 precise: false,
295 instance: rand::random(),
296 };
297
298 health.change_by(health_change);
299
300 let (damage, time) = health.damage_contributors.get(&damage_contrib).unwrap();
301
302 assert_eq!(
303 health_change.amount.abs() as u64 * Health::SCALING_FACTOR_INT as u64,
304 *damage
305 );
306 assert_eq!(health_change.time, *time);
307 }
308
309 #[test]
310 fn test_change_by_positive_health_change_does_not_add_damage_contributor() {
311 let mut health = Health::empty();
312 health.maximum = 100 * Health::SCALING_FACTOR_INT;
313 health.current = (health.maximum as f32 * 0.5) as u32;
314
315 let damage_contrib = DamageContributor::Solo(Uid(0));
316 let health_change = HealthChange {
317 amount: 20.0,
318 time: Time(123.0),
319 by: Some(damage_contrib),
320 cause: None,
321 precise: false,
322 instance: rand::random(),
323 };
324
325 health.change_by(health_change);
326
327 assert!(health.damage_contributors.is_empty());
328 }
329
330 #[test]
331 fn test_change_by_multiple_damage_from_same_damage_contributor() {
332 let mut health = Health::empty();
333 health.current = 100 * Health::SCALING_FACTOR_INT;
334 health.maximum = health.current;
335
336 let damage_contrib = DamageContributor::Solo(Uid(0));
337 let health_change = HealthChange {
338 amount: -5.0,
339 time: Time(123.0),
340 by: Some(damage_contrib),
341 cause: None,
342 precise: false,
343 instance: rand::random(),
344 };
345 health.change_by(health_change);
346 health.change_by(health_change);
347
348 let (damage, _) = health.damage_contributors.get(&damage_contrib).unwrap();
349
350 assert_eq!(
351 (health_change.amount.abs() * 2.0) as u64 * Health::SCALING_FACTOR_INT as u64,
352 *damage
353 );
354 assert_eq!(1, health.damage_contributors.len());
355 }
356
357 #[test]
358 fn test_change_by_damage_contributor_pruning() {
359 let mut health = Health::empty();
360 health.current = 100 * Health::SCALING_FACTOR_INT;
361 health.maximum = health.current;
362
363 let damage_contrib1 = DamageContributor::Solo(Uid(0));
364 let health_change = HealthChange {
365 amount: -5.0,
366 time: Time(10.0),
367 by: Some(damage_contrib1),
368 cause: None,
369 precise: false,
370 instance: rand::random(),
371 };
372 health.change_by(health_change);
373
374 let damage_contrib2 = DamageContributor::Solo(Uid(1));
375 let health_change = HealthChange {
376 amount: -5.0,
377 time: Time(100.0),
378 by: Some(damage_contrib2),
379 cause: None,
380 precise: false,
381 instance: rand::random(),
382 };
383 health.change_by(health_change);
384
385 assert!(health.damage_contributors.contains_key(&damage_contrib1));
386 assert!(health.damage_contributors.contains_key(&damage_contrib2));
387
388 let health_change = HealthChange {
391 amount: -5.0,
392 time: Time(620.0),
393 by: Some(damage_contrib2),
394 cause: None,
395 precise: false,
396 instance: rand::random(),
397 };
398 health.change_by(health_change);
399
400 assert!(!health.damage_contributors.contains_key(&damage_contrib1));
401 assert!(health.damage_contributors.contains_key(&damage_contrib2));
402 }
403}