Skip to content

Commit

Permalink
Store per-action cooldowns (#253)
Browse files Browse the repository at this point in the history
* Add very simple cooldowns

* Add cooldowns to ActionState and ActionData

* Add trigger and can_trigger methods to `ActionState`

* Clean up and demo API for ActionState cooldowns

* Update release notes
  • Loading branch information
alice-i-cecile authored Oct 12, 2022
1 parent 5ba68f2 commit 283d1d4
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 13 deletions.
14 changes: 9 additions & 5 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# Release Notes

## Unreleased
## Version 0.6 (Unreleased)

### Enhancements

- Improve `ToggleActions`.
- Make `_phantom` field public and rename into `phantom`.
- Add `ToggleActions::ENABLED` and `ToggleActions::DISABLED`.
- Added `SingleAxis::negative_only` and `SingleAxis::positive_only` for triggering separate actions for each direction of an axis.
- You can now store `Cooldown`s in the `ActionState` on a per-action basis.
- Set cooldowns for actions using `ActionState::set_cooldown(action, cooldown)`.
- Use `ActionState::ready` with `ActionState::trigger_cooldown` as part of your action loop!
- Cooldowns are automatically elapsed whenever `ActionState::tick` is called (this will happen automatically if you add the plugin).

### Usability

- Implemented `Eq` for `Timing` and `InputMap`.
- Held `ActionState` inputs will now be released when an `InputMap` is removed.
- Improve `ToggleActions`.
- Make `_phantom` field public and rename into `phantom`.
- Add `ToggleActions::ENABLED` and `ToggleActions::DISABLED`.
- Added `SingleAxis::negative_only` and `SingleAxis::positive_only` for triggering separate actions for each direction of an axis.

## Version 0.5

Expand Down
85 changes: 84 additions & 1 deletion src/action_state.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! This module contains [`ActionState`] and its supporting methods and impls.

use crate::cooldown::Cooldown;
use crate::Actionlike;
use crate::{axislike::DualAxisData, buttonlike::ButtonState};

Expand Down Expand Up @@ -33,6 +34,10 @@ pub struct ActionData {
/// Actions that are consumed cannot be pressed again until they are explicitly released.
/// This ensures that consumed actions are not immediately re-pressed by continued inputs.
pub consumed: bool,
/// The time until this action can be used again.
///
/// If [`None`], this action can always be used.
pub cooldown: Option<Cooldown>,
}

/// Stores the canonical input-method-agnostic representation of the inputs received
Expand Down Expand Up @@ -160,7 +165,10 @@ impl<A: Actionlike> ActionState<A> {
self.action_data.iter_mut().for_each(|ad| {
// Durations should not advance while actions are consumed
if !ad.consumed {
ad.timing.tick(current_instant, previous_instant)
ad.timing.tick(current_instant, previous_instant);
if let Some(cooldown) = ad.cooldown.as_mut() {
cooldown.tick(current_instant - previous_instant);
}
}
});
}
Expand Down Expand Up @@ -216,6 +224,81 @@ impl<A: Actionlike> ActionState<A> {
&mut self.action_data[action.index()]
}

/// Triggers the cooldown of the `action` if it is available to be used.
///
/// This should always be paired with [`ActionState::ready`], to check if the action can be used before triggering its cooldown.
///
/// ```rust
/// use leafwing_input_manager::prelude::*;
/// use bevy::utils::Duration;
///
/// #[derive(Actionlike, Clone, Copy, PartialEq, Eq, Debug)]
/// enum Action {
/// Run,
/// Jump,
/// }
/// let mut action_state = ActionState::<Action>::default();
/// action_state.set_cooldown(Action::Jump, Cooldown::new(Duration::from_secs(1)));
/// action_state.press(Action::Jump);
///
/// // WARNING: use the shortcircuiting &&, not & here,
/// // to avoid accidentally triggering the cooldown by side-effect when checking!
/// if action_state.just_pressed(Action::Jump) && action_state.ready(Action::Jump) {
/// // Actually do the jumping thing here
/// // Remember to actually begin the cooldown if you jumped!
/// action_state.trigger_cooldown(Action::Jump);
/// } else {
/// // In this trival test, we just jumped!
/// unreachable!()
/// }
///
/// // We just jumped, so the cooldown isn't ready yet
/// assert!(!action_state.ready(Action::Jump));
/// ```
#[inline]
pub fn trigger_cooldown(&mut self, action: A) {
if let Some(cooldown) = self.action_data_mut(action).cooldown.as_mut() {
cooldown.trigger();
}
}

/// Can the corresponding `action` be used?
///
/// This will be `true` if the underlying [`Cooldown::ready()`] call is true,
/// or if no cooldown is stored for this action.
#[inline]
#[must_use]
pub fn ready(&self, action: A) -> bool {
if let Some(cooldown) = self.action_data(action).cooldown {
cooldown.ready()
} else {
true
}
}

/// The cooldown associated with the specified `action`, if any.
///
/// This returns a clone; use `action_data_mut().cooldown` if you need to mutate the values.
#[inline]
#[must_use]
pub fn cooldown(&self, action: A) -> Option<Cooldown> {
self.action_data(action).cooldown
}

/// Set a cooldown for the specified `action`.
///
/// If a cooldown already existed, it will be replaced by a new cooldown with the specified duration.
#[inline]
pub fn set_cooldown(&mut self, action: A, cooldown: Cooldown) {
self.action_data_mut(action).cooldown.replace(cooldown);
}

/// Remove any cooldown for the specified `action`.
#[inline]
pub fn remove_cooldown(&mut self, action: A) {
self.action_data(action).cooldown.take();
}

/// Get the value associated with the corresponding `action`
///
/// Different kinds of bindings have different ways of calculating the value:
Expand Down
101 changes: 101 additions & 0 deletions src/cooldown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Cooldowns tick down until actions are ready to be used.

use bevy::utils::Duration;
use serde::{Deserialize, Serialize};

/// A timer-like struct that records the amount of time until an action is available to be used again.
///
/// Cooldowns are typically stored in an [`ActionState`](crate::action_state::ActionState), associated with an action that is to be
/// cooldown-regulated.
///
/// When initialized, cooldowns are always fully available.
///
/// ```rust
/// use bevy::utils::Duration;
/// use leafwing_input_manager::cooldown::Cooldown;
///
/// let mut cooldown = Cooldown::new(Duration::from_secs(3));
/// assert_eq!(cooldown.time_remaining(), Duration::ZERO);
///
/// cooldown.trigger();
/// assert_eq!(cooldown.time_remaining(), Duration::from_secs(3));
///
/// cooldown.tick(Duration::from_secs(1));
/// assert!(!cooldown.ready());
///
/// cooldown.tick(Duration::from_secs(5));
/// let triggered = cooldown.trigger();
/// assert!(triggered);
///
/// cooldown.refresh();
/// assert!(cooldown.ready());
/// ```
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub struct Cooldown {
max_time: Duration,
time_remaining: Duration,
}

impl Cooldown {
/// Creates a new [`Cooldown`], which will take `max_time` after it is used until it is ready again.
pub fn new(max_time: Duration) -> Cooldown {
Cooldown {
max_time,
time_remaining: Duration::ZERO,
}
}

/// Advance the cooldown by `delta_time`.
pub fn tick(&mut self, delta_time: Duration) {
self.time_remaining = self.time_remaining.saturating_sub(delta_time);
}

/// Is this action ready to be used?
///
/// This will be true if and only if the `time_remaining` is [`Duration::Zero`].
pub fn ready(&self) -> bool {
self.time_remaining == Duration::ZERO
}

/// Refreshes the cooldown, causing the underlying action to be ready to use immediately.
pub fn refresh(&mut self) {
self.time_remaining = Duration::ZERO;
}

/// Use the underlying cooldown if and only if it is ready, resetting the cooldown to its maximum value.
///
/// Returns a boolean indicating whether the cooldown was ready.
pub fn trigger(&mut self) -> bool {
if self.ready() {
self.time_remaining = self.max_time;
true
} else {
false
}
}

/// Returns the time that it will take for this action to be ready to use again after being triggered.
pub fn max_time(&self) -> Duration {
self.max_time
}

/// Sets the time that it will take for this action to be ready to use again after being triggered.
///
/// If the current time remaining is greater than the new max time, it will be clamped to the `max_time`.
pub fn set_max_time(&mut self, max_time: Duration) {
self.max_time = max_time;
self.time_remaining = self.time_remaining.min(max_time);
}

/// Returns the time remaining until the action is ready to use again.
pub fn time_remaining(&self) -> Duration {
self.time_remaining
}

/// Sets the time remaining until the action is ready to use again.
///
/// This will always be clamped between [`Duration::ZERO`] and the `max_time` of this cooldown.
pub fn set_time_remaining(&mut self, time_remaining: Duration) {
self.time_remaining = time_remaining.clamp(Duration::ZERO, self.max_time);
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub mod action_state;
pub mod axislike;
pub mod buttonlike;
pub mod clashing_inputs;
pub mod cooldown;
mod display_impl;
pub mod errors;
pub mod input_map;
Expand All @@ -31,6 +32,7 @@ pub mod prelude {
pub use crate::axislike::{DualAxis, MouseWheelAxisType, SingleAxis, VirtualDPad};
pub use crate::buttonlike::MouseWheelDirection;
pub use crate::clashing_inputs::ClashStrategy;
pub use crate::cooldown::Cooldown;
pub use crate::input_map::InputMap;
pub use crate::input_mocking::MockInput;
pub use crate::user_input::UserInput;
Expand Down
8 changes: 1 addition & 7 deletions src/user_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,7 @@ impl UserInput {
/// ```
pub fn n_matching(&self, buttons: &HashSet<InputKind>) -> usize {
match self {
UserInput::Single(button) => {
if buttons.contains(button) {
1
} else {
0
}
}
UserInput::Single(button) => usize::from(buttons.contains(button)),
UserInput::Chord(chord_buttons) => {
let mut n_matching = 0;
for button in buttons.iter() {
Expand Down

0 comments on commit 283d1d4

Please sign in to comment.