#![deny(missing_docs)]
#![forbid(unsafe_code)]
#![warn(clippy::doc_markdown)]
#![doc = include_str!("../README.md")]
use crate::cooldown::CooldownState;
use bevy::{ecs::prelude::*, reflect::Reflect};
use charges::{ChargeState, Charges};
use cooldown::Cooldown;
use leafwing_input_manager::Actionlike;
use pool::{AbilityCosts, Pool};
use thiserror::Error;
mod ability_state;
pub mod charges;
pub mod cooldown;
pub mod plugin;
pub mod pool;
#[cfg(feature = "premade_pools")]
pub mod premade_pools;
pub mod systems;
pub use ability_state::*;
pub use leafwing_abilities_macros::Abilitylike;
pub mod prelude {
pub use crate::charges::{ChargeState, Charges};
pub use crate::cooldown::{Cooldown, CooldownState};
pub use crate::pool::{AbilityCosts, Pool, PoolBundle};
pub use crate::plugin::AbilityPlugin;
pub use crate::CannotUseAbility;
pub use crate::{AbilitiesBundle, AbilityState, Abilitylike};
}
pub trait Abilitylike: Actionlike {
fn ready<P: Pool>(
&self,
charges: &ChargeState<Self>,
cooldowns: &CooldownState<Self>,
maybe_pool: Option<&P>,
maybe_costs: Option<&AbilityCosts<Self, P>>,
) -> Result<(), CannotUseAbility> {
let charges = charges.get(self);
let cooldown = cooldowns.get(self);
ability_ready(
charges,
cooldown,
maybe_pool,
maybe_costs.and_then(|costs| costs.get(self)).copied(),
)
}
fn trigger<P: Pool>(
&self,
charges: &mut ChargeState<Self>,
cooldowns: &mut CooldownState<Self>,
maybe_pool: Option<&mut P>,
maybe_costs: Option<&AbilityCosts<Self, P>>,
) -> Result<(), CannotUseAbility> {
let charges = charges.get_mut(self);
let cooldown = cooldowns.get_mut(self);
trigger_ability(
charges,
cooldown,
maybe_pool,
maybe_costs.and_then(|costs| costs.get(self)).copied(),
)
}
fn trigger_no_costs(
&self,
charges: &mut ChargeState<Self>,
cooldowns: &mut CooldownState<Self>,
) -> Result<(), CannotUseAbility> {
self.trigger::<NullPool>(charges, cooldowns, None, None)
}
fn ready_no_costs(
&self,
charges: &ChargeState<Self>,
cooldowns: &CooldownState<Self>,
) -> Result<(), CannotUseAbility> {
self.ready::<NullPool>(charges, cooldowns, None, None)
}
}
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum CannotUseAbility {
#[error("The ability was not pressed.")]
NotPressed,
#[error("No charges available.")]
NoCharges,
#[error("Cooldown not ready.")]
OnCooldown,
#[error("Global cooldown not ready.")]
OnGlobalCooldown,
#[error("Not enough resources.")]
PoolInsufficient,
}
#[inline]
pub fn ability_ready<P: Pool>(
charges: Option<&Charges>,
cooldown: Option<&Cooldown>,
pool: Option<&P>,
cost: Option<P::Quantity>,
) -> Result<(), CannotUseAbility> {
if let Some(charges) = charges {
if charges.charges() > 0 {
Ok(())
} else {
Err(CannotUseAbility::NoCharges)
}
} else if let Some(cooldown) = cooldown {
cooldown.ready()
} else if let Some(pool) = pool {
if let Some(cost) = cost {
pool.available(cost)
} else {
Ok(())
}
} else if let Some(cost) = cost {
if cost > P::MIN {
Err(CannotUseAbility::PoolInsufficient)
} else {
Ok(())
}
} else {
Ok(())
}
}
#[inline]
pub fn trigger_ability<P: Pool>(
mut charges: Option<&mut Charges>,
mut cooldown: Option<&mut Cooldown>,
pool: Option<&mut P>,
cost: Option<P::Quantity>,
) -> Result<(), CannotUseAbility> {
ability_ready(
charges.as_deref(),
cooldown.as_deref(),
pool.as_deref(),
cost,
)?;
if let Some(ref mut charges) = charges {
charges.expend()?;
} else if let Some(ref mut cooldown) = cooldown {
cooldown.trigger()?;
}
if let Some(pool) = pool {
if let Some(cost) = cost {
let _pool_result = pool.expend(cost);
debug_assert!(_pool_result.is_ok());
}
}
Ok(())
}
#[derive(Bundle, Clone, Debug, PartialEq, Eq, Reflect)]
pub struct AbilitiesBundle<A: Abilitylike> {
pub cooldowns: CooldownState<A>,
pub charges: ChargeState<A>,
}
impl<A: Abilitylike> Default for AbilitiesBundle<A> {
fn default() -> Self {
Self {
cooldowns: CooldownState::default(),
charges: ChargeState::default(),
}
}
}
#[cfg(test)]
mod tests {
use bevy::reflect::Reflect;
use leafwing_abilities_macros::Abilitylike;
use leafwing_input_manager::Actionlike;
use crate::charges::Charges;
use crate::cooldown::Cooldown;
use crate::NullPool;
use crate::{ability_ready, trigger_ability, CannotUseAbility};
use crate as leafwing_abilities;
#[derive(Abilitylike, Actionlike, Reflect, Clone, Hash, PartialEq, Eq, Debug)]
enum TestAbility {
TestAction,
}
#[test]
fn abilitylike_works() {}
#[test]
fn ability_ready_no_cooldown_no_charges() {
assert!(ability_ready::<NullPool>(None, None, None, None).is_ok());
}
#[test]
fn ability_ready_just_cooldown() {
let mut cooldown = Some(Cooldown::from_secs(1.));
assert!(ability_ready::<NullPool>(None, cooldown.as_ref(), None, None).is_ok());
cooldown.as_mut().map(|c| c.trigger());
assert_eq!(
ability_ready::<NullPool>(None, cooldown.as_ref(), None, None),
Err(CannotUseAbility::OnCooldown)
);
}
#[test]
fn ability_ready_just_charges() {
let mut charges = Some(Charges::simple(1));
assert!(ability_ready::<NullPool>(charges.as_ref(), None, None, None).is_ok());
charges.as_mut().map(|c| c.expend());
assert_eq!(
ability_ready::<NullPool>(charges.as_ref(), None, None, None),
Err(crate::CannotUseAbility::NoCharges)
);
}
#[test]
fn ability_ready_cooldown_and_charges() {
let mut charges = Some(Charges::simple(1));
let mut cooldown = Some(Cooldown::from_secs(1.));
assert!(ability_ready::<NullPool>(charges.as_ref(), cooldown.as_ref(), None, None).is_ok());
charges.as_mut().map(|c| c.expend());
assert_eq!(
ability_ready::<NullPool>(charges.as_ref(), cooldown.as_ref(), None, None),
Err(CannotUseAbility::NoCharges)
);
if let Some(c) = charges.as_mut() {
c.replenish()
}
cooldown.as_mut().map(|c| c.trigger());
assert!(ability_ready::<NullPool>(charges.as_ref(), cooldown.as_ref(), None, None).is_ok());
charges.as_mut().map(|c| c.expend());
assert_eq!(
ability_ready::<NullPool>(charges.as_ref(), cooldown.as_ref(), None, None),
Err(CannotUseAbility::NoCharges)
);
}
#[test]
fn trigger_ability_no_cooldown_no_charges() {
let outcome = trigger_ability::<NullPool>(None, None, None, None);
assert!(outcome.is_ok());
}
#[test]
fn trigger_ability_just_cooldown() {
let mut cooldown = Some(Cooldown::from_secs(1.));
assert!(trigger_ability::<NullPool>(None, cooldown.as_mut(), None, None).is_ok());
cooldown.as_mut().map(|c| c.trigger());
assert_eq!(
trigger_ability::<NullPool>(None, cooldown.as_mut(), None, None),
Err(CannotUseAbility::OnCooldown)
);
assert_eq!(
ability_ready::<NullPool>(None, cooldown.as_ref(), None, None),
Err(CannotUseAbility::OnCooldown)
);
}
#[test]
fn trigger_ability_just_charges() {
let mut charges = Some(Charges::simple(1));
assert!(trigger_ability::<NullPool>(charges.as_mut(), None, None, None).is_ok());
charges.as_mut().map(|c| c.expend());
assert_eq!(
trigger_ability::<NullPool>(charges.as_mut(), None, None, None),
Err(CannotUseAbility::NoCharges)
);
assert_eq!(
ability_ready::<NullPool>(charges.as_ref(), None, None, None),
Err(CannotUseAbility::NoCharges)
);
}
#[test]
fn trigger_ability_cooldown_and_charges() {
let mut charges = Some(Charges::simple(1));
let mut cooldown = Some(Cooldown::from_secs(1.));
assert!(
trigger_ability::<NullPool>(charges.as_mut(), cooldown.as_mut(), None, None).is_ok()
);
assert_eq!(
ability_ready::<NullPool>(charges.as_ref(), cooldown.as_ref(), None, None),
Err(CannotUseAbility::NoCharges)
);
assert_eq!(
trigger_ability::<NullPool>(charges.as_mut(), cooldown.as_mut(), None, None),
Err(CannotUseAbility::NoCharges)
);
if let Some(c) = charges.as_mut() {
c.replenish()
}
assert!(
trigger_ability::<NullPool>(charges.as_mut(), cooldown.as_mut(), None, None).is_ok()
);
charges.as_mut().map(|c| c.expend());
if let Some(c) = cooldown.as_mut() {
c.refresh()
}
assert_eq!(
trigger_ability::<NullPool>(charges.as_mut(), cooldown.as_mut(), None, None),
Err(CannotUseAbility::NoCharges)
);
}
}