├── clippy.toml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── usability.md │ ├── code-quality.md │ ├── documentation.md │ ├── bug.md │ └── enhancement.md ├── linters │ ├── .markdown-lint.yml │ └── markdown-link-check.json └── workflows │ └── ci.yml ├── .gitignore ├── .markdownlint.json ├── tools └── ci │ ├── Cargo.toml │ └── src │ └── main.rs ├── RELEASE-CHECKLIST.md ├── macros ├── Cargo.toml └── src │ ├── lib.rs │ └── abilitylike.rs ├── .gitattributes ├── LICENSE-MIT.md ├── Cargo.toml ├── src ├── systems.rs ├── plugin.rs ├── ability_state.rs ├── premade_pools.rs ├── pool.rs ├── lib.rs ├── charges.rs └── cooldown.rs ├── CONTRIBUTING.md ├── README.md ├── RELEASES.md ├── examples └── cooldown.rs ├── tests └── cooldowns.rs ├── LICENSE-APACHE.md └── LICENSE /clippy.toml: -------------------------------------------------------------------------------- 1 | type-complexity-threshold = 5000 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: alice-i-cecile 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line-length": { 4 | "line_length": 400 5 | }, 6 | "no-duplicate-header": false 7 | } -------------------------------------------------------------------------------- /tools/ci/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ci" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | xshell = "0.2" 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/usability.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Usability Friction 3 | about: Help us polish the user experience! 4 | title: '' 5 | labels: "usability" 6 | assignees: '' 7 | --- 8 | 9 | ## Which feature is frustrating to use or confusing? 10 | 11 | The rule, interaction or feature that creates friction. 12 | 13 | ## Expectation 14 | 15 | How do you intuitively expect this to behave? 16 | -------------------------------------------------------------------------------- /.github/linters/.markdown-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ########################### 3 | ########################### 4 | ## Markdown Linter rules ## 5 | ########################### 6 | ########################### 7 | 8 | # Linter rules doc: 9 | # - https://github.com/DavidAnson/markdownlint 10 | 11 | ################# 12 | # Rules by tags # 13 | ################# 14 | no-duplicate-heading: false 15 | line-length: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/code-quality.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Code Quality 3 | about: Help us reduce tech debt and write better code! 4 | title: '' 5 | labels: "code-quality" 6 | assignees: '' 7 | --- 8 | 9 | ## Which code could be improved? 10 | 11 | Provide a direct link to the source if possible. 12 | 13 | Mention other related code if relevant. 14 | 15 | ## How should this be changed? 16 | 17 | If it's not obvious, include a snippet and explain why this change is better. 18 | -------------------------------------------------------------------------------- /.github/linters/markdown-link-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^https?://github\\.com/" 5 | } 6 | ], 7 | "replacementPatterns": [], 8 | "httpHeaders": [ 9 | { 10 | "urls": [ 11 | "https://crates.io" 12 | ], 13 | "headers": { 14 | "Accept": "text/html" 15 | } 16 | } 17 | ], 18 | "timeout": "20s", 19 | "retryOn429": true, 20 | "retryCount": 5, 21 | "fallbackRetryDelay": "30s", 22 | "aliveStatusCodes": [ 23 | 200, 24 | 206 25 | ] 26 | } -------------------------------------------------------------------------------- /RELEASE-CHECKLIST.md: -------------------------------------------------------------------------------- 1 | # LWIM Release Checklist 2 | 3 | ## Adding a new input kind 4 | 5 | 1. Ensure that `reset_inputs` for `MutableInputStreams` is resetting all relevant fields. 6 | 2. Ensure that `RawInputs` struct has fields that cover all necessary input types. 7 | 3. Ensure that `send_input` and `release_input` check all possible fields on `RawInputs`. 8 | 9 | ## Before release 10 | 11 | 1. Ensure no tests (other than ones in the README) are ignored. 12 | 2. Manually verify that all examples work. 13 | -------------------------------------------------------------------------------- /macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leafwing_abilities_macros" 3 | description = "Macros for the `leafwing_abilities` crate" 4 | version = "0.3.0" 5 | 6 | license = "MIT OR Apache-2.0" 7 | edition = "2021" 8 | authors = ["Leafwing Studios"] 9 | homepage = "https://leafwing-studios.com/" 10 | repository = "https://github.com/leafwing-studios/leafwing_abilities" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [dependencies] 18 | syn = "1.0" 19 | quote = "1.0" 20 | proc-macro2 = "1.0" 21 | proc-macro-crate = "1.1" 22 | -------------------------------------------------------------------------------- /macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Derives the [`Abilitylike`] trait 2 | // 3 | //! This derive macro was inspired by the `strum` crate's `EnumIter` macro. 4 | //! Original source: https://github.com/Peternator7/strum, 5 | //! Copyright (c) 2019 Peter Glotfelty under the MIT License 6 | 7 | extern crate proc_macro; 8 | mod abilitylike; 9 | use proc_macro::TokenStream; 10 | use syn::DeriveInput; 11 | 12 | #[proc_macro_derive(Abilitylike)] 13 | pub fn abilitylike(input: TokenStream) -> TokenStream { 14 | let ast = syn::parse_macro_input!(input as DeriveInput); 15 | 16 | crate::abilitylike::abilitylike_inner(&ast).into() 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs Improvement 3 | about: Help us write better docs to catch common issues! 4 | title: '' 5 | labels: "documentation" 6 | assignees: '' 7 | --- 8 | 9 | ## What problem did the developer encounter? 10 | 11 | Briefly describe the problem the developer encountered. Links are welcome. 12 | 13 | ## What was the fix? 14 | 15 | Describe the solution that was suggested to fix their problem or the explanation that was given. 16 | 17 | ## How could this be better documented? 18 | 19 | Point to docs that were incomplete or misleading. 20 | If you have suggestions on exactly what the new docs should say, feel free to include it here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report a bug to help us improve! 4 | title: '' 5 | labels: "bug" 6 | assignees: '' 7 | --- 8 | 9 | ## Version 10 | 11 | The release number or commit hash of the version you're using. 12 | 13 | ## Operating system & version 14 | 15 | Ex: Windows 10, Ubuntu 18.04, iOS 14. 16 | 17 | ## What you did 18 | 19 | The steps you took to uncover this bug. 20 | Please list full reproduction steps if feasible. 21 | 22 | ## What you expected to happen 23 | 24 | What you think should've happened if everything was working properly. 25 | 26 | ## What actually happened 27 | 28 | The actual result of the actions you described. 29 | 30 | ## Additional information 31 | 32 | Any additional information you would like to add such as screenshots, logs, etc. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Propose a new feature! 4 | title: '' 5 | labels: "enhancement" 6 | assignees: '' 7 | --- 8 | 9 | ## What problem does this solve? 10 | 11 | What do you want to do? 12 | 13 | ## What solution would you like? 14 | 15 | Lay out what an ideal solution would look like, from a user-facing perspective. 16 | 17 | ## [Optional] How could this be implemented? 18 | 19 | Briefly sketch out how this could be put in place. 20 | 21 | ## [Optional] What alternatives have you considered? 22 | 23 | Mention other possible solutions to solve the problem presented. If you have strong opinions, explain why they wouldn't be good enough. 24 | 25 | If workarounds exist, briefly describe them and explain why they're inadequate. 26 | 27 | ## Related work 28 | 29 | Explain how any other related features or bugs tie into this feature request. 30 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.png filter=lfs diff=lfs merge=lfs -text 2 | *.svg filter=lfs diff=lfs merge=lfs -text 3 | *.jpg filter=lfs diff=lfs merge=lfs -text 4 | *.gif filter=lfs diff=lfs merge=lfs -text 5 | *.mpeg filter=lfs diff=lfs merge=lfs -text 6 | *.zip filter=lfs diff=lfs merge=lfs -text 7 | *.7z filter=lfs diff=lfs merge=lfs -text 8 | *.avi filter=lfs diff=lfs merge=lfs -text 9 | *.bmp filter=lfs diff=lfs merge=lfs -text 10 | *.mp3 filter=lfs diff=lfs merge=lfs -text 11 | *.wav filter=lfs diff=lfs merge=lfs -text 12 | *.png filter=lfs diff=lfs merge=lfs -text 13 | *.tar filter=lfs diff=lfs merge=lfs -text 14 | *.gltf filter=lfs diff=lfs merge=lfs -text 15 | *.ttf filter=lfs diff=lfs merge=lfs -text 16 | *.otf filter=lfs diff=lfs merge=lfs -text 17 | *.ods filter=lfs diff=lfs merge=lfs -text 18 | *.ogg filter=lfs diff=lfs merge=lfs -text 19 | *.psd filter=lfs diff=lfs merge=lfs -text 20 | *.max filter=lfs diff=lfs merge=lfs -text 21 | *.blend filter=lfs diff=lfs merge=lfs -text 22 | *.fbx filter=lfs diff=lfs merge=lfs -text 23 | *.dae filter=lfs diff=lfs merge=lfs -text 24 | *.obj filter=lfs diff=lfs merge=lfs -text 25 | -------------------------------------------------------------------------------- /LICENSE-MIT.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021 Leafwing Studios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "leafwing_abilities" 3 | description = "A convenient, well-tested ability management suite. Built for the Bevy game engine." 4 | version = "0.11.0" 5 | authors = ["Leafwing Studios"] 6 | homepage = "https://leafwing-studios.com/" 7 | repository = "https://github.com/leafwing-studios/leafwing_abilities" 8 | license = "MIT OR Apache-2.0" 9 | edition = "2021" 10 | categories = ["games", "game-development"] 11 | keywords = ["bevy"] 12 | exclude = ["assets/**/*", "tools/**/*", ".github/**/*"] 13 | 14 | [profile.dev] 15 | opt-level = 3 16 | 17 | [workspace] 18 | members = ["./", "tools/ci", "macros"] 19 | 20 | [features] 21 | default = ["premade_pools"] 22 | # Premade life and mana resource pools to get you started 23 | premade_pools = [] 24 | 25 | [dependencies] 26 | bevy = { version = "0.16", default-features = false, features = [ 27 | "serialize", 28 | "bevy_gilrs", 29 | ] } 30 | serde = { version = "1.0", features = ["derive"] } 31 | leafwing-input-manager = { version = "0.17", default-features = false } 32 | 33 | leafwing_abilities_macros = { path = "macros", version = "0.3" } 34 | thiserror = "1.0.37" 35 | derive_more = "0.99.17" 36 | 37 | [dev-dependencies] 38 | bevy = { version = "0.16", default-features = true, features = [ 39 | "default_font", 40 | ] } 41 | # Needed to provide implementations for standard input devices 42 | leafwing-input-manager = { version = "0.17", default-features = true } 43 | -------------------------------------------------------------------------------- /tools/ci/src/main.rs: -------------------------------------------------------------------------------- 1 | use xshell::{cmd, Shell}; 2 | 3 | fn main() { 4 | // When run locally, results may differ from actual CI runs triggered by 5 | // .github/workflows/ci.yml 6 | // - Official CI runs latest stable 7 | // - Local runs use whatever the default Rust is locally 8 | 9 | let sh = Shell::new().unwrap(); 10 | 11 | // See if any code needs to be formatted 12 | cmd!(sh, "cargo fmt --all -- --check") 13 | .run() 14 | .expect("Please run `cargo fmt --all` to format your code."); 15 | 16 | // See if clippy has any complaints. 17 | // - Type complexity must be ignored because we use huge templates for queries 18 | cmd!( 19 | sh, 20 | "cargo clippy --workspace --all-features -- -D warnings -A clippy::type_complexity" 21 | ) 22 | .run() 23 | .expect("Please fix `cargo clippy` errors with all features enabled."); 24 | 25 | // Check for errors with no features enabled 26 | cmd!(sh, "cargo check --workspace --no-default-features") 27 | .run() 28 | .expect("Please fix `cargo check` errors with no features enabled ."); 29 | 30 | // Check for errors with default features enabled 31 | cmd!(sh, "cargo check --workspace") 32 | .run() 33 | .expect("Please fix `cargo check` errors with default features enabled."); 34 | 35 | // Check the examples with clippy 36 | cmd!( 37 | sh, 38 | "cargo clippy --examples -- -D warnings -A clippy::type_complexity" 39 | ) 40 | .run() 41 | .expect("Please fix `cargo clippy` errors for the examples."); 42 | } 43 | -------------------------------------------------------------------------------- /macros/src/abilitylike.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use proc_macro2::TokenStream; 3 | use proc_macro_crate::{crate_name, FoundCrate}; 4 | use quote::quote; 5 | use syn::{DeriveInput, Ident}; 6 | 7 | /// This approach and implementation is inspired by the `strum` crate, 8 | /// Copyright (c) 2019 Peter Glotfelty 9 | /// available under the MIT License at https://github.com/Peternator7/strum 10 | pub(crate) fn abilitylike_inner(ast: &DeriveInput) -> TokenStream { 11 | // Splitting the abstract syntax tree 12 | let enum_name = &ast.ident; 13 | let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); 14 | 15 | let crate_path = if let Ok(found_crate) = crate_name("leafwing_abilities") { 16 | // The crate was found in the Cargo.toml 17 | match found_crate { 18 | FoundCrate::Itself => quote!(leafwing_abilities), 19 | FoundCrate::Name(name) => { 20 | let ident = Ident::new(&name, Span::call_site()); 21 | quote!(#ident) 22 | } 23 | } 24 | } else { 25 | // The crate was not found in the Cargo.toml, 26 | // so we assume that we are in the owning_crate itself 27 | // 28 | // In order for this to play nicely with unit tests within the crate itself, 29 | // `use crate as leafwing_input_manager` at the top of each test module 30 | // 31 | // Note that doc tests, integration tests and examples want the full standard import, 32 | // as they are evaluated as if they were external 33 | quote!(leafwing_abilities) 34 | }; 35 | 36 | quote! { 37 | impl #impl_generics #crate_path::Abilitylike for #enum_name #type_generics #where_clause {} 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/systems.rs: -------------------------------------------------------------------------------- 1 | //! The systems that power each [`InputManagerPlugin`](crate::plugin::InputManagerPlugin). 2 | 3 | use crate::pool::RegeneratingPool; 4 | use crate::{charges::ChargeState, cooldown::CooldownState, Abilitylike}; 5 | 6 | use bevy::ecs::prelude::*; 7 | use bevy::time::Time; 8 | 9 | /// Advances all [`CooldownState`] components and resources for ability type `A`. 10 | pub fn tick_cooldowns( 11 | mut query: Query< 12 | (Option<&mut CooldownState>, Option<&mut ChargeState>), 13 | Or<(With>, With>)>, 14 | >, 15 | cooldowns_res: Option>>, 16 | charges_res: Option>>, 17 | time: Res, 41 | } 42 | 43 | /// System sets provided by [`leafwing_abilities`](crate). 44 | #[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] 45 | pub enum AbilitySystem { 46 | /// Updates the cooldowns of all abilities, 47 | /// including their charges and global cooldowns if applicable. 48 | TickCooldowns, 49 | } 50 | 51 | // Deriving default induces an undesired bound on the generic 52 | impl Default for AbilityPlugin { 53 | fn default() -> Self { 54 | Self { 55 | _phantom: PhantomData, 56 | } 57 | } 58 | } 59 | 60 | impl Plugin for AbilityPlugin { 61 | fn build(&self, app: &mut App) { 62 | use crate::systems::*; 63 | 64 | // Systems 65 | app.add_systems( 66 | PreUpdate, 67 | tick_cooldowns:: 68 | .in_set(AbilitySystem::TickCooldowns) 69 | .in_set(InputManagerSystem::Tick) 70 | .before(InputManagerSystem::Update), 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | A fully-featured set of tools for managing abilities in [Bevy](https://bevyengine.org/). 4 | This crate is meant to be used with [Leafwing Input Manager](https://github.com/leafwing-studios/leafwing-input-manager), which converts inputs into actions. 5 | 6 | Some of those actions will be abilities! 7 | Abilities are intended for gameplay use, and follow complex but relatively standardized logic about how they might be used. 8 | 9 | ```rust 10 | use bevy::prelude::*; 11 | use bevy::reflect::Reflect; 12 | use leafwing_abilities::prelude::*; 13 | use leafwing_abilities::premade_pools::mana::{ManaPool, Mana}; 14 | use leafwing_abilities::premade_pools::life::{LifePool, Life}; 15 | use leafwing_input_manager::prelude::*; 16 | 17 | // We're modelling https://leagueoflegends.fandom.com/wiki/Zyra/LoL 18 | // to show off this crate's features! 19 | #[derive(Actionlike, Abilitylike, Debug, Clone, Copy, Hash, PartialEq, Eq, Reflect)] 20 | pub enum ZyraAbility { 21 | GardenOfThorns, 22 | DeadlySpines, 23 | RampantGrowth, 24 | GraspingRoots, 25 | Stranglethorns, 26 | } 27 | 28 | impl ZyraAbility { 29 | /// You could use the `strum` crate to derive this automatically! 30 | fn variants() -> Vec { 31 | use ZyraAbility::*; 32 | vec![GardenOfThorns, DeadlySpines, RampantGrowth, GraspingRoots, Stranglethorns] 33 | } 34 | 35 | fn input_map() -> InputMap { 36 | use ZyraAbility::*; 37 | 38 | // We can use this `new` idiom, which accepts an iterator of pairs 39 | InputMap::new([ 40 | (DeadlySpines, KeyCode::KeyQ), 41 | (RampantGrowth, KeyCode::KeyW), 42 | (GraspingRoots, KeyCode::KeyE), 43 | (Stranglethorns, KeyCode::KeyR), 44 | ]) 45 | } 46 | 47 | // This match pattern is super useful to be sure you've defined an attribute for every variant 48 | fn cooldown(&self) -> Cooldown { 49 | use ZyraAbility::*; 50 | 51 | let seconds: f32 = match *self { 52 | GardenOfThorns => 13.0, 53 | DeadlySpines => 7.0, 54 | RampantGrowth => 18.0, 55 | GraspingRoots => 12.0, 56 | Stranglethorns => 110.0, 57 | }; 58 | 59 | Cooldown::from_secs(seconds) 60 | } 61 | 62 | fn cooldowns() -> CooldownState { 63 | let mut cooldowns = CooldownState::default(); 64 | 65 | // Now, we can loop over all the variants to populate our struct 66 | for ability in ZyraAbility::variants() { 67 | cooldowns.set(ability, ability.cooldown()); 68 | } 69 | 70 | cooldowns 71 | } 72 | 73 | fn charges() -> ChargeState { 74 | // The builder API can be very convenient when you only need to set a couple of values 75 | ChargeState::default() 76 | .set(ZyraAbility::RampantGrowth, Charges::replenish_one(2)) 77 | .build() 78 | } 79 | 80 | fn mana_costs() -> AbilityCosts { 81 | use ZyraAbility::*; 82 | AbilityCosts::new([ 83 | (DeadlySpines, Mana(70.)), 84 | (GraspingRoots, Mana(70.)), 85 | (Stranglethorns, Mana(100.)), 86 | ]) 87 | } 88 | } 89 | 90 | /// Marker component for this champion 91 | #[derive(Component)] 92 | struct Zyra; 93 | 94 | #[derive(Bundle)] 95 | struct ZyraBundle { 96 | champion: Zyra, 97 | life_pool: LifePool, 98 | input_manager_bundle: InputManagerBundle, 99 | abilities_bundle: AbilitiesBundle, 100 | mana_bundle: PoolBundle, 101 | } 102 | 103 | impl Default for ZyraBundle { 104 | fn default() -> Self { 105 | ZyraBundle { 106 | champion: Zyra, 107 | // Max life, then regen 108 | life_pool: LifePool::new(Life(574.), Life(574.), (Life(5.5))), 109 | input_manager_bundle: InputManagerBundle:: { 110 | input_map: ZyraAbility::input_map(), 111 | ..default() 112 | }, 113 | abilities_bundle: AbilitiesBundle:: { 114 | cooldowns: ZyraAbility::cooldowns(), 115 | charges: ZyraAbility::charges(), 116 | }, 117 | mana_bundle: PoolBundle:: { 118 | pool: ManaPool::new(Mana(418.), Mana(418.), Mana(13.0)), 119 | ability_costs: ZyraAbility::mana_costs(), 120 | } 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | ## Features 127 | 128 | - track and automatically tick cooldowns 129 | - store multiple charges of abilities 130 | - Leafwing Studio's trademark `#[deny(missing_docs)]` 131 | 132 | Planned: 133 | 134 | - resource management (health, mana, energy etc) 135 | - damage 136 | - cast times 137 | - range checking 138 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## Version 0.11 4 | 5 | ## Dependencies (0.11) 6 | 7 | - now support `bevy` 0.16 and `leafwing-input-manager` 0.17 8 | 9 | ## Bugs (0.11) 10 | 11 | - Fixed `CooldownState::trigger` triggering an ability cooldown when the global cooldown was not ready. 12 | 13 | ### Usability (0.11) 14 | 15 | - `AbilityState`, `CooldownState`, and `ChargeState` now take a reference to an `AbilityLike` where possible. 16 | - Added an `OnGlobalCooldown` error variant to indicate whether `CooldownState::ready` or `CooldownState::trigger` failed due to the global cooldown or the abilities cooldown. 17 | - the `Pool` trait now requires the `Component` trait to simplify trait bounds, especially with the addition of immutable components 18 | 19 | ## Version 0.10 20 | 21 | ## Dependencies (0.10) 22 | 23 | - now support `bevy` 0.15 and `leafwing-input-manager` 0.16 24 | 25 | ## Version 0.9 26 | 27 | ## Dependencies (0.9) 28 | 29 | - now support `leafwing-input-manager` 0.15 30 | 31 | ## Usability (0.9) 32 | 33 | - all types provided by this library are now `Reflect` 34 | - removed `ToggleActions`: this functionality no longer makes sense with changes to how LWIM disables actions. Use run conditions directly on the new `AbilitySystem::TickCooldowns` system set 35 | - the associated type `Pool::Quantity` no longer needs to be able to be multiplied and divided by f32s to ease working with integer-based resource pools 36 | - in exchange, the `RegeneratingPool::regenerate` method no longer has a default implementation 37 | - to better support working with multiple resource pools for a single `Abilitylike`: 38 | - `ready_no_cost` and `trigger_no_cost` have been added to `Abilitylike` 39 | - when working with multiple resource pools, you should pass in `NullPool` as the type argument for `AbilityState` 40 | - `Default` is now implemented for `Charges` and `Cooldown` 41 | - added `Display` implementations for `Charges` and `Cooldown` 42 | 43 | ## Bugs (0.9) 44 | 45 | - `Actionlike::trigger` and friends now expends resource costs correctly 46 | - if you were working around this bug, remember to remove your workaround to avoid double-spending! 47 | 48 | ## Version 0.8 49 | 50 | - now supports Bevy 0.14 51 | 52 | ## Version 0.7 53 | 54 | ### Dependencies (0.7) 55 | 56 | - now supports Bevy 0.13 57 | 58 | ## Version 0.6 59 | 60 | ### Dependencies (0.6) 61 | 62 | - now supports Bevy 0.12 63 | 64 | ### Documentation (0.6) 65 | 66 | - fixed several typos (`@striezel`) 67 | - improved the documentation for `Pool::replenish` 68 | 69 | ### Usability (0.6) 70 | 71 | - removed the required `new` method from the `Pool` trait: this method was overly restrictive, and prevented the construction of more complex pools with custom initialization parameters 72 | - `LifePool::new` and `ManaPool::new` methods have been added to the premade pools: do similarly for your own `Pool` types 73 | - the `Pool::ZERO` associated constant has been renamed to the clearer `Pool::MIN`. 74 | - the `MaxPoolLessThanZero` error type has been renamed to `MaxPoolLessThanMin` to match. 75 | - the `Pool` trait has been split in two, with the regeneration-specific mechanics handled in `RegeneratingPool`, to make the construction of non-regenerating pools much more intuitive 76 | - added the `Pool::is_empty` and `Pool::is_full` helper methods to the `Pool` trait 77 | - added `Add`, `Sub`, `AddAssign` and `SubAssign` implementations to the premade `Life` and `Mana` types and their corresponding pools 78 | - added the `Display` trait to `Life`, `Mana`, `LifePool` and `ManaPool` 79 | - removed the useless `AbilityPlugin::server()` plugin creation method 80 | 81 | ## Version 0.5 82 | 83 | ### Dependencies (0.5) 84 | 85 | - now supports Bevy 0.11 86 | 87 | ## Version 0.4 88 | 89 | ### Dependencies (0.4) 90 | 91 | - now supports Bevy 0.10 92 | 93 | ### Usability (0.4) 94 | 95 | - the premade `LifePool` and `ManaPool` types now implement the `Resource` trait. 96 | - the premade `Life` and `Mana` types now implement `Mul for f32`, allowing you to have commutative multiplication 97 | 98 | ## Version 0.3 99 | 100 | ### Dependencies (0.3) 101 | 102 | - now supports Bevy 0.9 103 | 104 | ## Version 0.2 105 | 106 | ### Enhancements (0.2) 107 | 108 | - You can now store and check resource pools (like life, mana or energy) with the `Pool` trait! 109 | - All of the corresponding ability methods and `AbilityState` have been changed to account for this. 110 | - Pools have a zero value, a max and a regeneration rate, and are used to track the resource pools of specific actors. 111 | - The `Pool` trait has a `Quantity` associated type: this might be used to track the amount stored in a `Pool`, the amount of damage dealt, the life regeneration rate or the mana cost of each ability. 112 | - For example, you can add `PoolBundle` to your entity to track both the `ManaPool` and the `AbilityCosts`. 113 | - We've included a `LifePool` and `ManaPool` type to get you started; feel free to copy-and-paste to adapt them to your needs. 114 | 115 | ### Usability (0.2) 116 | 117 | - All methods and functions that returned a bool now return a `Result<(), CannotUseAbility>` which explains why an action failed. 118 | - the `trigger_action` and `action_ready` functions were renamed to `trigger_ability` and `ability_ready` 119 | 120 | ## Version 0.1 121 | 122 | ### Enhancements (0.1) 123 | 124 | - You can now store `Cooldowns` and `ActionCharges` on a per-action basis. 125 | - These new components are now included in the `InputManagerBundle`. 126 | - Like always, you can choose to use them as a resource instead. 127 | - Set cooldowns for actions using `CooldownState::set(action, cooldown)` or `CooldownState::new`. 128 | - Use `Actionlike::ready` with `Actionlike::trigger` as part of your action evaluation! 129 | - Cooldowns advance whenever `CooldownState::tick` is called (this will happen automatically if you add the plugin). 130 | - The exact strategy for how charges work for each action can be controlled by the `ReplenishStrategy` and `CooldownStrategy` enums. 131 | -------------------------------------------------------------------------------- /examples/cooldown.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to store (and use) per-action cooldowns 2 | //! 3 | //! This example shows off a tiny cookie clicker! 4 | use bevy::{prelude::*, reflect::Reflect}; 5 | use leafwing_abilities::prelude::*; 6 | use leafwing_input_manager::{plugin::InputManagerSystem, prelude::*}; 7 | 8 | use bevy::color::palettes::css::*; 9 | 10 | fn main() { 11 | App::new() 12 | .add_plugins(DefaultPlugins) 13 | .add_plugins(InputManagerPlugin::::default()) 14 | .add_plugins(AbilityPlugin::::default()) 15 | .add_systems(Startup, (spawn_cookie, spawn_camera, spawn_score_text)) 16 | .init_resource::() 17 | // We're manually calling ActionState::press, so we have to get the timing right so just_pressed isn't overridden 18 | .add_systems(PreUpdate, cookie_clicked.after(InputManagerSystem::Update)) 19 | .add_systems( 20 | Update, 21 | ( 22 | handle_add_one_ability, 23 | handle_double_cookies_ability, 24 | change_cookie_color_when_clicked.before(handle_add_one_ability), 25 | ), 26 | ) 27 | // Reset the cookie's color when clicked after a single frame 28 | // Rendering happens after CoreStage::Update, so this should do the trick 29 | .add_systems(PreUpdate, reset_cookie_color) 30 | // Only the freshest scores here 31 | .add_systems(PostUpdate, display_score) 32 | .run(); 33 | } 34 | 35 | #[derive(Actionlike, Reflect, Abilitylike, Clone, Copy, PartialEq, Debug, Default, Hash, Eq)] 36 | enum CookieAbility { 37 | #[default] 38 | AddOne, 39 | DoubleCookies, 40 | } 41 | 42 | impl CookieAbility { 43 | /// You could use the `strum` crate to derive this automatically! 44 | fn variants() -> impl Iterator { 45 | use CookieAbility::*; 46 | [AddOne, DoubleCookies].iter().copied() 47 | } 48 | 49 | fn cooldown(&self) -> Cooldown { 50 | match self { 51 | CookieAbility::AddOne => Cooldown::from_secs(0.1), 52 | CookieAbility::DoubleCookies => Cooldown::from_secs(5.0), 53 | } 54 | } 55 | 56 | fn cooldowns() -> CooldownState { 57 | let mut cooldowns = CooldownState::default(); 58 | for ability in CookieAbility::variants() { 59 | cooldowns.set(ability, ability.cooldown()); 60 | } 61 | cooldowns 62 | } 63 | 64 | fn key_bindings() -> InputMap { 65 | // CookieAbility::AddOne is pressed manually when the cookie is clicked on 66 | InputMap::default().with(CookieAbility::DoubleCookies, KeyCode::Space) 67 | } 68 | } 69 | 70 | /// Marker component for our clickable cookies 71 | #[derive(Component, Debug, Clone, Copy, PartialEq)] 72 | struct Cookie; 73 | 74 | #[derive(Bundle)] 75 | struct CookieBundle { 76 | cookie: Cookie, 77 | node: Node, 78 | background_color: BackgroundColor, 79 | abilities_bundle: AbilitiesBundle, 80 | input_map: InputMap, 81 | } 82 | 83 | impl CookieBundle { 84 | const COOKIE_CLICKED_COLOR: Srgba = BEIGE; 85 | const COOKIE_COLOR: Srgba = BROWN; 86 | 87 | /// Creates a Cookie bundle with a random position. 88 | fn new() -> CookieBundle { 89 | CookieBundle { 90 | cookie: Cookie, 91 | node: Node { 92 | height: Val::Px(100.), 93 | width: Val::Px(100.), 94 | ..Default::default() 95 | }, 96 | background_color: BackgroundColor(Self::COOKIE_COLOR.into()), 97 | abilities_bundle: AbilitiesBundle { 98 | cooldowns: CookieAbility::cooldowns(), 99 | ..default() 100 | }, 101 | input_map: CookieAbility::key_bindings(), 102 | } 103 | } 104 | } 105 | 106 | fn spawn_cookie(mut commands: Commands) { 107 | commands.spawn(CookieBundle::new()); 108 | } 109 | 110 | fn spawn_camera(mut commands: Commands) { 111 | commands.spawn(Camera2d); 112 | } 113 | 114 | // We need a huge amount of space to be able to let you play this game for long enough ;) 115 | #[derive(Resource, Default)] 116 | struct Score(u128); 117 | 118 | fn cookie_clicked(mut query: Query<(&Interaction, &mut ActionState)>) -> Result { 119 | let (cookie_interaction, mut cookie_action_state) = query.single_mut()?; 120 | // This indirection is silly here, but works well in larger games 121 | // by allowing you to hook into the ability state. 122 | if *cookie_interaction == Interaction::Pressed { 123 | cookie_action_state.press(&CookieAbility::AddOne); 124 | } 125 | 126 | Ok(()) 127 | } 128 | 129 | fn handle_add_one_ability( 130 | mut query: Query<( 131 | &ActionState, 132 | &mut CooldownState, 133 | )>, 134 | mut score: ResMut, 135 | ) -> Result { 136 | let (actions, mut cooldowns) = query.single_mut()?; 137 | // See the handle_double_cookies system for a more ergonomic, robust (and implicit) way to handle this pattern 138 | if actions.just_pressed(&CookieAbility::AddOne) { 139 | // Calling .trigger checks if the cooldown can be used, then triggers it if so 140 | // Note that this may miss other important limitations on when abilities can be used 141 | if cooldowns.trigger(&CookieAbility::AddOne).is_ok() { 142 | // The result returned should be checked to decide how to respond 143 | score.0 += 1; 144 | } 145 | } 146 | 147 | Ok(()) 148 | } 149 | 150 | fn handle_double_cookies_ability( 151 | mut query: Query>, 152 | mut score: ResMut, 153 | ) -> Result { 154 | let mut cookie_ability_state = query.single_mut()?; 155 | // Checks whether the action is pressed, and if it is ready. 156 | // If so, triggers the ability, resetting its cooldown. 157 | if cookie_ability_state 158 | .trigger_if_just_pressed(&CookieAbility::DoubleCookies) 159 | .is_ok() 160 | { 161 | score.0 *= 2; 162 | } 163 | 164 | Ok(()) 165 | } 166 | 167 | fn change_cookie_color_when_clicked( 168 | mut query: Query<(&mut BackgroundColor, AbilityState)>, 169 | ) -> Result { 170 | let (mut color, ability_state) = query.single_mut()?; 171 | if ability_state 172 | .ready_and_just_pressed(&CookieAbility::AddOne) 173 | .is_ok() 174 | { 175 | *color = CookieBundle::COOKIE_CLICKED_COLOR.into(); 176 | } 177 | 178 | Ok(()) 179 | } 180 | 181 | /// Resets the cookie's color after a frame 182 | fn reset_cookie_color(mut query: Query<&mut BackgroundColor, With>) -> Result { 183 | let mut color = query.single_mut()?; 184 | *color = CookieBundle::COOKIE_COLOR.into(); 185 | Ok(()) 186 | } 187 | 188 | #[derive(Component)] 189 | struct ScoreText; 190 | 191 | fn spawn_score_text(mut commands: Commands) { 192 | commands.spawn(Text::new("Score")).insert(ScoreText); 193 | } 194 | 195 | fn display_score(score: Res, mut text: Single<&mut Text, With>) { 196 | let score = score.0; 197 | **text = Text::new(format!("Score: {}", score)); 198 | } 199 | -------------------------------------------------------------------------------- /tests/cooldowns.rs: -------------------------------------------------------------------------------- 1 | // BLOCKED: these tests should set the time manually. 2 | // Requires https://github.com/bevyengine/bevy/issues/6146 to do so. 3 | 4 | use bevy::input::InputPlugin; 5 | use bevy::prelude::*; 6 | use core::time::Duration; 7 | use leafwing_abilities::prelude::*; 8 | use leafwing_input_manager::prelude::*; 9 | 10 | use std::thread::sleep; 11 | 12 | #[derive(Actionlike, Reflect, Abilitylike, Debug, Clone, Copy, Hash, PartialEq, Eq)] 13 | enum Action { 14 | NoCooldown, 15 | Short, 16 | Long, 17 | } 18 | 19 | impl Action { 20 | /// You could use the `strum` crate to derive this automatically! 21 | fn variants() -> impl Iterator { 22 | use Action::*; 23 | [NoCooldown, Short, Long].iter().copied() 24 | } 25 | 26 | fn cooldown(&self) -> Option { 27 | match self { 28 | Action::NoCooldown => None, 29 | Action::Short => Some(Cooldown::from_secs(0.1)), 30 | Action::Long => Some(Cooldown::from_secs(1.)), 31 | } 32 | } 33 | 34 | fn cooldowns() -> CooldownState { 35 | let mut cd = CooldownState::default(); 36 | for action in Action::variants() { 37 | if let Some(cooldown) = action.cooldown() { 38 | cd.set(action, cooldown); 39 | } 40 | } 41 | 42 | cd 43 | } 44 | } 45 | 46 | fn spawn(mut commands: Commands) { 47 | commands.spawn(AbilitiesBundle { 48 | cooldowns: Action::cooldowns(), 49 | ..default() 50 | }); 51 | } 52 | 53 | #[test] 54 | fn cooldowns_on_entity() { 55 | use Action::*; 56 | 57 | let mut app = App::new(); 58 | app.add_plugins(AbilityPlugin::::default()) 59 | .add_plugins(MinimalPlugins) 60 | .add_plugins(InputPlugin) 61 | .add_systems(Startup, spawn); 62 | 63 | // Spawn entities 64 | app.update(); 65 | 66 | // Cooldown start ready 67 | let mut query_state = app.world_mut().query::<&mut CooldownState>(); 68 | let mut cooldowns: Mut> = 69 | query_state.single_mut(app.world_mut()).unwrap(); 70 | for action in Action::variants() { 71 | assert!(cooldowns.ready(&action).is_ok()); 72 | // Trigger all the cooldowns once 73 | let _ = cooldowns.trigger(&action); 74 | } 75 | 76 | app.update(); 77 | 78 | // No waiting 79 | let mut query_state = app.world_mut().query::<&CooldownState>(); 80 | let cooldowns: &CooldownState = query_state.single(app.world()).unwrap(); 81 | assert!(cooldowns.ready(&NoCooldown).is_ok()); 82 | assert_eq!(cooldowns.ready(&Short), Err(CannotUseAbility::OnCooldown)); 83 | assert_eq!(cooldowns.ready(&Long), Err(CannotUseAbility::OnCooldown)); 84 | 85 | sleep(Duration::from_secs_f32(0.2)); 86 | app.update(); 87 | 88 | // Short wait 89 | let mut query_state = app.world_mut().query::<&CooldownState>(); 90 | let cooldowns: &CooldownState = query_state.single(&app.world()).unwrap(); 91 | assert!(cooldowns.ready(&NoCooldown).is_ok()); 92 | assert!(cooldowns.ready(&Short).is_ok()); 93 | assert_eq!(cooldowns.ready(&Long), Err(CannotUseAbility::OnCooldown)); 94 | } 95 | 96 | #[test] 97 | fn cooldowns_in_resource() { 98 | use Action::*; 99 | 100 | let mut app = App::new(); 101 | app.add_plugins(AbilityPlugin::::default()) 102 | .add_plugins(MinimalPlugins) 103 | .add_plugins(InputPlugin) 104 | .insert_resource(Action::cooldowns()); 105 | 106 | // Cooldown start ready 107 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 108 | for action in Action::variants() { 109 | assert!(cooldowns.ready(&action).is_ok()); 110 | let _ = cooldowns.trigger(&action); 111 | } 112 | 113 | app.update(); 114 | 115 | // No waiting 116 | let cooldowns: &CooldownState = app.world().resource(); 117 | assert!(cooldowns.ready(&NoCooldown).is_ok()); 118 | assert_eq!(cooldowns.ready(&Short), Err(CannotUseAbility::OnCooldown)); 119 | assert_eq!(cooldowns.ready(&Long), Err(CannotUseAbility::OnCooldown)); 120 | 121 | sleep(Duration::from_secs_f32(0.2)); 122 | app.update(); 123 | 124 | // Short wait 125 | let cooldowns: &CooldownState = app.world().resource(); 126 | assert!(cooldowns.ready(&NoCooldown).is_ok()); 127 | assert!(cooldowns.ready(&Short).is_ok()); 128 | assert_eq!(cooldowns.ready(&Long), Err(CannotUseAbility::OnCooldown)); 129 | } 130 | 131 | #[test] 132 | fn global_cooldowns_tick() { 133 | let mut app = App::new(); 134 | app.add_plugins(AbilityPlugin::::default()) 135 | .add_plugins(MinimalPlugins) 136 | .add_plugins(InputPlugin) 137 | .insert_resource(Action::cooldowns()); 138 | 139 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 140 | let initial_gcd = Some(Cooldown::new(Duration::from_micros(15))); 141 | cooldowns.global_cooldown = initial_gcd.clone(); 142 | // Trigger the GCD 143 | let _ = cooldowns.trigger(&Action::Long); 144 | 145 | app.update(); 146 | 147 | let cooldowns: &CooldownState = app.world().resource(); 148 | assert_ne!(initial_gcd, cooldowns.global_cooldown); 149 | } 150 | 151 | #[test] 152 | fn global_cooldown_blocks_cooldownless_actions() { 153 | let mut app = App::new(); 154 | app.add_plugins(AbilityPlugin::::default()) 155 | .add_plugins(MinimalPlugins) 156 | .add_plugins(InputPlugin) 157 | .insert_resource(Action::cooldowns()); 158 | 159 | // First delta time provided of each app is wonky 160 | app.update(); 161 | 162 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 163 | cooldowns.global_cooldown = Some(Cooldown::new(Duration::from_micros(15))); 164 | 165 | assert!(cooldowns.ready(&Action::NoCooldown).is_ok()); 166 | 167 | let _ = cooldowns.trigger(&Action::NoCooldown); 168 | assert_eq!( 169 | cooldowns.ready(&Action::NoCooldown), 170 | Err(CannotUseAbility::OnGlobalCooldown) 171 | ); 172 | 173 | sleep(Duration::from_micros(30)); 174 | app.update(); 175 | 176 | let cooldowns: &CooldownState = app.world().resource(); 177 | assert!(cooldowns.ready(&Action::NoCooldown).is_ok()); 178 | } 179 | 180 | #[test] 181 | fn global_cooldown_affects_other_actions() { 182 | let mut app = App::new(); 183 | app.add_plugins(( 184 | MinimalPlugins, 185 | InputPlugin, 186 | AbilityPlugin::::default(), 187 | )) 188 | .insert_resource(Action::cooldowns()); 189 | 190 | // First delta time provided of each app is wonky 191 | app.update(); 192 | 193 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 194 | cooldowns.global_cooldown = Some(Cooldown::new(Duration::from_micros(15))); 195 | let _ = cooldowns.trigger(&Action::Long); 196 | assert_eq!( 197 | cooldowns.ready(&Action::Short), 198 | Err(CannotUseAbility::OnGlobalCooldown) 199 | ); 200 | assert_eq!( 201 | cooldowns.ready(&Action::Long), 202 | Err(CannotUseAbility::OnCooldown) 203 | ); 204 | 205 | sleep(Duration::from_micros(30)); 206 | app.update(); 207 | 208 | let cooldowns: &CooldownState = app.world().resource(); 209 | assert!(cooldowns.ready(&Action::Short).is_ok()); 210 | assert_eq!( 211 | cooldowns.ready(&Action::Long), 212 | Err(CannotUseAbility::OnCooldown) 213 | ); 214 | } 215 | 216 | #[test] 217 | fn global_cooldown_overrides_short_cooldowns() { 218 | let mut app = App::new(); 219 | app.add_plugins(( 220 | MinimalPlugins, 221 | AbilityPlugin::::default(), 222 | InputPlugin, 223 | )) 224 | .insert_resource(Action::cooldowns()); 225 | 226 | // First delta time provided of each app is wonky 227 | app.update(); 228 | 229 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 230 | cooldowns.global_cooldown = Some(Cooldown::from_secs(0.5)); 231 | let _ = cooldowns.trigger(&Action::Short); 232 | assert_eq!( 233 | cooldowns.ready(&Action::Short), 234 | Err(CannotUseAbility::OnCooldown) 235 | ); 236 | 237 | // Let per-action cooldown elapse 238 | sleep(Duration::from_millis(250)); 239 | app.update(); 240 | 241 | let cooldowns: &CooldownState = app.world().resource(); 242 | assert_eq!( 243 | cooldowns.ready(&Action::Short), 244 | Err(CannotUseAbility::OnGlobalCooldown) 245 | ); 246 | 247 | // Wait for full GCD to expire 248 | sleep(Duration::from_millis(250)); 249 | app.update(); 250 | 251 | let cooldowns: &CooldownState = app.world().resource(); 252 | assert!(cooldowns.ready(&Action::Short).is_ok()); 253 | } 254 | 255 | #[test] 256 | fn cooldown_not_triggered_on_gcd() { 257 | let mut app = App::new(); 258 | app.add_plugins(( 259 | MinimalPlugins, 260 | AbilityPlugin::::default(), 261 | InputPlugin, 262 | )) 263 | .insert_resource(Action::cooldowns()); 264 | 265 | // First delta time provided of each app is wonky 266 | app.update(); 267 | 268 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 269 | cooldowns.global_cooldown = Some(Cooldown::from_secs(0.5)); 270 | let _ = cooldowns.trigger(&Action::Long); 271 | assert_eq!( 272 | cooldowns.ready(&Action::Long), 273 | Err(CannotUseAbility::OnCooldown) 274 | ); 275 | 276 | // Let per-action cooldown elapse 277 | sleep(Duration::from_millis(250)); 278 | app.update(); 279 | 280 | // Action::Short should be ready itself, but the GCD will prevent it 281 | // assert Action::Short is still ready after failing to trigger 282 | let mut cooldowns: Mut> = app.world_mut().resource_mut(); 283 | assert_eq!( 284 | cooldowns.trigger(&Action::Short), 285 | Err(CannotUseAbility::OnGlobalCooldown) 286 | ); 287 | 288 | let short_cooldown = cooldowns.get(&Action::Short).unwrap(); 289 | assert!(short_cooldown.ready().is_ok()); 290 | 291 | // Wait for full GCD to expire 292 | sleep(Duration::from_millis(250)); 293 | app.update(); 294 | 295 | let cooldowns: &CooldownState = app.world().resource(); 296 | assert!(cooldowns.ready(&Action::Short).is_ok()); 297 | } 298 | -------------------------------------------------------------------------------- /src/ability_state.rs: -------------------------------------------------------------------------------- 1 | // Docs are missing from generated types :( 2 | #![allow(missing_docs)] 3 | 4 | use crate::{ 5 | charges::ChargeState, 6 | cooldown::CooldownState, 7 | pool::{AbilityCosts, MaxPoolLessThanMin, Pool}, 8 | Abilitylike, CannotUseAbility, 9 | }; 10 | // Required due to poor macro hygiene in `WorldQuery` macro 11 | // Tracked in https://github.com/bevyengine/bevy/issues/6593 12 | use bevy::{ecs::component::Component, ecs::query::QueryData}; 13 | use leafwing_input_manager::action_state::ActionState; 14 | 15 | /// A custom [`WorldQuery`](bevy::ecs::query::WorldQuery) type that fetches all ability relevant data for you. 16 | /// 17 | /// This type is intended to make collecting the data for [`Abilitylike`] methods easier when working with a full [`AbilitiesBundle`](crate::AbilitiesBundle`). 18 | /// This struct can be used as the first type parameter in a [`Query`](bevy::ecs::system::Query) to fetch the appropriate data. 19 | /// 20 | /// If you want your abilities to require paying costs, pass in the appropriate [`Pool`] type `P`. 21 | /// Otherwise, don't specify `P`. 22 | /// 23 | /// Once you have a [`AbilityStateItem`] by calling `.iter_mut()` or `.single_mut` on your query 24 | /// (or a [`AbilityStateReadOnlyItem`] by calling `.iter()` or `.single`), 25 | /// you can use the methods defined there to perform common tasks quickly and reliably. 26 | /// 27 | /// ## No resource pool 28 | /// 29 | /// When working with abilities that don't require a resource pool, simply pass in [`NullPool`] as the pool type. 30 | /// The absence of a pool will be handled gracefully by the methods in [`Abilitylike`]. 31 | /// 32 | /// ## Multiple resource pools 33 | /// 34 | /// When working with abilities that require multiple resource pools, there are two options: 35 | /// 36 | /// 1. Create a new [`Pool`] type that contains all of the possible resource pools. 37 | /// 2. Pass in [`NullPool`] and handle the resource costs manually in your ability implementations. 38 | /// 39 | /// The first solution is reliable and type-safe, but limits you to a fixed collection of resource pools 40 | /// and can be wasteful and confusing, as the majority of abilities or characters will only use a single resource pool. 41 | /// 42 | /// The second solution is more flexible, but requires you to handle the resource costs manually. 43 | /// Make sure to check if the resource cost can be paid before calling [`Abilitylike::trigger`]! 44 | #[derive(QueryData)] 45 | #[query_data(mutable)] 46 | pub struct AbilityState { 47 | /// The [`ActionState`] of the abilities of this entity of type `A` 48 | pub action_state: &'static ActionState, 49 | /// The [`ChargeState`] associated with each action of type `A` for this entity 50 | pub charges: &'static mut ChargeState, 51 | /// The [`CooldownState`] associated with each action of type `A` for this entity 52 | pub cooldowns: &'static mut CooldownState, 53 | /// The [`Pool`] of resources of type `P` that should be spent 54 | pub pool: Option<&'static mut P>, 55 | /// The [`AbilityCosts`] of each ability, in terms of [`P::Quantity`](Pool::Quantity) 56 | pub ability_costs: Option<&'static mut AbilityCosts>, 57 | } 58 | 59 | impl AbilityStateItem<'_, A, P> { 60 | /// Is this ability ready? 61 | /// 62 | /// Calls [`Abilitylike::ready`] on the specified action. 63 | #[inline] 64 | pub fn ready(&self, action: &A) -> Result<(), CannotUseAbility> { 65 | let maybe_pool = self.pool.as_deref(); 66 | let maybe_ability_costs = self.ability_costs.as_deref(); 67 | 68 | action.ready( 69 | &*self.charges, 70 | &*self.cooldowns, 71 | maybe_pool, 72 | maybe_ability_costs, 73 | ) 74 | } 75 | 76 | /// Is this ability both ready and pressed? 77 | /// 78 | /// The error value for "this ability is not pressed" will be prioritized over "this ability is not ready". 79 | #[inline] 80 | pub fn ready_and_pressed(&self, action: &A) -> Result<(), CannotUseAbility> { 81 | if self.action_state.pressed(action) { 82 | self.ready(action)?; 83 | Ok(()) 84 | } else { 85 | Err(CannotUseAbility::NotPressed) 86 | } 87 | } 88 | 89 | /// Is this ability both ready and just pressed? 90 | /// 91 | /// The error value for "this ability is not pressed" will be prioritized over "this ability is not ready". 92 | #[inline] 93 | pub fn ready_and_just_pressed(&self, action: &A) -> Result<(), CannotUseAbility> { 94 | if self.action_state.just_pressed(action) { 95 | self.ready(action)?; 96 | Ok(()) 97 | } else { 98 | Err(CannotUseAbility::NotPressed) 99 | } 100 | } 101 | 102 | /// Triggers this ability, depleting a charge if available. 103 | /// 104 | /// Calls [`Abilitylike::trigger`] on the specified action. 105 | #[inline] 106 | pub fn trigger(&mut self, action: &A) -> Result<(), CannotUseAbility> { 107 | let maybe_pool = self.pool.as_deref_mut(); 108 | let maybe_ability_costs = self.ability_costs.as_deref(); 109 | 110 | action.trigger( 111 | &mut *self.charges, 112 | &mut *self.cooldowns, 113 | maybe_pool, 114 | maybe_ability_costs, 115 | ) 116 | } 117 | 118 | /// Triggers this ability (and depletes available charges), if action is pressed. 119 | /// 120 | /// Calls [`Abilitylike::trigger`] on the specified action. 121 | #[inline] 122 | pub fn trigger_if_pressed(&mut self, action: &A) -> Result<(), CannotUseAbility> { 123 | if self.action_state.just_pressed(action) { 124 | let maybe_pool = self.pool.as_deref_mut(); 125 | let maybe_ability_costs = self.ability_costs.as_deref(); 126 | 127 | action.trigger( 128 | &mut *self.charges, 129 | &mut *self.cooldowns, 130 | maybe_pool, 131 | maybe_ability_costs, 132 | ) 133 | } else { 134 | Err(CannotUseAbility::NotPressed) 135 | } 136 | } 137 | 138 | /// Triggers this ability (and depletes available charges), if action was just pressed. 139 | /// 140 | /// Calls [`Abilitylike::trigger`] on the specified action. 141 | #[inline] 142 | pub fn trigger_if_just_pressed(&mut self, action: &A) -> Result<(), CannotUseAbility> { 143 | if self.action_state.just_pressed(action) { 144 | let maybe_pool = self.pool.as_deref_mut(); 145 | let maybe_ability_costs = self.ability_costs.as_deref(); 146 | 147 | action.trigger( 148 | &mut *self.charges, 149 | &mut *self.cooldowns, 150 | maybe_pool, 151 | maybe_ability_costs, 152 | ) 153 | } else { 154 | Err(CannotUseAbility::NotPressed) 155 | } 156 | } 157 | } 158 | 159 | impl AbilityStateReadOnlyItem<'_, A, P> { 160 | /// Is this ability ready? 161 | /// 162 | /// Calls [`Abilitylike::ready`] on the specified action. 163 | #[inline] 164 | pub fn ready(&self, action: &A) -> Result<(), CannotUseAbility> { 165 | action.ready(self.charges, self.cooldowns, self.pool, self.ability_costs) 166 | } 167 | 168 | /// Is this ability both ready and pressed? 169 | /// 170 | /// The error value for "this ability is not pressed" will be prioritized over "this ability is not ready". 171 | #[inline] 172 | pub fn ready_and_pressed(&self, action: &A) -> Result<(), CannotUseAbility> { 173 | if self.action_state.pressed(action) { 174 | self.ready(action)?; 175 | Ok(()) 176 | } else { 177 | Err(CannotUseAbility::NotPressed) 178 | } 179 | } 180 | 181 | /// Is this ability both ready and just pressed? 182 | /// 183 | /// The error value for "this ability is not pressed" will be prioritized over "this ability is not ready". 184 | #[inline] 185 | pub fn ready_and_just_pressed(&self, action: &A) -> Result<(), CannotUseAbility> { 186 | if self.action_state.just_pressed(action) { 187 | self.ready(action)?; 188 | Ok(()) 189 | } else { 190 | Err(CannotUseAbility::NotPressed) 191 | } 192 | } 193 | } 194 | 195 | #[cfg(test)] 196 | mod tests { 197 | use crate as leafwing_abilities; 198 | use crate::{AbilitiesBundle, AbilityState, Abilitylike}; 199 | use bevy::{prelude::*, reflect::Reflect}; 200 | use leafwing_input_manager::{action_state::ActionState, Actionlike}; 201 | 202 | #[derive(Actionlike, Reflect, Abilitylike, Clone, Debug, Hash, PartialEq, Eq)] 203 | enum TestAction { 204 | Duck, 205 | Cover, 206 | } 207 | 208 | #[test] 209 | fn ability_state_methods_are_visible_from_query() { 210 | fn simple_system(mut query: Query>) { 211 | let mut ability_state = query.single_mut().unwrap(); 212 | let _triggered = ability_state.trigger(&TestAction::Duck); 213 | } 214 | 215 | let mut app = App::new(); 216 | app.add_systems(Update, simple_system); 217 | } 218 | 219 | #[test] 220 | fn ability_state_fetches_abilities_bundle() { 221 | let mut world = World::new(); 222 | world 223 | .spawn(AbilitiesBundle::::default()) 224 | .insert(ActionState::::default()); 225 | 226 | let mut query_state = world.query::>(); 227 | assert_eq!(query_state.iter(&world).len(), 1); 228 | } 229 | } 230 | 231 | /// A no-op type that implements [`Pool`] and [`Component`]. 232 | /// 233 | /// Used in [`AbilityState`] to get the type system to play nice when no resource pool type is needed. 234 | /// 235 | /// Values of this type should never be constructed. 236 | #[derive(Component, Debug, Default)] 237 | pub struct NullPool; 238 | 239 | impl Pool for NullPool { 240 | type Quantity = f32; 241 | const MIN: f32 = 0.0; 242 | 243 | fn current(&self) -> Self::Quantity { 244 | Self::MIN 245 | } 246 | 247 | fn set_current(&mut self, _new_quantity: Self::Quantity) -> Self::Quantity { 248 | Self::MIN 249 | } 250 | 251 | fn max(&self) -> Self::Quantity { 252 | Self::MIN 253 | } 254 | 255 | fn set_max(&mut self, _new_max: Self::Quantity) -> Result<(), MaxPoolLessThanMin> { 256 | Ok(()) 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/premade_pools.rs: -------------------------------------------------------------------------------- 1 | //! Convenient premade resource [`Pool`] types to get you started. 2 | //! 3 | //! These can be annoying due to orphan rules that prevent you from implementing your own methods, 4 | //! so feel free to copy-paste them (without attribution) into your own source to make new variants. 5 | 6 | use crate::pool::{MaxPoolLessThanMin, Pool}; 7 | use bevy::prelude::{Component, Resource}; 8 | use core::fmt::{Display, Formatter}; 9 | use core::ops::{Add, AddAssign, Div, Mul, Sub, SubAssign}; 10 | use derive_more::{Add, AddAssign, Sub, SubAssign}; 11 | 12 | /// A premade resource pool for life (aka health, hit points or HP). 13 | pub mod life { 14 | use bevy::reflect::Reflect; 15 | 16 | use crate::pool::RegeneratingPool; 17 | 18 | use super::*; 19 | 20 | /// The amount of life available to a unit. 21 | /// If they lose it all, they die or pass out. 22 | /// 23 | /// This is intended to be stored as a component on each entity. 24 | #[derive(Debug, Clone, PartialEq, Component, Resource, Reflect)] 25 | pub struct LifePool { 26 | /// The current life. 27 | current: Life, 28 | /// The maximum life that can be stored. 29 | max: Life, 30 | /// The amount of life regenerated per second. 31 | pub regen_per_second: Life, 32 | } 33 | 34 | impl LifePool { 35 | /// Creates a new [`LifePool`] with the supplied settings. 36 | /// 37 | /// # Panics 38 | /// Panics if `current` is greater than `max`. 39 | /// Panics if `current` or max is negative. 40 | pub fn new(current: Life, max: Life, regen_per_second: Life) -> Self { 41 | assert!(current <= max); 42 | assert!(current >= LifePool::MIN); 43 | assert!(max >= LifePool::MIN); 44 | Self { 45 | current, 46 | max, 47 | regen_per_second, 48 | } 49 | } 50 | } 51 | 52 | /// A quantity of life, used to modify a [`LifePool`]. 53 | /// 54 | /// This can be used for damage computations, life regeneration, healing and so on. 55 | #[derive( 56 | Debug, Clone, Copy, PartialEq, PartialOrd, Default, Add, Sub, AddAssign, SubAssign, Reflect, 57 | )] 58 | pub struct Life(pub f32); 59 | 60 | impl Mul for Life { 61 | type Output = Life; 62 | 63 | fn mul(self, rhs: f32) -> Life { 64 | Life(self.0 * rhs) 65 | } 66 | } 67 | 68 | impl Mul for f32 { 69 | type Output = Life; 70 | 71 | fn mul(self, rhs: Life) -> Life { 72 | Life(self * rhs.0) 73 | } 74 | } 75 | 76 | impl Div for Life { 77 | type Output = Life; 78 | 79 | fn div(self, rhs: f32) -> Life { 80 | Life(self.0 / rhs) 81 | } 82 | } 83 | 84 | impl Div for Life { 85 | type Output = f32; 86 | 87 | fn div(self, rhs: Life) -> f32 { 88 | self.0 / rhs.0 89 | } 90 | } 91 | 92 | impl Pool for LifePool { 93 | type Quantity = Life; 94 | const MIN: Life = Life(0.); 95 | 96 | fn current(&self) -> Self::Quantity { 97 | self.current 98 | } 99 | 100 | fn set_current(&mut self, new_quantity: Self::Quantity) -> Self::Quantity { 101 | let actual_value = Life(new_quantity.0.clamp(0., self.max.0)); 102 | self.current = actual_value; 103 | self.current 104 | } 105 | 106 | fn max(&self) -> Self::Quantity { 107 | self.max 108 | } 109 | 110 | fn set_max(&mut self, new_max: Self::Quantity) -> Result<(), MaxPoolLessThanMin> { 111 | if new_max < Self::MIN { 112 | Err(MaxPoolLessThanMin) 113 | } else { 114 | self.max = new_max; 115 | self.set_current(self.current); 116 | Ok(()) 117 | } 118 | } 119 | } 120 | 121 | impl RegeneratingPool for LifePool { 122 | fn regen_per_second(&self) -> Self::Quantity { 123 | self.regen_per_second 124 | } 125 | 126 | fn set_regen_per_second(&mut self, new_regen_per_second: Self::Quantity) { 127 | self.regen_per_second = new_regen_per_second; 128 | } 129 | 130 | fn regenerate(&mut self, delta_time: std::time::Duration) { 131 | self.set_current(self.current + self.regen_per_second * delta_time.as_secs_f32()); 132 | } 133 | } 134 | 135 | impl Add for LifePool { 136 | type Output = Self; 137 | 138 | fn add(mut self, rhs: Life) -> Self::Output { 139 | self.set_current(self.current + rhs); 140 | self 141 | } 142 | } 143 | 144 | impl Sub for LifePool { 145 | type Output = Self; 146 | 147 | fn sub(mut self, rhs: Life) -> Self::Output { 148 | self.set_current(self.current - rhs); 149 | self 150 | } 151 | } 152 | 153 | impl AddAssign for LifePool { 154 | fn add_assign(&mut self, rhs: Life) { 155 | self.set_current(self.current + rhs); 156 | } 157 | } 158 | 159 | impl SubAssign for LifePool { 160 | fn sub_assign(&mut self, rhs: Life) { 161 | self.set_current(self.current - rhs); 162 | } 163 | } 164 | 165 | impl Display for Life { 166 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 167 | write!(f, "{}", self.0) 168 | } 169 | } 170 | 171 | impl Display for LifePool { 172 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 173 | write!(f, "{}/{}", self.current, self.max) 174 | } 175 | } 176 | } 177 | 178 | /// A premade resource pool for mana (aka MP). 179 | pub mod mana { 180 | use bevy::reflect::Reflect; 181 | 182 | use crate::pool::RegeneratingPool; 183 | 184 | use super::*; 185 | 186 | /// The amount of mana available to a unit. 187 | /// Units must spend mana to cast spells according to their [`AbilityCosts`](crate::pool::AbilityCosts) component. 188 | /// 189 | /// This is intended to be stored as a component on each entity. 190 | #[derive(Debug, Clone, PartialEq, Component, Resource, Reflect)] 191 | pub struct ManaPool { 192 | /// The current mana. 193 | current: Mana, 194 | /// The maximum mana that can be stored. 195 | max: Mana, 196 | /// The amount of mana regenerated per second. 197 | pub regen_per_second: Mana, 198 | } 199 | 200 | impl ManaPool { 201 | /// Creates a new [`ManaPool`] with the supplied settings. 202 | /// 203 | /// # Panics 204 | /// Panics if `current` is greater than `max`. 205 | /// Panics if `current` or `max` is negative. 206 | pub fn new(current: Mana, max: Mana, regen_per_second: Mana) -> Self { 207 | assert!(current <= max); 208 | assert!(current >= ManaPool::MIN); 209 | assert!(max >= ManaPool::MIN); 210 | Self { 211 | current, 212 | max, 213 | regen_per_second, 214 | } 215 | } 216 | } 217 | 218 | /// A quantity of mana, used to modify a [`ManaPool`]. 219 | /// 220 | /// This can be used for ability costs, mana regeneration and so on. 221 | #[derive( 222 | Debug, Clone, Copy, PartialEq, PartialOrd, Default, Add, Sub, AddAssign, SubAssign, Reflect, 223 | )] 224 | pub struct Mana(pub f32); 225 | 226 | impl Mul for Mana { 227 | type Output = Mana; 228 | 229 | fn mul(self, rhs: f32) -> Mana { 230 | Mana(self.0 * rhs) 231 | } 232 | } 233 | 234 | impl Mul for f32 { 235 | type Output = Mana; 236 | 237 | fn mul(self, rhs: Mana) -> Mana { 238 | Mana(self * rhs.0) 239 | } 240 | } 241 | 242 | impl Div for Mana { 243 | type Output = Mana; 244 | 245 | fn div(self, rhs: f32) -> Mana { 246 | Mana(self.0 / rhs) 247 | } 248 | } 249 | 250 | impl Div for Mana { 251 | type Output = f32; 252 | 253 | fn div(self, rhs: Mana) -> f32 { 254 | self.0 / rhs.0 255 | } 256 | } 257 | 258 | impl Pool for ManaPool { 259 | type Quantity = Mana; 260 | const MIN: Mana = Mana(0.); 261 | 262 | fn current(&self) -> Self::Quantity { 263 | self.current 264 | } 265 | 266 | fn set_current(&mut self, new_quantity: Self::Quantity) -> Self::Quantity { 267 | let actual_value = Mana(new_quantity.0.clamp(0., self.max.0)); 268 | self.current = actual_value; 269 | self.current 270 | } 271 | 272 | fn max(&self) -> Self::Quantity { 273 | self.max 274 | } 275 | 276 | fn set_max(&mut self, new_max: Self::Quantity) -> Result<(), MaxPoolLessThanMin> { 277 | if new_max < Self::MIN { 278 | Err(MaxPoolLessThanMin) 279 | } else { 280 | self.max = new_max; 281 | self.set_current(self.current); 282 | Ok(()) 283 | } 284 | } 285 | } 286 | 287 | impl RegeneratingPool for ManaPool { 288 | fn regen_per_second(&self) -> Self::Quantity { 289 | self.regen_per_second 290 | } 291 | 292 | fn set_regen_per_second(&mut self, new_regen_per_second: Self::Quantity) { 293 | self.regen_per_second = new_regen_per_second; 294 | } 295 | 296 | fn regenerate(&mut self, delta_time: std::time::Duration) { 297 | self.set_current(self.current + self.regen_per_second * delta_time.as_secs_f32()); 298 | } 299 | } 300 | 301 | impl Add for ManaPool { 302 | type Output = Self; 303 | 304 | fn add(mut self, rhs: Mana) -> Self::Output { 305 | self.set_current(self.current + rhs); 306 | self 307 | } 308 | } 309 | 310 | impl Sub for ManaPool { 311 | type Output = Self; 312 | 313 | fn sub(mut self, rhs: Mana) -> Self::Output { 314 | self.set_current(self.current - rhs); 315 | self 316 | } 317 | } 318 | 319 | impl AddAssign for ManaPool { 320 | fn add_assign(&mut self, rhs: Mana) { 321 | self.set_current(self.current + rhs); 322 | } 323 | } 324 | 325 | impl SubAssign for ManaPool { 326 | fn sub_assign(&mut self, rhs: Mana) { 327 | self.set_current(self.current - rhs); 328 | } 329 | } 330 | 331 | impl Display for Mana { 332 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 333 | write!(f, "{}", self.0) 334 | } 335 | } 336 | 337 | impl Display for ManaPool { 338 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 339 | write!(f, "{}/{}", self.current, self.max) 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /LICENSE-APACHE.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2021 Leafwing Studios 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/pool.rs: -------------------------------------------------------------------------------- 1 | //! Pools are a reservoir of resources that can be used to pay for abilities, or keep track of character state. 2 | //! 3 | //! Unlike charges, pools are typically shared across abilities. 4 | //! 5 | //! Life, mana, energy and rage might all be modelled effectively as pools. 6 | //! Pools have a maximum value and a minimum value (almost always zero), can regenerate over time, and can be spent to pay for abilities. 7 | //! 8 | //! The [`regenerate_resource_pool`](crate::systems::regenerate_resource_pool) system will regenerate resource pools of a given type if manually added. 9 | //! 10 | //! Remember to manually register these types for reflection with [`App::register_type`](bevy::app::App::register_type) if you wish to serialize or inspect them. 11 | 12 | use bevy::ecs::component::Mutable; 13 | use bevy::{ecs::prelude::*, reflect::Reflect}; 14 | use core::ops::{Add, AddAssign, Sub, SubAssign}; 15 | use core::time::Duration; 16 | use std::{collections::HashMap, marker::PhantomData}; 17 | use thiserror::Error; 18 | 19 | use crate::{Abilitylike, CannotUseAbility}; 20 | 21 | /// A reservoir of a resource that can be used to pay for abilities, or keep track of character state. 22 | /// 23 | /// Each type that implements this trait should be stored on a component (or, if your actions are globally unique, a resource), 24 | /// and contains information about the current and max values. 25 | /// 26 | /// There are two core benefits to using pools, rather than creating your own solutions: 27 | /// 28 | /// 1. It is impossible to accidentally set the value outside of the bounds of the pool. 29 | /// 2. The [`AbilityCosts`] component can be used to automatically check if an ability can be used. 30 | /// 31 | /// See [`RegeneratingPool`] for pools that regenerate over time. 32 | pub trait Pool: Sized + Component { 33 | /// A type that tracks the quantity within a pool. 34 | /// 35 | /// Unlike a [`Pool`] type, which stores a max, min and regeneration, 36 | /// quantities are lighter weight and should be used for things like damage amounts, mana costs and regen rates. 37 | type Quantity: Add 38 | + Sub 39 | + AddAssign 40 | + SubAssign 41 | + PartialEq 42 | + PartialOrd 43 | + Clone 44 | + Copy 45 | + Send 46 | + Sync 47 | + 'static; 48 | 49 | /// The minimum value of the pool type. 50 | /// 51 | /// At this point, no resources remain to be spent. 52 | const MIN: Self::Quantity; 53 | 54 | /// The current quantity of resources in the pool. 55 | /// 56 | /// # Panics 57 | /// 58 | /// Panics if `max` is less than [`Pool::MIN`]. 59 | fn current(&self) -> Self::Quantity; 60 | 61 | /// Check if the given cost can be paid by this pool. 62 | fn available(&self, amount: Self::Quantity) -> Result<(), CannotUseAbility> { 63 | if self.current() >= amount { 64 | Ok(()) 65 | } else { 66 | Err(CannotUseAbility::PoolInsufficient) 67 | } 68 | } 69 | 70 | /// Sets the current quantity of resources in the pool. 71 | /// 72 | /// This will be bounded by the minimum and maximum values of this pool. 73 | /// The value that was actually set is returned. 74 | fn set_current(&mut self, new_quantity: Self::Quantity) -> Self::Quantity; 75 | 76 | /// The maximum quantity of resources that this pool can store. 77 | fn max(&self) -> Self::Quantity; 78 | 79 | /// Sets the maximum quantity of resources that this pool can store. 80 | /// 81 | /// The current value will be reduced to the new max if necessary. 82 | /// 83 | /// Has no effect if `new_max < Pool::MIN`. 84 | /// Returns a [`MaxPoolLessThanMin`] error if this occurs. 85 | fn set_max(&mut self, new_max: Self::Quantity) -> Result<(), MaxPoolLessThanMin>; 86 | 87 | /// Is the pool currently full? 88 | #[inline] 89 | #[must_use] 90 | fn is_full(&self) -> bool { 91 | self.current() == self.max() 92 | } 93 | 94 | /// Is the pool currently empty? 95 | /// 96 | /// Note that this compares the current value to [`Pool::MIN`], not `0`. 97 | #[inline] 98 | #[must_use] 99 | fn is_empty(&self) -> bool { 100 | self.current() == Self::MIN 101 | } 102 | 103 | /// Spend the specified amount from the pool, if there is that much available. 104 | /// 105 | /// Otherwise, return the error [`CannotUseAbility::PoolEmpty`]. 106 | fn expend(&mut self, amount: Self::Quantity) -> Result<(), CannotUseAbility> { 107 | self.available(amount)?; 108 | 109 | let new_current = self.current() - amount; 110 | self.set_current(new_current); 111 | Ok(()) 112 | } 113 | 114 | /// Replenish the pool by the specified amount. 115 | /// 116 | /// This cannot cause the pool to exceed maximum value that can be stored in the pool. 117 | /// This is the sign-flipped counterpart to [`Self::expend`], 118 | /// however, unlike [`Self::expend`], this method will not return an error if the pool is empty. 119 | fn replenish(&mut self, amount: Self::Quantity) { 120 | let new_current = self.current() + amount; 121 | self.set_current(new_current); 122 | } 123 | } 124 | 125 | /// A resource pool that regenerates (or decays) over time. 126 | /// 127 | /// Set the regeneration rate to a positive value to regenerate, or a negative value to decay. 128 | pub trait RegeneratingPool: Pool { 129 | /// The quantity recovered by the pool in one second. 130 | /// 131 | /// This value may be negative, in the case of automatically decaying pools (like rage). 132 | fn regen_per_second(&self) -> Self::Quantity; 133 | 134 | /// Set the quantity recovered by the pool in one second. 135 | /// 136 | /// This value may be negative, in the case of automatically decaying pools (like rage). 137 | fn set_regen_per_second(&mut self, new_regen_per_second: Self::Quantity); 138 | 139 | /// Regenerates this pool according to the elapsed `delta_time`. 140 | /// 141 | /// Called in the [`regenerate_resource_pool`](crate::systems::regenerate_resource_pool) system. 142 | /// Can also be called in your own regeneration systems. 143 | fn regenerate(&mut self, delta_time: Duration); 144 | } 145 | 146 | /// The maximum value for a [`Pool`] was set to be less than [`Pool::MIN`]. 147 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Error)] 148 | #[error( 149 | "The maximum quantity that can be stored in a pool must be greater than the minimum value." 150 | )] 151 | pub struct MaxPoolLessThanMin; 152 | 153 | /// Stores the cost (in terms of the [`Pool::Quantity`] of ability) associated with each ability of type `A`. 154 | #[derive(Component, Resource, Debug, Reflect)] 155 | pub struct AbilityCosts { 156 | /// The underlying cost of each ability. 157 | cost_map: HashMap, 158 | _phantom: PhantomData, 159 | } 160 | 161 | impl Clone for AbilityCosts { 162 | fn clone(&self) -> Self { 163 | AbilityCosts { 164 | cost_map: self.cost_map.clone(), 165 | _phantom: PhantomData, 166 | } 167 | } 168 | } 169 | 170 | impl Default for AbilityCosts { 171 | fn default() -> Self { 172 | AbilityCosts { 173 | cost_map: HashMap::new(), 174 | _phantom: PhantomData, 175 | } 176 | } 177 | } 178 | 179 | impl AbilityCosts { 180 | /// Creates a new [`AbilityCosts`] from an iterator of `(charges, action)` pairs 181 | /// 182 | /// If a [`Pool::Quantity`] is not provided for an action, that action will have no cost in terms of the stored resource pool. 183 | /// 184 | /// To create an empty [`AbilityCosts`] struct, use the [`Default::default`] method instead. 185 | #[must_use] 186 | pub fn new(action_cost_pairs: impl IntoIterator) -> Self { 187 | let mut ability_costs = AbilityCosts::default(); 188 | for (action, cost) in action_cost_pairs.into_iter() { 189 | ability_costs.set(action, cost); 190 | } 191 | ability_costs 192 | } 193 | 194 | /// Are enough resources available in the `pool` to use the `action`? 195 | /// 196 | /// Returns `true` if the underlying resource is [`None`]. 197 | #[inline] 198 | #[must_use] 199 | pub fn available(&self, action: &A, pool: &P) -> bool { 200 | if let Some(cost) = self.get(action) { 201 | pool.available(*cost).is_ok() 202 | } else { 203 | true 204 | } 205 | } 206 | 207 | /// Pay the ability cost for the `action` from the `pool`, if able 208 | /// 209 | /// The cost of the action is expended from the [`Pool`]. 210 | /// 211 | /// If the underlying pool does not have enough resources to pay the action's cost, 212 | /// a [`CannotUseAbility::PoolEmpty`] error is returned and this call has no effect. 213 | /// 214 | /// Returns [`Ok(())`] if the underlying [`Pool`] can support the cost of the action. 215 | #[inline] 216 | pub fn pay_cost(&mut self, action: &A, pool: &mut P) -> Result<(), CannotUseAbility> { 217 | if let Some(cost) = self.get(action) { 218 | pool.expend(*cost) 219 | } else { 220 | Ok(()) 221 | } 222 | } 223 | 224 | /// Returns a reference to the underlying [`Pool::Quantity`] cost for `action`, if set. 225 | #[inline] 226 | #[must_use] 227 | pub fn get(&self, action: &A) -> Option<&P::Quantity> { 228 | self.cost_map.get(action) 229 | } 230 | 231 | /// Returns a mutable reference to the underlying [`Pool::Quantity`] cost for `action`, if set. 232 | #[inline] 233 | #[must_use] 234 | pub fn get_mut(&mut self, action: &A) -> Option<&mut P::Quantity> { 235 | self.cost_map.get_mut(action) 236 | } 237 | 238 | /// Sets the underlying [`Pool::Quantity`] cost for `action` to the provided value. 239 | /// 240 | /// Unless you're building a new [`AbilityCosts`] struct, you likely want to use [`Self::get_mut`]. 241 | #[inline] 242 | pub fn set(&mut self, action: A, cost: P::Quantity) -> &mut Self { 243 | self.cost_map.insert(action, cost); 244 | self 245 | } 246 | 247 | /// Collects a `&mut Self` into a `Self`. 248 | /// 249 | /// Used to conclude the builder pattern. Actually just calls `self.clone()`. 250 | #[inline] 251 | #[must_use] 252 | pub fn build(&mut self) -> Self { 253 | self.clone() 254 | } 255 | 256 | /// Returns an iterator of references to the underlying non-[`None`] [`Charges`] 257 | #[inline] 258 | pub fn iter(&self) -> impl Iterator { 259 | self.cost_map.values() 260 | } 261 | 262 | /// Returns an iterator of mutable references to the underlying non-[`None`] [`Charges`] 263 | #[inline] 264 | pub fn iter_mut(&mut self) -> impl Iterator { 265 | self.cost_map.values_mut() 266 | } 267 | } 268 | 269 | /// Stores a resource pool and the associated costs for each ability. 270 | /// 271 | /// Note that if your abilities do not cost the given resource, 272 | /// you can still add your [`Pool`] type as a component. 273 | /// 274 | /// This is particularly common when working with life totals, 275 | /// as you want the other functionality of pools (current, max, regen, depletion) 276 | /// but often cannot spend it on abilities. 277 | /// 278 | /// # Usage 279 | /// 280 | /// Note that resource pools are not controlled by [`AbilityPlugin`](crate::plugin::AbilityPlugin). 281 | /// If you want regeneration to occur automatically, add [`regenerate_resource_pool`](crate::systems::regenerate_resource_pool) 282 | /// to your schedule. 283 | /// 284 | /// These types are not automatically registered by [`AbilityPlugin`](crate::plugin::AbilityPlugin). 285 | /// You must register them manually with [`App::register_type`](bevy::app::App::register_type) if you wish to serialize or inspect them. 286 | #[derive(Bundle, Reflect)] 287 | pub struct PoolBundle { 288 | /// The resource pool used to pay for abilities 289 | pub pool: P, 290 | /// The cost of each ability in terms of this pool 291 | pub ability_costs: AbilityCosts, 292 | } 293 | 294 | #[cfg(test)] 295 | mod tests { 296 | use super::*; 297 | use crate::premade_pools::mana::{Mana, ManaPool}; 298 | 299 | #[test] 300 | fn set_pool_cannot_exceed_min() { 301 | let mut mana_pool = ManaPool::new(Mana(0.), Mana(10.), Mana(0.)); 302 | mana_pool.set_current(Mana(-3.)); 303 | assert_eq!(mana_pool.current(), ManaPool::MIN); 304 | } 305 | 306 | #[test] 307 | fn set_pool_cannot_exceed_max() { 308 | let max_mana = Mana(10.); 309 | let mut mana_pool = ManaPool::new(max_mana, max_mana, Mana(0.)); 310 | mana_pool.set_current(Mana(100.0)); 311 | assert_eq!(mana_pool.current(), max_mana); 312 | } 313 | 314 | #[test] 315 | fn reducing_max_decreases_current() { 316 | let mut mana_pool = ManaPool::new(Mana(10.), Mana(10.), Mana(0.)); 317 | assert_eq!(mana_pool.current(), Mana(10.)); 318 | mana_pool.set_max(Mana(5.)).unwrap(); 319 | assert_eq!(mana_pool.current(), Mana(5.)); 320 | } 321 | 322 | #[test] 323 | fn setting_max_below_min_fails() { 324 | let mut mana_pool = ManaPool::new(Mana(10.), Mana(10.), Mana(0.)); 325 | let result = mana_pool.set_max(Mana(-7.)); 326 | assert_eq!(mana_pool.max(), Mana(10.)); 327 | assert_eq!(result, Err(MaxPoolLessThanMin)) 328 | } 329 | 330 | #[test] 331 | fn expending_depletes_pool() { 332 | let mut mana_pool = ManaPool::new(Mana(11.), Mana(11.), Mana(0.)); 333 | mana_pool.expend(Mana(5.)).unwrap(); 334 | assert_eq!(mana_pool.current(), Mana(6.)); 335 | mana_pool.expend(Mana(5.)).unwrap(); 336 | assert_eq!(mana_pool.current(), Mana(1.)); 337 | assert_eq!( 338 | mana_pool.expend(Mana(5.)), 339 | Err(CannotUseAbility::PoolInsufficient) 340 | ); 341 | } 342 | 343 | #[test] 344 | fn pool_can_regenerate() { 345 | let mut mana_pool = ManaPool::new(Mana(0.), Mana(10.), Mana(1.3)); 346 | mana_pool.regenerate(Duration::from_secs(1)); 347 | let expected = Mana(1.3); 348 | 349 | assert!((mana_pool.current() - expected).0.abs() < f32::EPSILON); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![forbid(unsafe_code)] 3 | #![warn(clippy::doc_markdown)] 4 | #![doc = include_str!("../README.md")] 5 | 6 | use crate::cooldown::CooldownState; 7 | use bevy::{ecs::prelude::*, reflect::Reflect}; 8 | use charges::{ChargeState, Charges}; 9 | use cooldown::Cooldown; 10 | use leafwing_input_manager::Actionlike; 11 | use pool::{AbilityCosts, Pool}; 12 | use thiserror::Error; 13 | 14 | mod ability_state; 15 | pub mod charges; 16 | pub mod cooldown; 17 | pub mod plugin; 18 | pub mod pool; 19 | #[cfg(feature = "premade_pools")] 20 | pub mod premade_pools; 21 | pub mod systems; 22 | pub use ability_state::*; 23 | 24 | // Importing the derive macro 25 | pub use leafwing_abilities_macros::Abilitylike; 26 | 27 | /// Everything you need to get started 28 | pub mod prelude { 29 | pub use crate::charges::{ChargeState, Charges}; 30 | pub use crate::cooldown::{Cooldown, CooldownState}; 31 | pub use crate::pool::{AbilityCosts, Pool, PoolBundle}; 32 | 33 | pub use crate::plugin::AbilityPlugin; 34 | pub use crate::CannotUseAbility; 35 | pub use crate::{AbilitiesBundle, AbilityState, Abilitylike}; 36 | } 37 | 38 | /// Allows a type to be used as a gameplay action in an input-agnostic fashion 39 | /// 40 | /// Actions are modelled as "virtual buttons", cleanly abstracting over messy, customizable inputs 41 | /// in a way that can be easily consumed by your game logic. 42 | /// 43 | /// This trait should be implemented on the `A` type that you want to pass into [`InputManagerPlugin`](crate::plugin::InputManagerPlugin). 44 | /// 45 | /// Generally, these types will be very small (often data-less) enums. 46 | /// As a result, the APIs in this crate accept actions by value, rather than reference. 47 | /// While `Copy` is not a required trait bound, 48 | /// users are strongly encouraged to derive `Copy` on these enums whenever possible to improve ergonomics. 49 | /// 50 | /// # Example 51 | /// ```rust 52 | /// use leafwing_input_manager::Actionlike; 53 | /// use bevy::reflect::Reflect; 54 | /// 55 | /// #[derive(Actionlike, Debug, PartialEq, Eq, Clone, Copy, Hash, Reflect)] 56 | /// enum PlayerAction { 57 | /// // Movement 58 | /// Up, 59 | /// Down, 60 | /// Left, 61 | /// Right, 62 | /// // Abilities 63 | /// Ability1, 64 | /// Ability2, 65 | /// Ability3, 66 | /// Ability4, 67 | /// Ultimate, 68 | /// } 69 | /// ``` 70 | pub trait Abilitylike: Actionlike { 71 | /// Is this ability ready? 72 | /// 73 | /// If this ability has charges, at least one charge must be available. 74 | /// If this ability has a cooldown but no charges, the cooldown must be ready. 75 | /// Otherwise, returns [`Ok(())`]. 76 | /// 77 | /// Calls [`ability_ready`], which can be used manually if you already know the [`Charges`] and [`Cooldown`] of interest. 78 | fn ready( 79 | &self, 80 | charges: &ChargeState, 81 | cooldowns: &CooldownState, 82 | maybe_pool: Option<&P>, 83 | maybe_costs: Option<&AbilityCosts>, 84 | ) -> Result<(), CannotUseAbility> { 85 | let charges = charges.get(self); 86 | let cooldown = cooldowns.get(self); 87 | 88 | ability_ready( 89 | charges, 90 | cooldown, 91 | maybe_pool, 92 | maybe_costs.and_then(|costs| costs.get(self)).copied(), 93 | ) 94 | } 95 | 96 | /// Triggers this ability, depleting a charge if available. 97 | /// 98 | /// Returns `true` if the ability could be used, and `false` if it could not be. 99 | /// Abilities can only be used if they are ready. 100 | /// 101 | /// Calls [`trigger_ability`], which can be used manually if you already know the [`Charges`] and [`Cooldown`] of interest. 102 | fn trigger( 103 | &self, 104 | charges: &mut ChargeState, 105 | cooldowns: &mut CooldownState, 106 | maybe_pool: Option<&mut P>, 107 | maybe_costs: Option<&AbilityCosts>, 108 | ) -> Result<(), CannotUseAbility> { 109 | let charges = charges.get_mut(self); 110 | let cooldown = cooldowns.get_mut(self); 111 | 112 | trigger_ability( 113 | charges, 114 | cooldown, 115 | maybe_pool, 116 | maybe_costs.and_then(|costs| costs.get(self)).copied(), 117 | ) 118 | } 119 | 120 | /// Triggers this ability, depleting a charge if available. 121 | /// 122 | /// Returns `true` if the ability could be used, and `false` if it could not be. 123 | /// Abilities can only be used if they are ready. 124 | /// 125 | /// Calls [`Abilitylike::trigger`], passing in [`None`] for both the pools or costs. 126 | /// This is useful when you don't have any pools or costs to check, 127 | /// or when multiple distinct pools may be needed. 128 | fn trigger_no_costs( 129 | &self, 130 | charges: &mut ChargeState, 131 | cooldowns: &mut CooldownState, 132 | ) -> Result<(), CannotUseAbility> { 133 | self.trigger::(charges, cooldowns, None, None) 134 | } 135 | 136 | /// Is this ability ready? 137 | /// 138 | /// If this ability has charges, at least one charge must be available. 139 | /// If this ability has a cooldown but no charges, the cooldown must be ready. 140 | /// Otherwise, returns [`Ok(())`]. 141 | /// 142 | /// Calls [`Abilitylike::ready`], passing in [`None`] for both the pools or costs. 143 | /// This is useful when you don't have any pools or costs to check, 144 | /// or when multiple distinct pools may be needed. 145 | fn ready_no_costs( 146 | &self, 147 | charges: &ChargeState, 148 | cooldowns: &CooldownState, 149 | ) -> Result<(), CannotUseAbility> { 150 | self.ready::(charges, cooldowns, None, None) 151 | } 152 | } 153 | 154 | /// An [`Error`](std::error::Error) type that explains why an ability could not be used. 155 | /// 156 | /// The priority of these errors follows the order of this enum. 157 | /// For example, if an ability is out of charges and also not pressed, 158 | /// [`ready_and_pressed`](crate::ability_state::AbilityStateItem) will return `Err(CannotUseAbility::NotPressed)`, 159 | /// rather than `Err(CannotUseAbility::NoCharges)`, even though both are true. 160 | #[derive(Error, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] 161 | pub enum CannotUseAbility { 162 | /// The corresponding [`ActionState`](leafwing_input_manager::action_state::ActionState) was not pressed 163 | #[error("The ability was not pressed.")] 164 | NotPressed, 165 | /// There were no [`Charges`] available for this ability 166 | #[error("No charges available.")] 167 | NoCharges, 168 | /// The [`Cooldown`] of this ability was not ready 169 | #[error("Cooldown not ready.")] 170 | OnCooldown, 171 | /// The Global [`Cooldown`] for this [`CooldownState`] was not ready 172 | #[error("Global cooldown not ready.")] 173 | OnGlobalCooldown, 174 | /// Not enough resources from the corresponding [`Pool`]s are available 175 | #[error("Not enough resources.")] 176 | PoolInsufficient, 177 | } 178 | 179 | /// Checks if a [`Charges`], [`Cooldown`] pair associated with an ability is ready to use. 180 | /// 181 | /// If this ability has charges, at least one charge must be available. 182 | /// If this ability has a cooldown but no charges, the cooldown must be ready. 183 | /// Otherwise, returns `true`. 184 | /// 185 | /// If you don't have an associated resource pool to check, pass in [`NullPool`] as `P`. 186 | #[inline] 187 | pub fn ability_ready( 188 | charges: Option<&Charges>, 189 | cooldown: Option<&Cooldown>, 190 | pool: Option<&P>, 191 | cost: Option, 192 | ) -> Result<(), CannotUseAbility> { 193 | if let Some(charges) = charges { 194 | if charges.charges() > 0 { 195 | Ok(()) 196 | } else { 197 | Err(CannotUseAbility::NoCharges) 198 | } 199 | } else if let Some(cooldown) = cooldown { 200 | cooldown.ready() 201 | } else if let Some(pool) = pool { 202 | if let Some(cost) = cost { 203 | pool.available(cost) 204 | } else { 205 | Ok(()) 206 | } 207 | // The pool does not exist, but the cost does 208 | } else if let Some(cost) = cost { 209 | if cost > P::MIN { 210 | Err(CannotUseAbility::PoolInsufficient) 211 | } else { 212 | Ok(()) 213 | } 214 | } else { 215 | Ok(()) 216 | } 217 | } 218 | 219 | /// Triggers an implicit ability, depleting a charge if available. 220 | /// 221 | /// If no `charges` is [`None`], this will be based off the [`Cooldown`] alone, triggering it if possible. 222 | /// If you don't have an associated resource pool to check, pass in [`NullPool`] as `P`. 223 | #[inline] 224 | pub fn trigger_ability( 225 | mut charges: Option<&mut Charges>, 226 | mut cooldown: Option<&mut Cooldown>, 227 | pool: Option<&mut P>, 228 | cost: Option, 229 | ) -> Result<(), CannotUseAbility> { 230 | ability_ready( 231 | charges.as_deref(), 232 | cooldown.as_deref(), 233 | pool.as_deref(), 234 | cost, 235 | )?; 236 | 237 | if let Some(ref mut charges) = charges { 238 | charges.expend()?; 239 | } else if let Some(ref mut cooldown) = cooldown { 240 | cooldown.trigger()?; 241 | } 242 | 243 | if let Some(pool) = pool { 244 | if let Some(cost) = cost { 245 | let _pool_result = pool.expend(cost); 246 | // This is good to check, but panics in release mode are miserable 247 | debug_assert!(_pool_result.is_ok()); 248 | } 249 | } 250 | 251 | Ok(()) 252 | } 253 | 254 | /// This [`Bundle`] allows entities to manage their [`Abilitylike`] actions effectively. 255 | /// 256 | /// Commonly combined with an [`InputManagerBundle`](leafwing_input_manager::InputManagerBundle), 257 | /// which tracks whether or not actions are pressed. 258 | /// 259 | /// If you would like to track resource costs for your abilities, combine this with a [`PoolBundle`](crate::pool::PoolBundle). 260 | /// 261 | /// Use with [`AbilityPlugin`](crate::plugin::AbilityPlugin), providing the same enum type to both. 262 | #[derive(Bundle, Clone, Debug, PartialEq, Eq, Reflect)] 263 | pub struct AbilitiesBundle { 264 | /// A [`CooldownState`] component 265 | pub cooldowns: CooldownState, 266 | /// A [`ChargeState`] component 267 | pub charges: ChargeState, 268 | } 269 | 270 | // Cannot use derive(Default), as it forces an undesirable bound on our generics 271 | impl Default for AbilitiesBundle { 272 | fn default() -> Self { 273 | Self { 274 | cooldowns: CooldownState::default(), 275 | charges: ChargeState::default(), 276 | } 277 | } 278 | } 279 | 280 | #[cfg(test)] 281 | mod tests { 282 | use bevy::reflect::Reflect; 283 | use leafwing_abilities_macros::Abilitylike; 284 | use leafwing_input_manager::Actionlike; 285 | 286 | use crate::charges::Charges; 287 | use crate::cooldown::Cooldown; 288 | use crate::NullPool; 289 | use crate::{ability_ready, trigger_ability, CannotUseAbility}; 290 | 291 | use crate as leafwing_abilities; 292 | 293 | #[derive(Abilitylike, Actionlike, Reflect, Clone, Hash, PartialEq, Eq, Debug)] 294 | enum TestAbility { 295 | TestAction, 296 | } 297 | 298 | #[test] 299 | fn abilitylike_works() {} 300 | 301 | #[test] 302 | fn ability_ready_no_cooldown_no_charges() { 303 | assert!(ability_ready::(None, None, None, None).is_ok()); 304 | } 305 | 306 | #[test] 307 | fn ability_ready_just_cooldown() { 308 | let mut cooldown = Some(Cooldown::from_secs(1.)); 309 | assert!(ability_ready::(None, cooldown.as_ref(), None, None).is_ok()); 310 | 311 | cooldown.as_mut().map(|c| c.trigger()); 312 | assert_eq!( 313 | ability_ready::(None, cooldown.as_ref(), None, None), 314 | Err(CannotUseAbility::OnCooldown) 315 | ); 316 | } 317 | 318 | #[test] 319 | fn ability_ready_just_charges() { 320 | let mut charges = Some(Charges::simple(1)); 321 | 322 | assert!(ability_ready::(charges.as_ref(), None, None, None).is_ok()); 323 | 324 | charges.as_mut().map(|c| c.expend()); 325 | assert_eq!( 326 | ability_ready::(charges.as_ref(), None, None, None), 327 | Err(crate::CannotUseAbility::NoCharges) 328 | ); 329 | } 330 | 331 | #[test] 332 | fn ability_ready_cooldown_and_charges() { 333 | let mut charges = Some(Charges::simple(1)); 334 | let mut cooldown = Some(Cooldown::from_secs(1.)); 335 | // Both available 336 | assert!(ability_ready::(charges.as_ref(), cooldown.as_ref(), None, None).is_ok()); 337 | 338 | // Out of charges, cooldown ready 339 | charges.as_mut().map(|c| c.expend()); 340 | assert_eq!( 341 | ability_ready::(charges.as_ref(), cooldown.as_ref(), None, None), 342 | Err(CannotUseAbility::NoCharges) 343 | ); 344 | 345 | // Just charges 346 | if let Some(c) = charges.as_mut() { 347 | c.replenish() 348 | } 349 | cooldown.as_mut().map(|c| c.trigger()); 350 | assert!(ability_ready::(charges.as_ref(), cooldown.as_ref(), None, None).is_ok()); 351 | 352 | // Neither 353 | charges.as_mut().map(|c| c.expend()); 354 | assert_eq!( 355 | ability_ready::(charges.as_ref(), cooldown.as_ref(), None, None), 356 | Err(CannotUseAbility::NoCharges) 357 | ); 358 | } 359 | 360 | #[test] 361 | fn trigger_ability_no_cooldown_no_charges() { 362 | let outcome = trigger_ability::(None, None, None, None); 363 | assert!(outcome.is_ok()); 364 | } 365 | 366 | #[test] 367 | fn trigger_ability_just_cooldown() { 368 | let mut cooldown = Some(Cooldown::from_secs(1.)); 369 | assert!(trigger_ability::(None, cooldown.as_mut(), None, None).is_ok()); 370 | 371 | cooldown.as_mut().map(|c| c.trigger()); 372 | assert_eq!( 373 | trigger_ability::(None, cooldown.as_mut(), None, None), 374 | Err(CannotUseAbility::OnCooldown) 375 | ); 376 | assert_eq!( 377 | ability_ready::(None, cooldown.as_ref(), None, None), 378 | Err(CannotUseAbility::OnCooldown) 379 | ); 380 | } 381 | 382 | #[test] 383 | fn trigger_ability_just_charges() { 384 | let mut charges = Some(Charges::simple(1)); 385 | 386 | assert!(trigger_ability::(charges.as_mut(), None, None, None).is_ok()); 387 | 388 | charges.as_mut().map(|c| c.expend()); 389 | assert_eq!( 390 | trigger_ability::(charges.as_mut(), None, None, None), 391 | Err(CannotUseAbility::NoCharges) 392 | ); 393 | assert_eq!( 394 | ability_ready::(charges.as_ref(), None, None, None), 395 | Err(CannotUseAbility::NoCharges) 396 | ); 397 | } 398 | 399 | #[test] 400 | fn trigger_ability_cooldown_and_charges() { 401 | let mut charges = Some(Charges::simple(1)); 402 | let mut cooldown = Some(Cooldown::from_secs(1.)); 403 | // Both available 404 | assert!( 405 | trigger_ability::(charges.as_mut(), cooldown.as_mut(), None, None).is_ok() 406 | ); 407 | assert_eq!( 408 | ability_ready::(charges.as_ref(), cooldown.as_ref(), None, None), 409 | Err(CannotUseAbility::NoCharges) 410 | ); 411 | 412 | // None available 413 | assert_eq!( 414 | trigger_ability::(charges.as_mut(), cooldown.as_mut(), None, None), 415 | Err(CannotUseAbility::NoCharges) 416 | ); 417 | 418 | // Just charges 419 | if let Some(c) = charges.as_mut() { 420 | c.replenish() 421 | } 422 | assert!( 423 | trigger_ability::(charges.as_mut(), cooldown.as_mut(), None, None).is_ok() 424 | ); 425 | 426 | // Just cooldown 427 | charges.as_mut().map(|c| c.expend()); 428 | if let Some(c) = cooldown.as_mut() { 429 | c.refresh() 430 | } 431 | assert_eq!( 432 | trigger_ability::(charges.as_mut(), cooldown.as_mut(), None, None), 433 | Err(CannotUseAbility::NoCharges) 434 | ); 435 | } 436 | } 437 | -------------------------------------------------------------------------------- /src/charges.rs: -------------------------------------------------------------------------------- 1 | //! Charges are "uses of an action". 2 | //! Actions may only be used if at least one charge is available. 3 | //! Unlike pools, charges are not shared across abilities. 4 | 5 | use bevy::{ 6 | ecs::prelude::{Component, Resource}, 7 | reflect::Reflect, 8 | }; 9 | use std::{fmt::Display, marker::PhantomData}; 10 | 11 | use crate::{Abilitylike, CannotUseAbility}; 12 | use std::collections::HashMap; 13 | 14 | /// A component / resource that stores the [`Charges`] for each [`Abilitylike`] action of type `A`. 15 | /// 16 | /// If [`Charges`] is set for an actions, it is only [`Abilitylike::ready`] when at least one charge is available. 17 | /// 18 | /// ```rust 19 | /// use bevy::reflect::Reflect; 20 | /// use leafwing_abilities::prelude::*; 21 | /// use leafwing_abilities::premade_pools::mana::{Mana, ManaPool}; 22 | /// use leafwing_input_manager::Actionlike; 23 | /// 24 | /// #[derive(Actionlike, Abilitylike, Debug, Clone, Reflect, Hash, PartialEq, Eq)] 25 | /// enum Action { 26 | /// // Neither cooldowns nor charges 27 | /// Move, 28 | /// // Double jump: 2 charges, no cooldowns 29 | /// Jump, 30 | /// // Simple cooldown 31 | /// Dash, 32 | /// // Cooldowns and charges, replenishing one at a time 33 | /// Spell, 34 | /// } 35 | /// 36 | /// impl Action { 37 | /// fn charges() -> ChargeState { 38 | /// // You can either use the builder pattern or the `new` init for both cooldowns and charges 39 | /// // The differences are largely aesthetic. 40 | /// ChargeState::default() 41 | /// // Double jump! 42 | /// .set(Action::Jump, Charges::replenish_all(2)) 43 | /// // Store up to 3 spells at once 44 | /// .set(Action::Spell, Charges::replenish_one(3)) 45 | /// .build() 46 | /// } 47 | /// 48 | /// fn cooldowns() -> CooldownState { 49 | /// // Omitted cooldowns and charges will cause the action to be treated as if it always had available cooldowns / charges to use. 50 | /// CooldownState::new([ 51 | /// (Action::Dash, Cooldown::from_secs(2.)), 52 | /// (Action::Spell, Cooldown::from_secs(4.5)), 53 | /// ]) 54 | /// } 55 | /// 56 | /// fn mana_costs() -> AbilityCosts { 57 | /// // Provide the Pool::Quantity value when setting costs 58 | /// AbilityCosts::new([ 59 | /// (Action::Spell, Mana(10.)), 60 | /// ]) 61 | /// } 62 | /// } 63 | /// 64 | /// // In a real game you'd spawn a bundle with the appropriate components. 65 | /// let mut abilities_bundle = AbilitiesBundle { 66 | /// cooldowns: Action::cooldowns(), 67 | /// charges: Action::charges(), 68 | /// ..Default::default() 69 | /// }; 70 | /// 71 | /// // You can also define resource pools using a separate bundle. 72 | /// // Typically, you'll want to nest both of these bundles under a custom Bundle type for your characters. 73 | /// let mut mana_bundle = PoolBundle { 74 | /// // Max mana of 100., regen rate of 1. 75 | /// pool: ManaPool::new(Mana(100.0), Mana(100.0), Mana(1.0)), 76 | /// ability_costs: Action::mana_costs(), 77 | /// }; 78 | /// 79 | /// // Then, you can check if an action is ready to be used. 80 | /// // Consider using the `AbilityState` `WorldQuery` type instead for convenience! 81 | /// if Action::Spell.ready(&abilities_bundle.charges, &abilities_bundle.cooldowns, Some(&mana_bundle.pool), Some(&mana_bundle.ability_costs)).is_ok() { 82 | /// // When you use an action, remember to trigger it! 83 | /// Action::Spell.trigger(&mut abilities_bundle.charges, &mut abilities_bundle.cooldowns, Some(&mut mana_bundle.pool), Some(&mut mana_bundle.ability_costs)); 84 | /// } 85 | /// ``` 86 | #[derive(Resource, Component, Clone, PartialEq, Eq, Debug, Reflect)] 87 | pub struct ChargeState { 88 | /// The underlying [`Charges`]. 89 | charges_map: HashMap, 90 | #[reflect(ignore)] 91 | _phantom: PhantomData, 92 | } 93 | 94 | impl Default for ChargeState { 95 | fn default() -> Self { 96 | ChargeState { 97 | charges_map: HashMap::new(), 98 | _phantom: PhantomData, 99 | } 100 | } 101 | } 102 | 103 | /// Stores how many times an action can be used. 104 | /// 105 | /// Charges refresh when [`Charges::refresh`] is called manually, 106 | /// or when the corresponding cooldown expires (if the [`InputManagerPlugin`](crate::plugin::InputManagerPlugin) is added). 107 | #[derive(Clone, Default, PartialEq, Eq, Debug, Reflect)] 108 | pub struct Charges { 109 | current: u8, 110 | max: u8, 111 | /// What should happen when the charges are refreshed? 112 | pub replenish_strat: ReplenishStrategy, 113 | /// How should the corresponding [`Cooldown`](crate::cooldown::Cooldown) interact with these charges? 114 | pub cooldown_strat: CooldownStrategy, 115 | } 116 | 117 | impl Display for Charges { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | write!(f, "{}/{}", self.current, self.max) 120 | } 121 | } 122 | 123 | /// What happens when [`Charges`] are replenished? 124 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)] 125 | pub enum ReplenishStrategy { 126 | /// A single charge will be recovered. 127 | /// 128 | /// Usually paired with [`CooldownStrategy::ConstantlyRefresh`]. 129 | #[default] 130 | OneAtATime, 131 | /// All charges will be recovered. 132 | /// 133 | /// Usually paired with [`CooldownStrategy::RefreshWhenEmpty`]. 134 | AllAtOnce, 135 | } 136 | 137 | /// How do these charges replenish when cooldowns are refreshed? 138 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)] 139 | pub enum CooldownStrategy { 140 | /// Cooldowns refresh will have no effect on the charges. 141 | Ignore, 142 | /// Cooldowns will replenish charges whenever the current charges are less than the max. 143 | /// 144 | /// Usually paired with [`ReplenishStrategy::OneAtATime`]. 145 | #[default] 146 | ConstantlyRefresh, 147 | /// Cooldowns will only replenish charges when 0 charges are available. 148 | /// 149 | /// Usually paired with [`ReplenishStrategy::AllAtOnce`]. 150 | RefreshWhenEmpty, 151 | } 152 | 153 | impl ChargeState { 154 | /// Creates a new [`ChargeState`] from an iterator of `(charges, action)` pairs 155 | /// 156 | /// If a [`Charges`] is not provided for an action, that action will be treated as if a charge was always available. 157 | /// 158 | /// To create an empty [`ChargeState`] struct, use the [`Default::default`] method instead. 159 | /// 160 | /// # Example 161 | /// ```rust 162 | /// use bevy::{input::keyboard::KeyCode, reflect::Reflect}; 163 | /// use leafwing_abilities::prelude::*; 164 | /// use leafwing_input_manager::Actionlike; 165 | /// 166 | /// #[derive(Actionlike, Abilitylike, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect)] 167 | /// enum Action { 168 | /// Run, 169 | /// Jump, 170 | /// Shoot, 171 | /// Dash, 172 | /// } 173 | /// 174 | /// let charge_state = ChargeState::new([ 175 | /// (Action::Shoot, Charges::replenish_all(6)), 176 | /// (Action::Dash, Charges::replenish_one(2)), 177 | /// ]); 178 | /// ``` 179 | #[must_use] 180 | pub fn new(action_chargestate_pairs: impl IntoIterator) -> Self { 181 | let mut charge_state = ChargeState::default(); 182 | for (action, charges) in action_chargestate_pairs.into_iter() { 183 | charge_state.set(action, charges); 184 | } 185 | charge_state 186 | } 187 | 188 | /// Is at least one charge available for `action`? 189 | /// 190 | /// Returns `true` if the underlying [`Charges`] is [`None`]. 191 | #[inline] 192 | #[must_use] 193 | pub fn available(&self, action: &A) -> bool { 194 | if let Some(charges) = self.get(action) { 195 | charges.available() 196 | } else { 197 | true 198 | } 199 | } 200 | 201 | /// Spends one charge for `action` if able. 202 | /// 203 | /// Returns a boolean indicating whether a charge was available. 204 | /// If no charges are available, `false` is returned and this call has no effect. 205 | /// 206 | /// Returns `true` if the underlying [`Charges`] is [`None`]. 207 | #[inline] 208 | pub fn expend(&mut self, action: &A) -> Result<(), CannotUseAbility> { 209 | if let Some(charges) = self.get_mut(action) { 210 | charges.expend() 211 | } else { 212 | Ok(()) 213 | } 214 | } 215 | 216 | /// Replenishes charges of `action`, up to its max charges. 217 | /// 218 | /// The exact effect is determined by the [`Charges`]'s [`ReplenishStrategy`]. 219 | /// If the `action` is not associated with a [`Charges`], this has no effect. 220 | #[inline] 221 | pub fn replenish(&mut self, action: &A) { 222 | if let Some(charges) = self.get_mut(action) { 223 | charges.replenish(); 224 | } 225 | } 226 | 227 | /// Returns a reference to the underlying [`Charges`] for `action`, if set. 228 | #[inline] 229 | #[must_use] 230 | pub fn get(&self, action: &A) -> Option<&Charges> { 231 | self.charges_map.get(action) 232 | } 233 | 234 | /// Returns a mutable reference to the underlying [`Charges`] for `action`, if set. 235 | #[inline] 236 | #[must_use] 237 | pub fn get_mut(&mut self, action: &A) -> Option<&mut Charges> { 238 | self.charges_map.get_mut(action) 239 | } 240 | 241 | /// Sets the underlying [`Charges`] for `action` to the provided value. 242 | /// 243 | /// Unless you're building a new [`ChargeState`] struct, you likely want to use [`Self::get_mut`]. 244 | #[inline] 245 | pub fn set(&mut self, action: A, charges: Charges) -> &mut Self { 246 | self.charges_map.insert(action, charges); 247 | 248 | self 249 | } 250 | 251 | /// Collects a `&mut Self` into a `Self`. 252 | /// 253 | /// Used to conclude the builder pattern. Actually just calls `self.clone()`. 254 | #[inline] 255 | #[must_use] 256 | pub fn build(&mut self) -> Self { 257 | self.clone() 258 | } 259 | 260 | /// Returns an iterator of references to the underlying non-[`None`] [`Charges`] 261 | #[inline] 262 | pub fn iter(&self) -> impl Iterator { 263 | self.charges_map.values() 264 | } 265 | 266 | /// Returns an iterator of mutable references to the underlying non-[`None`] [`Charges`] 267 | #[inline] 268 | pub fn iter_mut(&mut self) -> impl Iterator { 269 | self.charges_map.values_mut() 270 | } 271 | } 272 | 273 | impl Charges { 274 | /// Creates a new [`Charges`], which can be expended `max_charges` times before needing to be replenished. 275 | /// 276 | /// The current charges will be set to the max charges by default. 277 | #[inline] 278 | #[must_use] 279 | pub fn new( 280 | max_charges: u8, 281 | replenish_strat: ReplenishStrategy, 282 | cooldown_strat: CooldownStrategy, 283 | ) -> Charges { 284 | Charges { 285 | current: max_charges, 286 | max: max_charges, 287 | replenish_strat, 288 | cooldown_strat, 289 | } 290 | } 291 | 292 | /// Creates a new [`Charges`] with [`ReplenishStrategy::OneAtATime`] and [`CooldownStrategy::Ignore`]. 293 | pub fn simple(max_charges: u8) -> Charges { 294 | Charges { 295 | current: max_charges, 296 | max: max_charges, 297 | replenish_strat: ReplenishStrategy::OneAtATime, 298 | cooldown_strat: CooldownStrategy::Ignore, 299 | } 300 | } 301 | 302 | /// Creates a new [`Charges`] with [`ReplenishStrategy::AllAtOnce`] and [`CooldownStrategy::Ignore`]. 303 | pub fn ammo(max_charges: u8) -> Charges { 304 | Charges { 305 | current: max_charges, 306 | max: max_charges, 307 | replenish_strat: ReplenishStrategy::AllAtOnce, 308 | cooldown_strat: CooldownStrategy::Ignore, 309 | } 310 | } 311 | 312 | /// Creates a new [`Charges`] with [`ReplenishStrategy::OneAtATime`] and [`CooldownStrategy::ConstantlyRefresh`]. 313 | pub fn replenish_one(max_charges: u8) -> Charges { 314 | Charges { 315 | current: max_charges, 316 | max: max_charges, 317 | replenish_strat: ReplenishStrategy::OneAtATime, 318 | cooldown_strat: CooldownStrategy::ConstantlyRefresh, 319 | } 320 | } 321 | 322 | /// Creates a new [`Charges`] with [`ReplenishStrategy::AllAtOnce`] and [`CooldownStrategy::RefreshWhenEmpty`]. 323 | pub fn replenish_all(max_charges: u8) -> Charges { 324 | Charges { 325 | current: max_charges, 326 | max: max_charges, 327 | replenish_strat: ReplenishStrategy::AllAtOnce, 328 | cooldown_strat: CooldownStrategy::RefreshWhenEmpty, 329 | } 330 | } 331 | 332 | /// The current number of available charges 333 | #[inline] 334 | #[must_use] 335 | pub fn charges(&self) -> u8 { 336 | self.current 337 | } 338 | 339 | /// The maximum number of available charges 340 | #[inline] 341 | #[must_use] 342 | pub fn max_charges(&self) -> u8 { 343 | self.max 344 | } 345 | 346 | /// Adds `charges` to the current number of available charges 347 | /// 348 | /// This will never exceed the maximum number of charges. 349 | /// Returns the number of excess charges. 350 | #[inline] 351 | #[must_use] 352 | pub fn add_charges(&mut self, charges: u8) -> u8 { 353 | let new_total = self.current.saturating_add(charges); 354 | 355 | let excess = new_total.saturating_sub(self.max); 356 | self.current = new_total.min(self.max); 357 | excess 358 | } 359 | 360 | /// Set the current number of available charges 361 | /// 362 | /// This will never exceed the maximum number of charges. 363 | /// Returns the number of excess charges. 364 | #[inline] 365 | pub fn set_charges(&mut self, charges: u8) -> u8 { 366 | let excess = charges.saturating_sub(self.max); 367 | self.current = charges.min(self.max); 368 | excess 369 | } 370 | 371 | /// Set the maximmum number of available charges 372 | /// 373 | /// If the number of charges available is greater than this number, it will be reduced to the new cap. 374 | #[inline] 375 | pub fn set_max_charges(&mut self, max_charges: u8) { 376 | self.max = max_charges; 377 | self.current = self.current.min(self.max); 378 | } 379 | 380 | /// Is at least one charge available? 381 | #[inline] 382 | #[must_use] 383 | pub fn available(&self) -> bool { 384 | self.current > 0 385 | } 386 | 387 | /// Spends one charge for `action` if able. 388 | /// 389 | /// Returns a [`Result`] indicating whether a charge was available. 390 | /// If no charges are available, [`CannotUseAbility::NoCharges`] is returned and this call has no effect. 391 | #[inline] 392 | pub fn expend(&mut self) -> Result<(), CannotUseAbility> { 393 | if self.current == 0 { 394 | return Err(CannotUseAbility::NoCharges); 395 | } 396 | 397 | self.current = self.current.saturating_sub(1); 398 | Ok(()) 399 | } 400 | 401 | /// Replenishes charges of `action`, up to its max charges. 402 | /// 403 | /// The exact effect is determined by the [`ReplenishStrategy`] for this struct. 404 | #[inline] 405 | pub fn replenish(&mut self) { 406 | let charges_to_add = match self.replenish_strat { 407 | ReplenishStrategy::OneAtATime => 1, 408 | ReplenishStrategy::AllAtOnce => self.max, 409 | }; 410 | 411 | // We don't care about overflowing our charges here. 412 | let _ = self.add_charges(charges_to_add); 413 | } 414 | } 415 | 416 | #[cfg(test)] 417 | mod tests { 418 | use super::*; 419 | 420 | #[test] 421 | fn charges_start_full() { 422 | let charges = Charges::simple(3); 423 | assert_eq!(charges.charges(), 3); 424 | assert_eq!(charges.max_charges(), 3); 425 | } 426 | 427 | #[test] 428 | fn charges_available() { 429 | let mut charges = Charges::simple(3); 430 | assert!(charges.available()); 431 | charges.set_charges(1); 432 | assert!(charges.available()); 433 | charges.set_charges(0); 434 | assert!(!charges.available()); 435 | } 436 | 437 | #[test] 438 | fn charges_deplete() { 439 | let mut charges = Charges::simple(2); 440 | charges.expend().unwrap(); 441 | assert_eq!(charges.charges(), 1); 442 | charges.expend().unwrap(); 443 | assert_eq!(charges.charges(), 0); 444 | assert_eq!(charges.expend(), Err(CannotUseAbility::NoCharges)); 445 | assert_eq!(charges.charges(), 0); 446 | } 447 | 448 | #[test] 449 | fn charges_replenish_one_at_a_time() { 450 | let mut charges = Charges::replenish_one(3); 451 | charges.set_charges(0); 452 | assert_eq!(charges.charges(), 0); 453 | charges.replenish(); 454 | assert_eq!(charges.charges(), 1); 455 | charges.replenish(); 456 | assert_eq!(charges.charges(), 2); 457 | charges.replenish(); 458 | assert_eq!(charges.charges(), 3); 459 | charges.replenish(); 460 | assert_eq!(charges.charges(), 3); 461 | } 462 | 463 | #[test] 464 | fn charges_replenish_all_at_once() { 465 | let mut charges = Charges::replenish_all(3); 466 | charges.set_charges(0); 467 | assert_eq!(charges.charges(), 0); 468 | charges.replenish(); 469 | assert_eq!(charges.charges(), 3); 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/cooldown.rs: -------------------------------------------------------------------------------- 1 | //! Cooldowns tick down until actions are ready to be used. 2 | 3 | use crate::{ 4 | charges::{ChargeState, Charges}, 5 | Abilitylike, CannotUseAbility, 6 | }; 7 | 8 | use bevy::{ 9 | ecs::prelude::{Component, Resource}, 10 | reflect::Reflect, 11 | }; 12 | use core::time::Duration; 13 | use serde::{Deserialize, Serialize}; 14 | use std::{collections::HashMap, fmt::Display, marker::PhantomData}; 15 | 16 | /// The time until each action of type `A` can be used again. 17 | /// 18 | /// Each action may be associated with a [`Cooldown`]. 19 | /// If it is not, it always be treated as being ready. 20 | /// 21 | /// This is typically paired with an [`ActionState`](crate::action_state::ActionState): 22 | /// if the action state is just-pressed (or another triggering condition is met), 23 | /// and the cooldown is ready, then perform the action and trigger the cooldown. 24 | /// 25 | /// This type is included as part of the [`InputManagerBundle`](crate::InputManagerBundle), 26 | /// but can also be used as a resource for singleton game objects. 27 | /// 28 | /// 29 | /// ```rust 30 | /// use bevy::{utils::Duration, reflect::Reflect}; 31 | /// use leafwing_abilities::prelude::*; 32 | /// use leafwing_input_manager::prelude::*; 33 | /// 34 | /// #[derive(Actionlike, Abilitylike, Clone, Copy, Hash, PartialEq, Eq, Debug, Reflect)] 35 | /// enum Action { 36 | /// Run, 37 | /// Jump, 38 | /// } 39 | /// 40 | /// let mut action_state = ActionState::::default(); 41 | /// let mut cooldowns = CooldownState::new([(Action::Jump, Cooldown::from_secs(1.))]); 42 | /// 43 | /// action_state.press(&Action::Jump); 44 | /// 45 | /// // This will only perform a limited check; consider using the `Abilitylike::ready` method instead 46 | /// if action_state.just_pressed(&Action::Jump) && cooldowns.ready(&Action::Jump).is_ok() { 47 | /// // Actually do the jumping thing here 48 | /// // Remember to actually begin the cooldown if you jumped! 49 | /// cooldowns.trigger(&Action::Jump); 50 | /// } 51 | /// 52 | /// // We just jumped, so the cooldown isn't ready yet 53 | /// assert_eq!(cooldowns.ready(&Action::Jump), Err(CannotUseAbility::OnCooldown)); 54 | /// ``` 55 | #[derive(Resource, Component, Debug, Clone, PartialEq, Eq, Reflect)] 56 | pub struct CooldownState { 57 | /// The [`Cooldown`] of each action 58 | /// 59 | /// If [`None`], the action can always be used 60 | cooldown_map: HashMap, 61 | /// A shared cooldown between all actions of type `A`. 62 | /// 63 | /// No action of type `A` will be ready unless this is ready. 64 | /// Whenever any cooldown for an action of type `A` is triggered, 65 | /// this global cooldown is triggered. 66 | pub global_cooldown: Option, 67 | #[reflect(ignore)] 68 | _phantom: PhantomData, 69 | } 70 | 71 | impl Default for CooldownState { 72 | /// By default, cooldowns are not set. 73 | fn default() -> Self { 74 | CooldownState { 75 | cooldown_map: HashMap::new(), 76 | global_cooldown: None, 77 | _phantom: PhantomData, 78 | } 79 | } 80 | } 81 | 82 | impl CooldownState { 83 | /// Creates a new [`CooldownState`] from an iterator of `(cooldown, action)` pairs 84 | /// 85 | /// If a [`Cooldown`] is not provided for an action, that action will be treated as if its cooldown is always available. 86 | /// 87 | /// To create an empty [`CooldownState`] struct, use the [`Default::default`] method instead. 88 | /// 89 | /// # Example 90 | /// ```rust 91 | /// use bevy::{input::keyboard::KeyCode, reflect::Reflect}; 92 | /// use leafwing_abilities::cooldown::{Cooldown, CooldownState}; 93 | /// use leafwing_abilities::Abilitylike; 94 | /// use leafwing_input_manager::Actionlike; 95 | /// 96 | /// #[derive(Actionlike, Abilitylike, Clone, Copy, Hash, PartialEq, Eq, Debug, Reflect)] 97 | /// enum Action { 98 | /// Run, 99 | /// Jump, 100 | /// Shoot, 101 | /// Dash, 102 | /// } 103 | /// 104 | /// let input_map = CooldownState::new([ 105 | /// (Action::Shoot, Cooldown::from_secs(0.1)), 106 | /// (Action::Dash, Cooldown::from_secs(1.0)), 107 | /// ]); 108 | /// ``` 109 | #[must_use] 110 | pub fn new(action_cooldown_pairs: impl IntoIterator) -> Self { 111 | let mut cooldowns = CooldownState::default(); 112 | for (action, cooldown) in action_cooldown_pairs.into_iter() { 113 | cooldowns.set(action, cooldown); 114 | } 115 | cooldowns 116 | } 117 | 118 | /// Triggers the cooldown of the `action` if it is available to be used. 119 | /// 120 | /// This can be paired with [`Cooldowns::ready`], 121 | /// to check if the action can be used before triggering its cooldown, 122 | /// or this can be used on its own, 123 | /// reading the returned [`Result`] to determine if the ability was used. 124 | #[inline] 125 | pub fn trigger(&mut self, action: &A) -> Result<(), CannotUseAbility> { 126 | // Call `ready` here so that we don't trigger the actions cooldown when the GCD might fail 127 | self.ready(action)?; 128 | 129 | if let Some(cooldown) = self.get_mut(action) { 130 | cooldown.trigger()?; 131 | } 132 | 133 | if let Some(global_cooldown) = self.global_cooldown.as_mut() { 134 | global_cooldown.trigger()?; 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | /// Can the corresponding `action` be used? 141 | /// 142 | /// This will be `Ok` if the underlying [`Cooldown::ready`] call is true, 143 | /// or if no cooldown is stored for this action. 144 | #[inline] 145 | pub fn ready(&self, action: &A) -> Result<(), CannotUseAbility> { 146 | if let Some(cooldown) = self.get(action) { 147 | cooldown.ready()?; 148 | } 149 | 150 | self.gcd_ready() 151 | } 152 | 153 | /// Has the global cooldown for actions of type `A` expired? 154 | /// 155 | /// Returns `Ok(())` if no GCD is set. 156 | #[inline] 157 | pub fn gcd_ready(&self) -> Result<(), CannotUseAbility> { 158 | if let Some(global_cooldown) = self.global_cooldown.as_ref() { 159 | global_cooldown 160 | .ready() 161 | .map_err(|_| CannotUseAbility::OnGlobalCooldown) 162 | } else { 163 | Ok(()) 164 | } 165 | } 166 | 167 | /// Advances each underlying [`Cooldown`] according to the elapsed `delta_time`. 168 | /// 169 | /// When you have a [`Option>>`](bevy::ecs::change_detection::Mut), 170 | /// use `charges.map(|res| res.into_inner())` to convert it to the correct form. 171 | pub fn tick(&mut self, delta_time: Duration, maybe_charges: Option<&mut ChargeState>) { 172 | if let Some(charge_state) = maybe_charges { 173 | for (action, cooldown) in self.cooldown_map.iter_mut() { 174 | let charges = charge_state.get_mut(action); 175 | cooldown.tick(delta_time, charges); 176 | } 177 | } else { 178 | for (_, cooldown) in self.cooldown_map.iter_mut() { 179 | cooldown.tick(delta_time, None); 180 | } 181 | } 182 | 183 | if let Some(global_cooldown) = self.global_cooldown.as_mut() { 184 | global_cooldown.tick(delta_time, None); 185 | } 186 | } 187 | 188 | /// The cooldown associated with the specified `action`, if any. 189 | #[inline] 190 | #[must_use] 191 | pub fn get(&self, action: &A) -> Option<&Cooldown> { 192 | self.cooldown_map.get(action) 193 | } 194 | 195 | /// A mutable reference to the cooldown associated with the specified `action`, if any. 196 | #[inline] 197 | #[must_use] 198 | pub fn get_mut(&mut self, action: &A) -> Option<&mut Cooldown> { 199 | self.cooldown_map.get_mut(action) 200 | } 201 | 202 | /// Set a cooldown for the specified `action`. 203 | /// 204 | /// If a cooldown already existed, it will be replaced by a new cooldown with the specified duration. 205 | #[inline] 206 | pub fn set(&mut self, action: A, cooldown: Cooldown) -> &mut Self { 207 | self.cooldown_map.insert(action, cooldown); 208 | self 209 | } 210 | 211 | /// Collects a `&mut Self` into a `Self`. 212 | /// 213 | /// Used to conclude the builder pattern. Actually just calls `self.clone()`. 214 | #[inline] 215 | #[must_use] 216 | pub fn build(&mut self) -> Self { 217 | self.clone() 218 | } 219 | 220 | /// Returns an iterator of references to the underlying non-[`None`] [`Cooldown`]s 221 | #[inline] 222 | pub fn iter(&self) -> impl Iterator { 223 | self.cooldown_map.values() 224 | } 225 | 226 | /// Returns an iterator of mutable references to the underlying non-[`None`] [`Cooldown`]s 227 | #[inline] 228 | pub fn iter_mut(&mut self) -> impl Iterator { 229 | self.cooldown_map.values_mut() 230 | } 231 | } 232 | 233 | /// A timer-like struct that records the amount of time until an action is available to be used again. 234 | /// 235 | /// Cooldowns are typically stored in an [`ActionState`](crate::action_state::ActionState), associated with an action that is to be 236 | /// cooldown-regulated. 237 | /// 238 | /// When initialized, cooldowns are always fully available. 239 | /// 240 | /// ```rust 241 | /// use core::time::Duration; 242 | /// use leafwing_abilities::cooldown::Cooldown; 243 | /// use leafwing_abilities::CannotUseAbility; 244 | /// 245 | /// let mut cooldown = Cooldown::new(Duration::from_secs(3)); 246 | /// assert_eq!(cooldown.remaining(), Duration::ZERO); 247 | /// 248 | /// cooldown.trigger(); 249 | /// assert_eq!(cooldown.remaining(), Duration::from_secs(3)); 250 | /// 251 | /// cooldown.tick(Duration::from_secs(1), None); 252 | /// assert_eq!(cooldown.ready(), Err(CannotUseAbility::OnCooldown)); 253 | /// 254 | /// cooldown.tick(Duration::from_secs(5), None); 255 | /// let triggered = cooldown.trigger(); 256 | /// assert!(triggered.is_ok()); 257 | /// 258 | /// cooldown.refresh(); 259 | /// assert!(cooldown.ready().is_ok()); 260 | /// ``` 261 | #[derive(Clone, Default, PartialEq, Eq, Debug, Serialize, Deserialize, Reflect)] 262 | pub struct Cooldown { 263 | max_time: Duration, 264 | /// The amount of time that has elapsed since all [`Charges`](crate::charges::Charges) were fully replenished. 265 | elapsed_time: Duration, 266 | } 267 | 268 | impl Cooldown { 269 | /// Creates a new [`Cooldown`], which will take `max_time` after it is used until it is ready again. 270 | /// 271 | /// # Panics 272 | /// 273 | /// The provided max time cannot be [`Duration::ZERO`]. 274 | /// Instead, use [`None`] in the [`Cooldowns`] struct for an action without a cooldown. 275 | pub fn new(max_time: Duration) -> Cooldown { 276 | assert!(max_time != Duration::ZERO); 277 | 278 | Cooldown { 279 | max_time, 280 | elapsed_time: max_time, 281 | } 282 | } 283 | 284 | /// Creates a new [`Cooldown`] with a [`f32`] number of seconds, which will take `max_time` after it is used until it is ready again. 285 | /// 286 | /// # Panics 287 | /// 288 | /// The provided max time must be greater than 0. 289 | /// Instead, use [`None`] in the [`CooldownState`] struct for an action without a cooldown. 290 | pub fn from_secs(max_time: f32) -> Cooldown { 291 | assert!(max_time > 0.); 292 | let max_time = Duration::from_secs_f32(max_time); 293 | 294 | Cooldown::new(max_time) 295 | } 296 | 297 | /// Advance the cooldown by `delta_time`. 298 | /// 299 | /// If the elapsed time is enough to reset the cooldown, the number of available charges will 300 | /// increase by one. 301 | pub fn tick(&mut self, delta_time: Duration, charges: Option<&mut Charges>) { 302 | // Don't tick cooldowns when they are fully elapsed 303 | if self.elapsed_time == self.max_time { 304 | return; 305 | } 306 | 307 | assert!(self.max_time != Duration::ZERO); 308 | 309 | if let Some(charges) = charges { 310 | let total_time = self.elapsed_time.saturating_add(delta_time); 311 | 312 | let total_nanos: u64 = total_time.as_nanos().try_into().unwrap_or(u64::MAX); 313 | let max_nanos: u64 = self.max_time.as_nanos().try_into().unwrap_or(u64::MAX); 314 | 315 | let n_completed = (total_nanos / max_nanos).try_into().unwrap_or(u8::MAX); 316 | let extra_time = Duration::from_nanos(total_nanos % max_nanos); 317 | 318 | let excess_completions = charges.add_charges(n_completed); 319 | if excess_completions == 0 { 320 | self.elapsed_time = 321 | (self.elapsed_time.saturating_add(extra_time)).min(self.max_time); 322 | } else { 323 | self.elapsed_time = self.max_time; 324 | } 325 | } else { 326 | self.elapsed_time = self 327 | .elapsed_time 328 | .saturating_add(delta_time) 329 | .min(self.max_time); 330 | } 331 | } 332 | 333 | /// Is this action ready to be used? 334 | /// 335 | /// This will be true if and only if at least one charge is available. 336 | /// For cooldowns without charges, this will be true if `time_remaining` is [`Duration::Zero`]. 337 | pub fn ready(&self) -> Result<(), CannotUseAbility> { 338 | match self.elapsed_time >= self.max_time { 339 | true => Ok(()), 340 | false => Err(CannotUseAbility::OnCooldown), 341 | } 342 | } 343 | 344 | /// Refreshes the cooldown, causing the underlying action to be ready to use immediately. 345 | /// 346 | /// If this cooldown has charges, the number of available charges is increased by one (but the point within the cycle is unchanged). 347 | #[inline] 348 | pub fn refresh(&mut self) { 349 | self.elapsed_time = self.max_time 350 | } 351 | 352 | /// Use the underlying cooldown if and only if it is ready, resetting the cooldown to its maximum value. 353 | /// 354 | /// If this cooldown has multiple charges, only one will be consumed. 355 | /// 356 | /// Returns a result indicating whether the cooldown was ready. 357 | /// If the cooldown was not ready, [`CannotUseAbility::OnCooldown`] is returned and this call has no effect. 358 | #[inline] 359 | pub fn trigger(&mut self) -> Result<(), CannotUseAbility> { 360 | self.ready()?; 361 | self.elapsed_time = Duration::ZERO; 362 | 363 | Ok(()) 364 | } 365 | 366 | /// Returns the time that it will take for this action to be ready to use again after being triggered. 367 | #[inline] 368 | pub fn max_time(&self) -> Duration { 369 | self.max_time 370 | } 371 | 372 | /// Sets the time that it will take for this action to be ready to use again after being triggered. 373 | /// 374 | /// If the current time remaining is greater than the new max time, it will be clamped to the `max_time`. 375 | /// 376 | /// # Panics 377 | /// 378 | /// The provided max time cannot be [`Duration::ZERO`]. 379 | /// Instead, use [`None`] in the [`Cooldowns`] struct for an action without a cooldown. 380 | #[inline] 381 | pub fn set_max_time(&mut self, max_time: Duration) { 382 | assert!(max_time != Duration::ZERO); 383 | 384 | self.max_time = max_time; 385 | self.elapsed_time = self.elapsed_time.min(max_time); 386 | } 387 | 388 | /// Returns the time that has passed since the cooldown was triggered. 389 | #[inline] 390 | pub fn elapsed(&self) -> Duration { 391 | self.elapsed_time 392 | } 393 | 394 | /// Sets the time that has passed since the cooldown was triggered. 395 | /// 396 | /// This will always be clamped between [`Duration::ZERO`] and the `max_time` of this cooldown. 397 | #[inline] 398 | pub fn set_elapsed(&mut self, elapsed_time: Duration) { 399 | self.elapsed_time = elapsed_time.clamp(Duration::ZERO, self.max_time); 400 | } 401 | 402 | /// Returns the time remaining until the next charge is ready. 403 | /// 404 | /// When a cooldown is fully charged, this will return [`Duration::ZERO`]. 405 | #[inline] 406 | pub fn remaining(&self) -> Duration { 407 | self.max_time.saturating_sub(self.elapsed_time) 408 | } 409 | 410 | /// Sets the time remaining until the next charge is ready. 411 | /// 412 | /// This will always be clamped between [`Duration::ZERO`] and the `max_time` of this cooldown. 413 | #[inline] 414 | pub fn set_remaining(&mut self, time_remaining: Duration) { 415 | self.elapsed_time = self 416 | .max_time 417 | .saturating_sub(time_remaining.clamp(Duration::ZERO, self.max_time)); 418 | } 419 | } 420 | 421 | impl Display for Cooldown { 422 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 423 | write!(f, "{:?} / {:?}", self.elapsed_time, self.max_time) 424 | } 425 | } 426 | 427 | #[cfg(test)] 428 | mod tick_tests { 429 | use super::*; 430 | 431 | #[test] 432 | #[should_panic] 433 | fn zero_duration_cooldown_cannot_be_constructed() { 434 | Cooldown::new(Duration::ZERO); 435 | } 436 | 437 | #[test] 438 | fn tick_has_no_effect_on_fresh_cooldown() { 439 | let cooldown = Cooldown::from_secs(1.); 440 | let mut cloned_cooldown = cooldown.clone(); 441 | cloned_cooldown.tick(Duration::from_secs_f32(1.234), None); 442 | assert_eq!(cooldown, cloned_cooldown); 443 | } 444 | 445 | #[test] 446 | fn cooldowns_start_ready() { 447 | let cooldown = Cooldown::from_secs(1.); 448 | assert!(cooldown.ready().is_ok()); 449 | } 450 | 451 | #[test] 452 | fn cooldowns_are_ready_when_refreshed() { 453 | let mut cooldown = Cooldown::from_secs(1.); 454 | assert!(cooldown.ready().is_ok()); 455 | let _ = cooldown.trigger(); 456 | assert_eq!(cooldown.ready(), Err(CannotUseAbility::OnCooldown)); 457 | cooldown.refresh(); 458 | assert!(cooldown.ready().is_ok()); 459 | } 460 | 461 | #[test] 462 | fn ticking_changes_cooldown() { 463 | let cooldown = Cooldown::new(Duration::from_millis(1000)); 464 | let mut cloned_cooldown = cooldown.clone(); 465 | let _ = cloned_cooldown.trigger(); 466 | assert_ne!(cooldown, cloned_cooldown); 467 | 468 | cloned_cooldown.tick(Duration::from_millis(123), None); 469 | assert_ne!(cooldown, cloned_cooldown); 470 | } 471 | 472 | #[test] 473 | fn cooldowns_reset_after_being_ticked() { 474 | let mut cooldown = Cooldown::from_secs(1.); 475 | let _ = cooldown.trigger(); 476 | assert_eq!(cooldown.ready(), Err(CannotUseAbility::OnCooldown)); 477 | 478 | cooldown.tick(Duration::from_secs(3), None); 479 | assert!(cooldown.ready().is_ok()); 480 | } 481 | 482 | #[test] 483 | fn time_remaining_on_fresh_cooldown_is_zero() { 484 | let cooldown = Cooldown::from_secs(1.); 485 | assert_eq!(cooldown.remaining(), Duration::ZERO); 486 | } 487 | } 488 | --------------------------------------------------------------------------------