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 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 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}