Add GameStates, typed enums, and App::new constructor

This commit is contained in:
2026-03-12 00:19:22 +01:00
parent ec1aa4fbe8
commit f481f5dc87
8 changed files with 229 additions and 52 deletions
+46 -8
View File
@@ -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 clap::ValueEnum;
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, DefaultTerminal, Frame,
@@ -9,14 +15,19 @@ use std::{
sync::mpsc::{self, Receiver}, sync::mpsc::{self, Receiver},
}; };
pub enum Event {
Input(KeyEvent),
}
pub struct App { pub struct App {
pub exit: bool, pub exit: bool,
pub window: View, pub view: View,
pub game_states: GameStates,
pub username: String, pub username: String,
pub game_mode: String, pub game_mode: GameMode,
pub map_width: u8, pub map_width: u8,
pub map_height: u8, pub map_height: u8,
pub perk_deck: String, pub perk_deck: PerkDecks,
pub starting_wood: u8, pub starting_wood: u8,
pub starting_iron: u8, pub starting_iron: u8,
pub supply_limit: u8, pub supply_limit: u8,
@@ -24,7 +35,7 @@ pub struct App {
pub skill_points_limit: u16, pub skill_points_limit: u16,
} }
#[derive(Debug, Clone, ValueEnum)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
pub enum View { pub enum View {
MainMenu, MainMenu,
Skirmish, Skirmish,
@@ -33,11 +44,38 @@ pub enum View {
Settings, Settings,
} }
pub enum Event { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
Input(KeyEvent), pub enum GameMode {
LastManStanding,
Frontlines,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
pub enum PerkDecks {
Silesian,
BogeyMan,
Anteater,
} }
impl App { 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<Event>) -> Result<()> { pub fn run(&mut self, terminal: &mut DefaultTerminal, rx: Receiver<Event>) -> Result<()> {
while !self.exit { while !self.exit {
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
@@ -59,7 +97,7 @@ impl App {
} }
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
match self.window { match self.view {
View::MainMenu => main_menu_keybindings(self, &key_event), View::MainMenu => main_menu_keybindings(self, &key_event),
_ => { _ => {
if let Some(action) = event_to_action(&key_event) { if let Some(action) = event_to_action(&key_event) {
+73
View File
@@ -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,
},
}
}
}
+30 -2
View File
@@ -1,5 +1,5 @@
use crate::app::{ use crate::app::{
App, App, View,
keybindings::{Action, event_to_action}, keybindings::{Action, event_to_action},
}; };
use ratatui::crossterm::event::KeyEvent; 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) { if let Some(action) = event_to_action(&event) {
match action { match action {
Action::Quit => app.exit = true, 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;
}
} // _ => (),
} }
} }
} }
+5 -1
View File
@@ -1,6 +1,10 @@
pub mod app; pub mod app;
pub mod game_states;
pub mod keybindings; pub mod keybindings;
pub mod widget; pub mod widget;
pub mod widgets; 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,
};
+3 -3
View File
@@ -6,9 +6,9 @@ impl Widget for &App {
where where
Self: Sized, Self: Sized,
{ {
match self.window { match self.view {
View::MainMenu => main_menu_widget(area, buf), View::MainMenu => main_menu_widget(self, area, buf),
_ => panic!("This window doesn't have widget."), _ => (),
} }
} }
} }
+60 -15
View File
@@ -1,18 +1,49 @@
use crate::app::{View, widgets::keybindings_widget}; use crate::app::{App, View, widgets::keybindings_widget};
use clap::ValueEnum;
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::Color, style::Stylize,
text::Line,
widgets::{Block, Borders, Paragraph, Widget}, 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 = 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); 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<Rect> = 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![ let title_text: String = vec![
r" __ __ _ _____ _ ", r" __ __ _ _____ _ ",
r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ", r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ",
@@ -22,20 +53,34 @@ pub fn main_menu_widget(area: Rect, buf: &mut Buffer) {
] ]
.join("\n"); .join("\n");
let title: Paragraph<'_> = Paragraph::new(title_text) Paragraph::new(title_text)
.alignment(Alignment::Center) .alignment(Alignment::Center)
.style(Color::Yellow) .yellow()
.block( .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT))
Block::default() .render(title_area, buf);
.borders(Borders::LEFT | Borders::RIGHT | Borders::TOP)
.style(Color::Gray),
);
title.render(main_menu_area, buf);
} }
{ {
let keybindings: Paragraph<'_> = keybindings_widget(View::MainMenu); let options_area: Rect = main_menu_areas[1];
keybindings.render(keybindings_area, buf);
let lines: Vec<Line<'_>> = 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);
} }
+10 -8
View File
@@ -1,4 +1,4 @@
use crate::app::View; use crate::app::{GameMode, PerkDecks, View};
use clap::Parser; use clap::Parser;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -11,7 +11,7 @@ pub struct Cli {
default_value_t = View::MainMenu, default_value_t = View::MainMenu,
value_enum value_enum
)] )]
pub window: View, pub view: View,
#[arg( #[arg(
long, long,
@@ -24,10 +24,11 @@ pub struct Cli {
#[arg( #[arg(
long, long,
help = "Game mode", help = "Game mode",
value_name = "LastManStanding or FrontLines", value_name = "...",
default_value = "LastManStanding" default_value_t = GameMode::LastManStanding,
value_enum
)] )]
pub game_mode: String, pub game_mode: GameMode,
#[arg( #[arg(
long, long,
@@ -48,10 +49,11 @@ pub struct Cli {
#[arg( #[arg(
long, long,
help = "Perk Deck", help = "Perk Deck",
value_name = "String (check Perk Deck tab)", value_name = "...",
default_value = "Silesian" default_value_t = PerkDecks::Silesian,
value_enum
)] )]
pub perk_deck: String, pub perk_deck: PerkDecks,
#[arg( #[arg(
long, long,
+1 -14
View File
@@ -12,20 +12,7 @@ use war_in_tunnels::{
fn main() -> Result<()> { fn main() -> Result<()> {
let args: Cli = get_args(); let args: Cli = get_args();
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init(); let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
let mut app: App = App { let mut app: App = App::new(args);
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 (event_tx, event_rx) = mpsc::channel::<Event>(); let (event_tx, event_rx) = mpsc::channel::<Event>();