use crate::{
charges::{ChargeState, Charges},
Abilitylike, CannotUseAbility,
};
use bevy::{ecs::prelude::Component, reflect::Reflect};
use core::time::Duration;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display, marker::PhantomData};
#[derive(Component, Debug, Clone, PartialEq, Eq, Reflect)]
pub struct CooldownState<A: Abilitylike> {
cooldown_map: HashMap<A, Cooldown>,
pub global_cooldown: Option<Cooldown>,
#[reflect(ignore)]
_phantom: PhantomData<A>,
}
impl<A: Abilitylike> Default for CooldownState<A> {
fn default() -> Self {
CooldownState {
cooldown_map: HashMap::new(),
global_cooldown: None,
_phantom: PhantomData,
}
}
}
impl<A: Abilitylike> CooldownState<A> {
#[must_use]
pub fn new(action_cooldown_pairs: impl IntoIterator<Item = (A, Cooldown)>) -> Self {
let mut cooldowns = CooldownState::default();
for (action, cooldown) in action_cooldown_pairs.into_iter() {
cooldowns.set(action, cooldown);
}
cooldowns
}
#[inline]
pub fn trigger(&mut self, action: &A) -> Result<(), CannotUseAbility> {
self.ready(action)?;
if let Some(cooldown) = self.get_mut(action) {
cooldown.trigger()?;
}
if let Some(global_cooldown) = self.global_cooldown.as_mut() {
global_cooldown.trigger()?;
}
Ok(())
}
#[inline]
pub fn ready(&self, action: &A) -> Result<(), CannotUseAbility> {
if let Some(cooldown) = self.get(action) {
cooldown.ready()?;
}
self.gcd_ready()
}
#[inline]
pub fn gcd_ready(&self) -> Result<(), CannotUseAbility> {
if let Some(global_cooldown) = self.global_cooldown.as_ref() {
global_cooldown
.ready()
.map_err(|_| CannotUseAbility::OnGlobalCooldown)
} else {
Ok(())
}
}
pub fn tick(&mut self, delta_time: Duration, maybe_charges: Option<&mut ChargeState<A>>) {
if let Some(charge_state) = maybe_charges {
for (action, cooldown) in self.cooldown_map.iter_mut() {
let charges = charge_state.get_mut(action);
cooldown.tick(delta_time, charges);
}
} else {
for cooldown in self.cooldown_map.values_mut() {
cooldown.tick(delta_time, None);
}
}
if let Some(global_cooldown) = self.global_cooldown.as_mut() {
global_cooldown.tick(delta_time, None);
}
}
#[inline]
#[must_use]
pub fn get(&self, action: &A) -> Option<&Cooldown> {
self.cooldown_map.get(action)
}
#[inline]
#[must_use]
pub fn get_mut(&mut self, action: &A) -> Option<&mut Cooldown> {
self.cooldown_map.get_mut(action)
}
#[inline]
pub fn set(&mut self, action: A, cooldown: Cooldown) -> &mut Self {
self.cooldown_map.insert(action, cooldown);
self
}
#[inline]
#[must_use]
pub fn build(&mut self) -> Self {
self.clone()
}
#[inline]
pub fn iter(&self) -> impl Iterator<Item = &Cooldown> {
self.cooldown_map.values()
}
#[inline]
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Cooldown> {
self.cooldown_map.values_mut()
}
}
#[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize, Reflect)]
pub struct Cooldown {
max_time: Duration,
elapsed_time: Duration,
}
impl Cooldown {
pub fn new(max_time: Duration) -> Cooldown {
assert!(max_time != Duration::ZERO);
Cooldown {
max_time,
elapsed_time: max_time,
}
}
pub fn from_secs(max_time: f32) -> Cooldown {
assert!(max_time > 0.);
let max_time = Duration::from_secs_f32(max_time);
Cooldown::new(max_time)
}
pub fn tick(&mut self, delta_time: Duration, charges: Option<&mut Charges>) {
if self.elapsed_time == self.max_time {
return;
}
assert!(self.max_time != Duration::ZERO);
if let Some(charges) = charges {
let total_time = self.elapsed_time.saturating_add(delta_time);
let total_nanos: u64 = total_time.as_nanos().try_into().unwrap_or(u64::MAX);
let max_nanos: u64 = self.max_time.as_nanos().try_into().unwrap_or(u64::MAX);
let n_completed = (total_nanos / max_nanos).try_into().unwrap_or(u8::MAX);
let extra_time = Duration::from_nanos(total_nanos % max_nanos);
let excess_completions = charges.add_charges(n_completed);
if excess_completions == 0 {
self.elapsed_time =
(self.elapsed_time.saturating_add(extra_time)).min(self.max_time);
} else {
self.elapsed_time = self.max_time;
}
} else {
self.elapsed_time = self
.elapsed_time
.saturating_add(delta_time)
.min(self.max_time);
}
}
pub fn ready(&self) -> Result<(), CannotUseAbility> {
match self.elapsed_time >= self.max_time {
true => Ok(()),
false => Err(CannotUseAbility::OnCooldown),
}
}
#[inline]
pub fn refresh(&mut self) {
self.elapsed_time = self.max_time
}
#[inline]
pub fn trigger(&mut self) -> Result<(), CannotUseAbility> {
self.ready()?;
self.elapsed_time = Duration::ZERO;
Ok(())
}
#[inline]
pub fn max_time(&self) -> Duration {
self.max_time
}
#[inline]
pub fn set_max_time(&mut self, max_time: Duration) {
assert!(max_time != Duration::ZERO);
self.max_time = max_time;
self.elapsed_time = self.elapsed_time.min(max_time);
}
#[inline]
pub fn elapsed(&self) -> Duration {
self.elapsed_time
}
#[inline]
pub fn set_elapsed(&mut self, elapsed_time: Duration) {
self.elapsed_time = elapsed_time.clamp(Duration::ZERO, self.max_time);
}
#[inline]
pub fn remaining(&self) -> Duration {
self.max_time.saturating_sub(self.elapsed_time)
}
#[inline]
pub fn set_remaining(&mut self, time_remaining: Duration) {
self.elapsed_time = self
.max_time
.saturating_sub(time_remaining.clamp(Duration::ZERO, self.max_time));
}
}
impl Display for Cooldown {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?} / {:?}", self.elapsed_time, self.max_time)
}
}
#[cfg(test)]
mod tick_tests {
use super::*;
#[test]
#[should_panic]
fn zero_duration_cooldown_cannot_be_constructed() {
Cooldown::new(Duration::ZERO);
}
#[test]
fn tick_has_no_effect_on_fresh_cooldown() {
let cooldown = Cooldown::from_secs(1.);
let mut cloned_cooldown = cooldown.clone();
cloned_cooldown.tick(Duration::from_secs_f32(1.234), None);
assert_eq!(cooldown, cloned_cooldown);
}
#[test]
fn cooldowns_start_ready() {
let cooldown = Cooldown::from_secs(1.);
assert!(cooldown.ready().is_ok());
}
#[test]
fn cooldowns_are_ready_when_refreshed() {
let mut cooldown = Cooldown::from_secs(1.);
assert!(cooldown.ready().is_ok());
let _ = cooldown.trigger();
assert_eq!(cooldown.ready(), Err(CannotUseAbility::OnCooldown));
cooldown.refresh();
assert!(cooldown.ready().is_ok());
}
#[test]
fn ticking_changes_cooldown() {
let cooldown = Cooldown::new(Duration::from_millis(1000));
let mut cloned_cooldown = cooldown.clone();
let _ = cloned_cooldown.trigger();
assert_ne!(cooldown, cloned_cooldown);
cloned_cooldown.tick(Duration::from_millis(123), None);
assert_ne!(cooldown, cloned_cooldown);
}
#[test]
fn cooldowns_reset_after_being_ticked() {
let mut cooldown = Cooldown::from_secs(1.);
let _ = cooldown.trigger();
assert_eq!(cooldown.ready(), Err(CannotUseAbility::OnCooldown));
cooldown.tick(Duration::from_secs(3), None);
assert!(cooldown.ready().is_ok());
}
#[test]
fn time_remaining_on_fresh_cooldown_is_zero() {
let cooldown = Cooldown::from_secs(1.);
assert_eq!(cooldown.remaining(), Duration::ZERO);
}
}