generalize cooldown
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::{
|
||||
client::{audio::SoundSettings, control::CharacterInputEnabled},
|
||||
loading_assets::UIAssets,
|
||||
protocol::PlaySound,
|
||||
utils::Cooldown,
|
||||
};
|
||||
use bevy::{color::palettes::css::BLACK, prelude::*};
|
||||
|
||||
@@ -26,9 +27,6 @@ struct VolumeValue(ProgressBar);
|
||||
#[derive(Resource)]
|
||||
struct PauseMenuSelection(ProgressBar);
|
||||
|
||||
#[derive(Resource)]
|
||||
struct VolumeChangeCooldown(Timer);
|
||||
|
||||
#[derive(Message)]
|
||||
struct VolumeChangeEvent {
|
||||
direction: f32,
|
||||
@@ -108,10 +106,6 @@ fn setup(mut commands: Commands, assets: Res<UIAssets>, settings: Res<SoundSetti
|
||||
));
|
||||
|
||||
commands.insert_resource(PauseMenuSelection(ProgressBar::Music));
|
||||
commands.insert_resource(VolumeChangeCooldown(Timer::from_seconds(
|
||||
0.09,
|
||||
TimerMode::Repeating,
|
||||
)));
|
||||
}
|
||||
|
||||
fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundle {
|
||||
@@ -280,22 +274,15 @@ fn apply_volume_change(
|
||||
mut commands: Commands,
|
||||
selection: Res<PauseMenuSelection>,
|
||||
mut settings: ResMut<SoundSettings>,
|
||||
mut cooldown: ResMut<VolumeChangeCooldown>,
|
||||
time: Res<Time>,
|
||||
mut cooldown: Cooldown<90>,
|
||||
mut events: MessageReader<VolumeChangeEvent>,
|
||||
) {
|
||||
let Some(event) = events.read().last() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// On first press, apply immediately and reset timer
|
||||
if event.just_pressed {
|
||||
cooldown.0.reset();
|
||||
} else {
|
||||
cooldown.0.tick(time.delta());
|
||||
if !cooldown.0.just_finished() {
|
||||
return;
|
||||
}
|
||||
if !cooldown.ready(event.just_pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
commands.trigger(PlaySound::VolumeChange);
|
||||
|
||||
151
crates/hedz_reloaded/src/utils/cooldown.rs
Normal file
151
crates/hedz_reloaded/src/utils/cooldown.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use bevy::{ecs::system::SystemParam, prelude::*};
|
||||
|
||||
/// A cooldown timer that triggers immediately on first request, then throttles subsequent triggers.
|
||||
///
|
||||
/// Useful for input handling where you want immediate feedback on first action,
|
||||
/// but want to rate-limit continuous actions (like volume control).
|
||||
/// Automatically ticks with time, no manual `time` parameter needed.
|
||||
///
|
||||
/// # Type Parameters
|
||||
/// * `MILLIS` - The cooldown delay in milliseconds between triggers
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// fn handle_input(
|
||||
/// keyboard: Res<ButtonInput<KeyCode>>,
|
||||
/// mut cooldown: Cooldown<50>, // 50ms between repeats
|
||||
/// mut volume: ResMut<Volume>,
|
||||
/// ) {
|
||||
/// let is_new = keyboard.just_pressed(KeyCode::ArrowUp);
|
||||
/// let is_active = keyboard.pressed(KeyCode::ArrowUp);
|
||||
///
|
||||
/// if is_active && cooldown.ready(is_new) {
|
||||
/// volume.0 += 0.01;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(SystemParam)]
|
||||
pub struct Cooldown<'w, 's, const MILLIS: u64> {
|
||||
timer: Local<'s, CooldownTimer<MILLIS>>,
|
||||
time: Res<'w, Time>,
|
||||
}
|
||||
|
||||
struct CooldownTimer<const MILLIS: u64> {
|
||||
timer: Timer,
|
||||
}
|
||||
|
||||
impl<const MILLIS: u64> Default for CooldownTimer<MILLIS> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timer: Timer::from_seconds((MILLIS as f32) / 1000.0, TimerMode::Repeating),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'w, 's, const MILLIS: u64> Cooldown<'w, 's, MILLIS> {
|
||||
/// Check if the cooldown is ready to trigger.
|
||||
///
|
||||
/// Returns `true` if:
|
||||
/// - `reset` is true (immediate trigger and reset timer), or
|
||||
/// - The cooldown timer has finished (subsequent triggers after delay)
|
||||
///
|
||||
/// When `reset` is true, the timer is reset and returns immediately.
|
||||
/// Otherwise, the timer is ticked automatically.
|
||||
pub fn ready(&mut self, reset: bool) -> bool {
|
||||
if reset {
|
||||
self.timer.timer.reset();
|
||||
true
|
||||
} else {
|
||||
self.timer.timer.tick(self.time.delta()).just_finished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct Counter(u32);
|
||||
|
||||
#[test]
|
||||
fn cooldown_triggers_immediately_on_reset() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.init_resource::<Counter>();
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct ShouldReset(bool);
|
||||
|
||||
fn increment_with_cooldown(
|
||||
mut counter: ResMut<Counter>,
|
||||
mut cooldown: Cooldown<50>,
|
||||
should_reset: Res<ShouldReset>,
|
||||
) {
|
||||
if cooldown.ready(should_reset.0) {
|
||||
counter.0 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
app.init_resource::<ShouldReset>();
|
||||
app.add_systems(Update, increment_with_cooldown);
|
||||
|
||||
// First call with reset should trigger immediately
|
||||
app.world_mut().resource_mut::<ShouldReset>().0 = true;
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 1);
|
||||
|
||||
// Without reset, should not trigger
|
||||
app.world_mut().resource_mut::<ShouldReset>().0 = false;
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cooldown_throttles_continuous_triggers() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(MinimalPlugins);
|
||||
app.init_resource::<Counter>();
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct ShouldReset(bool);
|
||||
|
||||
fn increment_with_cooldown(
|
||||
mut counter: ResMut<Counter>,
|
||||
mut cooldown: Cooldown<50>,
|
||||
should_reset: Res<ShouldReset>,
|
||||
) {
|
||||
if cooldown.ready(should_reset.0) {
|
||||
counter.0 += 1;
|
||||
}
|
||||
}
|
||||
|
||||
app.init_resource::<ShouldReset>();
|
||||
app.add_systems(Update, increment_with_cooldown);
|
||||
|
||||
// First call with reset triggers immediately
|
||||
app.world_mut().resource_mut::<ShouldReset>().0 = true;
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 1);
|
||||
|
||||
// Continuous calls without reset should not trigger yet
|
||||
app.world_mut().resource_mut::<ShouldReset>().0 = false;
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 1);
|
||||
|
||||
// Sleep for less than cooldown
|
||||
std::thread::sleep(std::time::Duration::from_millis(40));
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 1);
|
||||
|
||||
// Sleep past cooldown - should trigger again
|
||||
std::thread::sleep(std::time::Duration::from_millis(15));
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 2);
|
||||
|
||||
// Sleep past cooldown again - should trigger a third time
|
||||
std::thread::sleep(std::time::Duration::from_millis(55));
|
||||
app.update();
|
||||
assert_eq!(app.world().resource::<Counter>().0, 3);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod auto_rotate;
|
||||
pub mod billboards;
|
||||
pub mod cooldown;
|
||||
pub mod debounce;
|
||||
pub mod explosions;
|
||||
pub mod observers;
|
||||
@@ -10,6 +11,7 @@ pub mod squish_animation;
|
||||
pub mod trail;
|
||||
|
||||
use bevy::prelude::*;
|
||||
pub use cooldown::Cooldown;
|
||||
pub use debounce::Debounce;
|
||||
pub(crate) use observers::global_observer;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user