Adopt actions framework

main
Elijah Voigt 6 days ago
parent 494cdce42e
commit 09e6c66788

@ -12,9 +12,13 @@ pub use bevy::{
color::palettes::basic::*, color::palettes::basic::*,
feathers::{ feathers::{
FeathersPlugins, FeathersPlugins,
constants::{fonts, size},
containers::*,
controls::*, controls::*,
dark_theme::create_dark_theme, dark_theme::create_dark_theme,
theme::{ThemeProps, ThemedText, UiTheme}, font_styles::InheritableFont,
theme::{InheritableThemeTextColor, ThemeProps, ThemedText, UiTheme},
tokens,
}, },
input::{ input::{
common_conditions::*, common_conditions::*,
@ -25,5 +29,10 @@ pub use bevy::{
platform::collections::{HashMap, HashSet}, platform::collections::{HashMap, HashSet},
prelude::*, prelude::*,
sprite_render::MeshMaterial2dTemplate, 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,
},
}; };

@ -0,0 +1,137 @@
use super::*;
pub(crate) struct GameActionsPlugin;
impl Plugin for GameActionsPlugin {
fn build(&self, app: &mut App) {
app.add_plugins((
ActionsPlugin::<SimAction>::new()
.bind(SimAction::Toggle, [Binding::Key(KeyCode::Space)])
.bind(SimAction::Step, [Binding::Key(KeyCode::KeyN)])
.bind(SimAction::Reset, [Binding::Key(KeyCode::KeyR)]),
ActionsPlugin::<AudioAction>::new()
.bind(AudioAction::Toggle, [Binding::Key(KeyCode::KeyM)]),
ActionsPlugin::<Action>::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<State<SimulationPlayback>>,
mut next: ResMut<NextState<SimulationPlayback>>,
mut last_board_state: ResMut<LastBoardState>,
coordinates: Query<&Coordinates, With<Cell>>,
) {
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<NextState<SimulationPlayback>>) {
next.set(SimulationPlayback::Step);
}
// Reset board to last saved state (before running, or before wiping)
fn reset(
last_board_state: Res<LastBoardState>,
mut lifecycle: MessageWriter<Lifecycle>,
cells: Query<Entity, With<Cell>>,
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<Entity, With<Cell>>, 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<AppExit>) {
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<State<AudioPlayback>>, mut next: ResMut<NextState<AudioPlayback>>) {
next.set(match curr.get() {
AudioPlayback::Play => AudioPlayback::Mute,
AudioPlayback::Mute => AudioPlayback::Play,
});
}
}

@ -11,23 +11,11 @@
// Only because of FromTemplat macros unfortunately... // Only because of FromTemplat macros unfortunately...
#![allow(dead_code)] #![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::*; use engine::*;
mod actions;
use actions::*;
const TILE: &str = "tileGrey_01.png"; const TILE: &str = "tileGrey_01.png";
const SIM_FRAME_DURATION: f32 = 0.5; const SIM_FRAME_DURATION: f32 = 0.5;
@ -38,6 +26,7 @@ fn main() {
..default() ..default()
})) }))
.add_plugins(FeathersPlugins) .add_plugins(FeathersPlugins)
.add_plugins(GameActionsPlugin)
.init_resource::<NextCoordinates>() .init_resource::<NextCoordinates>()
.init_resource::<CellAssets>() .init_resource::<CellAssets>()
.init_resource::<Panning>() .init_resource::<Panning>()
@ -58,42 +47,60 @@ fn main() {
load_meshes, load_meshes,
), ),
) )
// Cell management/Simulation
.add_systems( .add_systems(
Update, Update,
( (
cell_lifecycle.run_if(on_message::<Lifecycle>),
update_grid_position.run_if(on_message::<CursorMoved>), update_grid_position.run_if(on_message::<CursorMoved>),
// 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 de_spawn_cells
.run_if(not(is_ui_hovered)) .run_if(not(is_ui_hovered))
.run_if(not(is_panning)) .run_if(not(is_panning))
.run_if(input_just_released(MouseButton::Left)), .run_if(input_just_released(MouseButton::Left)),
assert_cell_uniqueness.run_if(any_with_component::<Cell>),
simulation_step.run_if(input_just_pressed(KeyCode::ArrowRight)),
simulation_step simulation_step
.run_if(in_state(SimulationPlayback::Run)) .run_if(in_state(SimulationPlayback::Run))
.run_if(cooldown_secs(SIM_FRAME_DURATION)), .run_if(cooldown_secs(SIM_FRAME_DURATION)),
simulation_step.run_if(state_changed::<SimulationPlayback>), simulation_step.run_if(state_changed::<SimulationPlayback>),
grid_gizmo, pause_simulation.run_if(in_state(SimulationPlayback::Step)),
// TODO: Visualize audio play/mute buttons enable/disable update_material,
toggle_interactive::<SimulationPlayback>, ),
// TODO Visualize simulation run/pause buttons enable/disable )
toggle_interactive::<AudioPlayback>, // Audio Systems
.add_systems(
Update,
(
control_volume,
update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>), update_pitch_map_resource_ui.run_if(resource_changed::<PitchMap>),
reassign_cell_pitch.run_if(resource_changed::<PitchMap>), reassign_cell_pitch.run_if(resource_changed::<PitchMap>),
update_audio, update_audio,
update_material, ),
)
// Debugging
.add_systems(
Update,
(
assert_cell_uniqueness.run_if(any_with_component::<Cell>),
grid_gizmo,
update_debug_info_cell_count, update_debug_info_cell_count,
update_debug_info_coordinates, update_debug_info_coordinates,
),
)
// Ui Controllers
.add_systems(
Update,
(
// TODO: Visualize audio play/mute buttons enable/disable
manage_interactive::<SimulationPlayback>,
// TODO Visualize simulation run/pause buttons enable/disable
manage_interactive::<AudioPlayback>,
update_note_interactivity, update_note_interactivity,
), ),
) )
.add_systems(Update, cell_lifecycle.run_if(on_message::<Lifecycle>)) // Input handling
.add_systems(Update, mouse_pan.run_if(on_message::<MouseMotion>)) .add_systems(
.add_systems(Update, set_pan_state) Update,
(mouse_pan.run_if(on_message::<MouseMotion>), set_pan_state),
)
.run(); .run();
} }
@ -354,81 +361,29 @@ fn primary_ui() -> impl Scene {
} }
Children [ Children [
ui_button(UiButton::Power) ui_button(UiButton::Power)
on(|_: On<Activate>, mut writer: MessageWriter<AppExit>| { ActionSource<Action>(Action::Quit),
writer.write(AppExit::Success);
}),
ui_button(UiButton::Pause) ui_button(UiButton::Pause)
SimulationPlayback::Pause SimulationPlayback::Pause
on(|_: On<Activate>, mut next: ResMut<NextState<SimulationPlayback>>| { ActionSource<SimAction>(SimAction::Toggle),
// Set simulation state
next.set(SimulationPlayback::Pause);
}),
ui_button(UiButton::Play) ui_button(UiButton::Play)
SimulationPlayback::Run SimulationPlayback::Run
on(| ActionSource<SimAction>(SimAction::Toggle),
_: On<Activate>,
mut next: ResMut<NextState<SimulationPlayback>>,
mut last_board_state: ResMut<LastBoardState>,
coordinates: Query<&Coordinates, With<Cell>>
| {
last_board_state.coordinates = coordinates.iter().cloned().collect();
// Set simulation state
next.set(SimulationPlayback::Run);
}),
ui_button(UiButton::Step) ui_button(UiButton::Step)
on(|_: On<Activate>, mut next: ResMut<NextState<SimulationPlayback>>| { ActionSource<SimAction>(SimAction::Step),
next.set(SimulationPlayback::Step);
}),
ui_button(UiButton::Reset) ui_button(UiButton::Reset)
on(| ActionSource<SimAction>(SimAction::Reset),
_: On<Activate>,
last_board_state: Res<LastBoardState>,
mut lifecycle: MessageWriter<Lifecycle>,
cells: Query<Entity, With<Cell>>,
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()));
});
}),
ui_button(UiButton::Wipe) ui_button(UiButton::Wipe)
on(|_: On<Activate>, cells: Query<Entity, With<Cell>>, mut commands: Commands| { ActionSource<SimAction>(SimAction::Wipe),
cells.iter().for_each(|e| {
commands.entity(e).despawn();
});
}),
ui_button(UiButton::ZoomOut) ui_button(UiButton::ZoomOut)
on(|_: On<Activate>, mut projection: Query<&mut Projection>| { ActionSource<Action>(Action::ZoomOut),
projection.iter_mut().for_each(|mut p| match *p {
Projection::Orthographic(ref mut o) => {
o.scale /= 0.5;
}
_ => todo!(),
});
}),
ui_button(UiButton::ZoomIn) ui_button(UiButton::ZoomIn)
on(|_: On<Activate>, mut projection: Query<&mut Projection>| { ActionSource<Action>(Action::ZoomIn),
projection.iter_mut().for_each(|mut p| match *p {
Projection::Orthographic(ref mut o) => {
o.scale *= 0.5;
}
_ => todo!(),
});
}),
ui_button(UiButton::MuteAudio) ui_button(UiButton::MuteAudio)
AudioPlayback::Mute AudioPlayback::Mute
on(|_: On<Activate>, mut next: ResMut<NextState<AudioPlayback>>| { ActionSource<AudioAction>(AudioAction::Toggle),
next.set(AudioPlayback::Mute);
}),
ui_button(UiButton::PlayAudio) ui_button(UiButton::PlayAudio)
AudioPlayback::Play AudioPlayback::Play
on(|_: On<Activate>, mut next: ResMut<NextState<AudioPlayback>>| { ActionSource<AudioAction>(AudioAction::Toggle),
next.set(AudioPlayback::Play);
}),
] ]
], ],
subpane() subpane()
@ -983,35 +938,6 @@ fn load_audio_tones(mut pitches: ResMut<Assets<Pitch>>, mut cell_assets: ResMut<
debug_assert_eq!(cell_assets.pitches.iter().len(), 132); 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<NextState<AudioPlayback>>,
curr_state: Res<State<AudioPlayback>>,
) {
// 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<NextState<SimulationPlayback>>,
curr_state: Res<State<SimulationPlayback>>,
) {
// 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( fn control_volume(
audio_state: Res<State<AudioPlayback>>, audio_state: Res<State<AudioPlayback>>,
simulation_state: Res<State<SimulationPlayback>>, simulation_state: Res<State<SimulationPlayback>>,
@ -1131,7 +1057,7 @@ fn create_light_theme() -> ThemeProps {
) )
} }
fn toggle_interactive<T: Component + States + PartialEq>( fn manage_interactive<T: Component + States + PartialEq>(
q: Query<(Entity, &T)>, q: Query<(Entity, &T)>,
s: Res<State<T>>, s: Res<State<T>>,
mut commands: Commands, mut commands: Commands,
@ -1259,3 +1185,8 @@ fn update_note_interactivity(
}); });
} }
} }
// Sets the sim state to paused
fn pause_simulation(mut next: ResMut<NextState<SimulationPlayback>>) {
next.set(SimulationPlayback::Pause)
}

Loading…
Cancel
Save