use bevy::{input::InputSystems, prelude::*}; use crate::sim::SimSpeed; pub struct ActionsPlugin; impl Plugin for ActionsPlugin { fn build(&self, app: &mut App) { app.add_message::() .add_systems(PreUpdate, keyboard_actions.after(InputSystems)); } } #[derive(Message, Debug, Clone, Copy, PartialEq, Eq)] pub enum Action { TogglePause, SetSpeed(SimSpeed), ToggleStockpileTool, ToggleFarmTool, CycleBuildTool, ToggleDesignateTool, Cancel, NewWorld, ToggleUi, ZoomIn, ZoomOut, Save, Load, } pub const KEYBINDS: [(KeyCode, Action); 16] = [ (KeyCode::Space, Action::TogglePause), (KeyCode::Digit1, Action::SetSpeed(SimSpeed::Normal)), (KeyCode::Digit2, Action::SetSpeed(SimSpeed::Fast)), (KeyCode::Digit3, Action::SetSpeed(SimSpeed::Fastest)), (KeyCode::Digit4, Action::SetSpeed(SimSpeed::Max)), (KeyCode::KeyZ, Action::ToggleStockpileTool), (KeyCode::KeyF, Action::ToggleFarmTool), (KeyCode::KeyB, Action::CycleBuildTool), (KeyCode::KeyX, Action::ToggleDesignateTool), (KeyCode::Escape, Action::Cancel), (KeyCode::KeyR, Action::NewWorld), (KeyCode::KeyH, Action::ToggleUi), (KeyCode::Equal, Action::ZoomIn), (KeyCode::Minus, Action::ZoomOut), (KeyCode::F5, Action::Save), (KeyCode::F9, Action::Load), ]; impl Action { pub fn label(self) -> &'static str { match self { Action::TogglePause => "Pause", Action::SetSpeed(SimSpeed::Normal) => "1x", Action::SetSpeed(SimSpeed::Fast) => "3x", Action::SetSpeed(SimSpeed::Fastest) => "6x", Action::SetSpeed(SimSpeed::Max) => "Max", Action::ToggleStockpileTool => "Stockpile", Action::ToggleFarmTool => "Farm", Action::CycleBuildTool => "Build", Action::ToggleDesignateTool => "Mark", Action::Cancel => "Cancel", Action::NewWorld => "New map", Action::ToggleUi => "Hide", Action::ZoomIn => "+", Action::ZoomOut => "-", Action::Save => "Save", Action::Load => "Load", } } pub fn key(self) -> Option { KEYBINDS .iter() .find(|(_, action)| *action == self) .map(|(key, _)| *key) } pub fn key_label(self) -> String { match self.key() { Some(KeyCode::Space) => "Space".into(), Some(KeyCode::Escape) => "Esc".into(), Some(KeyCode::Equal) => "=".into(), Some(KeyCode::Minus) => "-".into(), Some(key) => format!("{key:?}") .trim_start_matches("Digit") .trim_start_matches("Key") .to_string(), None => String::new(), } } } fn keyboard_actions(keys: Res>, mut actions: MessageWriter) { for (key, action) in KEYBINDS { if keys.just_pressed(key) { actions.write(action); } } } #[cfg(test)] mod tests { use super::*; #[test] fn pressing_a_bound_key_triggers_its_action() { let mut app = App::new(); app.init_resource::>() .add_message::() .add_systems(Update, keyboard_actions); app.world_mut() .resource_mut::>() .press(KeyCode::Digit3); app.update(); let actions: Vec = app .world_mut() .resource_mut::>() .drain() .collect(); assert_eq!(actions, vec![Action::SetSpeed(SimSpeed::Fastest)]); } #[test] fn every_action_has_exactly_one_key() { for (_, action) in KEYBINDS { let bound = KEYBINDS.iter().filter(|(_, a)| *a == action).count(); assert_eq!(bound, 1, "{action:?}"); } let mut keys: Vec<_> = KEYBINDS.iter().map(|(k, _)| format!("{k:?}")).collect(); keys.sort(); keys.dedup(); assert_eq!(keys.len(), KEYBINDS.len()); } #[test] fn key_labels_are_short() { assert_eq!(Action::SetSpeed(SimSpeed::Fast).key_label(), "2"); assert_eq!(Action::ToggleStockpileTool.key_label(), "Z"); assert_eq!(Action::Cancel.key_label(), "Esc"); } }