From 09e6c6678804d2df655fe43e82054fad61c2416e Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 16 Jul 2026 12:47:12 -0700 Subject: [PATCH] Adopt actions framework --- engine/src/lib.rs | 13 +++- life/src/actions.rs | 137 ++++++++++++++++++++++++++++++++++ life/src/main.rs | 177 ++++++++++++++------------------------------ 3 files changed, 202 insertions(+), 125 deletions(-) create mode 100644 life/src/actions.rs diff --git a/engine/src/lib.rs b/engine/src/lib.rs index c0b68f7..79c9579 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -12,9 +12,13 @@ pub use bevy::{ color::palettes::basic::*, feathers::{ FeathersPlugins, + constants::{fonts, size}, + containers::*, controls::*, dark_theme::create_dark_theme, - theme::{ThemeProps, ThemedText, UiTheme}, + font_styles::InheritableFont, + theme::{InheritableThemeTextColor, ThemeProps, ThemedText, UiTheme}, + tokens, }, input::{ common_conditions::*, @@ -25,5 +29,10 @@ pub use bevy::{ platform::collections::{HashMap, HashSet}, prelude::*, sprite_render::MeshMaterial2dTemplate, - ui_widgets::Activate, + text::FontWeight, + ui::{Checked, InteractionDisabled}, + ui_widgets::{ + Activate, ActivateOnPress, Checkbox, SliderPrecision, SliderStep, ValueChange, + checkbox_self_update, slider_self_update, + }, }; diff --git a/life/src/actions.rs b/life/src/actions.rs new file mode 100644 index 0000000..55cad0b --- /dev/null +++ b/life/src/actions.rs @@ -0,0 +1,137 @@ +use super::*; + +pub(crate) struct GameActionsPlugin; + +impl Plugin for GameActionsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + ActionsPlugin::::new() + .bind(SimAction::Toggle, [Binding::Key(KeyCode::Space)]) + .bind(SimAction::Step, [Binding::Key(KeyCode::KeyN)]) + .bind(SimAction::Reset, [Binding::Key(KeyCode::KeyR)]), + ActionsPlugin::::new() + .bind(AudioAction::Toggle, [Binding::Key(KeyCode::KeyM)]), + ActionsPlugin::::new() + .bind(Action::ZoomIn, [Binding::Key(KeyCode::Equal)]) + .bind(Action::ZoomOut, [Binding::Key(KeyCode::Minus)]) + .bind(Action::Quit, [Binding::Key(KeyCode::Escape)]), + )); + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)] +pub(crate) enum SimAction { + #[action_handler(SimAction::toggle)] + Toggle, + #[action_handler(SimAction::step)] + Step, + #[action_handler(SimAction::reset)] + Reset, + #[action_handler(SimAction::wipe)] + Wipe, + #[default] + None, +} + +impl SimAction { + /// When the user toggles between Play/Pause simulation, just update the state + fn toggle( + curr: Res>, + mut next: ResMut>, + mut last_board_state: ResMut, + coordinates: Query<&Coordinates, With>, + ) { + next.set(match curr.get() { + SimulationPlayback::Run | SimulationPlayback::Step => { + // Run | Step -> Pause + SimulationPlayback::Pause + } + SimulationPlayback::Pause => { + // Capture current state before running + last_board_state.coordinates = coordinates.iter().cloned().collect(); + // Pause -> Run + SimulationPlayback::Run + } + }); + } + + // When the user wants to step the simulation, just enter the SimulationStep state + // TODO: Should switch back to Pause automatically after step if Step is the current state + fn step(mut next: ResMut>) { + next.set(SimulationPlayback::Step); + } + + // Reset board to last saved state (before running, or before wiping) + fn reset( + last_board_state: Res, + mut lifecycle: MessageWriter, + cells: Query>, + mut commands: Commands, + ) { + // TODO: reconcile board instead of despawn/spawning the world + cells.iter().for_each(|e| { + commands.entity(e).despawn(); + }); + last_board_state.coordinates.iter().for_each(|c| { + lifecycle.write(Lifecycle::Alive(c.clone())); + }); + } + + // Wipe all cells from the board + fn wipe(cells: Query>, mut commands: Commands) { + cells.iter().for_each(|e| { + commands.entity(e).despawn(); + }); + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)] +pub(crate) enum Action { + #[action_handler(Action::zoom_in)] + ZoomIn, + #[action_handler(Action::zoom_out)] + ZoomOut, + #[action_handler(Action::quit)] + Quit, + #[default] + None, +} + +impl Action { + fn zoom_in(mut projection: Query<&mut Projection>) { + projection.iter_mut().for_each(|mut p| match *p { + Projection::Orthographic(ref mut o) => { + o.scale *= 0.5; + } + _ => todo!(), + }); + } + fn zoom_out(mut projection: Query<&mut Projection>) { + projection.iter_mut().for_each(|mut p| match *p { + Projection::Orthographic(ref mut o) => { + o.scale /= 0.5; + } + _ => todo!(), + }); + } + fn quit(mut writer: MessageWriter) { + writer.write(AppExit::Success); + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction, Default)] +pub(crate) enum AudioAction { + #[action_handler(AudioAction::toggle)] + Toggle, + #[default] + None, +} + +impl AudioAction { + fn toggle(curr: Res>, mut next: ResMut>) { + next.set(match curr.get() { + AudioPlayback::Play => AudioPlayback::Mute, + AudioPlayback::Mute => AudioPlayback::Play, + }); + } +} diff --git a/life/src/main.rs b/life/src/main.rs index d6719da..de35cb2 100644 --- a/life/src/main.rs +++ b/life/src/main.rs @@ -11,23 +11,11 @@ // Only because of FromTemplat macros unfortunately... #![allow(dead_code)] -use bevy::{ - feathers::{ - constants::{fonts, size}, - containers::*, - font_styles::InheritableFont, - theme::{InheritableThemeTextColor, ThemeProps}, - tokens, - }, - text::FontWeight, - ui::{Checked, InteractionDisabled}, - ui_widgets::{ - ActivateOnPress, Checkbox, SliderPrecision, SliderStep, ValueChange, checkbox_self_update, - slider_self_update, - }, -}; use engine::*; +mod actions; +use actions::*; + const TILE: &str = "tileGrey_01.png"; const SIM_FRAME_DURATION: f32 = 0.5; @@ -38,6 +26,7 @@ fn main() { ..default() })) .add_plugins(FeathersPlugins) + .add_plugins(GameActionsPlugin) .init_resource::() .init_resource::() .init_resource::() @@ -58,42 +47,60 @@ fn main() { load_meshes, ), ) + // Cell management/Simulation .add_systems( Update, ( + cell_lifecycle.run_if(on_message::), update_grid_position.run_if(on_message::), - // Run if any component added/removed: AudioSink - // Run if state changed: AudioPlayback - control_volume, - toggle_audio_state.run_if(input_just_pressed(KeyCode::KeyM)), - toggle_playback_state.run_if(input_just_pressed(KeyCode::Space)), de_spawn_cells .run_if(not(is_ui_hovered)) .run_if(not(is_panning)) .run_if(input_just_released(MouseButton::Left)), - assert_cell_uniqueness.run_if(any_with_component::), - simulation_step.run_if(input_just_pressed(KeyCode::ArrowRight)), simulation_step .run_if(in_state(SimulationPlayback::Run)) .run_if(cooldown_secs(SIM_FRAME_DURATION)), simulation_step.run_if(state_changed::), - grid_gizmo, - // TODO: Visualize audio play/mute buttons enable/disable - toggle_interactive::, - // TODO Visualize simulation run/pause buttons enable/disable - toggle_interactive::, + pause_simulation.run_if(in_state(SimulationPlayback::Step)), + update_material, + ), + ) + // Audio Systems + .add_systems( + Update, + ( + control_volume, update_pitch_map_resource_ui.run_if(resource_changed::), reassign_cell_pitch.run_if(resource_changed::), update_audio, - update_material, + ), + ) + // Debugging + .add_systems( + Update, + ( + assert_cell_uniqueness.run_if(any_with_component::), + grid_gizmo, update_debug_info_cell_count, update_debug_info_coordinates, + ), + ) + // Ui Controllers + .add_systems( + Update, + ( + // TODO: Visualize audio play/mute buttons enable/disable + manage_interactive::, + // TODO Visualize simulation run/pause buttons enable/disable + manage_interactive::, update_note_interactivity, ), ) - .add_systems(Update, cell_lifecycle.run_if(on_message::)) - .add_systems(Update, mouse_pan.run_if(on_message::)) - .add_systems(Update, set_pan_state) + // Input handling + .add_systems( + Update, + (mouse_pan.run_if(on_message::), set_pan_state), + ) .run(); } @@ -354,81 +361,29 @@ fn primary_ui() -> impl Scene { } Children [ ui_button(UiButton::Power) - on(|_: On, mut writer: MessageWriter| { - writer.write(AppExit::Success); - }), + ActionSource(Action::Quit), ui_button(UiButton::Pause) SimulationPlayback::Pause - on(|_: On, mut next: ResMut>| { - // Set simulation state - next.set(SimulationPlayback::Pause); - }), + ActionSource(SimAction::Toggle), ui_button(UiButton::Play) SimulationPlayback::Run - on(| - _: On, - mut next: ResMut>, - mut last_board_state: ResMut, - coordinates: Query<&Coordinates, With> - | { - last_board_state.coordinates = coordinates.iter().cloned().collect(); - // Set simulation state - next.set(SimulationPlayback::Run); - }), + ActionSource(SimAction::Toggle), ui_button(UiButton::Step) - on(|_: On, mut next: ResMut>| { - next.set(SimulationPlayback::Step); - }), + ActionSource(SimAction::Step), ui_button(UiButton::Reset) - on(| - _: On, - last_board_state: Res, - mut lifecycle: MessageWriter, - cells: Query>, - mut commands: Commands, - | { - // Todo: reconcile board instead of despawn/spawning the world - cells.iter().for_each(|e| { - commands.entity(e).despawn(); - }); - last_board_state.coordinates.iter().for_each(|c| { - lifecycle.write(Lifecycle::Alive(c.clone())); - }); - }), + ActionSource(SimAction::Reset), ui_button(UiButton::Wipe) - on(|_: On, cells: Query>, mut commands: Commands| { - cells.iter().for_each(|e| { - commands.entity(e).despawn(); - }); - }), + ActionSource(SimAction::Wipe), ui_button(UiButton::ZoomOut) - on(|_: On, mut projection: Query<&mut Projection>| { - projection.iter_mut().for_each(|mut p| match *p { - Projection::Orthographic(ref mut o) => { - o.scale /= 0.5; - } - _ => todo!(), - }); - }), + ActionSource(Action::ZoomOut), ui_button(UiButton::ZoomIn) - on(|_: On, mut projection: Query<&mut Projection>| { - projection.iter_mut().for_each(|mut p| match *p { - Projection::Orthographic(ref mut o) => { - o.scale *= 0.5; - } - _ => todo!(), - }); - }), + ActionSource(Action::ZoomIn), ui_button(UiButton::MuteAudio) AudioPlayback::Mute - on(|_: On, mut next: ResMut>| { - next.set(AudioPlayback::Mute); - }), + ActionSource(AudioAction::Toggle), ui_button(UiButton::PlayAudio) AudioPlayback::Play - on(|_: On, mut next: ResMut>| { - next.set(AudioPlayback::Play); - }), + ActionSource(AudioAction::Toggle), ] ], subpane() @@ -983,35 +938,6 @@ fn load_audio_tones(mut pitches: ResMut>, mut cell_assets: ResMut< debug_assert_eq!(cell_assets.pitches.iter().len(), 132); } -// When volume is toggled on/off make the following updates: -// * AudioPlayback state is flipped -fn toggle_audio_state( - mut next_state: ResMut>, - curr_state: Res>, -) { - // Set next audio playback state - next_state.set( - match curr_state.get() { - AudioPlayback::Play => AudioPlayback::Mute, - AudioPlayback::Mute => AudioPlayback::Play, - } - ); -} - -fn toggle_playback_state( - mut next_state: ResMut>, - curr_state: Res>, -) { - // Set next audio playback state - next_state.set( - match curr_state.get() { - SimulationPlayback::Run => SimulationPlayback::Pause, - SimulationPlayback::Step => SimulationPlayback::Pause, - SimulationPlayback::Pause => SimulationPlayback::Run, - } - ); -} - fn control_volume( audio_state: Res>, simulation_state: Res>, @@ -1131,7 +1057,7 @@ fn create_light_theme() -> ThemeProps { ) } -fn toggle_interactive( +fn manage_interactive( q: Query<(Entity, &T)>, s: Res>, mut commands: Commands, @@ -1259,3 +1185,8 @@ fn update_note_interactivity( }); } } + +// Sets the sim state to paused +fn pause_simulation(mut next: ResMut>) { + next.set(SimulationPlayback::Pause) +}