generalize cooldown
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::{
|
|||||||
client::{audio::SoundSettings, control::CharacterInputEnabled},
|
client::{audio::SoundSettings, control::CharacterInputEnabled},
|
||||||
loading_assets::UIAssets,
|
loading_assets::UIAssets,
|
||||||
protocol::PlaySound,
|
protocol::PlaySound,
|
||||||
|
utils::Cooldown,
|
||||||
};
|
};
|
||||||
use bevy::{color::palettes::css::BLACK, prelude::*};
|
use bevy::{color::palettes::css::BLACK, prelude::*};
|
||||||
|
|
||||||
@@ -26,9 +27,6 @@ struct VolumeValue(ProgressBar);
|
|||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct PauseMenuSelection(ProgressBar);
|
struct PauseMenuSelection(ProgressBar);
|
||||||
|
|
||||||
#[derive(Resource)]
|
|
||||||
struct VolumeChangeCooldown(Timer);
|
|
||||||
|
|
||||||
#[derive(Message)]
|
#[derive(Message)]
|
||||||
struct VolumeChangeEvent {
|
struct VolumeChangeEvent {
|
||||||
direction: f32,
|
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(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 {
|
fn spawn_progress(bar: ProgressBar, value: u8, font: Handle<Font>) -> impl Bundle {
|
||||||
@@ -280,23 +274,16 @@ fn apply_volume_change(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
selection: Res<PauseMenuSelection>,
|
selection: Res<PauseMenuSelection>,
|
||||||
mut settings: ResMut<SoundSettings>,
|
mut settings: ResMut<SoundSettings>,
|
||||||
mut cooldown: ResMut<VolumeChangeCooldown>,
|
mut cooldown: Cooldown<90>,
|
||||||
time: Res<Time>,
|
|
||||||
mut events: MessageReader<VolumeChangeEvent>,
|
mut events: MessageReader<VolumeChangeEvent>,
|
||||||
) {
|
) {
|
||||||
let Some(event) = events.read().last() else {
|
let Some(event) = events.read().last() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// On first press, apply immediately and reset timer
|
if !cooldown.ready(event.just_pressed) {
|
||||||
if event.just_pressed {
|
|
||||||
cooldown.0.reset();
|
|
||||||
} else {
|
|
||||||
cooldown.0.tick(time.delta());
|
|
||||||
if !cooldown.0.just_finished() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
commands.trigger(PlaySound::VolumeChange);
|
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 auto_rotate;
|
||||||
pub mod billboards;
|
pub mod billboards;
|
||||||
|
pub mod cooldown;
|
||||||
pub mod debounce;
|
pub mod debounce;
|
||||||
pub mod explosions;
|
pub mod explosions;
|
||||||
pub mod observers;
|
pub mod observers;
|
||||||
@@ -10,6 +11,7 @@ pub mod squish_animation;
|
|||||||
pub mod trail;
|
pub mod trail;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
pub use cooldown::Cooldown;
|
||||||
pub use debounce::Debounce;
|
pub use debounce::Debounce;
|
||||||
pub(crate) use observers::global_observer;
|
pub(crate) use observers::global_observer;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user