From f481f5dc8753b523226372c2d959ae1b1219aa8f Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Thu, 12 Mar 2026 00:19:22 +0100 Subject: [PATCH] Add GameStates, typed enums, and App::new constructor --- src/app/app.rs | 54 ++++++++++++++++++---- src/app/game_states.rs | 73 ++++++++++++++++++++++++++++++ src/app/keybindings/main_menu.rs | 32 ++++++++++++- src/app/mod.rs | 6 ++- src/app/widget.rs | 6 +-- src/app/widgets/main_menu.rs | 77 +++++++++++++++++++++++++------- src/cli.rs | 18 ++++---- src/main.rs | 15 +------ 8 files changed, 229 insertions(+), 52 deletions(-) create mode 100644 src/app/game_states.rs diff --git a/src/app/app.rs b/src/app/app.rs index 453256a..3414fa4 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,4 +1,10 @@ -use crate::app::keybindings::{Action, event_to_action, main_menu_keybindings}; +use crate::{ + app::{ + GameStates, + keybindings::{Action, event_to_action, main_menu_keybindings}, + }, + cli::Cli, +}; use clap::ValueEnum; use ratatui::{ DefaultTerminal, Frame, @@ -9,14 +15,19 @@ use std::{ sync::mpsc::{self, Receiver}, }; +pub enum Event { + Input(KeyEvent), +} + pub struct App { pub exit: bool, - pub window: View, + pub view: View, + pub game_states: GameStates, pub username: String, - pub game_mode: String, + pub game_mode: GameMode, pub map_width: u8, pub map_height: u8, - pub perk_deck: String, + pub perk_deck: PerkDecks, pub starting_wood: u8, pub starting_iron: u8, pub supply_limit: u8, @@ -24,7 +35,7 @@ pub struct App { pub skill_points_limit: u16, } -#[derive(Debug, Clone, ValueEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum View { MainMenu, Skirmish, @@ -33,11 +44,38 @@ pub enum View { Settings, } -pub enum Event { - Input(KeyEvent), +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] +pub enum GameMode { + LastManStanding, + Frontlines, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] +pub enum PerkDecks { + Silesian, + BogeyMan, + Anteater, } impl App { + pub fn new(args: Cli) -> Self { + Self { + exit: false, + game_states: GameStates::new(), + view: args.view, + username: args.username, + game_mode: args.game_mode, + map_width: args.map_width, + map_height: args.map_height, + perk_deck: args.perk_deck, + starting_wood: args.starting_wood, + starting_iron: args.starting_iron, + supply_limit: args.supply_limit, + xp_modifier: args.xp_modifier, + skill_points_limit: args.skill_points_limit, + } + } + pub fn run(&mut self, terminal: &mut DefaultTerminal, rx: Receiver) -> Result<()> { while !self.exit { terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; @@ -59,7 +97,7 @@ impl App { } fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { - match self.window { + match self.view { View::MainMenu => main_menu_keybindings(self, &key_event), _ => { if let Some(action) = event_to_action(&key_event) { diff --git a/src/app/game_states.rs b/src/app/game_states.rs new file mode 100644 index 0000000..9c7f738 --- /dev/null +++ b/src/app/game_states.rs @@ -0,0 +1,73 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct GameStates { + pub main_menu_state: MainMenuState, + pub skirmish_state: SkirmishState, + pub perk_decks_state: PerkDecksState, + pub skills_config_state: SkillsConfigState, + pub settings_state: SettingsState, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct MainMenuState { + pub id: usize, + pub name: &'static str, + pub selected_view: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SkirmishState { + pub id: usize, + pub name: &'static str, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct PerkDecksState { + pub id: usize, + pub name: &'static str, + pub selected_perk_deck: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SkillsConfigState { + pub id: usize, + pub name: &'static str, + pub selected_skill: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SettingsState { + pub id: usize, + pub name: &'static str, + pub selected_setting: usize, +} + +impl GameStates { + pub fn new() -> Self { + Self { + main_menu_state: MainMenuState { + id: 0, + name: "Main Menu", + selected_view: 1, + }, + skirmish_state: SkirmishState { + id: 1, + name: "Skirmish", + }, + perk_decks_state: PerkDecksState { + id: 2, + name: "Perk Decks", + selected_perk_deck: 0, + }, + skills_config_state: SkillsConfigState { + id: 3, + name: "Skills Config", + selected_skill: 0, + }, + settings_state: SettingsState { + id: 4, + name: "Settings", + selected_setting: 0, + }, + } + } +} diff --git a/src/app/keybindings/main_menu.rs b/src/app/keybindings/main_menu.rs index 76108f3..4f0586c 100644 --- a/src/app/keybindings/main_menu.rs +++ b/src/app/keybindings/main_menu.rs @@ -1,5 +1,5 @@ use crate::app::{ - App, + App, View, keybindings::{Action, event_to_action}, }; use ratatui::crossterm::event::KeyEvent; @@ -8,7 +8,35 @@ pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) { if let Some(action) = event_to_action(&event) { match action { Action::Quit => app.exit = true, - _ => (), + Action::ScrollUp => { + app.game_states.main_menu_state.selected_view = app + .game_states + .main_menu_state + .selected_view + .saturating_sub(1) + .max(1); + } + Action::ScrollDown => { + app.game_states.main_menu_state.selected_view = app + .game_states + .main_menu_state + .selected_view + .saturating_add(1) + .min(4); + } + Action::Select => { + let selected_view: usize = app.game_states.main_menu_state.selected_view; + + if selected_view == 1 { + app.view = View::Skirmish; + } else if selected_view == 2 { + app.view = View::PerkDecks; + } else if selected_view == 3 { + app.view = View::SkillsConfig; + } else if selected_view == 4 { + app.view = View::Settings; + } + } // _ => (), } } } diff --git a/src/app/mod.rs b/src/app/mod.rs index f20bb68..745c56a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,6 +1,10 @@ pub mod app; +pub mod game_states; pub mod keybindings; pub mod widget; pub mod widgets; -pub use app::{App, Event, View, handle_input_events}; +pub use app::{App, Event, GameMode, PerkDecks, View, handle_input_events}; +pub use game_states::{ + GameStates, MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState, +}; diff --git a/src/app/widget.rs b/src/app/widget.rs index e0cd9a9..38087e7 100644 --- a/src/app/widget.rs +++ b/src/app/widget.rs @@ -6,9 +6,9 @@ impl Widget for &App { where Self: Sized, { - match self.window { - View::MainMenu => main_menu_widget(area, buf), - _ => panic!("This window doesn't have widget."), + match self.view { + View::MainMenu => main_menu_widget(self, area, buf), + _ => (), } } } diff --git a/src/app/widgets/main_menu.rs b/src/app/widgets/main_menu.rs index 0ea594e..69669f1 100644 --- a/src/app/widgets/main_menu.rs +++ b/src/app/widgets/main_menu.rs @@ -1,41 +1,86 @@ -use crate::app::{View, widgets::keybindings_widget}; +use crate::app::{App, View, widgets::keybindings_widget}; +use clap::ValueEnum; use ratatui::{ buffer::Buffer, layout::{Alignment, Constraint, Layout, Rect}, - style::Color, + style::Stylize, + text::Line, widgets::{Block, Borders, Paragraph, Widget}, }; -pub fn main_menu_widget(area: Rect, buf: &mut Buffer) { +fn view_options() -> Vec<(usize, String)> { + View::value_variants() + .iter() + .enumerate() + .map(|(i, v)| { + let name = v + .to_possible_value() + .unwrap() + .get_name() + .replace('-', " ") + .to_uppercase() + .to_string(); + (i, name) + }) + .filter(|(_, v)| v != "MAIN MENU") + .collect() +} + +pub fn main_menu_widget(app: &App, area: Rect, buf: &mut Buffer) { let vertical_layout: Layout = - Layout::vertical([Constraint::Percentage(87), Constraint::Percentage(13)]); + Layout::vertical([Constraint::Percentage(88), Constraint::Percentage(12)]); let [main_menu_area, keybindings_area] = vertical_layout.areas(area); + Block::new() + .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT) + .render(main_menu_area, buf); + + let main_menu_areas: Vec = main_menu_area.layout_vec(&Layout::vertical([ + Constraint::Percentage(50), + Constraint::Percentage(50), + ])); + { + let title_area: Rect = main_menu_areas[0].centered_vertically(Constraint::Percentage(50)); + let title_text: String = vec![ r" __ __ _ _____ _ ", - r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___", + r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ", r"\ \/ \/ / _` | '__| | | '_ \ / /\/ | | | '_ \| '_ \ / _ \ / __|", r" \ /\ / (_| | | | | | | | / / | |_| | | | | | | | __/ \__ \", r" \/ \/ \__,_|_| |_|_| |_| \/ \__,_|_| |_|_| |_|\___|_|___/", ] .join("\n"); - let title: Paragraph<'_> = Paragraph::new(title_text) + Paragraph::new(title_text) .alignment(Alignment::Center) - .style(Color::Yellow) - .block( - Block::default() - .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP) - .style(Color::Gray), - ); - - title.render(main_menu_area, buf); + .yellow() + .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT)) + .render(title_area, buf); } { - let keybindings: Paragraph<'_> = keybindings_widget(View::MainMenu); - keybindings.render(keybindings_area, buf); + let options_area: Rect = main_menu_areas[1]; + + let lines: Vec> = view_options() + .into_iter() + .map(|(i, view)| { + let styled = if app.game_states.main_menu_state.selected_view == i { + Line::from(format!("> {}", view)).yellow() + } else { + Line::from(view).white() + }; + styled + }) + .collect(); + + Paragraph::new(lines) + .alignment(Alignment::Center) + .green() + .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT)) + .render(options_area, buf); } + + keybindings_widget(View::MainMenu).render(keybindings_area, buf); } diff --git a/src/cli.rs b/src/cli.rs index 04516b0..c637a41 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use crate::app::View; +use crate::app::{GameMode, PerkDecks, View}; use clap::Parser; #[derive(Parser, Debug)] @@ -11,7 +11,7 @@ pub struct Cli { default_value_t = View::MainMenu, value_enum )] - pub window: View, + pub view: View, #[arg( long, @@ -24,10 +24,11 @@ pub struct Cli { #[arg( long, help = "Game mode", - value_name = "LastManStanding or FrontLines", - default_value = "LastManStanding" + value_name = "...", + default_value_t = GameMode::LastManStanding, + value_enum )] - pub game_mode: String, + pub game_mode: GameMode, #[arg( long, @@ -48,10 +49,11 @@ pub struct Cli { #[arg( long, help = "Perk Deck", - value_name = "String (check Perk Deck tab)", - default_value = "Silesian" + value_name = "...", + default_value_t = PerkDecks::Silesian, + value_enum )] - pub perk_deck: String, + pub perk_deck: PerkDecks, #[arg( long, diff --git a/src/main.rs b/src/main.rs index 59b08e1..aa3ca74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,20 +12,7 @@ use war_in_tunnels::{ fn main() -> Result<()> { let args: Cli = get_args(); let mut terminal: Terminal> = ratatui::init(); - let mut app: App = App { - exit: false, - window: args.window, - username: args.username, - game_mode: args.game_mode, - map_width: args.map_width, - map_height: args.map_height, - perk_deck: args.perk_deck, - starting_wood: args.starting_wood, - starting_iron: args.starting_iron, - supply_limit: args.supply_limit, - xp_modifier: args.xp_modifier, - skill_points_limit: args.skill_points_limit, - }; + let mut app: App = App::new(args); let (event_tx, event_rx) = mpsc::channel::();