diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 030a78c..eed231a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -112,7 +112,7 @@ Zaktualizowano instrukcje agentów, mapę i opis testów. Nie zmieniano mechanik ## 2026-10-19 — formatowanie Rust z wcięciami po 2 spacje -Dodano `rustfmt.toml` z uzgodnionymi opcjami: edycja i styl 2024, `max_width = 100`, `hard_tabs = false`, `tab_spaces = 2`, zakończenia Unix, sortowanie importów i modułów oraz domyślne heurystyki krótkich konstrukcji. Zastosowano `nix develop -c cargo fmt --all` do źródeł i testów (56 zmienionych plików Rust). Zaktualizowano instrukcje agentów i mapę konfiguracji projektu. +Dodano `rustfmt.toml` z uzgodnionymi opcjami: edycja i styl 2024, `max_width = 100`, `hard_tabs = false`, `tab_spaces = 4`, zakończenia Unix, sortowanie importów i modułów oraz domyślne heurystyki krótkich konstrukcji. Zastosowano `nix develop -c cargo fmt --all` do źródeł i testów (56 zmienionych plików Rust). Zaktualizowano instrukcje agentów i mapę konfiguracji projektu. Weryfikacja przez `nix develop -c`: `cargo fmt --all -- --check`, `cargo check --all-targets` i `cargo test --all-targets` zakończone sukcesem; 45 testów zaliczonych. Nie wprowadzano zmian mechanik ani zależności. Nie powtarzano Clippy ani ręcznej sesji TUI przy zmianie formatowania. diff --git a/rustfmt.toml b/rustfmt.toml index 59a1c1c..6bf28ef 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -3,7 +3,7 @@ style_edition = "2024" max_width = 100 hard_tabs = false -tab_spaces = 2 +tab_spaces = 4 newline_style = "Unix" reorder_imports = true diff --git a/src/app/app.rs b/src/app/app.rs index 318d497..7939a6c 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,102 +1,103 @@ use crate::{ - app::{GameStates, handle_keybindings, threads::AudioCmd, view::View}, - cli::Cli, + app::{GameStates, handle_keybindings, threads::AudioCmd, view::View}, + cli::Cli, }; use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect}; use std::{ - io::Result, - sync::mpsc::{Receiver, Sender}, - thread::sleep, - time::Duration, + io::Result, + sync::mpsc::{Receiver, Sender}, + thread::sleep, + time::Duration, }; pub struct App { - pub exit: bool, - pub view: View, - pub window_area: Rect, - pub args: Cli, - pub states: Option, - pub audio_tx: Sender, + pub exit: bool, + pub view: View, + pub window_area: Rect, + pub args: Cli, + pub states: Option, + pub audio_tx: Sender, } impl App { - pub fn new(args: Cli, audio_tx: Sender) -> Self { - Self { - exit: false, - view: args.view, - window_area: Rect::default(), - args: args, - states: None, - audio_tx, - } - } - - pub fn states_ref(&self) -> Option<&GameStates> { - self.states.as_ref() - } - - pub fn states_mut(&mut self) -> Option<&mut GameStates> { - self.states.as_mut() - } - - pub fn run( - &mut self, - terminal: &mut DefaultTerminal, - input_rx: Receiver, - resize_rx: Receiver<(u16, u16)>, - tick_rx: Receiver<()>, - ) -> Result<()> { - while !self.exit { - terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; - - if let Ok(key) = input_rx.try_recv() { - self.handle_key_event(key)?; - } - - if let Ok((_, _)) = resize_rx.try_recv() { - let window_area: Rect = self.window_area; - if let Some(state) = self.states_mut() { - state - .skirmish - .board - .change_resize(&window_area, state.skirmish.side_panel); + pub fn new(args: Cli, audio_tx: Sender) -> Self { + Self { + exit: false, + view: args.view, + window_area: Rect::default(), + args: args, + states: None, + audio_tx, } - } - - if let Ok(()) = tick_rx.try_recv() { - self.update()?; - } - - sleep(Duration::from_millis(10)); } - Ok(()) - } - - fn draw(&mut self, frame: &mut Frame<'_>) { - let window_area: Rect = frame.area(); - - if window_area.width != self.window_area.width || window_area.height != self.window_area.height - { - self.window_area = window_area; - if self.states.is_none() { - self.states = Some(GameStates::new(&self.args, &self.window_area)) - } + pub fn states_ref(&self) -> Option<&GameStates> { + self.states.as_ref() } - frame.render_widget(self, window_area); - } - - fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { - handle_keybindings(self, key_event); - Ok(()) - } - - fn update(&mut self) -> Result<()> { - if let Some(state) = self.states_mut() { - state.skirmish.tick_update(); + pub fn states_mut(&mut self) -> Option<&mut GameStates> { + self.states.as_mut() } - Ok(()) - } + pub fn run( + &mut self, + terminal: &mut DefaultTerminal, + input_rx: Receiver, + resize_rx: Receiver<(u16, u16)>, + tick_rx: Receiver<()>, + ) -> Result<()> { + while !self.exit { + terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?; + + if let Ok(key) = input_rx.try_recv() { + self.handle_key_event(key)?; + } + + if let Ok((_, _)) = resize_rx.try_recv() { + let window_area: Rect = self.window_area; + if let Some(state) = self.states_mut() { + state + .skirmish + .board + .change_resize(&window_area, state.skirmish.side_panel); + } + } + + if let Ok(()) = tick_rx.try_recv() { + self.update()?; + } + + sleep(Duration::from_millis(10)); + } + + Ok(()) + } + + fn draw(&mut self, frame: &mut Frame<'_>) { + let window_area: Rect = frame.area(); + + if window_area.width != self.window_area.width + || window_area.height != self.window_area.height + { + self.window_area = window_area; + if self.states.is_none() { + self.states = Some(GameStates::new(&self.args, &self.window_area)) + } + } + + frame.render_widget(self, window_area); + } + + fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { + handle_keybindings(self, key_event); + Ok(()) + } + + fn update(&mut self) -> Result<()> { + if let Some(state) = self.states_mut() { + state.skirmish.tick_update(); + } + + Ok(()) + } } diff --git a/src/app/helpers/block_title.rs b/src/app/helpers/block_title.rs index d18421a..16ed894 100644 --- a/src/app/helpers/block_title.rs +++ b/src/app/helpers/block_title.rs @@ -1,6 +1,6 @@ use ratatui::{ - style::{Color, Style, Stylize}, - text::{Line, Span}, + style::{Color, Style, Stylize}, + text::{Line, Span}, }; /// Creates a styled title `Line` for a UI block. @@ -9,27 +9,27 @@ use ratatui::{ /// the spans, so the returned `Line` owns all its data and does not borrow from /// the caller. pub fn block_title_helper( - texts: &[(String, Color)], - separator: Option<&'static str>, + texts: &[(String, Color)], + separator: Option<&'static str>, ) -> Line<'static> { - let mut line: Line<'static> = Line::default(); + let mut line: Line<'static> = Line::default(); - line.spans.push(Span::from("[ ").gray()); + line.spans.push(Span::from("[ ").gray()); - for (i, (text, color)) in texts.iter().enumerate() { - let span: Span<'_> = Span::styled(text.clone(), Style::new().fg(*color)); - line.spans.push(span); + for (i, (text, color)) in texts.iter().enumerate() { + let span: Span<'_> = Span::styled(text.clone(), Style::new().fg(*color)); + line.spans.push(span); - if let Some(sep) = separator { - if i + 1 < texts.len() { - line.spans.push(Span::from(sep).gray()); - } + if let Some(sep) = separator { + if i + 1 < texts.len() { + line.spans.push(Span::from(sep).gray()); + } + } } - } - line.spans.push(Span::from(" ]").gray()); + line.spans.push(Span::from(" ]").gray()); - line + line } /// Convenience wrapper for a single‑title line. @@ -38,5 +38,5 @@ pub fn block_title_helper( /// forwards it to `block_title_helper`. Because `block_title_helper` clones the /// string, the temporary slice is safe. pub fn block_single_title_helper(text: String, color: Color) -> Line<'static> { - block_title_helper(&[(text, color)], None) + block_title_helper(&[(text, color)], None) } diff --git a/src/app/helpers/cells_area.rs b/src/app/helpers/cells_area.rs index 7571357..1598c90 100644 --- a/src/app/helpers/cells_area.rs +++ b/src/app/helpers/cells_area.rs @@ -1,17 +1,17 @@ use crate::app::views::{skirmish_layout, skirmish_main_area_layout}; use ratatui::{ - layout::{Margin, Rect}, - widgets::{Block, Borders}, + layout::{Margin, Rect}, + widgets::{Block, Borders}, }; pub fn cells_area_helper(area: &Rect, side_panel: bool) -> Rect { - let board_area: Rect = skirmish_main_area_layout(skirmish_layout(*area)[1], side_panel)[0]; + let board_area: Rect = skirmish_main_area_layout(skirmish_layout(*area)[1], side_panel)[0]; - Block::new() - .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT) - .inner(board_area) - .inner(Margin { - horizontal: 1, - vertical: 1, - }) + Block::new() + .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT) + .inner(board_area) + .inner(Margin { + horizontal: 1, + vertical: 1, + }) } diff --git a/src/app/helpers/main_menu_option.rs b/src/app/helpers/main_menu_option.rs index 8513d65..85311aa 100644 --- a/src/app/helpers/main_menu_option.rs +++ b/src/app/helpers/main_menu_option.rs @@ -1,13 +1,13 @@ pub fn main_menu_option_helper(s: String) -> String { - if let Some(pos) = s - .char_indices() - .filter(|(_, c)| c.is_ascii_uppercase()) - .nth(1) - .map(|(i, _)| i) - { - let (first, second) = s.split_at(pos); - format!("{first} {second}") - } else { - s - } + if let Some(pos) = s + .char_indices() + .filter(|(_, c)| c.is_ascii_uppercase()) + .nth(1) + .map(|(i, _)| i) + { + let (first, second) = s.split_at(pos); + format!("{first} {second}") + } else { + s + } } diff --git a/src/app/keybind.rs b/src/app/keybind.rs index 1f1ba82..4ff35eb 100644 --- a/src/app/keybind.rs +++ b/src/app/keybind.rs @@ -1,14 +1,14 @@ use crate::app::{ - App, - keybindings::{default_keybindings, main_menu_keybindings, skirmish_keybindings}, - view::View, + App, + keybindings::{default_keybindings, main_menu_keybindings, skirmish_keybindings}, + view::View, }; use ratatui::crossterm::event::KeyEvent; pub fn handle_keybindings(app: &mut App, key_event: KeyEvent) { - match app.view { - View::MainMenu => main_menu_keybindings(app, &key_event), - View::Skirmish => skirmish_keybindings(app, &key_event), - _ => default_keybindings(app, &key_event), - } + match app.view { + View::MainMenu => main_menu_keybindings(app, &key_event), + View::Skirmish => skirmish_keybindings(app, &key_event), + _ => default_keybindings(app, &key_event), + } } diff --git a/src/app/keybindings/default.rs b/src/app/keybindings/default.rs index 14430ac..15fe734 100644 --- a/src/app/keybindings/default.rs +++ b/src/app/keybindings/default.rs @@ -1,23 +1,23 @@ use crate::app::{ - App, View, - keybindings::{Action, event_to_action}, - threads::AudioCmd, + App, View, + keybindings::{Action, event_to_action}, + threads::AudioCmd, }; use ratatui::crossterm::event::KeyEvent; pub fn common_keybindings(app: &mut App, action: Action) { - match action { - Action::Quit | Action::Quit2 => app.exit = true, - Action::Esc => app.view = View::MainMenu, - Action::Mute => if let Err(_) = app.audio_tx.send(AudioCmd::Mute) {}, - Action::VolumeUp => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeUp) {}, - Action::VolumeDown => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeDown) {}, - _ => (), - } + match action { + Action::Quit | Action::Quit2 => app.exit = true, + Action::Esc => app.view = View::MainMenu, + Action::Mute => if let Err(_) = app.audio_tx.send(AudioCmd::Mute) {}, + Action::VolumeUp => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeUp) {}, + Action::VolumeDown => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeDown) {}, + _ => (), + } } pub fn default_keybindings(app: &mut App, key_event: &KeyEvent) { - if let Some(action) = event_to_action(&key_event) { - common_keybindings(app, action); - } + if let Some(action) = event_to_action(&key_event) { + common_keybindings(app, action); + } } diff --git a/src/app/keybindings/keybindings.rs b/src/app/keybindings/keybindings.rs index b718981..a642064 100644 --- a/src/app/keybindings/keybindings.rs +++ b/src/app/keybindings/keybindings.rs @@ -9,80 +9,80 @@ use std::collections::HashMap; /// character key and carries the actual character pressed. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Action { - /// Request the application to quit. - Quit, - /// Alternate quit action. - Quit2, - /// Move the selection cursor up. - Up, - /// Move the selection cursor down. - Down, - /// Move the selection cursor left. - Left, - /// Move the selection cursor right. - Right, - /// Scroll up without moving the cursor. - ScrollUp, - /// Scroll down without moving the cursor. - ScrollDown, - /// Scroll left without moving the cursor. - ScrollLeft, - /// Scroll right without moving the cursor. - ScrollRight, - /// Toggle selecting cell. - Space, - /// Open or close the construction view. - Building, - /// Open or close the recruitment view. - Recruitment, - /// Unmark last selected cell. - Backspace, - /// Cancel marking cell. - Delete, - /// Submit or confirm the current choice. - Enter, - /// Return to the main menu or previous screen. - Esc, - /// Zoom the view in. - ZoomIn, - /// Zoom the view out. - ZoomOut, - /// Open side panel. - Tab, - /// Toggle skirmish board/skills view. - ShiftTab, - /// Mute music. - Mute, - /// Volume up. - VolumeUp, - /// Volume down. - VolumeDown, - /// Matches any character key; the inner `char` is the actual key pressed. - WildCard(char), + /// Request the application to quit. + Quit, + /// Alternate quit action. + Quit2, + /// Move the selection cursor up. + Up, + /// Move the selection cursor down. + Down, + /// Move the selection cursor left. + Left, + /// Move the selection cursor right. + Right, + /// Scroll up without moving the cursor. + ScrollUp, + /// Scroll down without moving the cursor. + ScrollDown, + /// Scroll left without moving the cursor. + ScrollLeft, + /// Scroll right without moving the cursor. + ScrollRight, + /// Toggle selecting cell. + Space, + /// Open or close the construction view. + Building, + /// Open or close the recruitment view. + Recruitment, + /// Unmark last selected cell. + Backspace, + /// Cancel marking cell. + Delete, + /// Submit or confirm the current choice. + Enter, + /// Return to the main menu or previous screen. + Esc, + /// Zoom the view in. + ZoomIn, + /// Zoom the view out. + ZoomOut, + /// Open side panel. + Tab, + /// Toggle skirmish board/skills view. + ShiftTab, + /// Mute music. + Mute, + /// Volume up. + VolumeUp, + /// Volume down. + VolumeDown, + /// Matches any character key; the inner `char` is the actual key pressed. + WildCard(char), } /// Logical groups of key bindings used for organising help screens or /// configuration sections. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum Group { - /// Bindings that combine `Ctrl` with movement keys. - CtrlMovement, - /// Plain movement bindings. - Movement, - /// Scroll bindings. - Scroll, - /// Selection bindings. - Select, - /// Text input related bindings. - Input, - /// Zoom related bindings. - Zoom, - /// Music related bindings. - Music, - /// Opens/Closes/Toggles elements in current view. - Opener, - /// Quit related bindings. - Quit, + /// Bindings that combine `Ctrl` with movement keys. + CtrlMovement, + /// Plain movement bindings. + Movement, + /// Scroll bindings. + Scroll, + /// Selection bindings. + Select, + /// Text input related bindings. + Input, + /// Zoom related bindings. + Zoom, + /// Music related bindings. + Music, + /// Opens/Closes/Toggles elements in current view. + Opener, + /// Quit related bindings. + Quit, } /// Description of a single key binding. @@ -91,20 +91,20 @@ pub enum Group { /// together with metadata used for displaying help information. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct KeyBinding { - /// The logical action performed when this binding is triggered. - pub action: Action, - /// The raw `crossterm::event::KeyCode` that identifies the key. - pub code: KeyCode, - /// The kind of key event (crossterm::event::KeyEventKind). - pub kind: KeyEventKind, - /// Any modifier keys that must be held (crossterm::event::KeyModifiers). - pub modifiers: KeyModifiers, - /// The group this binding belongs to, useful for categorising help. - pub group: Group, - /// Human‑readable symbol shown in UI/help screens. - pub symbol: &'static str, - /// Short description of what the binding does. - pub description: &'static str, + /// The logical action performed when this binding is triggered. + pub action: Action, + /// The raw `crossterm::event::KeyCode` that identifies the key. + pub code: KeyCode, + /// The kind of key event (crossterm::event::KeyEventKind). + pub kind: KeyEventKind, + /// Any modifier keys that must be held (crossterm::event::KeyModifiers). + pub modifiers: KeyModifiers, + /// The group this binding belongs to, useful for categorising help. + pub group: Group, + /// Human‑readable symbol shown in UI/help screens. + pub symbol: &'static str, + /// Short description of what the binding does. + pub description: &'static str, } /// Complete list of all key bindings used by the application. @@ -112,245 +112,245 @@ pub struct KeyBinding { /// The slice is ordered primarily for readability; the runtime lookup scans /// this slice linearly, which is acceptable given the modest size of the list. static KEYBINDINGS: &[KeyBinding] = &[ - KeyBinding { - action: Action::Recruitment, - code: KeyCode::Char(' '), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::ALT, - group: Group::Opener, - symbol: "Alt + Space / r", - description: "Recruit", - }, - KeyBinding { - action: Action::Recruitment, - code: KeyCode::Char('r'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Opener, - symbol: "r", - description: "Recruit", - }, - KeyBinding { - action: Action::Building, - code: KeyCode::Char(' '), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::CONTROL, - group: Group::Opener, - symbol: "Ctrl + Space", - description: "Buildings", - }, - KeyBinding { - action: Action::Quit, - code: KeyCode::Char('q'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Quit, - symbol: "q", - description: "Quit", - }, - KeyBinding { - action: Action::Quit2, - code: KeyCode::Char('c'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::CONTROL, - group: Group::Quit, - symbol: "Ctrl + c", - description: "Quit", - }, - KeyBinding { - action: Action::Up, - code: KeyCode::Up, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Movement, - symbol: "↑", - description: "Up", - }, - KeyBinding { - action: Action::Down, - code: KeyCode::Down, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Movement, - symbol: "↓", - description: "Down", - }, - KeyBinding { - action: Action::Left, - code: KeyCode::Left, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Movement, - symbol: "←", - description: "Left", - }, - KeyBinding { - action: Action::Right, - code: KeyCode::Right, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Movement, - symbol: "→", - description: "Right", - }, - KeyBinding { - action: Action::ScrollUp, - code: KeyCode::Up, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::CONTROL, - group: Group::CtrlMovement, - symbol: "Ctrl + ↑", - description: "Scroll Up ", - }, - KeyBinding { - action: Action::ScrollDown, - code: KeyCode::Down, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::CONTROL, - group: Group::CtrlMovement, - symbol: "Ctrl + ↓", - description: "Scroll Down", - }, - KeyBinding { - action: Action::ScrollLeft, - code: KeyCode::Left, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::CONTROL, - group: Group::CtrlMovement, - symbol: "Ctrl + ←", - description: "Scroll Left", - }, - KeyBinding { - action: Action::ScrollRight, - code: KeyCode::Right, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::CONTROL, - group: Group::CtrlMovement, - symbol: "Ctrl + →", - description: "Scroll Right", - }, - KeyBinding { - action: Action::Space, - code: KeyCode::Char(' '), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Select, - symbol: "Space", - description: "Marking cells", - }, - KeyBinding { - action: Action::Backspace, - code: KeyCode::Backspace, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Select, - symbol: "Backspace", - description: "Unmark cell", - }, - KeyBinding { - action: Action::Delete, - code: KeyCode::Delete, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Select, - symbol: "Delete", - description: "Cancel marking", - }, - KeyBinding { - action: Action::Enter, - code: KeyCode::Enter, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Select, - symbol: "Enter", - description: "Submit", - }, - KeyBinding { - action: Action::Esc, - code: KeyCode::Esc, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Quit, - symbol: "Esc", - description: "Main menu", - }, - KeyBinding { - action: Action::ZoomIn, - code: KeyCode::Char(','), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Zoom, - symbol: ",", - description: "Zoom in", - }, - KeyBinding { - action: Action::ZoomOut, - code: KeyCode::Char('.'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Zoom, - symbol: ".", - description: "Zoom out", - }, - KeyBinding { - action: Action::Mute, - code: KeyCode::Char('m'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Music, - symbol: "m", - description: "🔈", - }, - KeyBinding { - action: Action::VolumeUp, - code: KeyCode::Char('b'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Music, - symbol: "b", - description: "🔊", - }, - KeyBinding { - action: Action::VolumeDown, - code: KeyCode::Char('n'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Music, - symbol: "n", - description: "🔉", - }, - KeyBinding { - action: Action::Tab, - code: KeyCode::Tab, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Opener, - symbol: "Tab", - description: "Toggle side panel", - }, - KeyBinding { - action: Action::ShiftTab, - code: KeyCode::Tab, - kind: KeyEventKind::Press, - modifiers: KeyModifiers::SHIFT, - group: Group::Opener, - symbol: "Shift + Tab", - description: "Toggle Map/Skills window", - }, - KeyBinding { - action: Action::WildCard('_'), - code: KeyCode::Char('_'), - kind: KeyEventKind::Press, - modifiers: KeyModifiers::NONE, - group: Group::Input, - symbol: "[All]", - description: "All keyboard characters", - }, + KeyBinding { + action: Action::Recruitment, + code: KeyCode::Char(' '), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::ALT, + group: Group::Opener, + symbol: "Alt + Space / r", + description: "Recruit", + }, + KeyBinding { + action: Action::Recruitment, + code: KeyCode::Char('r'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Opener, + symbol: "r", + description: "Recruit", + }, + KeyBinding { + action: Action::Building, + code: KeyCode::Char(' '), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::CONTROL, + group: Group::Opener, + symbol: "Ctrl + Space", + description: "Buildings", + }, + KeyBinding { + action: Action::Quit, + code: KeyCode::Char('q'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Quit, + symbol: "q", + description: "Quit", + }, + KeyBinding { + action: Action::Quit2, + code: KeyCode::Char('c'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::CONTROL, + group: Group::Quit, + symbol: "Ctrl + c", + description: "Quit", + }, + KeyBinding { + action: Action::Up, + code: KeyCode::Up, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Movement, + symbol: "↑", + description: "Up", + }, + KeyBinding { + action: Action::Down, + code: KeyCode::Down, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Movement, + symbol: "↓", + description: "Down", + }, + KeyBinding { + action: Action::Left, + code: KeyCode::Left, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Movement, + symbol: "←", + description: "Left", + }, + KeyBinding { + action: Action::Right, + code: KeyCode::Right, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Movement, + symbol: "→", + description: "Right", + }, + KeyBinding { + action: Action::ScrollUp, + code: KeyCode::Up, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::CONTROL, + group: Group::CtrlMovement, + symbol: "Ctrl + ↑", + description: "Scroll Up ", + }, + KeyBinding { + action: Action::ScrollDown, + code: KeyCode::Down, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::CONTROL, + group: Group::CtrlMovement, + symbol: "Ctrl + ↓", + description: "Scroll Down", + }, + KeyBinding { + action: Action::ScrollLeft, + code: KeyCode::Left, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::CONTROL, + group: Group::CtrlMovement, + symbol: "Ctrl + ←", + description: "Scroll Left", + }, + KeyBinding { + action: Action::ScrollRight, + code: KeyCode::Right, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::CONTROL, + group: Group::CtrlMovement, + symbol: "Ctrl + →", + description: "Scroll Right", + }, + KeyBinding { + action: Action::Space, + code: KeyCode::Char(' '), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Select, + symbol: "Space", + description: "Marking cells", + }, + KeyBinding { + action: Action::Backspace, + code: KeyCode::Backspace, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Select, + symbol: "Backspace", + description: "Unmark cell", + }, + KeyBinding { + action: Action::Delete, + code: KeyCode::Delete, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Select, + symbol: "Delete", + description: "Cancel marking", + }, + KeyBinding { + action: Action::Enter, + code: KeyCode::Enter, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Select, + symbol: "Enter", + description: "Submit", + }, + KeyBinding { + action: Action::Esc, + code: KeyCode::Esc, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Quit, + symbol: "Esc", + description: "Main menu", + }, + KeyBinding { + action: Action::ZoomIn, + code: KeyCode::Char(','), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Zoom, + symbol: ",", + description: "Zoom in", + }, + KeyBinding { + action: Action::ZoomOut, + code: KeyCode::Char('.'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Zoom, + symbol: ".", + description: "Zoom out", + }, + KeyBinding { + action: Action::Mute, + code: KeyCode::Char('m'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Music, + symbol: "m", + description: "🔈", + }, + KeyBinding { + action: Action::VolumeUp, + code: KeyCode::Char('b'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Music, + symbol: "b", + description: "🔊", + }, + KeyBinding { + action: Action::VolumeDown, + code: KeyCode::Char('n'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Music, + symbol: "n", + description: "🔉", + }, + KeyBinding { + action: Action::Tab, + code: KeyCode::Tab, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Opener, + symbol: "Tab", + description: "Toggle side panel", + }, + KeyBinding { + action: Action::ShiftTab, + code: KeyCode::Tab, + kind: KeyEventKind::Press, + modifiers: KeyModifiers::SHIFT, + group: Group::Opener, + symbol: "Shift + Tab", + description: "Toggle Map/Skills window", + }, + KeyBinding { + action: Action::WildCard('_'), + code: KeyCode::Char('_'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Input, + symbol: "[All]", + description: "All keyboard characters", + }, ]; /// Returns the `KeyBinding` associated with the given `Action`, if one exists. pub fn binding_for(action: Action) -> Option<&'static KeyBinding> { - KEYBINDINGS.iter().find(|b| b.action == action) + KEYBINDINGS.iter().find(|b| b.action == action) } /// Converts a raw `KeyEvent` into an `Action` if a matching binding is found. @@ -361,30 +361,30 @@ pub fn binding_for(action: Action) -> Option<&'static KeyBinding> { /// was pressed. Returns `None` when the event does not correspond to any known /// action. pub fn event_to_action(event: &KeyEvent) -> Option { - // Legacy terminals encode Ctrl+Space as NUL instead of a modified space. - if event.code == KeyCode::Null - && event.kind == KeyEventKind::Press - && (event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::CONTROL) - { - return Some(Action::Building); - } - if let Some(b) = KEYBINDINGS - .iter() - .find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers) - { - return Some(b.action); - } - - if KEYBINDINGS - .iter() - .any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind) - { - if let KeyCode::Char(c) = event.code { - return Some(Action::WildCard(c)); + // Legacy terminals encode Ctrl+Space as NUL instead of a modified space. + if event.code == KeyCode::Null + && event.kind == KeyEventKind::Press + && (event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::CONTROL) + { + return Some(Action::Building); + } + if let Some(b) = KEYBINDINGS + .iter() + .find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers) + { + return Some(b.action); } - } - None + if KEYBINDINGS + .iter() + .any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind) + { + if let KeyCode::Char(c) = event.code { + return Some(Action::WildCard(c)); + } + } + + None } /// Determines the size of the largest group among a slice of actions. @@ -393,13 +393,13 @@ pub fn event_to_action(event: &KeyEvent) -> Option { /// returns the highest count. This can be used, for example, to size UI /// elements that display groups of bindings. pub fn count_largest_group(actions: &[Action]) -> u16 { - let mut group_counts: HashMap = HashMap::new(); + let mut group_counts: HashMap = HashMap::new(); - for action in actions { - if let Some(binding) = binding_for(*action) { - *group_counts.entry(binding.group).or_insert(0) += 1; + for action in actions { + if let Some(binding) = binding_for(*action) { + *group_counts.entry(binding.group).or_insert(0) += 1; + } } - } - group_counts.values().copied().max().map_or(0, |v| v) + group_counts.values().copied().max().map_or(0, |v| v) } diff --git a/src/app/keybindings/main_menu.rs b/src/app/keybindings/main_menu.rs index d8274e2..7f1c895 100644 --- a/src/app/keybindings/main_menu.rs +++ b/src/app/keybindings/main_menu.rs @@ -1,36 +1,38 @@ use crate::app::{ - App, View, - keybindings::{Action, common_keybindings, event_to_action}, + App, View, + keybindings::{Action, common_keybindings, event_to_action}, }; use ratatui::crossterm::event::KeyEvent; pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) { - if let Some(action) = event_to_action(&event) { - common_keybindings(app, action); + if let Some(action) = event_to_action(&event) { + common_keybindings(app, action); - let Some(states) = app.states_mut() else { - return; - }; + let Some(states) = app.states_mut() else { + return; + }; - match action { - Action::Up => { - states.main_menu.selected_view = states.main_menu.selected_view.saturating_sub(1).max(1); - } - Action::Down => { - states.main_menu.selected_view = states.main_menu.selected_view.saturating_add(1).min(3); - } - Action::Space => { - let selected_view: usize = states.main_menu.selected_view; + match action { + Action::Up => { + states.main_menu.selected_view = + states.main_menu.selected_view.saturating_sub(1).max(1); + } + Action::Down => { + states.main_menu.selected_view = + states.main_menu.selected_view.saturating_add(1).min(3); + } + Action::Space => { + let selected_view: usize = states.main_menu.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; + 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; + } + } + _ => (), } - } - _ => (), } - } } diff --git a/src/app/keybindings/mod.rs b/src/app/keybindings/mod.rs index 88828ce..b3399c4 100644 --- a/src/app/keybindings/mod.rs +++ b/src/app/keybindings/mod.rs @@ -5,7 +5,7 @@ mod skirmish; pub use default::{common_keybindings, default_keybindings}; pub use keybindings::{ - Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action, + Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action, }; pub use main_menu::main_menu_keybindings; pub use skirmish::{available_skirmish_actions, skirmish_keybindings}; diff --git a/src/app/keybindings/skirmish.rs b/src/app/keybindings/skirmish.rs index 11cc22e..f6f1b26 100644 --- a/src/app/keybindings/skirmish.rs +++ b/src/app/keybindings/skirmish.rs @@ -1,213 +1,216 @@ use crate::app::{ - App, - keybindings::{Action, common_keybindings, event_to_action}, - states::{ - SkirmishState, - skirmish_states::{ - BoardState, FocusedCell, MoveFocusedCell, Players, ZoomLevel, - structures::{Structure, Structures}, - units::{Unit, Units}, + App, + keybindings::{Action, common_keybindings, event_to_action}, + states::{ + SkirmishState, + skirmish_states::{ + BoardState, FocusedCell, MoveFocusedCell, Players, ZoomLevel, + structures::{Structure, Structures}, + units::{Unit, Units}, + }, }, - }, }; use ratatui::{crossterm::event::KeyEvent, layout::Rect}; pub fn available_skirmish_actions(state: &SkirmishState) -> Vec { - if state.recruitment_view { - let mut actions: Vec = vec![Action::Recruitment, Action::Esc, Action::Quit2]; - if state.can_recruit_miner(state.board.get_focused_cell().get_coords(), Players::Player) { - actions.push(Action::Enter); + if state.recruitment_view { + let mut actions: Vec = vec![Action::Recruitment, Action::Esc, Action::Quit2]; + if state.can_recruit_miner(state.board.get_focused_cell().get_coords(), Players::Player) { + actions.push(Action::Enter); + } + return actions; } - return actions; - } - if state.building_view { - let mut actions: Vec = vec![Action::Building, Action::Esc, Action::Quit2]; - let coords: (usize, usize) = state.board.get_focused_cell().get_coords(); - if let Structures::MineConstruction(site) = - state.board.get_ref_cell(coords.0, coords.1).get_structure() - && site.get_owner() == Players::Player - { - actions.push(Action::Delete); - } else if state.can_start_mine_construction(coords, Players::Player) { - actions.push(Action::Enter); + if state.building_view { + let mut actions: Vec = vec![Action::Building, Action::Esc, Action::Quit2]; + let coords: (usize, usize) = state.board.get_focused_cell().get_coords(); + if let Structures::MineConstruction(site) = + state.board.get_ref_cell(coords.0, coords.1).get_structure() + && site.get_owner() == Players::Player + { + actions.push(Action::Delete); + } else if state.can_start_mine_construction(coords, Players::Player) { + actions.push(Action::Enter); + } + return actions; + } + let mut actions: Vec = vec![Action::Up, Action::Down, Action::Left, Action::Right]; + if state.board.marked_cells.marking_cells { + actions.extend([ + Action::Space, + Action::Backspace, + Action::Delete, + Action::Esc, + Action::Tab, + Action::Quit2, + ]); + return actions; } - return actions; - } - let mut actions: Vec = vec![Action::Up, Action::Down, Action::Left, Action::Right]; - if state.board.marked_cells.marking_cells { actions.extend([ - Action::Space, - Action::Backspace, - Action::Delete, - Action::Esc, - Action::Tab, - Action::Quit2, + Action::ScrollUp, + Action::ScrollDown, + Action::ScrollLeft, + Action::ScrollRight, + Action::Tab, + Action::Building, + Action::Mute, + Action::Quit, + Action::Esc, ]); - return actions; - } - actions.extend([ - Action::ScrollUp, - Action::ScrollDown, - Action::ScrollLeft, - Action::ScrollRight, - Action::Tab, - Action::Building, - Action::Mute, - Action::Quit, - Action::Esc, - ]); - if state.board.zoom_level != ZoomLevel::ZoomedIn { - actions.push(Action::ZoomIn); - } - if state.board.zoom_level != ZoomLevel::ZoomedOut { - actions.push(Action::ZoomOut); - } - let cell: &FocusedCell = state.board.get_focused_cell(); - let (row, col): (usize, usize) = cell.get_coords(); - if matches!(state.board.get_ref_cell(row, col).get_structure(), Structures::Base(base) if base.get_owner() == Players::Player) - { - actions.push(Action::Recruitment); - } - let unit: &Option = state.board.get_ref_cell(row, col).get_ref_option_unit(); - if unit.get_owner() == Players::Player && unit.get_can_dig() { - actions.push(Action::Space); - } - actions + if state.board.zoom_level != ZoomLevel::ZoomedIn { + actions.push(Action::ZoomIn); + } + if state.board.zoom_level != ZoomLevel::ZoomedOut { + actions.push(Action::ZoomOut); + } + let cell: &FocusedCell = state.board.get_focused_cell(); + let (row, col): (usize, usize) = cell.get_coords(); + if matches!(state.board.get_ref_cell(row, col).get_structure(), Structures::Base(base) if base.get_owner() == Players::Player) + { + actions.push(Action::Recruitment); + } + let unit: &Option = state.board.get_ref_cell(row, col).get_ref_option_unit(); + if unit.get_owner() == Players::Player && unit.get_can_dig() { + actions.push(Action::Space); + } + actions } pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) -> () { - if let Some(action) = event_to_action(key_event) { - let Some(states) = app.states_mut() else { - common_keybindings(app, action); - return; - }; - if states.skirmish.recruitment_view { - match action { - Action::Recruitment | Action::Esc => states.skirmish.recruitment_view = false, - Action::Enter => { - let coords: (usize, usize) = states.skirmish.board.get_focused_cell().get_coords(); - states.skirmish.recruit_miner(coords, Players::Player); + if let Some(action) = event_to_action(key_event) { + let Some(states) = app.states_mut() else { + common_keybindings(app, action); + return; + }; + if states.skirmish.recruitment_view { + match action { + Action::Recruitment | Action::Esc => states.skirmish.recruitment_view = false, + Action::Enter => { + let coords: (usize, usize) = + states.skirmish.board.get_focused_cell().get_coords(); + states.skirmish.recruit_miner(coords, Players::Player); + } + _ => common_keybindings(app, action), + } + return; } - _ => common_keybindings(app, action), - } - return; - } - if states.skirmish.building_view { - match action { - Action::Building | Action::Esc => states.skirmish.building_view = false, - Action::Enter => { - let coords: (usize, usize) = states.skirmish.board.get_focused_cell().get_coords(); - states - .skirmish - .start_mine_construction(coords, Players::Player); + if states.skirmish.building_view { + match action { + Action::Building | Action::Esc => states.skirmish.building_view = false, + Action::Enter => { + let coords: (usize, usize) = + states.skirmish.board.get_focused_cell().get_coords(); + states + .skirmish + .start_mine_construction(coords, Players::Player); + } + Action::Delete => { + let coords: (usize, usize) = + states.skirmish.board.get_focused_cell().get_coords(); + states + .skirmish + .cancel_mine_construction(coords, Players::Player); + } + _ => common_keybindings(app, action), + } + return; } - Action::Delete => { - let coords: (usize, usize) = states.skirmish.board.get_focused_cell().get_coords(); - states - .skirmish - .cancel_mine_construction(coords, Players::Player); + if action == Action::Esc && states.skirmish.board.marked_cells.marking_cells { + states.skirmish.board.marked_cells.marking_cells = false; + states.skirmish.board.end_marking_cells(true); + return; + } + let available: bool = available_skirmish_actions(&states.skirmish).contains(&action); + common_keybindings(app, action); + if !available { + return; } - _ => common_keybindings(app, action), - } - return; - } - if action == Action::Esc && states.skirmish.board.marked_cells.marking_cells { - states.skirmish.board.marked_cells.marking_cells = false; - states.skirmish.board.end_marking_cells(true); - return; - } - let available: bool = available_skirmish_actions(&states.skirmish).contains(&action); - common_keybindings(app, action); - if !available { - return; - } - let window_area: Rect = app.window_area; + let window_area: Rect = app.window_area; - let Some(states) = app.states_mut() else { - return; - }; + let Some(states) = app.states_mut() else { + return; + }; - let board: &mut BoardState = &mut states.skirmish.board; + let board: &mut BoardState = &mut states.skirmish.board; - match action { - Action::Up => { - board.change_focused_cell(MoveFocusedCell::Up); - if !board.is_focused_cell_visible() { - board.vertical_offset.prev(); + match action { + Action::Up => { + board.change_focused_cell(MoveFocusedCell::Up); + if !board.is_focused_cell_visible() { + board.vertical_offset.prev(); + } + } + Action::Down => { + board.change_focused_cell(MoveFocusedCell::Down); + if !board.is_focused_cell_visible() { + board.vertical_offset.next(); + } + } + Action::Left => { + board.change_focused_cell(MoveFocusedCell::Left); + if !board.is_focused_cell_visible() { + board.horizontal_offset.prev(); + } + } + Action::Right => { + board.change_focused_cell(MoveFocusedCell::Right); + if !board.is_focused_cell_visible() { + board.horizontal_offset.next(); + } + } + Action::ScrollUp => { + board.vertical_offset.prev(); + board.change_focused_cell(MoveFocusedCell::Up); + } + Action::ScrollDown => { + board.vertical_offset.next(); + board.change_focused_cell(MoveFocusedCell::Down); + } + Action::ScrollLeft => { + board.horizontal_offset.prev(); + board.change_focused_cell(MoveFocusedCell::Left); + } + Action::ScrollRight => { + board.horizontal_offset.next(); + board.change_focused_cell(MoveFocusedCell::Right); + } + Action::ZoomIn => match board.zoom_level { + ZoomLevel::ZoomedIn => {} + ZoomLevel::Default => { + board.change_zoom(ZoomLevel::ZoomedIn); + board.vertical_offset.next(); + } + ZoomLevel::ZoomedOut => { + board.change_zoom(ZoomLevel::Default); + board.vertical_offset.next(); + board.vertical_offset.next(); + } + }, + Action::ZoomOut => match board.zoom_level { + ZoomLevel::ZoomedIn => { + board.change_zoom(ZoomLevel::Default); + board.vertical_offset.prev(); + } + ZoomLevel::Default => { + board.change_zoom(ZoomLevel::ZoomedOut); + board.vertical_offset.prev(); + board.vertical_offset.prev(); + } + ZoomLevel::ZoomedOut => {} + }, + Action::Space => board.toggle_marking(), + Action::Building => states.skirmish.building_view = true, + Action::Recruitment => states.skirmish.recruitment_view = true, + Action::Backspace => board.pop_marked_cell(), + Action::Delete => { + board.marked_cells.marking_cells = false; + board.end_marking_cells(true) + } + Action::Tab => { + states.skirmish.side_panel = !states.skirmish.side_panel; + board.change_resize(&window_area, states.skirmish.side_panel); + } + _ => (), } - } - Action::Down => { - board.change_focused_cell(MoveFocusedCell::Down); - if !board.is_focused_cell_visible() { - board.vertical_offset.next(); - } - } - Action::Left => { - board.change_focused_cell(MoveFocusedCell::Left); - if !board.is_focused_cell_visible() { - board.horizontal_offset.prev(); - } - } - Action::Right => { - board.change_focused_cell(MoveFocusedCell::Right); - if !board.is_focused_cell_visible() { - board.horizontal_offset.next(); - } - } - Action::ScrollUp => { - board.vertical_offset.prev(); - board.change_focused_cell(MoveFocusedCell::Up); - } - Action::ScrollDown => { - board.vertical_offset.next(); - board.change_focused_cell(MoveFocusedCell::Down); - } - Action::ScrollLeft => { - board.horizontal_offset.prev(); - board.change_focused_cell(MoveFocusedCell::Left); - } - Action::ScrollRight => { - board.horizontal_offset.next(); - board.change_focused_cell(MoveFocusedCell::Right); - } - Action::ZoomIn => match board.zoom_level { - ZoomLevel::ZoomedIn => {} - ZoomLevel::Default => { - board.change_zoom(ZoomLevel::ZoomedIn); - board.vertical_offset.next(); - } - ZoomLevel::ZoomedOut => { - board.change_zoom(ZoomLevel::Default); - board.vertical_offset.next(); - board.vertical_offset.next(); - } - }, - Action::ZoomOut => match board.zoom_level { - ZoomLevel::ZoomedIn => { - board.change_zoom(ZoomLevel::Default); - board.vertical_offset.prev(); - } - ZoomLevel::Default => { - board.change_zoom(ZoomLevel::ZoomedOut); - board.vertical_offset.prev(); - board.vertical_offset.prev(); - } - ZoomLevel::ZoomedOut => {} - }, - Action::Space => board.toggle_marking(), - Action::Building => states.skirmish.building_view = true, - Action::Recruitment => states.skirmish.recruitment_view = true, - Action::Backspace => board.pop_marked_cell(), - Action::Delete => { - board.marked_cells.marking_cells = false; - board.end_marking_cells(true) - } - Action::Tab => { - states.skirmish.side_panel = !states.skirmish.side_panel; - board.change_resize(&window_area, states.skirmish.side_panel); - } - _ => (), } - } } diff --git a/src/app/state.rs b/src/app/state.rs index b3c01b3..b7612c7 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,66 +1,66 @@ use crate::{ - app::states::{ - MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState, - skirmish_states::{BoardState, Resources}, - }, - cli::Cli, + app::states::{ + MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState, + skirmish_states::{BoardState, Resources}, + }, + cli::Cli, }; use ratatui::layout::Rect; #[derive(Debug, Clone, PartialEq)] pub struct GameStates { - pub main_menu: MainMenuState, - pub skirmish: SkirmishState, - pub perk_decks: PerkDecksState, - pub skills_config: SkillsConfigState, - pub settings: SettingsState, + pub main_menu: MainMenuState, + pub skirmish: SkirmishState, + pub perk_decks: PerkDecksState, + pub skills_config: SkillsConfigState, + pub settings: SettingsState, } impl GameStates { - pub fn new(args: &Cli, area: &Rect) -> Self { - Self { - main_menu: MainMenuState { - id: 0, - name: "Main Menu", - selected_view: 1, - }, - skirmish: SkirmishState { - id: 1, - name: "Skirmish", - board: BoardState::new( - area, - args.map_width as usize, - args.map_height as usize, - args.zoom_level, - false, - ), - side_panel: false, - turn_counter: 0, - player_resources: Resources::new(args.starting_wood, args.starting_metal), - enemy_resources: Resources::new(args.starting_wood, args.starting_metal), - building_view: false, - recruitment_view: false, - }, - perk_decks: PerkDecksState { - id: 2, - name: "Perk Decks", - selected_perk_deck: 0, - }, - skills_config: SkillsConfigState { - id: 3, - name: "Skills Config", - selected_skill: 0, - }, - settings: SettingsState { - username: args.username.clone(), - game_mode: args.game_mode.clone(), - perk_deck: args.perk_deck.clone(), - starting_wood: args.starting_wood.clone(), - starting_metal: args.starting_metal, - supply_limit: args.supply_limit.clone(), - xp_modifier: args.xp_modifier.clone(), - skill_points_limit: args.skill_points_limit.clone(), - }, + pub fn new(args: &Cli, area: &Rect) -> Self { + Self { + main_menu: MainMenuState { + id: 0, + name: "Main Menu", + selected_view: 1, + }, + skirmish: SkirmishState { + id: 1, + name: "Skirmish", + board: BoardState::new( + area, + args.map_width as usize, + args.map_height as usize, + args.zoom_level, + false, + ), + side_panel: false, + turn_counter: 0, + player_resources: Resources::new(args.starting_wood, args.starting_metal), + enemy_resources: Resources::new(args.starting_wood, args.starting_metal), + building_view: false, + recruitment_view: false, + }, + perk_decks: PerkDecksState { + id: 2, + name: "Perk Decks", + selected_perk_deck: 0, + }, + skills_config: SkillsConfigState { + id: 3, + name: "Skills Config", + selected_skill: 0, + }, + settings: SettingsState { + username: args.username.clone(), + game_mode: args.game_mode.clone(), + perk_deck: args.perk_deck.clone(), + starting_wood: args.starting_wood.clone(), + starting_metal: args.starting_metal, + supply_limit: args.supply_limit.clone(), + xp_modifier: args.xp_modifier.clone(), + skill_points_limit: args.skill_points_limit.clone(), + }, + } } - } } diff --git a/src/app/states/main_menu.rs b/src/app/states/main_menu.rs index 597d06a..d603b59 100644 --- a/src/app/states/main_menu.rs +++ b/src/app/states/main_menu.rs @@ -1,6 +1,6 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct MainMenuState { - pub id: usize, - pub name: &'static str, - pub selected_view: usize, + pub id: usize, + pub name: &'static str, + pub selected_view: usize, } diff --git a/src/app/states/perk_decks.rs b/src/app/states/perk_decks.rs index bda9d1d..ff3fd18 100644 --- a/src/app/states/perk_decks.rs +++ b/src/app/states/perk_decks.rs @@ -3,24 +3,24 @@ use std::fmt::{Display, Formatter, Result}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct PerkDecksState { - pub id: usize, - pub name: &'static str, - pub selected_perk_deck: usize, + pub id: usize, + pub name: &'static str, + pub selected_perk_deck: usize, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum PerkDecks { - Silesian, - BogeyMan, - Anteater, + Silesian, + BogeyMan, + Anteater, } impl Display for PerkDecks { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - PerkDecks::Silesian => write!(f, "Silesian"), - PerkDecks::BogeyMan => write!(f, "Bogey Man"), - PerkDecks::Anteater => write!(f, "Anteater"), + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + PerkDecks::Silesian => write!(f, "Silesian"), + PerkDecks::BogeyMan => write!(f, "Bogey Man"), + PerkDecks::Anteater => write!(f, "Anteater"), + } } - } } diff --git a/src/app/states/settings.rs b/src/app/states/settings.rs index 6c513ec..67db361 100644 --- a/src/app/states/settings.rs +++ b/src/app/states/settings.rs @@ -2,12 +2,12 @@ use crate::app::states::{PerkDecks, skirmish_states::GameMode}; #[derive(Debug, Clone, PartialEq)] pub struct SettingsState { - pub username: String, - pub game_mode: GameMode, - pub perk_deck: PerkDecks, - pub starting_wood: u16, - pub starting_metal: u16, - pub supply_limit: u8, - pub xp_modifier: f32, - pub skill_points_limit: u16, + pub username: String, + pub game_mode: GameMode, + pub perk_deck: PerkDecks, + pub starting_wood: u16, + pub starting_metal: u16, + pub supply_limit: u8, + pub xp_modifier: f32, + pub skill_points_limit: u16, } diff --git a/src/app/states/skills_config.rs b/src/app/states/skills_config.rs index 15e885e..7af21bf 100644 --- a/src/app/states/skills_config.rs +++ b/src/app/states/skills_config.rs @@ -1,6 +1,6 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SkillsConfigState { - pub id: usize, - pub name: &'static str, - pub selected_skill: usize, + pub id: usize, + pub name: &'static str, + pub selected_skill: usize, } diff --git a/src/app/states/skirmish.rs b/src/app/states/skirmish.rs index 4193bb0..dd9b519 100644 --- a/src/app/states/skirmish.rs +++ b/src/app/states/skirmish.rs @@ -1,127 +1,128 @@ use crate::app::states::skirmish_states::{ - BoardState, Players, Resources, - resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS}, - structures::{BuildingTerrain, Mine, Structure, Structures}, - units::MinerUnit, + BoardState, Players, Resources, + resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS}, + structures::{BuildingTerrain, Mine, Structure, Structures}, + units::MinerUnit, }; use crate::app::widgets::CellWidget; #[derive(Debug, Clone, PartialEq)] pub struct SkirmishState { - pub id: usize, - pub name: &'static str, - pub board: BoardState, - pub side_panel: bool, - pub turn_counter: u64, - pub player_resources: Resources, - pub enemy_resources: Resources, - pub building_view: bool, - pub recruitment_view: bool, + pub id: usize, + pub name: &'static str, + pub board: BoardState, + pub side_panel: bool, + pub turn_counter: u64, + pub player_resources: Resources, + pub enemy_resources: Resources, + pub building_view: bool, + pub recruitment_view: bool, } impl SkirmishState { - pub fn can_recruit_miner(&self, (row, col): (usize, usize), owner: Players) -> bool { - let resources: &Resources = match owner { - Players::Player => &self.player_resources, - Players::Enemy => &self.enemy_resources, - Players::Unclaimed => return false, - }; - if !resources.can_afford(MinerUnit::RECRUIT_COST) { - return false; - } - self.board.cells.get(row).and_then(|cells: &Vec| -> Option<&CellWidget> { cells.get(col) }) + pub fn can_recruit_miner(&self, (row, col): (usize, usize), owner: Players) -> bool { + let resources: &Resources = match owner { + Players::Player => &self.player_resources, + Players::Enemy => &self.enemy_resources, + Players::Unclaimed => return false, + }; + if !resources.can_afford(MinerUnit::RECRUIT_COST) { + return false; + } + self.board.cells.get(row).and_then(|cells: &Vec| -> Option<&CellWidget> { cells.get(col) }) .is_some_and(|cell: &CellWidget| -> bool { matches!(cell.get_structure(), Structures::Base(base) if base.get_owner() == owner && base.can_queue_miner()) }) - } - - pub fn recruit_miner(&mut self, coords: (usize, usize), owner: Players) -> bool { - if !self.can_recruit_miner(coords, owner) { - return false; } - let cell: &mut CellWidget = &mut self.board.cells[coords.0][coords.1]; - let Structures::Base(mut base) = cell.get_structure() else { - return false; - }; - if !base.queue_miner() { - return false; - } - cell.set_structure(Structures::Base(base)); - let resources: &mut Resources = if owner == Players::Player { - &mut self.player_resources - } else { - &mut self.enemy_resources - }; - resources.spend(MinerUnit::RECRUIT_COST) - } - pub fn can_start_mine_construction(&self, coords: (usize, usize), owner: Players) -> bool { - let resources: &Resources = match owner { - Players::Player => &self.player_resources, - Players::Enemy => &self.enemy_resources, - Players::Unclaimed => return false, - }; - resources.can_afford(Mine::COST) && self.board.can_build_at(coords, owner, BuildingTerrain::Ore) - } + pub fn recruit_miner(&mut self, coords: (usize, usize), owner: Players) -> bool { + if !self.can_recruit_miner(coords, owner) { + return false; + } + let cell: &mut CellWidget = &mut self.board.cells[coords.0][coords.1]; + let Structures::Base(mut base) = cell.get_structure() else { + return false; + }; + if !base.queue_miner() { + return false; + } + cell.set_structure(Structures::Base(base)); + let resources: &mut Resources = if owner == Players::Player { + &mut self.player_resources + } else { + &mut self.enemy_resources + }; + resources.spend(MinerUnit::RECRUIT_COST) + } + + pub fn can_start_mine_construction(&self, coords: (usize, usize), owner: Players) -> bool { + let resources: &Resources = match owner { + Players::Player => &self.player_resources, + Players::Enemy => &self.enemy_resources, + Players::Unclaimed => return false, + }; + resources.can_afford(Mine::COST) + && self.board.can_build_at(coords, owner, BuildingTerrain::Ore) + } - pub fn start_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool { - if !self.can_start_mine_construction(coords, owner) - || !self.board.start_mine_construction(coords, owner) - { - return false; + pub fn start_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool { + if !self.can_start_mine_construction(coords, owner) + || !self.board.start_mine_construction(coords, owner) + { + return false; + } + let resources: &mut Resources = match owner { + Players::Player => &mut self.player_resources, + Players::Enemy => &mut self.enemy_resources, + Players::Unclaimed => return false, + }; + resources.spend(Mine::COST) } - let resources: &mut Resources = match owner { - Players::Player => &mut self.player_resources, - Players::Enemy => &mut self.enemy_resources, - Players::Unclaimed => return false, - }; - resources.spend(Mine::COST) - } - pub fn cancel_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool { - if owner == Players::Unclaimed || !self.board.cancel_mine_construction(coords, owner) { - return false; + pub fn cancel_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool { + if owner == Players::Unclaimed || !self.board.cancel_mine_construction(coords, owner) { + return false; + } + let resources: &mut Resources = if owner == Players::Player { + &mut self.player_resources + } else { + &mut self.enemy_resources + }; + resources.add_wood(Mine::COST.wood); + resources.add_metal(Mine::COST.metal); + true } - let resources: &mut Resources = if owner == Players::Player { - &mut self.player_resources - } else { - &mut self.enemy_resources - }; - resources.add_wood(Mine::COST.wood); - resources.add_metal(Mine::COST.metal); - true - } - pub fn tick_update(&mut self) -> () { - self.board.advance_turn(); - self.board.advance_recruitment(); - self.board.advance_construction(); + pub fn tick_update(&mut self) -> () { + self.board.advance_turn(); + self.board.advance_recruitment(); + self.board.advance_construction(); - // if self.board.is_victory() {} + // if self.board.is_victory() {} - self.turn_counter += 1; - if self.turn_counter.is_multiple_of(WOOD_DELIVERY_TICKS) { - self.player_resources.add_wood(WOOD_DELIVERY_AMOUNT); - } - if self.turn_counter.is_multiple_of(MINE_PRODUCTION_TICKS) { - for row in &mut self.board.cells { - for cell in row { - if let Structures::Mine(mut mine) = cell.get_structure() { - let staffed: bool = cell.get_ref_option_unit().as_ref().is_some_and( - |unit: &crate::app::states::skirmish_states::units::Units| -> bool { - mine.accepts_unit(unit) - }, - ); - let amount: u32 = u32::from(mine.produce_with_miner(staffed)); - match mine.get_owner() { - Players::Player => self.player_resources.add_metal(amount), - Players::Enemy => self.enemy_resources.add_metal(amount), - Players::Unclaimed => (), + self.turn_counter += 1; + if self.turn_counter.is_multiple_of(WOOD_DELIVERY_TICKS) { + self.player_resources.add_wood(WOOD_DELIVERY_AMOUNT); + } + if self.turn_counter.is_multiple_of(MINE_PRODUCTION_TICKS) { + for row in &mut self.board.cells { + for cell in row { + if let Structures::Mine(mut mine) = cell.get_structure() { + let staffed: bool = cell.get_ref_option_unit().as_ref().is_some_and( + |unit: &crate::app::states::skirmish_states::units::Units| -> bool { + mine.accepts_unit(unit) + }, + ); + let amount: u32 = u32::from(mine.produce_with_miner(staffed)); + match mine.get_owner() { + Players::Player => self.player_resources.add_metal(amount), + Players::Enemy => self.enemy_resources.add_metal(amount), + Players::Unclaimed => (), + } + cell.set_structure(Structures::Mine(mine)); + } + } } - cell.set_structure(Structures::Mine(mine)); - } } - } } - } } diff --git a/src/app/states/skirmish_states/board.rs b/src/app/states/skirmish_states/board.rs index b23cd6a..d50405f 100644 --- a/src/app/states/skirmish_states/board.rs +++ b/src/app/states/skirmish_states/board.rs @@ -1,601 +1,594 @@ use crate::app::{ - helpers::cells_area_helper, - states::{ - FocusedCell, Offset, - skirmish_states::{ - CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel, - structures::{ - BaseBuilding, BuildingTerrain, MineConstruction, Ore, Stone, Structure, Structures, Tunnel, - }, - tasks::{DiggingTask, MovingTask, Task, Tasks}, - units::{MinerUnit, Unit, Units}, + helpers::cells_area_helper, + states::{ + FocusedCell, Offset, + skirmish_states::{ + CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel, + structures::{ + BaseBuilding, BuildingTerrain, MineConstruction, Ore, Stone, Structure, Structures, + Tunnel, + }, + tasks::{DiggingTask, MovingTask, Task, Tasks}, + units::{MinerUnit, Unit, Units}, + }, }, - }, - widgets::CellWidget, + widgets::CellWidget, }; use ratatui::layout::Rect; use std::collections::VecDeque; #[derive(Debug, Clone, PartialEq)] pub struct BoardState { - cells_area: Rect, - pub cell_width: usize, - pub cell_height: usize, - pub map_width: usize, - pub map_height: usize, - pub cols: usize, - pub rows: usize, - pub vertical_offset: Offset, - pub horizontal_offset: Offset, - pub cells: Vec>, - pub zoom_level: ZoomLevel, - focused_cell: FocusedCell, - pub marked_cells: MarkedCells, + cells_area: Rect, + pub cell_width: usize, + pub cell_height: usize, + pub map_width: usize, + pub map_height: usize, + pub cols: usize, + pub rows: usize, + pub vertical_offset: Offset, + pub horizontal_offset: Offset, + pub cells: Vec>, + pub zoom_level: ZoomLevel, + focused_cell: FocusedCell, + pub marked_cells: MarkedCells, } impl BoardState { - pub fn advance_recruitment(&mut self) -> () { - for (row, cells) in self.cells.iter_mut().enumerate() { - for (col, cell) in cells.iter_mut().enumerate() { - if let Structures::Base(mut base) = cell.get_structure() { - if base.advance_recruitment(cell.get_ref_option_unit().is_some()) { - *cell.get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(base.get_owner(), (row, col)))); - } - cell.set_structure(Structures::Base(base)); + pub fn advance_recruitment(&mut self) -> () { + for (row, cells) in self.cells.iter_mut().enumerate() { + for (col, cell) in cells.iter_mut().enumerate() { + if let Structures::Base(mut base) = cell.get_structure() { + if base.advance_recruitment(cell.get_ref_option_unit().is_some()) { + *cell.get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(base.get_owner(), (row, col)))); + } + cell.set_structure(Structures::Base(base)); + } + } } - } } - } - fn adjacent_cells(&self, (row, col): (usize, usize)) -> Vec<&CellWidget> { - let coordinates: [(usize, usize); 4] = [ - (row.wrapping_sub(1), col), - (row.saturating_add(1), col), - (row, col.wrapping_sub(1)), - (row, col.saturating_add(1)), - ]; - coordinates - .into_iter() - .filter_map(|(row, col): (usize, usize)| -> Option<&CellWidget> { - self - .cells - .get(row) - .and_then(|cells: &Vec| -> Option<&CellWidget> { cells.get(col) }) - }) - .collect() - } + fn adjacent_cells(&self, (row, col): (usize, usize)) -> Vec<&CellWidget> { + let coordinates: [(usize, usize); 4] = [ + (row.wrapping_sub(1), col), + (row.saturating_add(1), col), + (row, col.wrapping_sub(1)), + (row, col.saturating_add(1)), + ]; + coordinates + .into_iter() + .filter_map(|(row, col): (usize, usize)| -> Option<&CellWidget> { + self.cells + .get(row) + .and_then(|cells: &Vec| -> Option<&CellWidget> { cells.get(col) }) + }) + .collect() + } - pub fn has_adjacent_builder(&self, coords: (usize, usize), owner: Players) -> bool { - owner != Players::Unclaimed - && self - .adjacent_cells(coords) - .iter() - .any(|cell: &&CellWidget| -> bool { - let unit: &Option = cell.get_ref_option_unit(); - unit.get_owner() == owner && unit.get_can_build() - }) - } + pub fn has_adjacent_builder(&self, coords: (usize, usize), owner: Players) -> bool { + owner != Players::Unclaimed + && self + .adjacent_cells(coords) + .iter() + .any(|cell: &&CellWidget| -> bool { + let unit: &Option = cell.get_ref_option_unit(); + unit.get_owner() == owner && unit.get_can_build() + }) + } - pub fn can_build_at( - &self, - (row, col): (usize, usize), - owner: Players, - terrain: BuildingTerrain, - ) -> bool { - if owner == Players::Unclaimed { - return false; + pub fn can_build_at( + &self, + (row, col): (usize, usize), + owner: Players, + terrain: BuildingTerrain, + ) -> bool { + if owner == Players::Unclaimed { + return false; + } + let Some(cell) = self + .cells + .get(row) + .and_then(|cells: &Vec| -> Option<&CellWidget> { cells.get(col) }) + else { + return false; + }; + if cell.get_ref_option_unit().is_some() { + return false; + } + if !self.has_adjacent_builder((row, col), owner) { + return false; + } + match (terrain, cell.get_structure()) { + (BuildingTerrain::Ore, Structures::Ore(ore)) => { + ore.get_owner() == Players::Unclaimed && ore.get_ore_amount() > 0 + } + (BuildingTerrain::Stone, Structures::Stone(_)) => self + .adjacent_cells((row, col)) + .iter() + .any(|cell: &&CellWidget| -> bool { + matches!(cell.get_structure(), Structures::Tunnel(_)) + || matches!(cell.get_structure(), Structures::Base(_)) + && cell.get_structure().get_owner() == owner + }), + _ => false, + } } - let Some(cell) = self - .cells - .get(row) - .and_then(|cells: &Vec| -> Option<&CellWidget> { cells.get(col) }) - else { - return false; - }; - if cell.get_ref_option_unit().is_some() { - return false; - } - if !self.has_adjacent_builder((row, col), owner) { - return false; - } - match (terrain, cell.get_structure()) { - (BuildingTerrain::Ore, Structures::Ore(ore)) => { - ore.get_owner() == Players::Unclaimed && ore.get_ore_amount() > 0 - } - (BuildingTerrain::Stone, Structures::Stone(_)) => { - self - .adjacent_cells((row, col)) - .iter() - .any(|cell: &&CellWidget| -> bool { - matches!(cell.get_structure(), Structures::Tunnel(_)) - || matches!(cell.get_structure(), Structures::Base(_)) - && cell.get_structure().get_owner() == owner - }) - } - _ => false, - } - } - pub fn start_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool { - if !self.can_build_at(coords, owner, BuildingTerrain::Ore) { - return false; - } - let cell: &mut CellWidget = self.get_mut_cell(coords.0, coords.1); - if let Structures::Ore(ore) = cell.get_structure() - && let Some(site) = MineConstruction::new(owner, ore) - { - cell.set_structure(Structures::MineConstruction(site)); - return true; - } - false - } - - pub fn cancel_mine_construction(&mut self, (row, col): (usize, usize), owner: Players) -> bool { - let Some(cell): Option<&mut CellWidget> = self - .cells - .get_mut(row) - .and_then(|cells: &mut Vec| -> Option<&mut CellWidget> { cells.get_mut(col) }) - else { - return false; - }; - if let Structures::MineConstruction(site) = cell.get_structure() - && site.get_owner() == owner - { - cell.set_structure(Structures::Ore(site.cancel())); - return true; - } - false - } - - pub fn advance_construction(&mut self) -> () { - for row in 0..self.cells.len() { - for col in 0..self.cells[row].len() { - if let Structures::MineConstruction(mut site) = self.cells[row][col].get_structure() - && self.has_adjacent_builder((row, col), site.get_owner()) + pub fn start_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool { + if !self.can_build_at(coords, owner, BuildingTerrain::Ore) { + return false; + } + let cell: &mut CellWidget = self.get_mut_cell(coords.0, coords.1); + if let Structures::Ore(ore) = cell.get_structure() + && let Some(site) = MineConstruction::new(owner, ore) { - site.advance(); - let structure: Structures = if let Some(mine) = site.into_mine() { - Structures::Mine(mine) - } else { - Structures::MineConstruction(site) - }; - self.cells[row][col].set_structure(structure); + cell.set_structure(Structures::MineConstruction(site)); + return true; } - } + false } - } - pub fn new( - area: &Rect, - map_width: usize, - map_height: usize, - zoom_level: ZoomLevel, - side_panel: bool, - ) -> Self { - let cells_area: Rect = cells_area_helper(area, side_panel); - - let cell_width: usize = zoom_level.get_cell_size(CellSizes::Width); - let cell_height: usize = zoom_level.get_cell_size(CellSizes::Height); - - let cols: usize = (cells_area.width / cell_width as u16) as usize; - let rows: usize = (cells_area.height / cell_height as u16) as usize; - - let v_max_offset: usize = Self::max_offset(map_height, rows); - let h_max_offset: usize = Self::max_offset(map_width, cols); - - let vertical_offset: Offset = Offset::new( - Some(map_height.saturating_sub(rows).div_ceil(2)), - Some(v_max_offset), - ); - let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset)); - - let focused_cell: FocusedCell = FocusedCell::new((map_height) / 2, 3, map_height, map_width); - - let mut cells: Vec> = Vec::new(); - - let player_base_coords: (usize, usize) = ((map_height) / 2, 2); - let enemy_base_coords: (usize, usize) = ((map_height) / 2, map_width - 3); - - for row in 0..map_height { - let mut rows: Vec = Vec::new(); - - for col in 0..map_width { - let (focused_row, focused_col) = focused_cell.get_coords(); - let selected: bool = row == focused_row && col == focused_col; - let player_base: bool = row == player_base_coords.0 && col == player_base_coords.1; - let player_ore: bool = row == player_base_coords.0 && col == player_base_coords.1 - 1; - let enemy_base: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1; - let enemy_ore: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1 + 1; - - let structure: Structures = if player_base { - Structures::Base(BaseBuilding::new(Players::Player)) - } else if enemy_base { - Structures::Base(BaseBuilding::new(Players::Enemy)) - } else if player_ore { - Structures::Ore(Ore::new(Players::Unclaimed, 1)) - } else if enemy_ore { - Structures::Ore(Ore::new(Players::Unclaimed, 1)) - } else { - Structures::Stone(Stone::new()) + pub fn cancel_mine_construction(&mut self, (row, col): (usize, usize), owner: Players) -> bool { + let Some(cell): Option<&mut CellWidget> = self.cells.get_mut(row).and_then( + |cells: &mut Vec| -> Option<&mut CellWidget> { cells.get_mut(col) }, + ) else { + return false; }; + if let Structures::MineConstruction(site) = cell.get_structure() + && site.get_owner() == owner + { + cell.set_structure(Structures::Ore(site.cancel())); + return true; + } + false + } - let unit: Option = if player_base { - Some(Units::Miner(MinerUnit::new( - Players::Player, - player_base_coords, - ))) - } else if enemy_base { - Some(Units::Miner(MinerUnit::new( - Players::Enemy, - enemy_base_coords, - ))) + pub fn advance_construction(&mut self) -> () { + for row in 0..self.cells.len() { + for col in 0..self.cells[row].len() { + if let Structures::MineConstruction(mut site) = self.cells[row][col].get_structure() + && self.has_adjacent_builder((row, col), site.get_owner()) + { + site.advance(); + let structure: Structures = if let Some(mine) = site.into_mine() { + Structures::Mine(mine) + } else { + Structures::MineConstruction(site) + }; + self.cells[row][col].set_structure(structure); + } + } + } + } + + pub fn new( + area: &Rect, + map_width: usize, + map_height: usize, + zoom_level: ZoomLevel, + side_panel: bool, + ) -> Self { + let cells_area: Rect = cells_area_helper(area, side_panel); + + let cell_width: usize = zoom_level.get_cell_size(CellSizes::Width); + let cell_height: usize = zoom_level.get_cell_size(CellSizes::Height); + + let cols: usize = (cells_area.width / cell_width as u16) as usize; + let rows: usize = (cells_area.height / cell_height as u16) as usize; + + let v_max_offset: usize = Self::max_offset(map_height, rows); + let h_max_offset: usize = Self::max_offset(map_width, cols); + + let vertical_offset: Offset = Offset::new( + Some(map_height.saturating_sub(rows).div_ceil(2)), + Some(v_max_offset), + ); + let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset)); + + let focused_cell: FocusedCell = + FocusedCell::new((map_height) / 2, 3, map_height, map_width); + + let mut cells: Vec> = Vec::new(); + + let player_base_coords: (usize, usize) = ((map_height) / 2, 2); + let enemy_base_coords: (usize, usize) = ((map_height) / 2, map_width - 3); + + for row in 0..map_height { + let mut rows: Vec = Vec::new(); + + for col in 0..map_width { + let (focused_row, focused_col) = focused_cell.get_coords(); + let selected: bool = row == focused_row && col == focused_col; + let player_base: bool = row == player_base_coords.0 && col == player_base_coords.1; + let player_ore: bool = + row == player_base_coords.0 && col == player_base_coords.1 - 1; + let enemy_base: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1; + let enemy_ore: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1 + 1; + + let structure: Structures = if player_base { + Structures::Base(BaseBuilding::new(Players::Player)) + } else if enemy_base { + Structures::Base(BaseBuilding::new(Players::Enemy)) + } else if player_ore { + Structures::Ore(Ore::new(Players::Unclaimed, 1)) + } else if enemy_ore { + Structures::Ore(Ore::new(Players::Unclaimed, 1)) + } else { + Structures::Stone(Stone::new()) + }; + + let unit: Option = if player_base { + Some(Units::Miner(MinerUnit::new( + Players::Player, + player_base_coords, + ))) + } else if enemy_base { + Some(Units::Miner(MinerUnit::new( + Players::Enemy, + enemy_base_coords, + ))) + } else { + None + }; + + rows.push(CellWidget::new( + row, col, zoom_level, selected, structure, unit, + )); + } + + cells.push(rows); + } + + let marked_cells: MarkedCells = MarkedCells::new(); + + Self { + cells_area, + cell_width, + cell_height, + map_width, + map_height, + cols, + rows, + vertical_offset, + horizontal_offset, + cells, + zoom_level, + focused_cell, + marked_cells, + } + } + + pub fn get_ref_cell(&self, row: usize, col: usize) -> &CellWidget { + &self.cells[row][col] + } + + fn get_mut_cell(&mut self, row: usize, col: usize) -> &mut CellWidget { + &mut self.cells[row][col] + } + + pub fn get_marked_cells_widgets(&self) -> VecDeque<&CellWidget> { + self.marked_cells + .marked_cells + .iter() + .map(|&(row, col)| &self.cells[row][col]) + .collect() + } + + pub fn get_marked_cells(&self) -> &VecDeque<(usize, usize)> { + &self.marked_cells.marked_cells + } + + pub fn toggle_marking(&mut self) { + self.marked_cells.marking_cells = !self.marked_cells.marking_cells; + + if self.marked_cells.marking_cells { + self.start_marking_cells(); } else { - None - }; - - rows.push(CellWidget::new( - row, col, zoom_level, selected, structure, unit, - )); - } - - cells.push(rows); + self.end_marking_cells(false); + } } - let marked_cells: MarkedCells = MarkedCells::new(); + pub fn push_marked_cell(&mut self, new_cell: (usize, usize)) { + if self.marked_cells.marked_cells.back() == Some(&new_cell) { + return; + } + let cell: &mut CellWidget = self.get_mut_cell(new_cell.0, new_cell.1); - Self { - cells_area, - cell_width, - cell_height, - map_width, - map_height, - cols, - rows, - vertical_offset, - horizontal_offset, - cells, - zoom_level, - focused_cell, - marked_cells, - } - } - - pub fn get_ref_cell(&self, row: usize, col: usize) -> &CellWidget { - &self.cells[row][col] - } - - fn get_mut_cell(&mut self, row: usize, col: usize) -> &mut CellWidget { - &mut self.cells[row][col] - } - - pub fn get_marked_cells_widgets(&self) -> VecDeque<&CellWidget> { - self - .marked_cells - .marked_cells - .iter() - .map(|&(row, col)| &self.cells[row][col]) - .collect() - } - - pub fn get_marked_cells(&self) -> &VecDeque<(usize, usize)> { - &self.marked_cells.marked_cells - } - - pub fn toggle_marking(&mut self) { - self.marked_cells.marking_cells = !self.marked_cells.marking_cells; - - if self.marked_cells.marking_cells { - self.start_marking_cells(); - } else { - self.end_marking_cells(false); - } - } - - pub fn push_marked_cell(&mut self, new_cell: (usize, usize)) { - if self.marked_cells.marked_cells.back() == Some(&new_cell) { - return; - } - let cell: &mut CellWidget = self.get_mut_cell(new_cell.0, new_cell.1); - - if !cell.get_marked() { - cell.set_marked(true); - } - - self - .marked_cells - .marked_cells - .push_back((new_cell.0, new_cell.1)); - } - - pub fn pop_marked_cell(&mut self) { - if self.marked_cells.marked_cells.len() < 2 { - return; - } - - let old: (usize, usize) = - self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 1]; - let new: (usize, usize) = - self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 2]; - let old_is_unique: bool = self - .marked_cells - .marked_cells - .iter() - .filter(|x| **x == old) - .count() - == 1; - - let old_cell: &mut CellWidget = self.get_mut_cell(old.0, old.1).set_selected(false); - if old_is_unique { - old_cell.set_marked(false); - } - - self.get_mut_cell(new.0, new.1).set_selected(true); - - self.marked_cells.marked_cells.pop_back(); - - self.focused_cell.set_focused_cell(new); - } - - pub fn start_marking_cells(&mut self) { - let (row, col) = self.focused_cell.get_coords(); - - let selected_unit: Option = self.get_ref_cell(row, col).get_option_unit(); - if selected_unit.get_owner() != Players::Player || !selected_unit.get_can_dig() { - self.marked_cells.marking_cells = false; - return; - } - // Extend queued work from its final destination, not the miner's current cell. - let start: (usize, usize) = selected_unit - .get_ref_tasks() - .back() - .and_then(|task| task.get_path().back()) - .copied() - .unwrap_or((row, col)); - self.focused_cell.set_focused_cell(start); - let start: (usize, usize) = self.focused_cell.get_coords(); - self.get_mut_cell(row, col).set_selected(false); - self - .get_mut_cell(start.0, start.1) - .set_selected(true) - .set_marked(true); - if !self.is_focused_cell_visible() { - self - .vertical_offset - .set_value(start.0.saturating_sub(self.rows / 2)); - self - .horizontal_offset - .set_value(start.1.saturating_sub(self.cols / 2)); - } - self.marked_cells.selected_unit = selected_unit; - self.marked_cells.marked_cells.push_back(start); - } - - pub fn end_marking_cells(&mut self, del: bool) { - self.marked_cells.marking_cells = false; - for (row, col) in self.marked_cells.marked_cells.clone() { - self.get_mut_cell(row, col).set_marked(false); - } - - let (row, col) = self.marked_cells.selected_unit.get_coords(); - - if self.marked_cells.marked_cells.len() > 1 && !del { - let path: Vec<(usize, usize)> = self - .marked_cells - .marked_cells - .iter() - .skip(1) - .cloned() - .collect(); - - let tick_increase: f32 = self.marked_cells.selected_unit.get_digging_power(); - - let tasks: VecDeque = path - .chunk_by(|a, b| self.is_walkable(*a) == self.is_walkable(*b)) - .map(|segment| { - let path = segment.iter().copied().collect(); - if self.is_walkable(segment[0]) { - Tasks::Moving(MovingTask::new(path)) - } else { - Tasks::Digging(DiggingTask::new(path, tick_increase)) - } - }) - .collect(); - if let Some(unit) = self.get_mut_cell(row, col).get_mut_option_unit().as_mut() { - unit.get_mut_tasks().extend(tasks); - } - } - - self.marked_cells.selected_unit = None; - self.marked_cells.marked_cells.clear(); - } - - fn max_offset(map_size: usize, size: usize) -> usize { - if map_size > size { map_size - size } else { 0 } - } - - fn is_walkable(&self, (row, col): (usize, usize)) -> bool { - self - .cells - .get(row) - .and_then(|cells| cells.get(col)) - .is_some_and(|cell| { - self - .marked_cells - .selected_unit - .as_ref() - .is_some_and(|unit: &Units| -> bool { cell.get_structure().can_enter(unit) }) - }) - } - - pub fn get_focused_cell(&self) -> &FocusedCell { - &self.focused_cell - } - - pub fn is_focused_cell_visible(&self) -> bool { - let vertical_offset: usize = self.vertical_offset.get_value(); - let horizontal_offset: usize = self.horizontal_offset.get_value(); - - let (row, col) = self.focused_cell.get_coords(); - - if 0usize.saturating_add(vertical_offset) > row - || row >= self.rows.saturating_add(vertical_offset) - { - return false; - } - if 0usize.saturating_add(horizontal_offset) > col - || col >= self.cols.saturating_add(horizontal_offset) - { - return false; - } - - true - } - - pub fn change_resize(&mut self, area: &Rect, side_panel: bool) { - self.cells_area = cells_area_helper(area, side_panel); - - self.cols = (self.cells_area.width / self.cell_width as u16) as usize; - self.rows = (self.cells_area.height / self.cell_height as u16) as usize; - - let v_max_offset: usize = Self::max_offset(self.map_height, self.rows); - let h_max_offset: usize = Self::max_offset(self.map_width, self.cols); - - self.horizontal_offset = - Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset)); - self.vertical_offset = Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset)); - } - - pub fn change_zoom(&mut self, new_zoom_level: ZoomLevel) { - self.zoom_level = new_zoom_level; - - self.cell_width = new_zoom_level.get_cell_size(CellSizes::Width); - self.cell_height = new_zoom_level.get_cell_size(CellSizes::Height); - - self.cols = (self.cells_area.width / self.cell_width as u16) as usize; - self.rows = (self.cells_area.height / self.cell_height as u16) as usize; - - let v_max_offset: usize = Self::max_offset(self.map_height, self.rows); - let h_max_offset: usize = Self::max_offset(self.map_width, self.cols); - - self.vertical_offset = Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset)); - self.horizontal_offset = - Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset)); - - let (focused_row, focused_coll) = self.focused_cell.get_coords(); - - self.focused_cell = - FocusedCell::new(focused_row, focused_coll, self.map_height, self.map_width); - - for row in 0..self.map_height { - for col in 0..self.map_width { - self.get_mut_cell(row, col).set_zoom_level(new_zoom_level); - } - } - } - - pub fn change_focused_cell(&mut self, direction: MoveFocusedCell) { - let old_cell: (usize, usize) = self.focused_cell.get_coords(); - let new_cell: (usize, usize) = self.focused_cell.move_focused_cell(direction); - - self - .get_mut_cell(old_cell.0, old_cell.1) - .set_selected(false); - self.get_mut_cell(new_cell.0, new_cell.1).set_selected(true); - - if self.marked_cells.marking_cells { - self.push_marked_cell(new_cell); - } - } - - pub fn advance_turn(&mut self) { - // Snapshot positions so moving to a later cell cannot grant another turn. - let active: Vec<(usize, usize)> = self - .cells - .iter() - .enumerate() - .flat_map(|(row, cells)| { - cells.iter().enumerate().filter_map(move |(col, cell)| { - cell - .get_ref_option_unit() - .as_ref() - .filter(|unit| !unit.get_ref_tasks().is_empty()) - .map(|_| (row, col)) - }) - }) - .collect(); - - for (row, col) in active { - // Keep the route's origin stable while the player is drawing it. - if self.marked_cells.marking_cells - && self.marked_cells.selected_unit.get_coords() == (row, col) - { - continue; - } - let mut unit: Units = self - .get_mut_cell(row, col) - .get_mut_option_unit() - .take() - .unwrap(); - let mut task: Tasks = unit.get_mut_tasks().pop_front().unwrap(); - let mut destination = (row, col); - - if let Some((target_row, target_col)) = task.get_path().front().copied() { - let adjacent: bool = row - .abs_diff(target_row) - .saturating_add(col.abs_diff(target_col)) - == 1; - if target_row >= self.map_height || target_col >= self.map_width || !adjacent { - // Discard invalid routes rather than teleporting or indexing outside the map. - *self.get_mut_cell(row, col).get_mut_option_unit() = Some(unit); - continue; + if !cell.get_marked() { + cell.set_marked(true); } - let cell: &mut CellWidget = self.get_mut_cell(target_row, target_col); - if cell.get_ref_option_unit().is_none() { - let structure: Structures = cell.get_structure(); - let walkable: bool = structure.can_enter(&unit); - // Previously planned stone may already be a tunnel when this task starts. - task = match (task, structure) { - (Tasks::Digging(dig), _) if walkable => Tasks::Moving(MovingTask::from_digging(dig)), - (Tasks::Moving(moving), Structures::Stone(_)) => { - Tasks::Digging(DiggingTask::from_moving(moving, unit.get_digging_power())) + self.marked_cells + .marked_cells + .push_back((new_cell.0, new_cell.1)); + } + + pub fn pop_marked_cell(&mut self) { + if self.marked_cells.marked_cells.len() < 2 { + return; + } + + let old: (usize, usize) = + self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 1]; + let new: (usize, usize) = + self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 2]; + let old_is_unique: bool = self + .marked_cells + .marked_cells + .iter() + .filter(|x| **x == old) + .count() + == 1; + + let old_cell: &mut CellWidget = self.get_mut_cell(old.0, old.1).set_selected(false); + if old_is_unique { + old_cell.set_marked(false); + } + + self.get_mut_cell(new.0, new.1).set_selected(true); + + self.marked_cells.marked_cells.pop_back(); + + self.focused_cell.set_focused_cell(new); + } + + pub fn start_marking_cells(&mut self) { + let (row, col) = self.focused_cell.get_coords(); + + let selected_unit: Option = self.get_ref_cell(row, col).get_option_unit(); + if selected_unit.get_owner() != Players::Player || !selected_unit.get_can_dig() { + self.marked_cells.marking_cells = false; + return; + } + // Extend queued work from its final destination, not the miner's current cell. + let start: (usize, usize) = selected_unit + .get_ref_tasks() + .back() + .and_then(|task| task.get_path().back()) + .copied() + .unwrap_or((row, col)); + self.focused_cell.set_focused_cell(start); + let start: (usize, usize) = self.focused_cell.get_coords(); + self.get_mut_cell(row, col).set_selected(false); + self.get_mut_cell(start.0, start.1) + .set_selected(true) + .set_marked(true); + if !self.is_focused_cell_visible() { + self.vertical_offset + .set_value(start.0.saturating_sub(self.rows / 2)); + self.horizontal_offset + .set_value(start.1.saturating_sub(self.cols / 2)); + } + self.marked_cells.selected_unit = selected_unit; + self.marked_cells.marked_cells.push_back(start); + } + + pub fn end_marking_cells(&mut self, del: bool) { + self.marked_cells.marking_cells = false; + for (row, col) in self.marked_cells.marked_cells.clone() { + self.get_mut_cell(row, col).set_marked(false); + } + + let (row, col) = self.marked_cells.selected_unit.get_coords(); + + if self.marked_cells.marked_cells.len() > 1 && !del { + let path: Vec<(usize, usize)> = self + .marked_cells + .marked_cells + .iter() + .skip(1) + .cloned() + .collect(); + + let tick_increase: f32 = self.marked_cells.selected_unit.get_digging_power(); + + let tasks: VecDeque = path + .chunk_by(|a, b| self.is_walkable(*a) == self.is_walkable(*b)) + .map(|segment| { + let path = segment.iter().copied().collect(); + if self.is_walkable(segment[0]) { + Tasks::Moving(MovingTask::new(path)) + } else { + Tasks::Digging(DiggingTask::new(path, tick_increase)) + } + }) + .collect(); + if let Some(unit) = self.get_mut_cell(row, col).get_mut_option_unit().as_mut() { + unit.get_mut_tasks().extend(tasks); + } + } + + self.marked_cells.selected_unit = None; + self.marked_cells.marked_cells.clear(); + } + + fn max_offset(map_size: usize, size: usize) -> usize { + if map_size > size { map_size - size } else { 0 } + } + + fn is_walkable(&self, (row, col): (usize, usize)) -> bool { + self.cells + .get(row) + .and_then(|cells| cells.get(col)) + .is_some_and(|cell| { + self.marked_cells + .selected_unit + .as_ref() + .is_some_and(|unit: &Units| -> bool { cell.get_structure().can_enter(unit) }) + }) + } + + pub fn get_focused_cell(&self) -> &FocusedCell { + &self.focused_cell + } + + pub fn is_focused_cell_visible(&self) -> bool { + let vertical_offset: usize = self.vertical_offset.get_value(); + let horizontal_offset: usize = self.horizontal_offset.get_value(); + + let (row, col) = self.focused_cell.get_coords(); + + if 0usize.saturating_add(vertical_offset) > row + || row >= self.rows.saturating_add(vertical_offset) + { + return false; + } + if 0usize.saturating_add(horizontal_offset) > col + || col >= self.cols.saturating_add(horizontal_offset) + { + return false; + } + + true + } + + pub fn change_resize(&mut self, area: &Rect, side_panel: bool) { + self.cells_area = cells_area_helper(area, side_panel); + + self.cols = (self.cells_area.width / self.cell_width as u16) as usize; + self.rows = (self.cells_area.height / self.cell_height as u16) as usize; + + let v_max_offset: usize = Self::max_offset(self.map_height, self.rows); + let h_max_offset: usize = Self::max_offset(self.map_width, self.cols); + + self.horizontal_offset = + Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset)); + self.vertical_offset = + Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset)); + } + + pub fn change_zoom(&mut self, new_zoom_level: ZoomLevel) { + self.zoom_level = new_zoom_level; + + self.cell_width = new_zoom_level.get_cell_size(CellSizes::Width); + self.cell_height = new_zoom_level.get_cell_size(CellSizes::Height); + + self.cols = (self.cells_area.width / self.cell_width as u16) as usize; + self.rows = (self.cells_area.height / self.cell_height as u16) as usize; + + let v_max_offset: usize = Self::max_offset(self.map_height, self.rows); + let h_max_offset: usize = Self::max_offset(self.map_width, self.cols); + + self.vertical_offset = + Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset)); + self.horizontal_offset = + Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset)); + + let (focused_row, focused_coll) = self.focused_cell.get_coords(); + + self.focused_cell = + FocusedCell::new(focused_row, focused_coll, self.map_height, self.map_width); + + for row in 0..self.map_height { + for col in 0..self.map_width { + self.get_mut_cell(row, col).set_zoom_level(new_zoom_level); } - (task, _) => task, - }; - match (&mut task, structure) { - (Tasks::Digging(dig), Structures::Stone(mut stone)) => { - let max: f32 = f32::from(stone.get_max_durability()); - let before: u16 = (dig.get_progress() * max / 100.0) as u16; - dig.progress(); - let after: u16 = (dig.get_progress() * max / 100.0) as u16; - stone.take_damage(after.saturating_sub(before)); - if stone.get_durability() == 0 { - cell.set_structure(Structures::Tunnel(Tunnel::new())); - destination = (target_row, target_col); - dig.pop_front(); - } else { - cell.set_structure(Structures::Stone(stone)); - } + } + } + + pub fn change_focused_cell(&mut self, direction: MoveFocusedCell) { + let old_cell: (usize, usize) = self.focused_cell.get_coords(); + let new_cell: (usize, usize) = self.focused_cell.move_focused_cell(direction); + + self.get_mut_cell(old_cell.0, old_cell.1) + .set_selected(false); + self.get_mut_cell(new_cell.0, new_cell.1).set_selected(true); + + if self.marked_cells.marking_cells { + self.push_marked_cell(new_cell); + } + } + + pub fn advance_turn(&mut self) { + // Snapshot positions so moving to a later cell cannot grant another turn. + let active: Vec<(usize, usize)> = self + .cells + .iter() + .enumerate() + .flat_map(|(row, cells)| { + cells.iter().enumerate().filter_map(move |(col, cell)| { + cell.get_ref_option_unit() + .as_ref() + .filter(|unit| !unit.get_ref_tasks().is_empty()) + .map(|_| (row, col)) + }) + }) + .collect(); + + for (row, col) in active { + // Keep the route's origin stable while the player is drawing it. + if self.marked_cells.marking_cells + && self.marked_cells.selected_unit.get_coords() == (row, col) + { + continue; } - (Tasks::Moving(moving), _) if walkable => { - moving.progress(); - if moving.is_finished() { - destination = (target_row, target_col); - moving.pop_front(); - } + let mut unit: Units = self + .get_mut_cell(row, col) + .get_mut_option_unit() + .take() + .unwrap(); + let mut task: Tasks = unit.get_mut_tasks().pop_front().unwrap(); + let mut destination = (row, col); + + if let Some((target_row, target_col)) = task.get_path().front().copied() { + let adjacent: bool = row + .abs_diff(target_row) + .saturating_add(col.abs_diff(target_col)) + == 1; + if target_row >= self.map_height || target_col >= self.map_width || !adjacent { + // Discard invalid routes rather than teleporting or indexing outside the map. + *self.get_mut_cell(row, col).get_mut_option_unit() = Some(unit); + continue; + } + + let cell: &mut CellWidget = self.get_mut_cell(target_row, target_col); + if cell.get_ref_option_unit().is_none() { + let structure: Structures = cell.get_structure(); + let walkable: bool = structure.can_enter(&unit); + // Previously planned stone may already be a tunnel when this task starts. + task = match (task, structure) { + (Tasks::Digging(dig), _) if walkable => { + Tasks::Moving(MovingTask::from_digging(dig)) + } + (Tasks::Moving(moving), Structures::Stone(_)) => Tasks::Digging( + DiggingTask::from_moving(moving, unit.get_digging_power()), + ), + (task, _) => task, + }; + match (&mut task, structure) { + (Tasks::Digging(dig), Structures::Stone(mut stone)) => { + let max: f32 = f32::from(stone.get_max_durability()); + let before: u16 = (dig.get_progress() * max / 100.0) as u16; + dig.progress(); + let after: u16 = (dig.get_progress() * max / 100.0) as u16; + stone.take_damage(after.saturating_sub(before)); + if stone.get_durability() == 0 { + cell.set_structure(Structures::Tunnel(Tunnel::new())); + destination = (target_row, target_col); + dig.pop_front(); + } else { + cell.set_structure(Structures::Stone(stone)); + } + } + (Tasks::Moving(moving), _) if walkable => { + moving.progress(); + if moving.is_finished() { + destination = (target_row, target_col); + moving.pop_front(); + } + } + _ => {} + } + } + if !task.get_path().is_empty() { + unit.get_mut_tasks().push_front(task); + } } - _ => {} - } - } - if !task.get_path().is_empty() { - unit.get_mut_tasks().push_front(task); + unit.set_coords(destination); + *self + .get_mut_cell(destination.0, destination.1) + .get_mut_option_unit() = Some(unit); } - } - unit.set_coords(destination); - *self - .get_mut_cell(destination.0, destination.1) - .get_mut_option_unit() = Some(unit); } - } } diff --git a/src/app/states/skirmish_states/focused_cell.rs b/src/app/states/skirmish_states/focused_cell.rs index ebc0cb5..1bcef2a 100644 --- a/src/app/states/skirmish_states/focused_cell.rs +++ b/src/app/states/skirmish_states/focused_cell.rs @@ -1,53 +1,53 @@ pub enum MoveFocusedCell { - Up, - Down, - Left, - Right, + Up, + Down, + Left, + Right, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct FocusedCell { - row: usize, - col: usize, - max_row: usize, - max_col: usize, + row: usize, + col: usize, + max_row: usize, + max_col: usize, } impl FocusedCell { - pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self { - Self { - row, - col, - max_row, - max_col, - } - } - - pub fn get_coords(&self) -> (usize, usize) { - (self.row, self.col) - } - - pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) { - match direction { - MoveFocusedCell::Up => { - self.row = self.row.saturating_sub(1).max(0); - } - MoveFocusedCell::Down => { - self.row = self.row.saturating_add(1).min(self.max_row - 1); - } - MoveFocusedCell::Left => { - self.col = self.col.saturating_sub(1).max(0); - } - MoveFocusedCell::Right => { - self.col = self.col.saturating_add(1).min(self.max_col - 1); - } + pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self { + Self { + row, + col, + max_row, + max_col, + } } - (self.row, self.col) - } + pub fn get_coords(&self) -> (usize, usize) { + (self.row, self.col) + } - pub fn set_focused_cell(&mut self, cell: (usize, usize)) { - self.row = cell.0.max(0).min(self.max_row - 1); - self.col = cell.1.max(0).min(self.max_col - 1); - } + pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) { + match direction { + MoveFocusedCell::Up => { + self.row = self.row.saturating_sub(1).max(0); + } + MoveFocusedCell::Down => { + self.row = self.row.saturating_add(1).min(self.max_row - 1); + } + MoveFocusedCell::Left => { + self.col = self.col.saturating_sub(1).max(0); + } + MoveFocusedCell::Right => { + self.col = self.col.saturating_add(1).min(self.max_col - 1); + } + } + + (self.row, self.col) + } + + pub fn set_focused_cell(&mut self, cell: (usize, usize)) { + self.row = cell.0.max(0).min(self.max_row - 1); + self.col = cell.1.max(0).min(self.max_col - 1); + } } diff --git a/src/app/states/skirmish_states/game_mode.rs b/src/app/states/skirmish_states/game_mode.rs index 66d312f..03c8ff5 100644 --- a/src/app/states/skirmish_states/game_mode.rs +++ b/src/app/states/skirmish_states/game_mode.rs @@ -2,6 +2,6 @@ use clap::ValueEnum; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum GameMode { - LastManStanding, - FrontLines, + LastManStanding, + FrontLines, } diff --git a/src/app/states/skirmish_states/marked_cells.rs b/src/app/states/skirmish_states/marked_cells.rs index b035125..1b0d6c2 100644 --- a/src/app/states/skirmish_states/marked_cells.rs +++ b/src/app/states/skirmish_states/marked_cells.rs @@ -3,17 +3,17 @@ use std::collections::VecDeque; #[derive(Debug, Clone, PartialEq)] pub struct MarkedCells { - pub marking_cells: bool, - pub marked_cells: VecDeque<(usize, usize)>, - pub selected_unit: Option, + pub marking_cells: bool, + pub marked_cells: VecDeque<(usize, usize)>, + pub selected_unit: Option, } impl MarkedCells { - pub fn new() -> Self { - Self { - marking_cells: false, - marked_cells: VecDeque::new(), - selected_unit: None, + pub fn new() -> Self { + Self { + marking_cells: false, + marked_cells: VecDeque::new(), + selected_unit: None, + } } - } } diff --git a/src/app/states/skirmish_states/offset.rs b/src/app/states/skirmish_states/offset.rs index 8eb008f..1d5c008 100644 --- a/src/app/states/skirmish_states/offset.rs +++ b/src/app/states/skirmish_states/offset.rs @@ -1,35 +1,35 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Offset { - value: usize, - max: usize, + value: usize, + max: usize, } impl Offset { - pub fn new(initial_value: Option, initial_max: Option) -> Self { - Self { - value: initial_value.unwrap_or(0).min(initial_max.unwrap_or(0)), - max: initial_max.unwrap_or(0), + pub fn new(initial_value: Option, initial_max: Option) -> Self { + Self { + value: initial_value.unwrap_or(0).min(initial_max.unwrap_or(0)), + max: initial_max.unwrap_or(0), + } } - } - pub fn get_value(&self) -> usize { - self.value - } + pub fn get_value(&self) -> usize { + self.value + } - pub fn set_value(&mut self, value: usize) { - self.value = value.min(self.max); - } + pub fn set_value(&mut self, value: usize) { + self.value = value.min(self.max); + } - pub fn set_max(&mut self, max: usize) { - self.max = max; - self.value = self.value.min(max); - } + pub fn set_max(&mut self, max: usize) { + self.max = max; + self.value = self.value.min(max); + } - pub fn next(&mut self) { - self.value = self.value.saturating_add(1).min(self.max); - } + pub fn next(&mut self) { + self.value = self.value.saturating_add(1).min(self.max); + } - pub fn prev(&mut self) { - self.value = self.value.saturating_sub(1).max(0) - } + pub fn prev(&mut self) { + self.value = self.value.saturating_sub(1).max(0) + } } diff --git a/src/app/states/skirmish_states/players.rs b/src/app/states/skirmish_states/players.rs index f6c1270..b7895d2 100644 --- a/src/app/states/skirmish_states/players.rs +++ b/src/app/states/skirmish_states/players.rs @@ -1,37 +1,37 @@ use ratatui::{ - style::{Color, Stylize}, - text::Span, + style::{Color, Stylize}, + text::Span, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Players { - Player, - Enemy, - Unclaimed, + Player, + Enemy, + Unclaimed, } impl Players { - pub fn get_text(&self) -> &'static str { - match self { - Self::Player => "Player", - Self::Enemy => "Enemy", - Self::Unclaimed => "Unclaimed", + pub fn get_text(&self) -> &'static str { + match self { + Self::Player => "Player", + Self::Enemy => "Enemy", + Self::Unclaimed => "Unclaimed", + } } - } - pub fn get_color(&self) -> Color { - match self { - Self::Player => Color::LightBlue, - Self::Enemy => Color::LightRed, - Self::Unclaimed => Color::White, + pub fn get_color(&self) -> Color { + match self { + Self::Player => Color::LightBlue, + Self::Enemy => Color::LightRed, + Self::Unclaimed => Color::White, + } } - } - pub fn get_span(&self) -> Span<'static> { - match self { - Self::Player => self.get_text().fg(self.get_color()), - Self::Enemy => self.get_text().fg(self.get_color()), - Self::Unclaimed => self.get_text().fg(self.get_color()), + pub fn get_span(&self) -> Span<'static> { + match self { + Self::Player => self.get_text().fg(self.get_color()), + Self::Enemy => self.get_text().fg(self.get_color()), + Self::Unclaimed => self.get_text().fg(self.get_color()), + } } - } } diff --git a/src/app/states/skirmish_states/resources.rs b/src/app/states/skirmish_states/resources.rs index 025f71a..3decc69 100644 --- a/src/app/states/skirmish_states/resources.rs +++ b/src/app/states/skirmish_states/resources.rs @@ -7,35 +7,35 @@ pub const MINE_PRODUCTION_TICKS: u64 = 1000_u64.div_ceil(TICK_MS as u64); #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Resources { - pub wood: u32, - pub metal: u32, + pub wood: u32, + pub metal: u32, } impl Resources { - pub fn can_afford(&self, cost: Self) -> bool { - self.wood >= cost.wood && self.metal >= cost.metal - } - - pub fn spend(&mut self, cost: Self) -> bool { - if !self.can_afford(cost) { - return false; + pub fn can_afford(&self, cost: Self) -> bool { + self.wood >= cost.wood && self.metal >= cost.metal } - self.wood -= cost.wood; - self.metal -= cost.metal; - true - } - pub fn new(wood: u16, metal: u16) -> Self { - Self { - wood: u32::from(wood), - metal: u32::from(metal), + + pub fn spend(&mut self, cost: Self) -> bool { + if !self.can_afford(cost) { + return false; + } + self.wood -= cost.wood; + self.metal -= cost.metal; + true + } + pub fn new(wood: u16, metal: u16) -> Self { + Self { + wood: u32::from(wood), + metal: u32::from(metal), + } } - } - pub fn add_wood(&mut self, amount: u32) -> () { - self.wood = self.wood.saturating_add(amount); - } + pub fn add_wood(&mut self, amount: u32) -> () { + self.wood = self.wood.saturating_add(amount); + } - pub fn add_metal(&mut self, amount: u32) -> () { - self.metal = self.metal.saturating_add(amount); - } + pub fn add_metal(&mut self, amount: u32) -> () { + self.metal = self.metal.saturating_add(amount); + } } diff --git a/src/app/states/skirmish_states/structures/base.rs b/src/app/states/skirmish_states/structures/base.rs index e8fc0e1..9d71d51 100644 --- a/src/app/states/skirmish_states/structures/base.rs +++ b/src/app/states/skirmish_states/structures/base.rs @@ -4,135 +4,135 @@ use ratatui::{style::Color, text::Line}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BaseBuilding { - durability: u16, - stress: u8, - owner: Players, - level: u8, - queued_miners: u32, - recruitment_ticks: u16, - waiting_for_departure: bool, + durability: u16, + stress: u8, + owner: Players, + level: u8, + queued_miners: u32, + recruitment_ticks: u16, + waiting_for_departure: bool, } impl BaseBuilding { - pub const RECRUITMENT_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16); + pub const RECRUITMENT_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16); - pub fn queued_miners(&self) -> u32 { - self.queued_miners - } - - pub fn recruitment_progress(&self) -> u16 { - (u32::from(self.recruitment_ticks) * 100 / u32::from(Self::RECRUITMENT_TICKS)) as u16 - } - - pub fn can_queue_miner(&self) -> bool { - self.owner != Players::Unclaimed && self.queued_miners < u32::MAX - } - - pub fn queue_miner(&mut self) -> bool { - if !self.can_queue_miner() { - return false; + pub fn queued_miners(&self) -> u32 { + self.queued_miners } - self.queued_miners += 1; - true - } - pub fn advance_recruitment(&mut self, occupied: bool) -> bool { - if self.waiting_for_departure { - if occupied { - return false; - } - self.waiting_for_departure = false; + pub fn recruitment_progress(&self) -> u16 { + (u32::from(self.recruitment_ticks) * 100 / u32::from(Self::RECRUITMENT_TICKS)) as u16 } - if self.queued_miners == 0 { - return false; - } - self.recruitment_ticks = self - .recruitment_ticks - .saturating_add(1) - .min(Self::RECRUITMENT_TICKS); - if self.recruitment_ticks < Self::RECRUITMENT_TICKS || occupied { - return false; - } - self.queued_miners -= 1; - self.recruitment_ticks = 0; - self.waiting_for_departure = true; - true - } - pub fn recruitment_status(&self, occupied: bool) -> &'static str { - if self.queued_miners == 0 { - "Queue empty" - } else if self.waiting_for_departure && occupied { - "Paused: waiting for Miner to leave base" - } else if self.recruitment_ticks == Self::RECRUITMENT_TICKS && occupied { - "Ready: waiting for free base cell" - } else { - "Recruiting" + pub fn can_queue_miner(&self) -> bool { + self.owner != Players::Unclaimed && self.queued_miners < u32::MAX } - } - pub fn new(owner: Players) -> Self { - Self { - durability: 1500, - stress: 0, - owner, - level: 1, - queued_miners: 0, - recruitment_ticks: 0, - waiting_for_departure: false, + pub fn queue_miner(&mut self) -> bool { + if !self.can_queue_miner() { + return false; + } + self.queued_miners += 1; + true + } + + pub fn advance_recruitment(&mut self, occupied: bool) -> bool { + if self.waiting_for_departure { + if occupied { + return false; + } + self.waiting_for_departure = false; + } + if self.queued_miners == 0 { + return false; + } + self.recruitment_ticks = self + .recruitment_ticks + .saturating_add(1) + .min(Self::RECRUITMENT_TICKS); + if self.recruitment_ticks < Self::RECRUITMENT_TICKS || occupied { + return false; + } + self.queued_miners -= 1; + self.recruitment_ticks = 0; + self.waiting_for_departure = true; + true + } + + pub fn recruitment_status(&self, occupied: bool) -> &'static str { + if self.queued_miners == 0 { + "Queue empty" + } else if self.waiting_for_departure && occupied { + "Paused: waiting for Miner to leave base" + } else if self.recruitment_ticks == Self::RECRUITMENT_TICKS && occupied { + "Ready: waiting for free base cell" + } else { + "Recruiting" + } + } + + pub fn new(owner: Players) -> Self { + Self { + durability: 1500, + stress: 0, + owner, + level: 1, + queued_miners: 0, + recruitment_ticks: 0, + waiting_for_departure: false, + } } - } } impl Structure for BaseBuilding { - fn get_tag(&self) -> char { - 'B' - } - - fn get_color(&self) -> Color { - match self.owner { - Players::Player => Color::LightBlue, - Players::Enemy => Color::LightRed, - Players::Unclaimed => Color::LightGreen, + fn get_tag(&self) -> char { + 'B' } - } - fn get_level(&self) -> char { - (self.level + b'0') as char - } + fn get_color(&self) -> Color { + match self.owner { + Players::Player => Color::LightBlue, + Players::Enemy => Color::LightRed, + Players::Unclaimed => Color::LightGreen, + } + } - fn get_durability(&self) -> u16 { - self.durability - } + fn get_level(&self) -> char { + (self.level + b'0') as char + } - fn get_max_durability(&self) -> u16 { - 1500 - } + fn get_durability(&self) -> u16 { + self.durability + } - fn get_stress(&self) -> u8 { - self.stress - } + fn get_max_durability(&self) -> u16 { + 1500 + } - fn get_name(&self) -> &'static str { - "Base" - } + fn get_stress(&self) -> u8 { + self.stress + } - fn get_owner(&self) -> Players { - self.owner - } + fn get_name(&self) -> &'static str { + "Base" + } - fn text(&self) -> Vec> { - let mut lines: Vec> = self.base_text(); - lines.push(Line::from(format!( - "Recruitment queue: {}", - self.queued_miners - ))); - if self.queued_miners > 0 { - lines.push(Line::from(format!( - "Miner progress: {}%", - self.recruitment_progress() - ))); + fn get_owner(&self) -> Players { + self.owner + } + + fn text(&self) -> Vec> { + let mut lines: Vec> = self.base_text(); + lines.push(Line::from(format!( + "Recruitment queue: {}", + self.queued_miners + ))); + if self.queued_miners > 0 { + lines.push(Line::from(format!( + "Miner progress: {}%", + self.recruitment_progress() + ))); + } + lines } - lines - } } diff --git a/src/app/states/skirmish_states/structures/mine.rs b/src/app/states/skirmish_states/structures/mine.rs index 6effa36..146a8d8 100644 --- a/src/app/states/skirmish_states/structures/mine.rs +++ b/src/app/states/skirmish_states/structures/mine.rs @@ -1,96 +1,96 @@ use crate::app::states::skirmish_states::{ - Players, Resources, - structures::{Ore, Structure}, - units::{Unit, Units}, + Players, Resources, + structures::{Ore, Structure}, + units::{Unit, Units}, }; use ratatui::{ - style::{Color, Stylize}, - text::Line, + style::{Color, Stylize}, + text::Line, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Mine { - owner: Players, - ore: Ore, + owner: Players, + ore: Ore, } impl Mine { - pub const COST: Resources = Resources { - wood: 20, - metal: 10, - }; - pub const METAL_PER_CYCLE: u16 = 1; - pub const STAFFED_METAL_PER_CYCLE: u16 = 2; - - pub fn new(owner: Players, ore: Ore) -> Option { - if owner == Players::Unclaimed || ore.get_owner() != Players::Unclaimed { - return None; - } - Some(Self { owner, ore }) - } - - pub fn get_ore_amount(&self) -> u16 { - self.ore.get_ore_amount() - } - - pub fn into_ore(self) -> Ore { - self.ore - } - - pub fn produce(&mut self) -> u16 { - self.produce_with_miner(false) - } - - pub fn accepts_unit(&self, unit: &Units) -> bool { - matches!(unit, Units::Miner(_)) && unit.get_owner() == self.owner - } - - pub fn production_per_cycle(&self, staffed: bool) -> u16 { - let rate: u16 = if staffed { - Self::STAFFED_METAL_PER_CYCLE - } else { - Self::METAL_PER_CYCLE + pub const COST: Resources = Resources { + wood: 20, + metal: 10, }; - rate.min(self.get_ore_amount()) - } + pub const METAL_PER_CYCLE: u16 = 1; + pub const STAFFED_METAL_PER_CYCLE: u16 = 2; - pub fn produce_with_miner(&mut self, staffed: bool) -> u16 { - self.ore.extract(self.production_per_cycle(staffed)) - } + pub fn new(owner: Players, ore: Ore) -> Option { + if owner == Players::Unclaimed || ore.get_owner() != Players::Unclaimed { + return None; + } + Some(Self { owner, ore }) + } + + pub fn get_ore_amount(&self) -> u16 { + self.ore.get_ore_amount() + } + + pub fn into_ore(self) -> Ore { + self.ore + } + + pub fn produce(&mut self) -> u16 { + self.produce_with_miner(false) + } + + pub fn accepts_unit(&self, unit: &Units) -> bool { + matches!(unit, Units::Miner(_)) && unit.get_owner() == self.owner + } + + pub fn production_per_cycle(&self, staffed: bool) -> u16 { + let rate: u16 = if staffed { + Self::STAFFED_METAL_PER_CYCLE + } else { + Self::METAL_PER_CYCLE + }; + rate.min(self.get_ore_amount()) + } + + pub fn produce_with_miner(&mut self, staffed: bool) -> u16 { + self.ore.extract(self.production_per_cycle(staffed)) + } } impl Structure for Mine { - fn get_tag(&self) -> char { - 'M' - } - fn get_color(&self) -> Color { - self.owner.get_color() - } - fn get_level(&self) -> char { - self.ore.get_level() - } - fn get_stress(&self) -> u8 { - self.ore.get_stress() - } - fn get_durability(&self) -> u16 { - self.ore.get_durability() - } - fn get_max_durability(&self) -> u16 { - self.ore.get_max_durability() - } - fn get_name(&self) -> &'static str { - "Mine" - } - fn get_owner(&self) -> Players { - self.owner - } - fn text(&self) -> Vec> { - let mut lines: Vec> = self.base_text(); - lines.push(Line::from_iter([ - "Ore: ".gray(), - self.get_ore_amount().to_string().cyan(), - format!("/{}", self.ore.get_max_ore_amount()).gray(), - ])); - lines - } + fn get_tag(&self) -> char { + 'M' + } + fn get_color(&self) -> Color { + self.owner.get_color() + } + fn get_level(&self) -> char { + self.ore.get_level() + } + fn get_stress(&self) -> u8 { + self.ore.get_stress() + } + fn get_durability(&self) -> u16 { + self.ore.get_durability() + } + fn get_max_durability(&self) -> u16 { + self.ore.get_max_durability() + } + fn get_name(&self) -> &'static str { + "Mine" + } + fn get_owner(&self) -> Players { + self.owner + } + fn text(&self) -> Vec> { + let mut lines: Vec> = self.base_text(); + lines.push(Line::from_iter([ + "Ore: ".gray(), + self.get_ore_amount().to_string().cyan(), + format!("/{}", self.ore.get_max_ore_amount()).gray(), + ])); + lines + } } diff --git a/src/app/states/skirmish_states/structures/mine_construction.rs b/src/app/states/skirmish_states/structures/mine_construction.rs index 45310be..fe130d5 100644 --- a/src/app/states/skirmish_states/structures/mine_construction.rs +++ b/src/app/states/skirmish_states/structures/mine_construction.rs @@ -1,75 +1,75 @@ use crate::app::{ - states::skirmish_states::{ - Players, - structures::{Mine, Ore, Structure}, - }, - threads::TICK_MS, + states::skirmish_states::{ + Players, + structures::{Mine, Ore, Structure}, + }, + threads::TICK_MS, }; use ratatui::{style::Color, text::Line}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct MineConstruction { - mine: Mine, - ticks: u16, + mine: Mine, + ticks: u16, } impl MineConstruction { - pub const BUILD_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16); + pub const BUILD_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16); - pub fn new(owner: Players, ore: Ore) -> Option { - Mine::new(owner, ore).map(|mine: Mine| -> Self { Self { mine, ticks: 0 } }) - } + pub fn new(owner: Players, ore: Ore) -> Option { + Mine::new(owner, ore).map(|mine: Mine| -> Self { Self { mine, ticks: 0 } }) + } - pub fn advance(&mut self) -> () { - self.ticks = self.ticks.saturating_add(1).min(Self::BUILD_TICKS); - } + pub fn advance(&mut self) -> () { + self.ticks = self.ticks.saturating_add(1).min(Self::BUILD_TICKS); + } - pub fn is_finished(&self) -> bool { - self.ticks == Self::BUILD_TICKS - } + pub fn is_finished(&self) -> bool { + self.ticks == Self::BUILD_TICKS + } - pub fn progress_percent(&self) -> u16 { - (u32::from(self.ticks) * 100 / u32::from(Self::BUILD_TICKS)) as u16 - } + pub fn progress_percent(&self) -> u16 { + (u32::from(self.ticks) * 100 / u32::from(Self::BUILD_TICKS)) as u16 + } - pub fn into_mine(self) -> Option { - self.is_finished().then_some(self.mine) - } + pub fn into_mine(self) -> Option { + self.is_finished().then_some(self.mine) + } - pub fn cancel(self) -> Ore { - self.mine.into_ore() - } + pub fn cancel(self) -> Ore { + self.mine.into_ore() + } } impl Structure for MineConstruction { - fn get_tag(&self) -> char { - 'C' - } - fn get_color(&self) -> Color { - self.mine.get_color() - } - fn get_level(&self) -> char { - self.mine.get_level() - } - fn get_stress(&self) -> u8 { - self.mine.get_stress() - } - fn get_durability(&self) -> u16 { - self.mine.get_durability() - } - fn get_max_durability(&self) -> u16 { - self.mine.get_max_durability() - } - fn get_name(&self) -> &'static str { - "Mine construction" - } - fn get_owner(&self) -> Players { - self.mine.get_owner() - } - fn text(&self) -> Vec> { - vec![ - Line::from(format!("Construction: {}%", self.progress_percent())), - Line::from(format!("Ore: {}", self.mine.get_ore_amount())), - ] - } + fn get_tag(&self) -> char { + 'C' + } + fn get_color(&self) -> Color { + self.mine.get_color() + } + fn get_level(&self) -> char { + self.mine.get_level() + } + fn get_stress(&self) -> u8 { + self.mine.get_stress() + } + fn get_durability(&self) -> u16 { + self.mine.get_durability() + } + fn get_max_durability(&self) -> u16 { + self.mine.get_max_durability() + } + fn get_name(&self) -> &'static str { + "Mine construction" + } + fn get_owner(&self) -> Players { + self.mine.get_owner() + } + fn text(&self) -> Vec> { + vec![ + Line::from(format!("Construction: {}%", self.progress_percent())), + Line::from(format!("Ore: {}", self.mine.get_ore_amount())), + ] + } } diff --git a/src/app/states/skirmish_states/structures/mod.rs b/src/app/states/skirmish_states/structures/mod.rs index 8f33ad2..b2da252 100644 --- a/src/app/states/skirmish_states/structures/mod.rs +++ b/src/app/states/skirmish_states/structures/mod.rs @@ -18,6 +18,6 @@ pub use tunnel::Tunnel; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BuildingTerrain { - Stone, - Ore, + Stone, + Ore, } diff --git a/src/app/states/skirmish_states/structures/ore.rs b/src/app/states/skirmish_states/structures/ore.rs index ebe3921..c07f390 100644 --- a/src/app/states/skirmish_states/structures/ore.rs +++ b/src/app/states/skirmish_states/structures/ore.rs @@ -1,100 +1,100 @@ use crate::app::states::skirmish_states::{Players, structures::Structure}; use ratatui::{ - style::{Color, Stylize}, - text::Line, + style::{Color, Stylize}, + text::Line, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Ore { - owner: Players, - ore_amount: u16, - durability: u16, - stress: u8, - level: u8, + owner: Players, + ore_amount: u16, + durability: u16, + stress: u8, + level: u8, } impl Ore { - pub fn new(owner: Players, level: u8) -> Self { - Self { - owner, - ore_amount: 5000, - durability: 1000, - stress: 0, - level, + pub fn new(owner: Players, level: u8) -> Self { + Self { + owner, + ore_amount: 5000, + durability: 1000, + stress: 0, + level, + } } - } - pub fn get_ore_amount(&self) -> u16 { - self.ore_amount - } + pub fn get_ore_amount(&self) -> u16 { + self.ore_amount + } - pub fn get_max_ore_amount(&self) -> u16 { - 5000 - } + pub fn get_max_ore_amount(&self) -> u16 { + 5000 + } - pub fn extract(&mut self, amount: u16) -> u16 { - let extracted: u16 = amount.min(self.ore_amount); - self.ore_amount -= extracted; - extracted - } + pub fn extract(&mut self, amount: u16) -> u16 { + let extracted: u16 = amount.min(self.ore_amount); + self.ore_amount -= extracted; + extracted + } - pub fn set_owner(&mut self, new_owner: Players) -> &mut Self { - self.owner = new_owner; - self - } + pub fn set_owner(&mut self, new_owner: Players) -> &mut Self { + self.owner = new_owner; + self + } } impl Structure for Ore { - fn get_tag(&self) -> char { - 'O' - } - - fn get_color(&self) -> Color { - match self.owner { - Players::Player => self.owner.get_color(), - Players::Enemy => self.owner.get_color(), - Players::Unclaimed => Color::Indexed(217), + fn get_tag(&self) -> char { + 'O' } - } - fn get_durability(&self) -> u16 { - self.durability - } - - fn get_level(&self) -> char { - if self.level != 0 { - (self.level + b'0') as char - } else { - ' ' + fn get_color(&self) -> Color { + match self.owner { + Players::Player => self.owner.get_color(), + Players::Enemy => self.owner.get_color(), + Players::Unclaimed => Color::Indexed(217), + } } - } - fn get_max_durability(&self) -> u16 { - 1000 - } + fn get_durability(&self) -> u16 { + self.durability + } - fn get_name(&self) -> &'static str { - "Ore" - } + fn get_level(&self) -> char { + if self.level != 0 { + (self.level + b'0') as char + } else { + ' ' + } + } - fn get_owner(&self) -> Players { - self.owner - } + fn get_max_durability(&self) -> u16 { + 1000 + } - fn get_stress(&self) -> u8 { - self.stress - } + fn get_name(&self) -> &'static str { + "Ore" + } - fn text(&self) -> Vec> { - let mut lines: Vec> = Structure::base_text(self); + fn get_owner(&self) -> Players { + self.owner + } - lines.push(Line::from_iter([ - "Ore: ".gray(), - self.get_ore_amount().to_string().cyan(), - "/".gray(), - self.get_max_ore_amount().to_string().cyan(), - ])); + fn get_stress(&self) -> u8 { + self.stress + } - lines - } + fn text(&self) -> Vec> { + let mut lines: Vec> = Structure::base_text(self); + + lines.push(Line::from_iter([ + "Ore: ".gray(), + self.get_ore_amount().to_string().cyan(), + "/".gray(), + self.get_max_ore_amount().to_string().cyan(), + ])); + + lines + } } diff --git a/src/app/states/skirmish_states/structures/stone.rs b/src/app/states/skirmish_states/structures/stone.rs index 5f6583a..ffb9fb8 100644 --- a/src/app/states/skirmish_states/structures/stone.rs +++ b/src/app/states/skirmish_states/structures/stone.rs @@ -3,53 +3,53 @@ use ratatui::style::Color; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Stone { - durability: u16, - stress: u8, + durability: u16, + stress: u8, } impl Stone { - pub fn take_damage(&mut self, damage: u16) { - self.durability = self.durability.saturating_sub(damage); - } + pub fn take_damage(&mut self, damage: u16) { + self.durability = self.durability.saturating_sub(damage); + } - pub fn new() -> Self { - Self { - durability: 1000, - stress: 0, + pub fn new() -> Self { + Self { + durability: 1000, + stress: 0, + } } - } } impl Structure for Stone { - fn get_tag(&self) -> char { - ' ' - } + fn get_tag(&self) -> char { + ' ' + } - fn get_color(&self) -> Color { - Color::White - } + fn get_color(&self) -> Color { + Color::White + } - fn get_level(&self) -> char { - ' ' - } + fn get_level(&self) -> char { + ' ' + } - fn get_durability(&self) -> u16 { - self.durability - } + fn get_durability(&self) -> u16 { + self.durability + } - fn get_max_durability(&self) -> u16 { - 1000 - } + fn get_max_durability(&self) -> u16 { + 1000 + } - fn get_stress(&self) -> u8 { - self.stress - } + fn get_stress(&self) -> u8 { + self.stress + } - fn get_name(&self) -> &'static str { - "Stone" - } + fn get_name(&self) -> &'static str { + "Stone" + } - fn get_owner(&self) -> Players { - Players::Unclaimed - } + fn get_owner(&self) -> Players { + Players::Unclaimed + } } diff --git a/src/app/states/skirmish_states/structures/structure_trait.rs b/src/app/states/skirmish_states/structures/structure_trait.rs index 3dc2b57..db4843d 100644 --- a/src/app/states/skirmish_states/structures/structure_trait.rs +++ b/src/app/states/skirmish_states/structures/structure_trait.rs @@ -1,57 +1,57 @@ use crate::app::states::skirmish_states::Players; use ratatui::{ - style::{Color, Stylize}, - text::Line, + style::{Color, Stylize}, + text::Line, }; pub trait Structure { - fn get_tag(&self) -> char; + fn get_tag(&self) -> char; - fn get_color(&self) -> Color; + fn get_color(&self) -> Color; - fn get_level(&self) -> char; + fn get_level(&self) -> char; - fn get_stress(&self) -> u8; + fn get_stress(&self) -> u8; - fn get_durability(&self) -> u16; + fn get_durability(&self) -> u16; - fn get_max_durability(&self) -> u16; + fn get_max_durability(&self) -> u16; - fn get_name(&self) -> &'static str; + fn get_name(&self) -> &'static str; - fn get_owner(&self) -> Players; + fn get_owner(&self) -> Players; - fn base_text(&self) -> Vec> { - let durability: u16 = self.get_durability(); - let max_durability: u16 = self.get_max_durability(); - let durability_percent: u32 = durability as u32 * 100 / max_durability as u32; - let stress: u8 = self.get_stress(); - let level: char = self.get_level(); + fn base_text(&self) -> Vec> { + let durability: u16 = self.get_durability(); + let max_durability: u16 = self.get_max_durability(); + let durability_percent: u32 = durability as u32 * 100 / max_durability as u32; + let stress: u8 = self.get_stress(); + let level: char = self.get_level(); - let mut lines: Vec> = vec![ - Line::from_iter([ - "Durability: ".gray(), - durability.to_string().cyan(), - "/".gray(), - max_durability.to_string().cyan(), - " ( ".gray(), - durability_percent.to_string().cyan(), - "% )".gray(), - ]), - Line::from_iter(["Stress: ".gray(), stress.to_string().cyan(), "%".gray()]), - ]; + let mut lines: Vec> = vec![ + Line::from_iter([ + "Durability: ".gray(), + durability.to_string().cyan(), + "/".gray(), + max_durability.to_string().cyan(), + " ( ".gray(), + durability_percent.to_string().cyan(), + "% )".gray(), + ]), + Line::from_iter(["Stress: ".gray(), stress.to_string().cyan(), "%".gray()]), + ]; - if level != ' ' { - lines.push(Line::from_iter([ - "Level: ".gray(), - level.to_string().cyan(), - ])); + if level != ' ' { + lines.push(Line::from_iter([ + "Level: ".gray(), + level.to_string().cyan(), + ])); + } + + lines } - lines - } - - fn text(&self) -> Vec> { - self.base_text() - } + fn text(&self) -> Vec> { + self.base_text() + } } diff --git a/src/app/states/skirmish_states/structures/structures_enum.rs b/src/app/states/skirmish_states/structures/structures_enum.rs index 7922c45..bcfbd34 100644 --- a/src/app/states/skirmish_states/structures/structures_enum.rs +++ b/src/app/states/skirmish_states/structures/structures_enum.rs @@ -1,100 +1,100 @@ use crate::app::states::skirmish_states::{ - Players, - structures::{BaseBuilding, Mine, MineConstruction, Ore, Stone, Structure, Tunnel}, - units::Units, + Players, + structures::{BaseBuilding, Mine, MineConstruction, Ore, Stone, Structure, Tunnel}, + units::Units, }; use ratatui::{ - style::{Color, Stylize}, - text::Line, + style::{Color, Stylize}, + text::Line, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Structures { - Base(BaseBuilding), - Tunnel(Tunnel), - Stone(Stone), - Ore(Ore), - Mine(Mine), - MineConstruction(MineConstruction), + Base(BaseBuilding), + Tunnel(Tunnel), + Stone(Stone), + Ore(Ore), + Mine(Mine), + MineConstruction(MineConstruction), } impl Structures { - pub fn can_enter(&self, unit: &Units) -> bool { - match self { - Self::Base(_) | Self::Tunnel(_) => true, - Self::Mine(mine) => mine.accepts_unit(unit), - _ => false, + pub fn can_enter(&self, unit: &Units) -> bool { + match self { + Self::Base(_) | Self::Tunnel(_) => true, + Self::Mine(mine) => mine.accepts_unit(unit), + _ => false, + } } - } - pub fn build_mine(&mut self, owner: Players) -> bool { - if let Self::Ore(ore) = *self - && let Some(mine) = Mine::new(owner, ore) - { - *self = Self::Mine(mine); - return true; + pub fn build_mine(&mut self, owner: Players) -> bool { + if let Self::Ore(ore) = *self + && let Some(mine) = Mine::new(owner, ore) + { + *self = Self::Mine(mine); + return true; + } + false } - false - } - #[inline] - fn s(&self) -> &dyn Structure { - match self { - Self::Base(b) => b, - Self::Tunnel(t) => t, - Self::Stone(s) => s, - Self::Ore(o) => o, - Self::Mine(mine) => mine, - Self::MineConstruction(site) => site, + #[inline] + fn s(&self) -> &dyn Structure { + match self { + Self::Base(b) => b, + Self::Tunnel(t) => t, + Self::Stone(s) => s, + Self::Ore(o) => o, + Self::Mine(mine) => mine, + Self::MineConstruction(site) => site, + } } - } } impl Structure for Structures { - fn get_color(&self) -> Color { - self.s().get_color() - } - - fn get_tag(&self) -> char { - self.s().get_tag() - } - - fn get_level(&self) -> char { - self.s().get_level() - } - - fn get_name(&self) -> &'static str { - self.s().get_name() - } - - fn get_durability(&self) -> u16 { - self.s().get_durability() - } - - fn get_max_durability(&self) -> u16 { - self.s().get_max_durability() - } - - fn get_stress(&self) -> u8 { - self.s().get_stress() - } - - fn get_owner(&self) -> Players { - self.s().get_owner() - } - - fn text(&self) -> Vec> { - match self.s().get_owner() { - Players::Unclaimed => self.s().text(), - Players::Player | Players::Enemy => { - let mut lines: Vec> = self.s().text(); - - lines.push(Line::from_iter([ - "Owner: ".gray(), - self.s().get_owner().get_span(), - ])); - - lines - } + fn get_color(&self) -> Color { + self.s().get_color() + } + + fn get_tag(&self) -> char { + self.s().get_tag() + } + + fn get_level(&self) -> char { + self.s().get_level() + } + + fn get_name(&self) -> &'static str { + self.s().get_name() + } + + fn get_durability(&self) -> u16 { + self.s().get_durability() + } + + fn get_max_durability(&self) -> u16 { + self.s().get_max_durability() + } + + fn get_stress(&self) -> u8 { + self.s().get_stress() + } + + fn get_owner(&self) -> Players { + self.s().get_owner() + } + + fn text(&self) -> Vec> { + match self.s().get_owner() { + Players::Unclaimed => self.s().text(), + Players::Player | Players::Enemy => { + let mut lines: Vec> = self.s().text(); + + lines.push(Line::from_iter([ + "Owner: ".gray(), + self.s().get_owner().get_span(), + ])); + + lines + } + } } - } } diff --git a/src/app/states/skirmish_states/structures/tunnel.rs b/src/app/states/skirmish_states/structures/tunnel.rs index b741c43..b4ae309 100644 --- a/src/app/states/skirmish_states/structures/tunnel.rs +++ b/src/app/states/skirmish_states/structures/tunnel.rs @@ -3,55 +3,55 @@ use ratatui::style::Color; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Tunnel { - durability: u16, - stress: u8, - roof_support: bool, - rail: bool, - lamp: bool, + durability: u16, + stress: u8, + roof_support: bool, + rail: bool, + lamp: bool, } impl Tunnel { - pub fn new() -> Self { - Self { - durability: 500, - stress: 25, - roof_support: false, - rail: false, - lamp: false, + pub fn new() -> Self { + Self { + durability: 500, + stress: 25, + roof_support: false, + rail: false, + lamp: false, + } } - } } impl Structure for Tunnel { - fn get_tag(&self) -> char { - 'T' - } + fn get_tag(&self) -> char { + 'T' + } - fn get_color(&self) -> Color { - Color::Gray - } + fn get_color(&self) -> Color { + Color::Gray + } - fn get_level(&self) -> char { - ' ' - } + fn get_level(&self) -> char { + ' ' + } - fn get_durability(&self) -> u16 { - self.durability - } + fn get_durability(&self) -> u16 { + self.durability + } - fn get_max_durability(&self) -> u16 { - 500 - } + fn get_max_durability(&self) -> u16 { + 500 + } - fn get_stress(&self) -> u8 { - self.stress - } + fn get_stress(&self) -> u8 { + self.stress + } - fn get_name(&self) -> &'static str { - "Tunnel" - } + fn get_name(&self) -> &'static str { + "Tunnel" + } - fn get_owner(&self) -> Players { - Players::Unclaimed - } + fn get_owner(&self) -> Players { + Players::Unclaimed + } } diff --git a/src/app/states/skirmish_states/tasks/digging.rs b/src/app/states/skirmish_states/tasks/digging.rs index a0022c1..71cc00e 100644 --- a/src/app/states/skirmish_states/tasks/digging.rs +++ b/src/app/states/skirmish_states/tasks/digging.rs @@ -3,71 +3,71 @@ use std::collections::VecDeque; #[derive(Debug, Clone, PartialEq)] pub struct DiggingTask { - path: VecDeque<(usize, usize)>, - progress: f32, - tick_increase: f32, - total_steps: usize, + path: VecDeque<(usize, usize)>, + progress: f32, + tick_increase: f32, + total_steps: usize, } impl DiggingTask { - pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self { - Self { - total_steps: path.len(), - path, - progress: 0.0, - tick_increase, + pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self { + Self { + total_steps: path.len(), + path, + progress: 0.0, + tick_increase, + } } - } - pub fn pop_front(&mut self) { - self.path.pop_front(); - self.progress = 0.0; - } - - pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self { - Self { - path: task.get_path().clone(), - progress: 0.0, - tick_increase, - total_steps: task.get_total_steps(), + pub fn pop_front(&mut self) { + self.path.pop_front(); + self.progress = 0.0; } - } - pub fn current_target(&self) -> Option<(usize, usize)> { - self.path.front().copied() - } + pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self { + Self { + path: task.get_path().clone(), + progress: 0.0, + tick_increase, + total_steps: task.get_total_steps(), + } + } + + pub fn current_target(&self) -> Option<(usize, usize)> { + self.path.front().copied() + } } impl Task for DiggingTask { - fn get_icon(&self) -> char { - '⛏' - } + fn get_icon(&self) -> char { + '⛏' + } - fn get_path(&self) -> &VecDeque<(usize, usize)> { - &self.path - } + fn get_path(&self) -> &VecDeque<(usize, usize)> { + &self.path + } - fn get_path_front_coords(&self) -> &(usize, usize) { - self.path.front().map_or(&(0, 0), |c| c) - } + fn get_path_front_coords(&self) -> &(usize, usize) { + self.path.front().map_or(&(0, 0), |c| c) + } - fn get_path_back_coords(&self) -> &(usize, usize) { - self.path.back().map_or(&(0, 0), |c| c) - } + fn get_path_back_coords(&self) -> &(usize, usize) { + self.path.back().map_or(&(0, 0), |c| c) + } - fn get_progress(&self) -> f32 { - self.progress - } + fn get_progress(&self) -> f32 { + self.progress + } - fn get_total_steps(&self) -> usize { - self.total_steps - } + fn get_total_steps(&self) -> usize { + self.total_steps + } - fn progress(&mut self) { - self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0); - } + fn progress(&mut self) { + self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0); + } - fn is_finished(&self) -> bool { - self.progress >= 100.0 - } + fn is_finished(&self) -> bool { + self.progress >= 100.0 + } } diff --git a/src/app/states/skirmish_states/tasks/moving.rs b/src/app/states/skirmish_states/tasks/moving.rs index 74cc164..91b3161 100644 --- a/src/app/states/skirmish_states/tasks/moving.rs +++ b/src/app/states/skirmish_states/tasks/moving.rs @@ -1,6 +1,6 @@ use crate::app::{ - states::skirmish_states::tasks::{DiggingTask, Task}, - threads::TICK_MS, + states::skirmish_states::tasks::{DiggingTask, Task}, + threads::TICK_MS, }; use std::collections::VecDeque; @@ -8,68 +8,68 @@ const MOVEMENT_TICKS: u16 = 3000_u16.div_ceil(TICK_MS as u16); #[derive(Debug, Clone, PartialEq)] pub struct MovingTask { - path: VecDeque<(usize, usize)>, - ticks: u16, - total_steps: usize, + path: VecDeque<(usize, usize)>, + ticks: u16, + total_steps: usize, } impl MovingTask { - pub fn new(path: VecDeque<(usize, usize)>) -> Self { - Self { - total_steps: path.len(), - path, - ticks: 0, + pub fn new(path: VecDeque<(usize, usize)>) -> Self { + Self { + total_steps: path.len(), + path, + ticks: 0, + } } - } - pub fn from_digging(task: DiggingTask) -> Self { - Self { - total_steps: task.get_total_steps(), - path: task.get_path().clone(), - ticks: 0, + pub fn from_digging(task: DiggingTask) -> Self { + Self { + total_steps: task.get_total_steps(), + path: task.get_path().clone(), + ticks: 0, + } } - } - pub fn pop_front(&mut self) { - self.path.pop_front(); - self.ticks = 0; - } + pub fn pop_front(&mut self) { + self.path.pop_front(); + self.ticks = 0; + } - pub fn get_movement_progress(&self) -> Option { - (self.ticks > 0).then(|| self.ticks * 100 / MOVEMENT_TICKS) - } + pub fn get_movement_progress(&self) -> Option { + (self.ticks > 0).then(|| self.ticks * 100 / MOVEMENT_TICKS) + } } impl Task for MovingTask { - fn get_icon(&self) -> char { - '→' - } + fn get_icon(&self) -> char { + '→' + } - fn get_path(&self) -> &VecDeque<(usize, usize)> { - &self.path - } + fn get_path(&self) -> &VecDeque<(usize, usize)> { + &self.path + } - fn get_path_front_coords(&self) -> &(usize, usize) { - self.path.front().unwrap_or(&(0, 0)) - } + fn get_path_front_coords(&self) -> &(usize, usize) { + self.path.front().unwrap_or(&(0, 0)) + } - fn get_path_back_coords(&self) -> &(usize, usize) { - self.path.back().unwrap_or(&(0, 0)) - } + fn get_path_back_coords(&self) -> &(usize, usize) { + self.path.back().unwrap_or(&(0, 0)) + } - fn get_progress(&self) -> f32 { - f32::from(self.ticks) * 100.0 / f32::from(MOVEMENT_TICKS) - } + fn get_progress(&self) -> f32 { + f32::from(self.ticks) * 100.0 / f32::from(MOVEMENT_TICKS) + } - fn get_total_steps(&self) -> usize { - self.total_steps - } + fn get_total_steps(&self) -> usize { + self.total_steps + } - fn progress(&mut self) { - self.ticks = (self.ticks + 1).min(MOVEMENT_TICKS); - } + fn progress(&mut self) { + self.ticks = (self.ticks + 1).min(MOVEMENT_TICKS); + } - fn is_finished(&self) -> bool { - self.ticks == MOVEMENT_TICKS - } + fn is_finished(&self) -> bool { + self.ticks == MOVEMENT_TICKS + } } diff --git a/src/app/states/skirmish_states/tasks/task_trait.rs b/src/app/states/skirmish_states/tasks/task_trait.rs index 44e0242..c69965d 100644 --- a/src/app/states/skirmish_states/tasks/task_trait.rs +++ b/src/app/states/skirmish_states/tasks/task_trait.rs @@ -1,28 +1,28 @@ use std::collections::VecDeque; pub trait Task { - fn get_icon(&self) -> char; + fn get_icon(&self) -> char; - fn get_path(&self) -> &VecDeque<(usize, usize)>; + fn get_path(&self) -> &VecDeque<(usize, usize)>; - fn get_path_front_coords(&self) -> &(usize, usize); + fn get_path_front_coords(&self) -> &(usize, usize); - fn get_path_back_coords(&self) -> &(usize, usize); + fn get_path_back_coords(&self) -> &(usize, usize); - fn get_progress(&self) -> f32; + fn get_progress(&self) -> f32; - fn get_total_steps(&self) -> usize; + fn get_total_steps(&self) -> usize; - fn get_total_progress(&self) -> f32 { - let total: usize = self.get_total_steps(); - if total == 0 { - return 100.0; + fn get_total_progress(&self) -> f32 { + let total: usize = self.get_total_steps(); + if total == 0 { + return 100.0; + } + let completed: usize = total.saturating_sub(self.get_path().len()); + ((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0) } - let completed: usize = total.saturating_sub(self.get_path().len()); - ((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0) - } - fn progress(&mut self); + fn progress(&mut self); - fn is_finished(&self) -> bool; + fn is_finished(&self) -> bool; } diff --git a/src/app/states/skirmish_states/tasks/tasks_enum.rs b/src/app/states/skirmish_states/tasks/tasks_enum.rs index 8f21085..ebc5c56 100644 --- a/src/app/states/skirmish_states/tasks/tasks_enum.rs +++ b/src/app/states/skirmish_states/tasks/tasks_enum.rs @@ -3,58 +3,58 @@ use std::collections::VecDeque; #[derive(Debug, Clone, PartialEq)] pub enum Tasks { - Digging(DiggingTask), - Moving(MovingTask), + Digging(DiggingTask), + Moving(MovingTask), } impl Tasks { - #[inline] - fn t(&self) -> &dyn Task { - match self { - Self::Digging(d) => d, - Self::Moving(m) => m, + #[inline] + fn t(&self) -> &dyn Task { + match self { + Self::Digging(d) => d, + Self::Moving(m) => m, + } } - } - #[inline] - fn t_mut(&mut self) -> &mut dyn Task { - match self { - Self::Digging(d) => d, - Self::Moving(m) => m, + #[inline] + fn t_mut(&mut self) -> &mut dyn Task { + match self { + Self::Digging(d) => d, + Self::Moving(m) => m, + } } - } } impl Task for Tasks { - fn get_icon(&self) -> char { - self.t().get_icon() - } + fn get_icon(&self) -> char { + self.t().get_icon() + } - fn get_path(&self) -> &VecDeque<(usize, usize)> { - self.t().get_path() - } + fn get_path(&self) -> &VecDeque<(usize, usize)> { + self.t().get_path() + } - fn get_path_front_coords(&self) -> &(usize, usize) { - self.t().get_path_front_coords() - } + fn get_path_front_coords(&self) -> &(usize, usize) { + self.t().get_path_front_coords() + } - fn get_path_back_coords(&self) -> &(usize, usize) { - self.t().get_path_back_coords() - } + fn get_path_back_coords(&self) -> &(usize, usize) { + self.t().get_path_back_coords() + } - fn get_progress(&self) -> f32 { - self.t().get_progress() - } + fn get_progress(&self) -> f32 { + self.t().get_progress() + } - fn get_total_steps(&self) -> usize { - self.t().get_total_steps() - } + fn get_total_steps(&self) -> usize { + self.t().get_total_steps() + } - fn is_finished(&self) -> bool { - self.t().is_finished() - } + fn is_finished(&self) -> bool { + self.t().is_finished() + } - fn progress(&mut self) { - self.t_mut().progress(); - } + fn progress(&mut self) { + self.t_mut().progress(); + } } diff --git a/src/app/states/skirmish_states/units/miner.rs b/src/app/states/skirmish_states/units/miner.rs index 1239b45..bdd473a 100644 --- a/src/app/states/skirmish_states/units/miner.rs +++ b/src/app/states/skirmish_states/units/miner.rs @@ -1,133 +1,132 @@ use ratatui::{style::Stylize, text::Line}; use crate::app::states::skirmish_states::{ - Players, Resources, - tasks::{Task, Tasks}, - units::Unit, + Players, Resources, + tasks::{Task, Tasks}, + units::Unit, }; use std::collections::VecDeque; #[derive(Debug, Clone, PartialEq)] pub struct MinerUnit { - owner: Players, - hp: u16, - can_dig: bool, - digging_power: f32, - coords: (usize, usize), - tasks: VecDeque, + owner: Players, + hp: u16, + can_dig: bool, + digging_power: f32, + coords: (usize, usize), + tasks: VecDeque, } impl MinerUnit { - pub const RECRUIT_COST: Resources = Resources { - wood: 10, - metal: 15, - }; - pub fn new(owner: Players, coords: (usize, usize)) -> Self { - Self { - owner, - hp: 100, - can_dig: true, - digging_power: 0.55, - coords, - tasks: VecDeque::new(), - } - } - - fn col_to_letters(&self, col: usize) -> String { - let mut col: usize = col + 1; - let mut letters: Vec = Vec::new(); - - while col > 0 { - letters.push((b'A' + ((col - 1) % 26) as u8) as char); - col = (col - 1) / 26; + pub const RECRUIT_COST: Resources = Resources { + wood: 10, + metal: 15, + }; + pub fn new(owner: Players, coords: (usize, usize)) -> Self { + Self { + owner, + hp: 100, + can_dig: true, + digging_power: 0.55, + coords, + tasks: VecDeque::new(), + } } - letters.iter().rev().collect() - } + fn col_to_letters(&self, col: usize) -> String { + let mut col: usize = col + 1; + let mut letters: Vec = Vec::new(); - fn display_coords(&self, coords: &(usize, usize)) -> String { - format!("{}{}", self.col_to_letters(coords.0), coords.1) - } + while col > 0 { + letters.push((b'A' + ((col - 1) % 26) as u8) as char); + col = (col - 1) / 26; + } + + letters.iter().rev().collect() + } + + fn display_coords(&self, coords: &(usize, usize)) -> String { + format!("{}{}", self.col_to_letters(coords.0), coords.1) + } } impl Unit for MinerUnit { - fn get_owner(&self) -> Players { - self.owner - } + fn get_owner(&self) -> Players { + self.owner + } - fn get_tag(&self) -> char { - 'M' - } + fn get_tag(&self) -> char { + 'M' + } - fn get_name(&self) -> &'static str { - "Miner" - } + fn get_name(&self) -> &'static str { + "Miner" + } - fn get_hp(&self) -> u16 { - self.hp - } + fn get_hp(&self) -> u16 { + self.hp + } - fn get_max_hp(&self) -> u16 { - 100 - } + fn get_max_hp(&self) -> u16 { + 100 + } - fn get_can_dig(&self) -> bool { - self.can_dig - } + fn get_can_dig(&self) -> bool { + self.can_dig + } - fn get_can_build(&self) -> bool { - true - } + fn get_can_build(&self) -> bool { + true + } - fn get_digging_power(&self) -> f32 { - self.digging_power - } + fn get_digging_power(&self) -> f32 { + self.digging_power + } - fn get_coords(&self) -> (usize, usize) { - self.coords - } + fn get_coords(&self) -> (usize, usize) { + self.coords + } - fn set_coords(&mut self, coords: (usize, usize)) { - self.coords = coords; - } + fn set_coords(&mut self, coords: (usize, usize)) { + self.coords = coords; + } - fn get_ref_tasks(&self) -> &VecDeque { - &self.tasks - } + fn get_ref_tasks(&self) -> &VecDeque { + &self.tasks + } - fn get_mut_tasks(&mut self) -> &mut VecDeque { - &mut self.tasks - } + fn get_mut_tasks(&mut self) -> &mut VecDeque { + &mut self.tasks + } - fn get_tasks_formatted(&self) -> Vec> { - Vec::from( - self - .tasks - .iter() - .enumerate() - .flat_map(|(i, task): (usize, &Tasks)| -> [Line<'_>; 2] { - [ - Line::from_iter([ - format!("{}. ", i + 1).gray(), - task.get_icon().gray(), - " ".gray(), - self.display_coords(task.get_path_front_coords()).green(), - " -> ".gray(), - self.display_coords(task.get_path_back_coords()).yellow(), - ]), - Line::from(format!( - " Step: {}% | Total: {}%", - task.get_progress() as u16, - task.get_total_progress() as u16, - )) - .cyan(), - ] - }) - .collect::>>(), - ) - } + fn get_tasks_formatted(&self) -> Vec> { + Vec::from( + self.tasks + .iter() + .enumerate() + .flat_map(|(i, task): (usize, &Tasks)| -> [Line<'_>; 2] { + [ + Line::from_iter([ + format!("{}. ", i + 1).gray(), + task.get_icon().gray(), + " ".gray(), + self.display_coords(task.get_path_front_coords()).green(), + " -> ".gray(), + self.display_coords(task.get_path_back_coords()).yellow(), + ]), + Line::from(format!( + " Step: {}% | Total: {}%", + task.get_progress() as u16, + task.get_total_progress() as u16, + )) + .cyan(), + ] + }) + .collect::>>(), + ) + } - fn set_task(&mut self, task: Tasks) { - self.tasks.push_back(task); - } + fn set_task(&mut self, task: Tasks) { + self.tasks.push_back(task); + } } diff --git a/src/app/states/skirmish_states/units/unit_trait.rs b/src/app/states/skirmish_states/units/unit_trait.rs index 55ae201..6269701 100644 --- a/src/app/states/skirmish_states/units/unit_trait.rs +++ b/src/app/states/skirmish_states/units/unit_trait.rs @@ -1,90 +1,90 @@ use crate::app::states::skirmish_states::{Players, tasks::Tasks}; use ratatui::{ - style::Stylize, - text::{Line, Span}, + style::Stylize, + text::{Line, Span}, }; use std::collections::VecDeque; pub trait Unit { - fn is_unit(&self) -> bool { - true - } - - fn get_owner(&self) -> Players; - - fn get_tag(&self) -> char; - - fn get_name(&self) -> &'static str; - - fn get_hp(&self) -> u16; - - fn get_max_hp(&self) -> u16; - - fn get_can_dig(&self) -> bool; - - fn get_can_build(&self) -> bool { - false - } - - fn get_digging_power(&self) -> f32; - - fn get_coords(&self) -> (usize, usize); - - fn set_coords(&mut self, coords: (usize, usize)); - - fn get_ref_tasks(&self) -> &VecDeque; - - fn get_mut_tasks(&mut self) -> &mut VecDeque; - - fn set_task(&mut self, task: Tasks); - - fn get_tasks_formatted(&self) -> Vec>; - - fn base_text(&self) -> Vec> { - let owner: Span<'_> = self.get_owner().get_span(); - let hp: u16 = self.get_hp(); - let max_hp: u16 = self.get_max_hp(); - let hp_percent: u16 = hp * 100 / max_hp; - let can_dig: bool = self.get_can_dig(); - let digging_power: f32 = self.get_digging_power(); - let have_any_tasks: bool = self.get_ref_tasks().is_empty(); - let tasks: Vec> = self.get_tasks_formatted(); - - let mut lines: Vec> = Vec::from([ - Line::from_iter(["Owner: ".gray(), owner]), - Line::from_iter([ - "HP: ".gray(), - hp.green(), - "/".gray(), - max_hp.green(), - " ( ".gray(), - hp_percent.green(), - "% )".gray(), - ]), - ]); - - if can_dig { - lines.push(Line::from_iter([ - "Digging power: ".gray(), - digging_power.green(), - ])); + fn is_unit(&self) -> bool { + true } - if self.get_can_build() { - lines.push(Line::from("Can build: Yes").green()); + fn get_owner(&self) -> Players; + + fn get_tag(&self) -> char; + + fn get_name(&self) -> &'static str; + + fn get_hp(&self) -> u16; + + fn get_max_hp(&self) -> u16; + + fn get_can_dig(&self) -> bool; + + fn get_can_build(&self) -> bool { + false } - if !have_any_tasks { - lines.push(Line::from("Tasks Queue: ".gray())); - for task in tasks { - lines.push(task); - } + fn get_digging_power(&self) -> f32; + + fn get_coords(&self) -> (usize, usize); + + fn set_coords(&mut self, coords: (usize, usize)); + + fn get_ref_tasks(&self) -> &VecDeque; + + fn get_mut_tasks(&mut self) -> &mut VecDeque; + + fn set_task(&mut self, task: Tasks); + + fn get_tasks_formatted(&self) -> Vec>; + + fn base_text(&self) -> Vec> { + let owner: Span<'_> = self.get_owner().get_span(); + let hp: u16 = self.get_hp(); + let max_hp: u16 = self.get_max_hp(); + let hp_percent: u16 = hp * 100 / max_hp; + let can_dig: bool = self.get_can_dig(); + let digging_power: f32 = self.get_digging_power(); + let have_any_tasks: bool = self.get_ref_tasks().is_empty(); + let tasks: Vec> = self.get_tasks_formatted(); + + let mut lines: Vec> = Vec::from([ + Line::from_iter(["Owner: ".gray(), owner]), + Line::from_iter([ + "HP: ".gray(), + hp.green(), + "/".gray(), + max_hp.green(), + " ( ".gray(), + hp_percent.green(), + "% )".gray(), + ]), + ]); + + if can_dig { + lines.push(Line::from_iter([ + "Digging power: ".gray(), + digging_power.green(), + ])); + } + + if self.get_can_build() { + lines.push(Line::from("Can build: Yes").green()); + } + + if !have_any_tasks { + lines.push(Line::from("Tasks Queue: ".gray())); + for task in tasks { + lines.push(task); + } + } + + lines } - lines - } - - fn text(&self) -> Vec> { - self.base_text() - } + fn text(&self) -> Vec> { + self.base_text() + } } diff --git a/src/app/states/skirmish_states/units/units_enum.rs b/src/app/states/skirmish_states/units/units_enum.rs index 1492913..e84635a 100644 --- a/src/app/states/skirmish_states/units/units_enum.rs +++ b/src/app/states/skirmish_states/units/units_enum.rs @@ -1,158 +1,154 @@ use crate::app::states::skirmish_states::{ - Players, - tasks::Tasks, - units::{MinerUnit, Unit}, + Players, + tasks::Tasks, + units::{MinerUnit, Unit}, }; use ratatui::text::Line; use std::collections::VecDeque; #[derive(Debug, Clone, PartialEq)] pub enum Units { - Miner(MinerUnit), + Miner(MinerUnit), } impl Units { - #[inline] - pub fn u(&self) -> &dyn Unit { - match self { - Self::Miner(m) => m, + #[inline] + pub fn u(&self) -> &dyn Unit { + match self { + Self::Miner(m) => m, + } } - } - #[inline] - pub fn u_mut(&mut self) -> &mut dyn Unit { - match self { - Self::Miner(m) => m, + #[inline] + pub fn u_mut(&mut self) -> &mut dyn Unit { + match self { + Self::Miner(m) => m, + } } - } } impl Unit for Units { - fn get_can_build(&self) -> bool { - self.u().get_can_build() - } - fn get_tag(&self) -> char { - self.u().get_tag() - } + fn get_can_build(&self) -> bool { + self.u().get_can_build() + } + fn get_tag(&self) -> char { + self.u().get_tag() + } - fn get_name(&self) -> &'static str { - self.u().get_name() - } + fn get_name(&self) -> &'static str { + self.u().get_name() + } - fn get_owner(&self) -> Players { - self.u().get_owner() - } + fn get_owner(&self) -> Players { + self.u().get_owner() + } - fn get_hp(&self) -> u16 { - self.u().get_hp() - } + fn get_hp(&self) -> u16 { + self.u().get_hp() + } - fn get_max_hp(&self) -> u16 { - self.u().get_max_hp() - } + fn get_max_hp(&self) -> u16 { + self.u().get_max_hp() + } - fn get_can_dig(&self) -> bool { - self.u().get_can_dig() - } + fn get_can_dig(&self) -> bool { + self.u().get_can_dig() + } - fn get_digging_power(&self) -> f32 { - self.u().get_digging_power() - } + fn get_digging_power(&self) -> f32 { + self.u().get_digging_power() + } - fn get_coords(&self) -> (usize, usize) { - self.u().get_coords() - } + fn get_coords(&self) -> (usize, usize) { + self.u().get_coords() + } - fn set_coords(&mut self, coords: (usize, usize)) { - self.u_mut().set_coords(coords); - } + fn set_coords(&mut self, coords: (usize, usize)) { + self.u_mut().set_coords(coords); + } - fn get_ref_tasks(&self) -> &VecDeque { - self.u().get_ref_tasks() - } + fn get_ref_tasks(&self) -> &VecDeque { + self.u().get_ref_tasks() + } - fn get_mut_tasks(&mut self) -> &mut VecDeque { - self.u_mut().get_mut_tasks() - } + fn get_mut_tasks(&mut self) -> &mut VecDeque { + self.u_mut().get_mut_tasks() + } - fn get_tasks_formatted(&self) -> Vec> { - self.u().get_tasks_formatted() - } + fn get_tasks_formatted(&self) -> Vec> { + self.u().get_tasks_formatted() + } - fn set_task(&mut self, task: Tasks) { - self.u_mut().set_task(task); - } + fn set_task(&mut self, task: Tasks) { + self.u_mut().set_task(task); + } } impl Unit for Option { - fn get_can_build(&self) -> bool { - self - .as_ref() - .is_some_and(|unit: &Units| -> bool { unit.get_can_build() }) - } - fn is_unit(&self) -> bool { - self.as_ref().map_or(false, |u| u.is_unit()) - } - - fn get_tag(&self) -> char { - self.as_ref().map_or(' ', |u| u.get_tag()) - } - - fn get_name(&self) -> &'static str { - self.as_ref().map_or("", |u| u.get_name()) - } - - fn get_owner(&self) -> Players { - self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner()) - } - - fn get_hp(&self) -> u16 { - self.as_ref().map_or(0, |u| u.get_hp()) - } - - fn get_max_hp(&self) -> u16 { - self.as_ref().map_or(0, |u| u.get_max_hp()) - } - - fn get_can_dig(&self) -> bool { - self.as_ref().map_or(false, |u| u.get_can_dig()) - } - - fn get_digging_power(&self) -> f32 { - self.as_ref().map_or(0.0, |u| u.get_digging_power()) - } - - fn get_coords(&self) -> (usize, usize) { - self.as_ref().map_or((0, 0), |u| u.get_coords()) - } - - fn set_coords(&mut self, coords: (usize, usize)) { - if let Some(unit) = self.as_mut() { - unit.set_coords(coords); + fn get_can_build(&self) -> bool { + self.as_ref() + .is_some_and(|unit: &Units| -> bool { unit.get_can_build() }) + } + fn is_unit(&self) -> bool { + self.as_ref().map_or(false, |u| u.is_unit()) } - } - fn get_ref_tasks(&self) -> &VecDeque { - self - .as_ref() - .expect("attempted to get referenced tasks from `None`") - .get_ref_tasks() - } + fn get_tag(&self) -> char { + self.as_ref().map_or(' ', |u| u.get_tag()) + } - fn get_mut_tasks(&mut self) -> &mut VecDeque { - self - .as_mut() - .expect("attempted to get mutable tasks from `None`") - .get_mut_tasks() - } + fn get_name(&self) -> &'static str { + self.as_ref().map_or("", |u| u.get_name()) + } - fn get_tasks_formatted(&self) -> Vec> { - self - .as_ref() - .map_or(Vec::new(), |u| u.get_tasks_formatted()) - } + fn get_owner(&self) -> Players { + self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner()) + } - fn set_task(&mut self, task: Tasks) { - self.as_mut().map_or((), |u| u.set_task(task)) - } + fn get_hp(&self) -> u16 { + self.as_ref().map_or(0, |u| u.get_hp()) + } + + fn get_max_hp(&self) -> u16 { + self.as_ref().map_or(0, |u| u.get_max_hp()) + } + + fn get_can_dig(&self) -> bool { + self.as_ref().map_or(false, |u| u.get_can_dig()) + } + + fn get_digging_power(&self) -> f32 { + self.as_ref().map_or(0.0, |u| u.get_digging_power()) + } + + fn get_coords(&self) -> (usize, usize) { + self.as_ref().map_or((0, 0), |u| u.get_coords()) + } + + fn set_coords(&mut self, coords: (usize, usize)) { + if let Some(unit) = self.as_mut() { + unit.set_coords(coords); + } + } + + fn get_ref_tasks(&self) -> &VecDeque { + self.as_ref() + .expect("attempted to get referenced tasks from `None`") + .get_ref_tasks() + } + + fn get_mut_tasks(&mut self) -> &mut VecDeque { + self.as_mut() + .expect("attempted to get mutable tasks from `None`") + .get_mut_tasks() + } + + fn get_tasks_formatted(&self) -> Vec> { + self.as_ref() + .map_or(Vec::new(), |u| u.get_tasks_formatted()) + } + + fn set_task(&mut self, task: Tasks) { + self.as_mut().map_or((), |u| u.set_task(task)) + } } diff --git a/src/app/states/skirmish_states/zoom_level.rs b/src/app/states/skirmish_states/zoom_level.rs index e954293..e9df983 100644 --- a/src/app/states/skirmish_states/zoom_level.rs +++ b/src/app/states/skirmish_states/zoom_level.rs @@ -3,56 +3,56 @@ use ratatui::text::{Line, Span}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum ZoomLevel { - ZoomedIn, - Default, - ZoomedOut, + ZoomedIn, + Default, + ZoomedOut, } pub enum CellSizes { - Width, - Height, + Width, + Height, } impl ZoomLevel { - pub fn get_cell_size(&self, cell_size: CellSizes) -> usize { - match (cell_size, self) { - (CellSizes::Width, ZoomLevel::ZoomedIn) => 13, - (CellSizes::Width, ZoomLevel::Default) => 9, - (CellSizes::Width, ZoomLevel::ZoomedOut) => 5, - (CellSizes::Height, ZoomLevel::ZoomedIn) => 7, - (CellSizes::Height, ZoomLevel::Default) => 5, - (CellSizes::Height, ZoomLevel::ZoomedOut) => 3, + pub fn get_cell_size(&self, cell_size: CellSizes) -> usize { + match (cell_size, self) { + (CellSizes::Width, ZoomLevel::ZoomedIn) => 13, + (CellSizes::Width, ZoomLevel::Default) => 9, + (CellSizes::Width, ZoomLevel::ZoomedOut) => 5, + (CellSizes::Height, ZoomLevel::ZoomedIn) => 7, + (CellSizes::Height, ZoomLevel::Default) => 5, + (CellSizes::Height, ZoomLevel::ZoomedOut) => 3, + } } - } - pub fn get_cell_text_area<'a>( - &self, - tag: char, - structure_level: char, - unit: Span<'a>, - ) -> Vec> { - match self { - ZoomLevel::ZoomedIn => vec![ - Line::from(vec![ - Span::raw(format!(" {} ", structure_level)), - unit, - Span::raw(" "), - ]), - Line::from(format!(" ")), - Line::from(format!(" {} ", tag)), - Line::from(format!(" ")), - Line::from(format!(" ")), - ], - ZoomLevel::Default => vec![ - Line::from(vec![ - Span::raw(format!(" {} ", structure_level)), - unit, - Span::raw(" "), - ]), - Line::from(format!(" {} ", tag)), - Line::from(format!(" ")), - ], - ZoomLevel::ZoomedOut => vec![Line::from(format!(" {} ", tag))], + pub fn get_cell_text_area<'a>( + &self, + tag: char, + structure_level: char, + unit: Span<'a>, + ) -> Vec> { + match self { + ZoomLevel::ZoomedIn => vec![ + Line::from(vec![ + Span::raw(format!(" {} ", structure_level)), + unit, + Span::raw(" "), + ]), + Line::from(format!(" ")), + Line::from(format!(" {} ", tag)), + Line::from(format!(" ")), + Line::from(format!(" ")), + ], + ZoomLevel::Default => vec![ + Line::from(vec![ + Span::raw(format!(" {} ", structure_level)), + unit, + Span::raw(" "), + ]), + Line::from(format!(" {} ", tag)), + Line::from(format!(" ")), + ], + ZoomLevel::ZoomedOut => vec![Line::from(format!(" {} ", tag))], + } } - } } diff --git a/src/app/threads/audio.rs b/src/app/threads/audio.rs index 8db4907..c94e392 100644 --- a/src/app/threads/audio.rs +++ b/src/app/threads/audio.rs @@ -1,185 +1,185 @@ use clap::ValueEnum; use once_cell::sync::Lazy; use rodio::{ - Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, - decoder::LoopedDecoder, source::Amplify, + Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, + decoder::LoopedDecoder, source::Amplify, }; use std::{ - collections::HashMap, - fs::read, - io::{BufReader, Cursor}, - path::{Path, PathBuf}, - process::exit, - sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver}, + collections::HashMap, + fs::read, + io::{BufReader, Cursor}, + path::{Path, PathBuf}, + process::exit, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver}, }; #[derive(Debug)] pub enum AudioCmd { - Mute, - VolumeUp, - VolumeDown, - ChangePart(SoundrackParts), - ChangeSoundtrack(Soundtrack), + Mute, + VolumeUp, + VolumeDown, + ChangePart(SoundrackParts), + ChangeSoundtrack(Soundtrack), } #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, ValueEnum)] pub enum Soundtrack { - Default, + Default, } #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)] pub enum SoundrackParts { - Calm, - Buildup, - Assault, - Outro, + Calm, + Buildup, + Assault, + Outro, } type SoundtrackCache = HashMap>; static PRELOADED_SOUNDTRACKS: Lazy>>> = - Lazy::new(|| { - let mut map: HashMap>>> = HashMap::new(); + Lazy::new(|| { + let mut map: HashMap>>> = HashMap::new(); - map.insert( - Soundtrack::Default, - Arc::new(load_soundtrack_folder("default")), - ); + map.insert( + Soundtrack::Default, + Arc::new(load_soundtrack_folder("default")), + ); - RwLock::new(map) - }); + RwLock::new(map) + }); fn load_soundtrack_folder(folder: &str) -> SoundtrackCache { - let soundtrack_root: PathBuf = std::env::current_exe() - .ok() - .and_then(|executable: PathBuf| -> Option { - executable - .parent() - .map(|directory: &Path| -> PathBuf { directory.join("soundtracks") }) - }) - .filter(|directory: &PathBuf| -> bool { directory.is_dir() }) - .unwrap_or_else(|| -> PathBuf { PathBuf::from("soundtracks") }); - let base: PathBuf = soundtrack_root.join(folder); - let mut cache: HashMap> = HashMap::new(); + let soundtrack_root: PathBuf = std::env::current_exe() + .ok() + .and_then(|executable: PathBuf| -> Option { + executable + .parent() + .map(|directory: &Path| -> PathBuf { directory.join("soundtracks") }) + }) + .filter(|directory: &PathBuf| -> bool { directory.is_dir() }) + .unwrap_or_else(|| -> PathBuf { PathBuf::from("soundtracks") }); + let base: PathBuf = soundtrack_root.join(folder); + let mut cache: HashMap> = HashMap::new(); - macro_rules! load { - ($part:ident, $file:expr) => { - let path: PathBuf = base.join($file); - cache.insert( - SoundrackParts::$part, - read(&path).expect(&format!("cannot read {}", path.display())), - ); - }; - } + macro_rules! load { + ($part:ident, $file:expr) => { + let path: PathBuf = base.join($file); + cache.insert( + SoundrackParts::$part, + read(&path).expect(&format!("cannot read {}", path.display())), + ); + }; + } - load!(Calm, "calm.ogg"); // calm.ogg - load!(Buildup, "test.ogg"); // buildup.ogg - load!(Assault, "test.ogg"); // assault.ogg - load!(Outro, "test.ogg"); // outro.ogg + load!(Calm, "calm.ogg"); // calm.ogg + load!(Buildup, "test.ogg"); // buildup.ogg + load!(Assault, "test.ogg"); // assault.ogg + load!(Outro, "test.ogg"); // outro.ogg - cache + cache } pub fn load_audio( - soundtrack: &Soundtrack, - part: &SoundrackParts, + soundtrack: &Soundtrack, + part: &SoundrackParts, ) -> Amplify>>>> { - { - let guard: RwLockReadGuard<'_, HashMap>>>> = - PRELOADED_SOUNDTRACKS - .read() - .map_err(|e| { - eprintln!("RwLock poisoned: {}", e); - exit(1); - }) - .expect("lock issue"); + { + let guard: RwLockReadGuard<'_, HashMap>>>> = + PRELOADED_SOUNDTRACKS + .read() + .map_err(|e| { + eprintln!("RwLock poisoned: {}", e); + exit(1); + }) + .expect("lock issue"); - if let Some(soundtrack_cache) = guard.get(soundtrack) { - let data: &Vec = soundtrack_cache.get(part).expect("part missing in cache"); + if let Some(soundtrack_cache) = guard.get(soundtrack) { + let data: &Vec = soundtrack_cache.get(part).expect("part missing in cache"); - return Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) - .expect("decoder issue") - .amplify(0.20); - } - } - - { - let mut guard: RwLockWriteGuard< - '_, - HashMap>>>, - > = PRELOADED_SOUNDTRACKS - .write() - .map_err(|e| { - eprintln!("RwLock poisoned: {}", e); - exit(1); - }) - .expect("lock issue"); - - if let Some(soundtrack_cache) = guard.get(soundtrack) { - let data: &Vec = soundtrack_cache.get(part).expect("part missing in cache"); - - return Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) - .expect("decoder issue") - .amplify(0.20); + return Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) + .expect("decoder issue") + .amplify(0.20); + } } - let folder_name: &str = match soundtrack { - Soundtrack::Default => "default", - }; + { + let mut guard: RwLockWriteGuard< + '_, + HashMap>>>, + > = PRELOADED_SOUNDTRACKS + .write() + .map_err(|e| { + eprintln!("RwLock poisoned: {}", e); + exit(1); + }) + .expect("lock issue"); - let new_cache: Arc>> = - Arc::new(load_soundtrack_folder(folder_name)); + if let Some(soundtrack_cache) = guard.get(soundtrack) { + let data: &Vec = soundtrack_cache.get(part).expect("part missing in cache"); - let data: &Vec = new_cache.get(&part).expect("part missing after load"); + return Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) + .expect("decoder issue") + .amplify(0.20); + } - guard.insert(*soundtrack, Arc::clone(&new_cache)); + let folder_name: &str = match soundtrack { + Soundtrack::Default => "default", + }; - Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) - .expect("decoder issue") - .amplify(0.20) - } + let new_cache: Arc>> = + Arc::new(load_soundtrack_folder(folder_name)); + + let data: &Vec = new_cache.get(&part).expect("part missing after load"); + + guard.insert(*soundtrack, Arc::clone(&new_cache)); + + Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) + .expect("decoder issue") + .amplify(0.20) + } } pub fn handle_audio(rx: Receiver, mute: bool, soundtrack: Soundtrack) { - let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device() - .expect("get default device") - .with_buffer_size(BufferSize::Fixed(4096)) - .open_sink_or_fallback() - .expect("open device audio stream"); - handle.log_on_drop(false); + let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device() + .expect("get default device") + .with_buffer_size(BufferSize::Fixed(4096)) + .open_sink_or_fallback() + .expect("open device audio stream"); + handle.log_on_drop(false); - let player: Player = Player::connect_new(&handle.mixer()); + let player: Player = Player::connect_new(&handle.mixer()); - let mut volume: f32 = player.volume(); - player.set_volume(if mute { 0.0 } else { volume }); + let mut volume: f32 = player.volume(); + player.set_volume(if mute { 0.0 } else { volume }); - let mut current_soundtrack: Soundtrack = soundtrack; - let mut current_part: SoundrackParts = SoundrackParts::Calm; - player.append(load_audio(¤t_soundtrack, ¤t_part)); + let mut current_soundtrack: Soundtrack = soundtrack; + let mut current_part: SoundrackParts = SoundrackParts::Calm; + player.append(load_audio(¤t_soundtrack, ¤t_part)); - for cmd in rx { - match cmd { - AudioCmd::Mute => { - player.set_volume(if player.volume() == 0.0 { volume } else { 0.0 }); - } - AudioCmd::VolumeUp => { - volume = (volume + 0.1).min(1.0); - player.set_volume(volume); - } - AudioCmd::VolumeDown => { - volume = (volume - 0.1).max(0.0); - player.set_volume(volume); - } - AudioCmd::ChangePart(part) => { - current_part = part; - player.append(load_audio(¤t_soundtrack, ¤t_part)); - player.skip_one(); - } - AudioCmd::ChangeSoundtrack(st) => { - current_soundtrack = st; - player.append(load_audio(¤t_soundtrack, ¤t_part)); - player.skip_one(); - } + for cmd in rx { + match cmd { + AudioCmd::Mute => { + player.set_volume(if player.volume() == 0.0 { volume } else { 0.0 }); + } + AudioCmd::VolumeUp => { + volume = (volume + 0.1).min(1.0); + player.set_volume(volume); + } + AudioCmd::VolumeDown => { + volume = (volume - 0.1).max(0.0); + player.set_volume(volume); + } + AudioCmd::ChangePart(part) => { + current_part = part; + player.append(load_audio(¤t_soundtrack, ¤t_part)); + player.skip_one(); + } + AudioCmd::ChangeSoundtrack(st) => { + current_soundtrack = st; + player.append(load_audio(¤t_soundtrack, ¤t_part)); + player.skip_one(); + } + } } - } } diff --git a/src/app/threads/events.rs b/src/app/threads/events.rs index 0aab851..e9d8781 100644 --- a/src/app/threads/events.rs +++ b/src/app/threads/events.rs @@ -2,20 +2,20 @@ use ratatui::crossterm::event::{Event, KeyEvent, read}; use std::sync::mpsc::Sender; pub fn handle_ct_events(input_tx: Sender, resize_tx: Sender<(u16, u16)>) { - loop { - match read() { - Ok(Event::Key(k)) => { - if input_tx.send(k).is_err() { - break; + loop { + match read() { + Ok(Event::Key(k)) => { + if input_tx.send(k).is_err() { + break; + } + } + Ok(Event::Resize(cols, rows)) => { + if resize_tx.send((cols, rows)).is_err() { + break; + } + } + Ok(_) => {} + Err(_) => continue, } - } - Ok(Event::Resize(cols, rows)) => { - if resize_tx.send((cols, rows)).is_err() { - break; - } - } - Ok(_) => {} - Err(_) => continue, } - } } diff --git a/src/app/threads/tick.rs b/src/app/threads/tick.rs index 4137a6f..58a457c 100644 --- a/src/app/threads/tick.rs +++ b/src/app/threads/tick.rs @@ -1,21 +1,21 @@ use std::{ - sync::mpsc::Sender, - thread, - time::{Duration, Instant}, + sync::mpsc::Sender, + thread, + time::{Duration, Instant}, }; pub const TICK_MS: u8 = 33; pub fn handle_tick_event(tx: Sender<()>) { - let interval: Duration = Duration::from_millis(TICK_MS as u64); - loop { - if tx.send(()).is_err() { - break; - } + let interval: Duration = Duration::from_millis(TICK_MS as u64); + loop { + if tx.send(()).is_err() { + break; + } - let elapsed: Duration = Instant::now().elapsed(); - if interval > elapsed { - thread::sleep(interval - elapsed); + let elapsed: Duration = Instant::now().elapsed(); + if interval > elapsed { + thread::sleep(interval - elapsed); + } } - } } diff --git a/src/app/view.rs b/src/app/view.rs index 4235991..b7d29b5 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1,27 +1,27 @@ use crate::app::{ - App, - views::{default_view, main_menu_view, skirmish_view}, + App, + views::{default_view, main_menu_view, skirmish_view}, }; use clap::ValueEnum; use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] pub enum View { - MainMenu, - Skirmish, - PerkDecks, - SkillsConfig, + MainMenu, + Skirmish, + PerkDecks, + SkillsConfig, } impl Widget for &mut App { - fn render(self, area: Rect, buf: &mut Buffer) - where - Self: Sized, - { - match self.view { - View::MainMenu => main_menu_view(self, area, buf), - View::Skirmish => skirmish_view(self, area, buf), - _ => default_view(area, buf), + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + match self.view { + View::MainMenu => main_menu_view(self, area, buf), + View::Skirmish => skirmish_view(self, area, buf), + _ => default_view(area, buf), + } } - } } diff --git a/src/app/views/default.rs b/src/app/views/default.rs index 790c9bd..129b387 100644 --- a/src/app/views/default.rs +++ b/src/app/views/default.rs @@ -1,40 +1,40 @@ use crate::app::{ - keybindings::{Action, count_largest_group}, - widgets::KeybindingsWidget, + keybindings::{Action, count_largest_group}, + widgets::KeybindingsWidget, }; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Rect}, - style::Stylize, - widgets::{Block, Borders, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Rect}, + style::Stylize, + widgets::{Block, Borders, Paragraph, Widget}, }; const ACTIONS: &[Action] = &[Action::Quit, Action::Quit2, Action::Esc]; fn default_view_layout(area: Rect) -> [Rect; 2] { - Layout::vertical([ - Constraint::Fill(1), - Constraint::Length(count_largest_group(&ACTIONS) + 2), - ]) - .areas(area) + Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(count_largest_group(&ACTIONS) + 2), + ]) + .areas(area) } pub fn default_view(area: Rect, buf: &mut Buffer) { - let [main_area, keybindings_area] = default_view_layout(area); + let [main_area, keybindings_area] = default_view_layout(area); - { - Paragraph::new("Work in progress") - .alignment(Alignment::Center) - .yellow() - .block( - Block::new() - .gray() - .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT), - ) - .render(main_area, buf); - } + { + Paragraph::new("Work in progress") + .alignment(Alignment::Center) + .yellow() + .block( + Block::new() + .gray() + .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT), + ) + .render(main_area, buf); + } - { - KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf); - } + { + KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf); + } } diff --git a/src/app/views/main_menu.rs b/src/app/views/main_menu.rs index ddc2f18..a3a63b6 100644 --- a/src/app/views/main_menu.rs +++ b/src/app/views/main_menu.rs @@ -1,100 +1,100 @@ use crate::app::{ - App, View, - helpers::main_menu_option_helper, - keybindings::{Action, count_largest_group}, - widgets::KeybindingsWidget, + App, View, + helpers::main_menu_option_helper, + keybindings::{Action, count_largest_group}, + widgets::KeybindingsWidget, }; use clap::ValueEnum; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Rect}, - style::Stylize, - text::Line, - widgets::{Block, Borders, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Rect}, + style::Stylize, + text::Line, + widgets::{Block, Borders, Paragraph, Widget}, }; const ACTIONS: &[Action] = &[ - Action::Up, - Action::Down, - Action::Space, - Action::VolumeUp, - Action::VolumeDown, - Action::Mute, - Action::Quit, - Action::Quit2, + Action::Up, + Action::Down, + Action::Space, + Action::VolumeUp, + Action::VolumeDown, + Action::Mute, + Action::Quit, + Action::Quit2, ]; fn main_menu_layout(area: Rect) -> [Rect; 2] { - Layout::vertical([ - Constraint::Fill(1), - Constraint::Length(count_largest_group(&ACTIONS) + 2), - ]) - .areas(area) + Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(count_largest_group(&ACTIONS) + 2), + ]) + .areas(area) } pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) { - let Some(states) = app.states_ref() else { - return; - }; + let Some(states) = app.states_ref() else { + return; + }; - let [main_menu_area, keybindings_area] = main_menu_layout(area); + let [main_menu_area, keybindings_area] = main_menu_layout(area); - Block::new() - .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT) - .render(main_menu_area, buf); + 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 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_area: Rect = main_menu_areas[0].centered_vertically(Constraint::Percentage(50)); - let title_text: String = vec![ - r" __ __ _ _____ _ ", - r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ", - r"\ \/ \/ / _` | '__| | | '_ \ / /\/ | | | '_ \| '_ \ / _ \ / __|", - r" \ /\ / (_| | | | | | | | / / | |_| | | | | | | | __/ \__ \", - r" \/ \/ \__,_|_| |_|_| |_| \/ \__,_|_| |_|_| |_|\___|_|___/", - ] - .join("\n"); + let title_text: String = vec![ + r" __ __ _ _____ _ ", + r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ", + r"\ \/ \/ / _` | '__| | | '_ \ / /\/ | | | '_ \| '_ \ / _ \ / __|", + r" \ /\ / (_| | | | | | | | / / | |_| | | | | | | | __/ \__ \", + r" \/ \/ \__,_|_| |_|_| |_| \/ \__,_|_| |_|_| |_|\___|_|___/", + ] + .join("\n"); - Paragraph::new(title_text) - .alignment(Alignment::Center) - .yellow() - .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT)) - .render(title_area, buf); - } + Paragraph::new(title_text) + .alignment(Alignment::Center) + .yellow() + .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT)) + .render(title_area, buf); + } - { - let options_area: Rect = main_menu_areas[1]; + { + let options_area: Rect = main_menu_areas[1]; - let lines: Vec> = View::value_variants() - .iter() - .enumerate() - .filter(|(_, v)| v != &&View::MainMenu) - .map(|(i, view)| { - let view_string: String = main_menu_option_helper(format!("{:?}", view)); + let lines: Vec> = View::value_variants() + .iter() + .enumerate() + .filter(|(_, v)| v != &&View::MainMenu) + .map(|(i, view)| { + let view_string: String = main_menu_option_helper(format!("{:?}", view)); - let styled: Line<'_> = if states.main_menu.selected_view == i { - Line::from_iter(["> ".cyan(), view_string.yellow()]).yellow() - } else { - Line::from(format!(" {view_string}")).white() - }; + let styled: Line<'_> = if states.main_menu.selected_view == i { + Line::from_iter(["> ".cyan(), view_string.yellow()]).yellow() + } else { + Line::from(format!(" {view_string}")).white() + }; - styled - }) - .collect(); + styled + }) + .collect(); - Paragraph::new(lines) - .alignment(Alignment::Center) - .green() - .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT)) - .render(options_area, buf); - } + Paragraph::new(lines) + .alignment(Alignment::Center) + .green() + .block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT)) + .render(options_area, buf); + } - { - KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf); - } + { + KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf); + } } diff --git a/src/app/views/skirmish.rs b/src/app/views/skirmish.rs index 61c2602..b712ed3 100644 --- a/src/app/views/skirmish.rs +++ b/src/app/views/skirmish.rs @@ -1,265 +1,269 @@ use crate::app::{ - App, - helpers::block_title_helper, - keybindings::{Action, available_skirmish_actions}, - states::skirmish_states::{ - BoardState, Players, - resources::{WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_INTERVAL_MS}, - structures::{Mine, Structure, Structures}, - units::MinerUnit, - }, - widgets::{BoardWidget, CellWidget, KeybindingsWidget, SidePanelWidget}, + App, + helpers::block_title_helper, + keybindings::{Action, available_skirmish_actions}, + states::skirmish_states::{ + BoardState, Players, + resources::{WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_INTERVAL_MS}, + structures::{Mine, Structure, Structures}, + units::MinerUnit, + }, + widgets::{BoardWidget, CellWidget, KeybindingsWidget, SidePanelWidget}, }; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Margin, Rect}, - style::{Color, Stylize}, - text::Line, - widgets::{Block, Borders, Clear, Padding, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Margin, Rect}, + style::{Color, Stylize}, + text::Line, + widgets::{Block, Borders, Clear, Padding, Paragraph, Widget}, }; pub fn skirmish_layout(area: Rect) -> [Rect; 3] { - Layout::vertical([ - Constraint::Length(4), - Constraint::Fill(1), - // Four movement rows plus borders keep the board geometry stable across modes. - Constraint::Length(6), - ]) - .areas(area) + Layout::vertical([ + Constraint::Length(4), + Constraint::Fill(1), + // Four movement rows plus borders keep the board geometry stable across modes. + Constraint::Length(6), + ]) + .areas(area) } pub fn skirmish_main_area_layout(area: Rect, side_panel: bool) -> [Rect; 2] { - let constraint: [Constraint; 2] = if side_panel { - [Constraint::Fill(1), Constraint::Length(39)] - } else { - [Constraint::Percentage(100), Constraint::Percentage(0)] - }; + let constraint: [Constraint; 2] = if side_panel { + [Constraint::Fill(1), Constraint::Length(39)] + } else { + [Constraint::Percentage(100), Constraint::Percentage(0)] + }; - Layout::horizontal(constraint).areas(area) + Layout::horizontal(constraint).areas(area) } pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) { - let Some(states) = app.states_ref() else { - return; - }; - - let board: &BoardState = &states.skirmish.board; - - let [title_area, main_area, keybindings_area] = skirmish_layout(area); - - { - let lines: Vec> = Vec::from_iter([ - Line::raw("War in Tunnels").yellow(), - Line::from_iter([ - format!( - "Wood: {} (+{}/{}s) | ", - states.skirmish.player_resources.wood, - WOOD_DELIVERY_AMOUNT, - WOOD_DELIVERY_INTERVAL_MS / 1000 - ), - format!("Metal: {} | ", states.skirmish.player_resources.metal), - format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit), - format!( - "Skills points: {} ({}/{}) | ", - 1, 20, states.settings.skill_points_limit - ), - format!("Perk Deck: {}/9 | ", 5), - format!("Tick: {}", states.skirmish.turn_counter), - ]), - ]); - - Paragraph::new(lines) - .alignment(Alignment::Left) - .block( - Block::new() - .gray() - .padding(Padding { - left: 1, - right: 1, - top: 0, - bottom: 1, - }) - .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT), - ) - .render(title_area, buf); - } - - { - let main_block: Block = Block::new() - .gray() - .title(block_title_helper( - &[ - ("Skirmish".to_string(), Color::Magenta), - ("Map".to_string(), Color::Green), - ], - Some(" - "), - )) - .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT); - - let main_inner_area: Rect = main_block.inner(main_area); - main_block.render(main_area, buf); - - let [board_area, side_panel_area] = - skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel); - - let cells_area: Rect = board_area.inner(Margin { - horizontal: 1, - vertical: 1, - }); - // .centered( - // Constraint::Length( - // (board.cell_width * board.cols) as u16, - // ), - // Constraint::Length( - // (board.cell_height * board.rows) as u16, - // ), - // ); - - BoardWidget::new(&board).render(cells_area, buf); - - if states.skirmish.side_panel { - let (row, col) = board.get_focused_cell().get_coords(); - let mut is_unit_marked: bool = false; - - if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) { - is_unit_marked = first_row == &row && first_col == &col; - } - - SidePanelWidget::new( - (row, col), - &board.get_ref_cell(row, col).get_structure(), - &board.get_ref_cell(row, col).get_ref_option_unit(), - is_unit_marked, - ) - .render(side_panel_area, buf); - } - } - - { - let actions: Vec = available_skirmish_actions(&states.skirmish); - let descriptions: [(Action, &'static str); 11] = [ - ( - Action::Enter, - if states.skirmish.recruitment_view { - "Queue Miner" - } else { - "Build Mine" - }, - ), - ( - Action::Recruitment, - if states.skirmish.recruitment_view { - "Close recruitment" - } else { - "Recruit" - }, - ), - ( - Action::Delete, - if states.skirmish.building_view { - "Cancel construction" - } else { - "Cancel marking" - }, - ), - ( - Action::Space, - if board.marked_cells.marking_cells { - "Confirm route" - } else { - "Select miner" - }, - ), - ( - Action::Esc, - if states.skirmish.recruitment_view { - "Close recruitment" - } else if states.skirmish.building_view { - "Close buildings" - } else if board.marked_cells.marking_cells { - "Cancel route" - } else { - "Menu" - }, - ), - ( - Action::Building, - if states.skirmish.building_view { - "Close buildings" - } else { - "Build" - }, - ), - (Action::Tab, "Details"), - (Action::ScrollUp, "Scroll"), - (Action::ScrollDown, "Scroll"), - (Action::ScrollLeft, "Scroll"), - (Action::ScrollRight, "Scroll"), - ]; - KeybindingsWidget::with_descriptions(&actions, keybindings_area.height, &descriptions) - .render(keybindings_area, buf); - } - if states.skirmish.building_view { - let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14)); - let coords: (usize, usize) = board.get_focused_cell().get_coords(); - let structure: Structures = board.get_ref_cell(coords.0, coords.1).get_structure(); - let status: String = if let Structures::MineConstruction(site) = structure - && site.get_owner() == Players::Player - { - format!( - "Construction: {}%\n{}\nDelete: cancel and refund full cost", - site.progress_percent(), - if board.has_adjacent_builder(coords, Players::Player) { - "Builder working" - } else { - "Paused: adjacent friendly builder required" - } - ) - } else if matches!(structure, Structures::Mine(_)) && structure.get_owner() == Players::Player { - "Mine completed".to_string() - } else if states - .skirmish - .can_start_mine_construction(coords, Players::Player) - { - "Ready to build: Enter".to_string() - } else { - "Construction unavailable: check site, builder and resources".to_string() + let Some(states) = app.states_ref() else { + return; }; - Clear.render(popup, buf); - Paragraph::new(format!("Mine\nCost: {} Wood + {} Metal | Build time: 10s\nRequires: neutral Ore + adjacent friendly builder\nProduces: 1 Metal/s (2 with a Miner inside)\n{status}", Mine::COST.wood, Mine::COST.metal)) + + let board: &BoardState = &states.skirmish.board; + + let [title_area, main_area, keybindings_area] = skirmish_layout(area); + + { + let lines: Vec> = Vec::from_iter([ + Line::raw("War in Tunnels").yellow(), + Line::from_iter([ + format!( + "Wood: {} (+{}/{}s) | ", + states.skirmish.player_resources.wood, + WOOD_DELIVERY_AMOUNT, + WOOD_DELIVERY_INTERVAL_MS / 1000 + ), + format!("Metal: {} | ", states.skirmish.player_resources.metal), + format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit), + format!( + "Skills points: {} ({}/{}) | ", + 1, 20, states.settings.skill_points_limit + ), + format!("Perk Deck: {}/9 | ", 5), + format!("Tick: {}", states.skirmish.turn_counter), + ]), + ]); + + Paragraph::new(lines) + .alignment(Alignment::Left) + .block( + Block::new() + .gray() + .padding(Padding { + left: 1, + right: 1, + top: 0, + bottom: 1, + }) + .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT), + ) + .render(title_area, buf); + } + + { + let main_block: Block = Block::new() + .gray() + .title(block_title_helper( + &[ + ("Skirmish".to_string(), Color::Magenta), + ("Map".to_string(), Color::Green), + ], + Some(" - "), + )) + .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT); + + let main_inner_area: Rect = main_block.inner(main_area); + main_block.render(main_area, buf); + + let [board_area, side_panel_area] = + skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel); + + let cells_area: Rect = board_area.inner(Margin { + horizontal: 1, + vertical: 1, + }); + // .centered( + // Constraint::Length( + // (board.cell_width * board.cols) as u16, + // ), + // Constraint::Length( + // (board.cell_height * board.rows) as u16, + // ), + // ); + + BoardWidget::new(&board).render(cells_area, buf); + + if states.skirmish.side_panel { + let (row, col) = board.get_focused_cell().get_coords(); + let mut is_unit_marked: bool = false; + + if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) { + is_unit_marked = first_row == &row && first_col == &col; + } + + SidePanelWidget::new( + (row, col), + &board.get_ref_cell(row, col).get_structure(), + &board.get_ref_cell(row, col).get_ref_option_unit(), + is_unit_marked, + ) + .render(side_panel_area, buf); + } + } + + { + let actions: Vec = available_skirmish_actions(&states.skirmish); + let descriptions: [(Action, &'static str); 11] = [ + ( + Action::Enter, + if states.skirmish.recruitment_view { + "Queue Miner" + } else { + "Build Mine" + }, + ), + ( + Action::Recruitment, + if states.skirmish.recruitment_view { + "Close recruitment" + } else { + "Recruit" + }, + ), + ( + Action::Delete, + if states.skirmish.building_view { + "Cancel construction" + } else { + "Cancel marking" + }, + ), + ( + Action::Space, + if board.marked_cells.marking_cells { + "Confirm route" + } else { + "Select miner" + }, + ), + ( + Action::Esc, + if states.skirmish.recruitment_view { + "Close recruitment" + } else if states.skirmish.building_view { + "Close buildings" + } else if board.marked_cells.marking_cells { + "Cancel route" + } else { + "Menu" + }, + ), + ( + Action::Building, + if states.skirmish.building_view { + "Close buildings" + } else { + "Build" + }, + ), + (Action::Tab, "Details"), + (Action::ScrollUp, "Scroll"), + (Action::ScrollDown, "Scroll"), + (Action::ScrollLeft, "Scroll"), + (Action::ScrollRight, "Scroll"), + ]; + KeybindingsWidget::with_descriptions(&actions, keybindings_area.height, &descriptions) + .render(keybindings_area, buf); + } + if states.skirmish.building_view { + let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14)); + let coords: (usize, usize) = board.get_focused_cell().get_coords(); + let structure: Structures = board.get_ref_cell(coords.0, coords.1).get_structure(); + let status: String = if let Structures::MineConstruction(site) = structure + && site.get_owner() == Players::Player + { + format!( + "Construction: {}%\n{}\nDelete: cancel and refund full cost", + site.progress_percent(), + if board.has_adjacent_builder(coords, Players::Player) { + "Builder working" + } else { + "Paused: adjacent friendly builder required" + } + ) + } else if matches!(structure, Structures::Mine(_)) + && structure.get_owner() == Players::Player + { + "Mine completed".to_string() + } else if states + .skirmish + .can_start_mine_construction(coords, Players::Player) + { + "Ready to build: Enter".to_string() + } else { + "Construction unavailable: check site, builder and resources".to_string() + }; + Clear.render(popup, buf); + Paragraph::new(format!("Mine\nCost: {} Wood + {} Metal | Build time: 10s\nRequires: neutral Ore + adjacent friendly builder\nProduces: 1 Metal/s (2 with a Miner inside)\n{status}", Mine::COST.wood, Mine::COST.metal)) .block(Block::bordered().title("Buildings").padding(Padding::uniform(1))) .render(popup, buf); - } - if states.skirmish.recruitment_view { - let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14)); - let coords: (usize, usize) = board.get_focused_cell().get_coords(); - let cell: &CellWidget = board.get_ref_cell(coords.0, coords.1); - let text: String = if let Structures::Base(base) = cell.get_structure() - && base.get_owner() == Players::Player - { - let status: &'static str = base.recruitment_status(cell.get_ref_option_unit().is_some()); - let action: &'static str = if states.skirmish.can_recruit_miner(coords, Players::Player) { - "Enter: queue Miner" - } else { - "Insufficient resources or queue full" - }; - format!( - "Miner\nCost: {} Wood + {} Metal | Recruit time: 10s\nQueued Miners: {} (includes active)\nCurrent progress: {}%\n{status}\n{action}", - MinerUnit::RECRUIT_COST.wood, - MinerUnit::RECRUIT_COST.metal, - base.queued_miners(), - base.recruitment_progress() - ) - } else { - "Select your Base to recruit Miners".to_string() - }; - Clear.render(popup, buf); - Paragraph::new(text) - .block( - Block::bordered() - .title("Recruitment") - .padding(Padding::uniform(1)), - ) - .render(popup, buf); - } + } + if states.skirmish.recruitment_view { + let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14)); + let coords: (usize, usize) = board.get_focused_cell().get_coords(); + let cell: &CellWidget = board.get_ref_cell(coords.0, coords.1); + let text: String = if let Structures::Base(base) = cell.get_structure() + && base.get_owner() == Players::Player + { + let status: &'static str = + base.recruitment_status(cell.get_ref_option_unit().is_some()); + let action: &'static str = if states.skirmish.can_recruit_miner(coords, Players::Player) + { + "Enter: queue Miner" + } else { + "Insufficient resources or queue full" + }; + format!( + "Miner\nCost: {} Wood + {} Metal | Recruit time: 10s\nQueued Miners: {} (includes active)\nCurrent progress: {}%\n{status}\n{action}", + MinerUnit::RECRUIT_COST.wood, + MinerUnit::RECRUIT_COST.metal, + base.queued_miners(), + base.recruitment_progress() + ) + } else { + "Select your Base to recruit Miners".to_string() + }; + Clear.render(popup, buf); + Paragraph::new(text) + .block( + Block::bordered() + .title("Recruitment") + .padding(Padding::uniform(1)), + ) + .render(popup, buf); + } } diff --git a/src/app/widgets/board.rs b/src/app/widgets/board.rs index 884b4b4..d4bdb85 100644 --- a/src/app/widgets/board.rs +++ b/src/app/widgets/board.rs @@ -1,46 +1,45 @@ use crate::app::states::skirmish_states::BoardState; use ratatui::{ - buffer::Buffer, - layout::{Constraint, Layout, Rect}, - widgets::Widget, + buffer::Buffer, + layout::{Constraint, Layout, Rect}, + widgets::Widget, }; use std::rc::Rc; pub struct BoardWidget<'a> { - state: &'a BoardState, + state: &'a BoardState, } impl<'a> BoardWidget<'a> { - pub fn new(state: &'a BoardState) -> Self { - Self { state } - } + pub fn new(state: &'a BoardState) -> Self { + Self { state } + } } impl Widget for BoardWidget<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let horizontal: Rc<[Rect]> = - Layout::horizontal(vec![ - Constraint::Length(self.state.cell_width as u16); - self.state.cols - ]) - .split(area); + fn render(self, area: Rect, buf: &mut Buffer) { + let horizontal: Rc<[Rect]> = + Layout::horizontal(vec![ + Constraint::Length(self.state.cell_width as u16); + self.state.cols + ]) + .split(area); - for (col_idx, col_area) in horizontal.iter().enumerate() { - let vertical: Rc<[Rect]> = - Layout::vertical(vec![ - Constraint::Length(self.state.cell_height as u16); - self.state.rows - ]) - .split(*col_area); + for (col_idx, col_area) in horizontal.iter().enumerate() { + let vertical: Rc<[Rect]> = + Layout::vertical(vec![ + Constraint::Length(self.state.cell_height as u16); + self.state.rows + ]) + .split(*col_area); - for (row_idx, cell_area) in vertical.iter().enumerate() { - self - .state - .cells - .get(row_idx + self.state.vertical_offset.get_value()) - .and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value())) - .map(|cell| cell.clone().render(*cell_area, buf)); - } + for (row_idx, cell_area) in vertical.iter().enumerate() { + self.state + .cells + .get(row_idx + self.state.vertical_offset.get_value()) + .and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value())) + .map(|cell| cell.clone().render(*cell_area, buf)); + } + } } - } } diff --git a/src/app/widgets/cell.rs b/src/app/widgets/cell.rs index c11aaa8..4aaeaa2 100644 --- a/src/app/widgets/cell.rs +++ b/src/app/widgets/cell.rs @@ -1,166 +1,165 @@ use crate::app::states::skirmish_states::{ - ZoomLevel, - structures::{Stone, Structure, Structures}, - tasks::Tasks, - units::{Unit, Units}, + ZoomLevel, + structures::{Stone, Structure, Structures}, + tasks::Tasks, + units::{Unit, Units}, }; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Rect}, - style::{Color, Style, Stylize}, - text::{Line, Span, ToSpan}, - widgets::{Block, BorderType, Borders, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Rect}, + style::{Color, Style, Stylize}, + text::{Line, Span, ToSpan}, + widgets::{Block, BorderType, Borders, Paragraph, Widget}, }; #[derive(Debug, Clone, PartialEq)] pub struct CellWidget { - row: usize, - col: usize, - selected: bool, - zoom_level: ZoomLevel, - structure: Structures, - marked: bool, - unit: Option, + row: usize, + col: usize, + selected: bool, + zoom_level: ZoomLevel, + structure: Structures, + marked: bool, + unit: Option, } impl CellWidget { - pub fn new( - row: usize, - col: usize, - zoom_level: ZoomLevel, - selected: bool, - structure: Structures, - unit: Option, - ) -> Self { - Self { - row, - col, - selected, - zoom_level, - marked: false, - structure, - unit, - } - } - - pub fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - pub fn set_zoom_level(&mut self, zoom_level: ZoomLevel) -> &mut Self { - self.zoom_level = zoom_level; - self - } - - pub fn get_structure(&self) -> Structures { - self.structure - } - - pub fn get_option_unit(&self) -> Option { - self.unit.clone() - } - - pub fn get_ref_option_unit(&self) -> &Option { - &self.unit - } - - pub fn get_mut_option_unit(&mut self) -> &mut Option { - &mut self.unit - } - - pub fn set_structure(&mut self, structure: Structures) -> &mut Self { - self.structure = structure; - self - } - - pub fn get_marked(&self) -> bool { - self.marked - } - - pub fn set_marked(&mut self, marked: bool) -> &mut Self { - self.marked = marked; - self - } - - fn col_to_letters(&self) -> String { - let mut col: usize = self.col + 1; - let mut letters: Vec = Vec::new(); - - while col > 0 { - letters.push((b'A' + ((col - 1) % 26) as u8) as char); - col = (col - 1) / 26; + pub fn new( + row: usize, + col: usize, + zoom_level: ZoomLevel, + selected: bool, + structure: Structures, + unit: Option, + ) -> Self { + Self { + row, + col, + selected, + zoom_level, + marked: false, + structure, + unit, + } } - letters.iter().rev().collect() - } + pub fn set_selected(&mut self, selected: bool) -> &mut Self { + self.selected = selected; + self + } - fn display_coords(&self) -> Span<'_> { - if self.selected || self.structure != Structures::Stone(Stone::new()) || self.marked { - format!("{}{}", self.col_to_letters(), self.row).green() - } else { - "".to_span() + pub fn set_zoom_level(&mut self, zoom_level: ZoomLevel) -> &mut Self { + self.zoom_level = zoom_level; + self } - } - fn fg_color(&self) -> Color { - if self.marked && self.selected { - Color::Magenta - } else if self.marked && !self.selected { - Color::LightMagenta - } else if !self.marked && self.selected { - Color::LightYellow - } else { - self.structure.get_color() + pub fn get_structure(&self) -> Structures { + self.structure } - } - fn get_text_area(&self) -> Vec> { - let mut lines: Vec> = self.zoom_level.get_cell_text_area( - self.structure.get_tag(), - self.structure.get_level(), - self - .unit - .get_tag() - .to_string() - .fg(self.unit.get_owner().get_color()), - ); - if let Structures::Stone(stone) = self.structure - && stone.get_durability() < stone.get_max_durability() - { - let percent: u32 = - u32::from(stone.get_durability()) * 100 / u32::from(stone.get_max_durability()); - let middle = lines.len() / 2; - lines[middle] = Line::from(format!("{percent}%")).cyan(); + pub fn get_option_unit(&self) -> Option { + self.unit.clone() } - if let Structures::MineConstruction(site) = self.structure { - let middle: usize = lines.len() / 2; - lines[middle] = Line::from(format!("{}%", site.progress_percent())).cyan(); - } - if let Some(unit) = &self.unit - && let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front() - && let Some(percent) = task.get_movement_progress() - { - let middle = lines.len() / 2; - lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color()); - } - lines - } - fn get_block(&self) -> Block<'_> { - Block::default() - .borders(Borders::ALL) - .style(Style::default().fg(self.fg_color())) - .title(self.display_coords()) - .border_type(BorderType::Rounded) - } + pub fn get_ref_option_unit(&self) -> &Option { + &self.unit + } + + pub fn get_mut_option_unit(&mut self) -> &mut Option { + &mut self.unit + } + + pub fn set_structure(&mut self, structure: Structures) -> &mut Self { + self.structure = structure; + self + } + + pub fn get_marked(&self) -> bool { + self.marked + } + + pub fn set_marked(&mut self, marked: bool) -> &mut Self { + self.marked = marked; + self + } + + fn col_to_letters(&self) -> String { + let mut col: usize = self.col + 1; + let mut letters: Vec = Vec::new(); + + while col > 0 { + letters.push((b'A' + ((col - 1) % 26) as u8) as char); + col = (col - 1) / 26; + } + + letters.iter().rev().collect() + } + + fn display_coords(&self) -> Span<'_> { + if self.selected || self.structure != Structures::Stone(Stone::new()) || self.marked { + format!("{}{}", self.col_to_letters(), self.row).green() + } else { + "".to_span() + } + } + + fn fg_color(&self) -> Color { + if self.marked && self.selected { + Color::Magenta + } else if self.marked && !self.selected { + Color::LightMagenta + } else if !self.marked && self.selected { + Color::LightYellow + } else { + self.structure.get_color() + } + } + + fn get_text_area(&self) -> Vec> { + let mut lines: Vec> = self.zoom_level.get_cell_text_area( + self.structure.get_tag(), + self.structure.get_level(), + self.unit + .get_tag() + .to_string() + .fg(self.unit.get_owner().get_color()), + ); + if let Structures::Stone(stone) = self.structure + && stone.get_durability() < stone.get_max_durability() + { + let percent: u32 = + u32::from(stone.get_durability()) * 100 / u32::from(stone.get_max_durability()); + let middle = lines.len() / 2; + lines[middle] = Line::from(format!("{percent}%")).cyan(); + } + if let Structures::MineConstruction(site) = self.structure { + let middle: usize = lines.len() / 2; + lines[middle] = Line::from(format!("{}%", site.progress_percent())).cyan(); + } + if let Some(unit) = &self.unit + && let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front() + && let Some(percent) = task.get_movement_progress() + { + let middle = lines.len() / 2; + lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color()); + } + lines + } + + fn get_block(&self) -> Block<'_> { + Block::default() + .borders(Borders::ALL) + .style(Style::default().fg(self.fg_color())) + .title(self.display_coords()) + .border_type(BorderType::Rounded) + } } impl Widget for CellWidget { - fn render(self, area: Rect, buf: &mut Buffer) { - Paragraph::new(self.get_text_area()) - .alignment(Alignment::Center) - .block(self.get_block()) - .render(area, buf); - } + fn render(self, area: Rect, buf: &mut Buffer) { + Paragraph::new(self.get_text_area()) + .alignment(Alignment::Center) + .block(self.get_block()) + .render(area, buf); + } } diff --git a/src/app/widgets/keybindings.rs b/src/app/widgets/keybindings.rs index 90c6044..3b1b1f9 100644 --- a/src/app/widgets/keybindings.rs +++ b/src/app/widgets/keybindings.rs @@ -1,14 +1,14 @@ use crate::app::{ - helpers::block_single_title_helper, - keybindings::{Action, Group, KeyBinding, binding_for}, + helpers::block_single_title_helper, + keybindings::{Action, Group, KeyBinding, binding_for}, }; use clap::ValueEnum; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Rect}, - style::{Color, Stylize}, - text::Line, - widgets::{Block, Borders, Padding, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Rect}, + style::{Color, Stylize}, + text::Line, + widgets::{Block, Borders, Padding, Paragraph, Widget}, }; use std::collections::HashSet; @@ -18,136 +18,141 @@ use std::collections::HashSet; /// logical group of keybindings (e.g. navigation, editing, etc.). The paragraphs /// are rendered side‑by‑side inside a bordered container. pub struct KeybindingsWidget { - /// Paragraphs, one per used `Group`, containing the formatted keybinding lines. - grouped: Vec>, - widths: Vec, + /// Paragraphs, one per used `Group`, containing the formatted keybinding lines. + grouped: Vec>, + widths: Vec, } impl KeybindingsWidget { - /// Create a new widget from a list of actions. - /// - /// For each action, the corresponding static [`KeyBinding`] is looked up - /// using [`binding_for`]. Only groups that have at least one binding are - /// displayed. The resulting widget contains a paragraph for each group, - /// formatted with the key symbol (red & bold) followed by its description. - /// - /// # Parameters - /// - /// * `actions` – A slice of actions for which keybindings should be shown. - /// * `area_height` – The height of the area in which the widget will be rendered. - /// This value is used to calculate vertical padding so that the keybinding rows - /// are vertically centered when the widget's allocated height is larger than the - /// number of lines needed to display the bindings. - /// - /// # Returns - /// - /// A fully‑initialised `KeybindingsWidget` ready for rendering. - pub fn new(actions: &[Action], area_height: u16) -> Self { - Self::with_descriptions(actions, area_height, &[]) - } - - pub fn with_descriptions( - actions: &[Action], - area_height: u16, - descriptions: &[(Action, &'static str)], - ) -> Self { - let keybindings: Vec> = - actions.iter().map(|a| binding_for(*a)).collect(); - - let used_groups: HashSet = keybindings - .iter() - .filter_map(|b| b.map(|binding| binding.group)) - .collect(); - - let mut grouped_keybindings: Vec> = Vec::new(); - let mut widths: Vec = Vec::new(); - - for group in Group::value_variants().iter() { - if !used_groups.contains(group) { - continue; - } - - let grouped_lines: Vec> = keybindings - .iter() - .filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group)) - .filter_map(|b| { - b.as_ref().map(|kb| { - let description: &'static str = descriptions - .iter() - .find(|(action, _): &&(Action, &'static str)| -> bool { *action == kb.action }) - .map_or( - kb.description, - |(_, description): &(Action, &'static str)| -> &'static str { description }, - ); - Line::from_iter([kb.symbol.red().bold(), " - ".into(), description.into()]) - }) - }) - .collect(); - - let width: usize = grouped_lines.iter().map(Line::width).max().unwrap_or(0); - widths.push(u16::try_from(width).unwrap_or(u16::MAX).saturating_add(3)); - let height_diff: u16 = - area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16); - - let padding_top: u16 = if height_diff <= 1 { - height_diff - } else { - height_diff / 2 - }; - - let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0)); - - if !grouped_keybindings.is_empty() { - block = block.borders(Borders::LEFT); - } - - grouped_keybindings.push( - Paragraph::new(grouped_lines) - .alignment(Alignment::Center) - .block(block), - ); + /// Create a new widget from a list of actions. + /// + /// For each action, the corresponding static [`KeyBinding`] is looked up + /// using [`binding_for`]. Only groups that have at least one binding are + /// displayed. The resulting widget contains a paragraph for each group, + /// formatted with the key symbol (red & bold) followed by its description. + /// + /// # Parameters + /// + /// * `actions` – A slice of actions for which keybindings should be shown. + /// * `area_height` – The height of the area in which the widget will be rendered. + /// This value is used to calculate vertical padding so that the keybinding rows + /// are vertically centered when the widget's allocated height is larger than the + /// number of lines needed to display the bindings. + /// + /// # Returns + /// + /// A fully‑initialised `KeybindingsWidget` ready for rendering. + pub fn new(actions: &[Action], area_height: u16) -> Self { + Self::with_descriptions(actions, area_height, &[]) } - Self { - grouped: grouped_keybindings, - widths, + pub fn with_descriptions( + actions: &[Action], + area_height: u16, + descriptions: &[(Action, &'static str)], + ) -> Self { + let keybindings: Vec> = + actions.iter().map(|a| binding_for(*a)).collect(); + + let used_groups: HashSet = keybindings + .iter() + .filter_map(|b| b.map(|binding| binding.group)) + .collect(); + + let mut grouped_keybindings: Vec> = Vec::new(); + let mut widths: Vec = Vec::new(); + + for group in Group::value_variants().iter() { + if !used_groups.contains(group) { + continue; + } + + let grouped_lines: Vec> = keybindings + .iter() + .filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group)) + .filter_map(|b| { + b.as_ref().map(|kb| { + let description: &'static str = descriptions + .iter() + .find(|(action, _): &&(Action, &'static str)| -> bool { + *action == kb.action + }) + .map_or( + kb.description, + |(_, description): &(Action, &'static str)| -> &'static str { + description + }, + ); + Line::from_iter([kb.symbol.red().bold(), " - ".into(), description.into()]) + }) + }) + .collect(); + + let width: usize = grouped_lines.iter().map(Line::width).max().unwrap_or(0); + widths.push(u16::try_from(width).unwrap_or(u16::MAX).saturating_add(3)); + let height_diff: u16 = + area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16); + + let padding_top: u16 = if height_diff <= 1 { + height_diff + } else { + height_diff / 2 + }; + + let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0)); + + if !grouped_keybindings.is_empty() { + block = block.borders(Borders::LEFT); + } + + grouped_keybindings.push( + Paragraph::new(grouped_lines) + .alignment(Alignment::Center) + .block(block), + ); + } + + Self { + grouped: grouped_keybindings, + widths, + } } - } } impl Widget for KeybindingsWidget { - /// Render the widget inside the given rectangular area. - /// - /// The widget first draws a surrounding border with the title *Keybindings*. - /// It then divides the inner area proportionally to each group's text width—one per - /// used group—and renders each group's paragraph into its slice. - /// - /// If no groups are present, the function returns early without drawing - /// anything. - fn render(self, area: Rect, buf: &mut Buffer) { - let count: u16 = self.grouped.len() as u16; + /// Render the widget inside the given rectangular area. + /// + /// The widget first draws a surrounding border with the title *Keybindings*. + /// It then divides the inner area proportionally to each group's text width—one per + /// used group—and renders each group's paragraph into its slice. + /// + /// If no groups are present, the function returns early without drawing + /// anything. + fn render(self, area: Rect, buf: &mut Buffer) { + let count: u16 = self.grouped.len() as u16; - if count == 0 { - return; + if count == 0 { + return; + } + + let block: Block<'_> = + Block::default() + .borders(Borders::ALL) + .title(block_single_title_helper( + "Keybindings".to_string(), + Color::Magenta, + )); + + let inner: Rect = block.inner(area); + + block.render(area, buf); + + let constraints: Vec = self.widths.into_iter().map(Constraint::Fill).collect(); + + let chunks: Vec = Layout::horizontal(constraints).split(inner).to_vec(); + + for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) { + paragraph.render(chunk, buf); + } } - - let block: Block<'_> = Block::default() - .borders(Borders::ALL) - .title(block_single_title_helper( - "Keybindings".to_string(), - Color::Magenta, - )); - - let inner: Rect = block.inner(area); - - block.render(area, buf); - - let constraints: Vec = self.widths.into_iter().map(Constraint::Fill).collect(); - - let chunks: Vec = Layout::horizontal(constraints).split(inner).to_vec(); - - for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) { - paragraph.render(chunk, buf); - } - } } diff --git a/src/app/widgets/side_panel.rs b/src/app/widgets/side_panel.rs index dea1699..2c07b12 100644 --- a/src/app/widgets/side_panel.rs +++ b/src/app/widgets/side_panel.rs @@ -1,127 +1,127 @@ use crate::app::{ - helpers::block_single_title_helper, - states::skirmish_states::{ - structures::{Structure, Structures}, - units::{Unit, Units}, - }, + helpers::block_single_title_helper, + states::skirmish_states::{ + structures::{Structure, Structures}, + units::{Unit, Units}, + }, }; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Margin, Rect}, - style::Color, - text::Line, - widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Margin, Rect}, + style::Color, + text::Line, + widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget}, }; #[derive(Debug, Clone, PartialEq)] pub struct SidePanelWidget<'a> { - coords: (usize, usize), - structure: &'a Structures, - unit: &'a Option, - is_unit_marked: bool, -} - -impl<'a> SidePanelWidget<'a> { - pub fn new( coords: (usize, usize), structure: &'a Structures, unit: &'a Option, is_unit_marked: bool, - ) -> Self { - Self { - coords, - structure, - unit, - is_unit_marked, - } - } +} - fn col_to_letters(&self) -> String { - let mut col: usize = self.coords.1 + 1; - let mut letters: Vec = Vec::new(); - - while col > 0 { - letters.push((b'A' + ((col - 1) % 26) as u8) as char); - col = (col - 1) / 26; +impl<'a> SidePanelWidget<'a> { + pub fn new( + coords: (usize, usize), + structure: &'a Structures, + unit: &'a Option, + is_unit_marked: bool, + ) -> Self { + Self { + coords, + structure, + unit, + is_unit_marked, + } } - letters.iter().rev().collect() - } + fn col_to_letters(&self) -> String { + let mut col: usize = self.coords.1 + 1; + let mut letters: Vec = Vec::new(); - fn get_outer_block(&self) -> Block<'_> { - Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Double) - .title(block_single_title_helper( - format!("{}{}", self.col_to_letters(), self.coords.0), - Color::Yellow, - )) - .title_alignment(Alignment::Center) - } + while col > 0 { + letters.push((b'A' + ((col - 1) % 26) as u8) as char); + col = (col - 1) / 26; + } - fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> { - Block::default() - .borders(Borders::TOP) - .title(block_single_title_helper(title, color)) - .title_alignment(Alignment::Center) - .padding(Padding::symmetric(1, 1)) - } + letters.iter().rev().collect() + } - fn get_area_constraints(&self) -> [Constraint; 2] { - if self.unit.is_unit() { - [Constraint::Percentage(50), Constraint::Percentage(50)] - } else { - [Constraint::Percentage(100), Constraint::Percentage(0)] + fn get_outer_block(&self) -> Block<'_> { + Block::default() + .borders(Borders::ALL) + .border_type(BorderType::Double) + .title(block_single_title_helper( + format!("{}{}", self.col_to_letters(), self.coords.0), + Color::Yellow, + )) + .title_alignment(Alignment::Center) + } + + fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> { + Block::default() + .borders(Borders::TOP) + .title(block_single_title_helper(title, color)) + .title_alignment(Alignment::Center) + .padding(Padding::symmetric(1, 1)) + } + + fn get_area_constraints(&self) -> [Constraint; 2] { + if self.unit.is_unit() { + [Constraint::Percentage(50), Constraint::Percentage(50)] + } else { + [Constraint::Percentage(100), Constraint::Percentage(0)] + } } - } } impl Widget for SidePanelWidget<'_> { - fn render(self, area: Rect, buf: &mut Buffer) { - let block: Block<'_> = self.get_outer_block(); - let inner_area: Rect = block.inner(area).inner(Margin { - horizontal: 2, - vertical: 1, - }); - block.render(area, buf); + fn render(self, area: Rect, buf: &mut Buffer) { + let block: Block<'_> = self.get_outer_block(); + let inner_area: Rect = block.inner(area).inner(Margin { + horizontal: 2, + vertical: 1, + }); + block.render(area, buf); - let [structure_area, unit_area] = - Layout::vertical(self.get_area_constraints()).areas(inner_area); + let [structure_area, unit_area] = + Layout::vertical(self.get_area_constraints()).areas(inner_area); - let mut structure_text: Vec> = self.structure.text(); - if let Structures::Base(base) = self.structure - && base.queued_miners() > 0 - && self.unit.is_some() - { - structure_text.push(Line::from(base.recruitment_status(true))); + let mut structure_text: Vec> = self.structure.text(); + if let Structures::Base(base) = self.structure + && base.queued_miners() > 0 + && self.unit.is_some() + { + structure_text.push(Line::from(base.recruitment_status(true))); + } + if let Structures::Mine(mine) = self.structure { + let staffed: bool = self + .unit + .as_ref() + .is_some_and(|unit: &Units| -> bool { mine.accepts_unit(unit) }); + structure_text.push(Line::from(format!( + "Metal: {}/s{}", + mine.production_per_cycle(staffed), + if staffed { " (Miner)" } else { "" } + ))); + } + Paragraph::new(structure_text) + .alignment(Alignment::Left) + .block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan)) + .render(structure_area, buf); + + if self.unit.is_unit() { + let mut unit_name: String = self.unit.get_name().to_string(); + + if self.is_unit_marked { + unit_name = format!("> {} <", unit_name); + } + + Paragraph::new(self.unit.text()) + .alignment(Alignment::Left) + .block(self.get_inner_block(unit_name, Color::Green)) + .render(unit_area, buf); + } } - if let Structures::Mine(mine) = self.structure { - let staffed: bool = self - .unit - .as_ref() - .is_some_and(|unit: &Units| -> bool { mine.accepts_unit(unit) }); - structure_text.push(Line::from(format!( - "Metal: {}/s{}", - mine.production_per_cycle(staffed), - if staffed { " (Miner)" } else { "" } - ))); - } - Paragraph::new(structure_text) - .alignment(Alignment::Left) - .block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan)) - .render(structure_area, buf); - - if self.unit.is_unit() { - let mut unit_name: String = self.unit.get_name().to_string(); - - if self.is_unit_marked { - unit_name = format!("> {} <", unit_name); - } - - Paragraph::new(self.unit.text()) - .alignment(Alignment::Left) - .block(self.get_inner_block(unit_name, Color::Green)) - .render(unit_area, buf); - } - } } diff --git a/src/cli.rs b/src/cli.rs index eb889c0..5e3c714 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,13 @@ use crate::{ - app::{ - states::{ - PerkDecks, - skirmish_states::{GameMode, ZoomLevel}, + app::{ + states::{ + PerkDecks, + skirmish_states::{GameMode, ZoomLevel}, + }, + threads::Soundtrack, + view::View, }, - threads::Soundtrack, - view::View, - }, - logs::init_logger, + logs::init_logger, }; use clap::{Error, Parser, error::ErrorKind, value_parser}; use std::num::ParseFloatError; @@ -21,8 +21,8 @@ use std::num::ParseFloatError; #[derive(Parser, Debug, Clone)] #[command(version, about = "War in Tunnels", long_about = "War in Tunnels")] pub struct Cli { - /// The initial view/window to display. - #[arg( + /// The initial view/window to display. + #[arg( long, short, help = "Default window", @@ -30,20 +30,20 @@ pub struct Cli { default_value_t = View::MainMenu, value_enum )] - pub view: View, + pub view: View, - /// Player name shown in the UI. - #[arg( - long, - short, - help = "Username", - value_name = "String", - default_value = "Player" - )] - pub username: String, + /// Player name shown in the UI. + #[arg( + long, + short, + help = "Username", + value_name = "String", + default_value = "Player" + )] + pub username: String, - /// Game mode selection. - #[arg( + /// Game mode selection. + #[arg( long, short, help = "Game mode", @@ -51,10 +51,10 @@ pub struct Cli { default_value_t = GameMode::LastManStanding, value_enum )] - pub game_mode: GameMode, + pub game_mode: GameMode, - /// Width of the generated map (36–100 tiles). - #[arg( + /// Width of the generated map (36–100 tiles). + #[arg( long, short = 'W', help = "Map width", @@ -62,10 +62,10 @@ pub struct Cli { default_value = "42", value_parser = value_parser!(u8).range(36..=108) )] - pub map_width: u8, + pub map_width: u8, - /// Height of the generated map (11–50 tiles). - #[arg( + /// Height of the generated map (11–50 tiles). + #[arg( long, short = 'H', help = "Map height", @@ -73,10 +73,10 @@ pub struct Cli { default_value = "11", value_parser = value_parser!(u8).range(11..=39) )] - pub map_height: u8, + pub map_height: u8, - /// Zoom level used for the UI. - #[arg( + /// Zoom level used for the UI. + #[arg( long, short, help = "Zoom level", @@ -84,10 +84,10 @@ pub struct Cli { default_value_t = ZoomLevel::Default, value_enum )] - pub zoom_level: ZoomLevel, + pub zoom_level: ZoomLevel, - /// Which perk deck to use. - #[arg( + /// Which perk deck to use. + #[arg( long, short, help = "Perk Deck", @@ -95,10 +95,10 @@ pub struct Cli { default_value_t = PerkDecks::Silesian, value_enum )] - pub perk_deck: PerkDecks, + pub perk_deck: PerkDecks, - /// Starting amount of wood (1–150). - #[arg( + /// Starting amount of wood (1–150). + #[arg( long, short = 'w', help = "Starting wood", @@ -106,10 +106,10 @@ pub struct Cli { default_value = "50", value_parser = value_parser!(u16).range(1..=150) )] - pub starting_wood: u16, + pub starting_wood: u16, - /// Starting amount of metal (1–150). - #[arg( + /// Starting amount of metal (1–150). + #[arg( long, short = 'i', help = "Starting metal", @@ -118,10 +118,10 @@ pub struct Cli { default_value = "25", value_parser = value_parser!(u16).range(1..=150) )] - pub starting_metal: u16, + pub starting_metal: u16, - /// Maximum amount of units (49–149). - #[arg( + /// Maximum amount of units (49–149). + #[arg( long, short = 'L', help = "Supply limit", @@ -129,10 +129,10 @@ pub struct Cli { default_value = "99", value_parser = value_parser!(u8).range(49..=149) )] - pub supply_limit: u8, + pub supply_limit: u8, - /// Modifier applied to experience points (0.5–2.0). - #[arg( + /// Modifier applied to experience points (0.5–2.0). + #[arg( long, short, help = "XP modifier", @@ -140,10 +140,10 @@ pub struct Cli { default_value = "1.0", value_parser = parse_xp )] - pub xp_modifier: f32, + pub xp_modifier: f32, - /// Upper bound for skill points that can be earned (90–690). - #[arg( + /// Upper bound for skill points that can be earned (90–690). + #[arg( long, short = 'P', help = "Skill points limit", @@ -151,10 +151,10 @@ pub struct Cli { default_value = "120", value_parser = value_parser!(u16).range(90..=690) )] - pub skill_points_limit: u16, + pub skill_points_limit: u16, - /// Which soundtrack to play (default: default) - #[arg( + /// Which soundtrack to play (default: default) + #[arg( long, short = 'S', help = "Soundtrack", @@ -162,34 +162,34 @@ pub struct Cli { default_value_t = Soundtrack::Default, value_enum )] - pub soundtrack: Soundtrack, + pub soundtrack: Soundtrack, - /// Disable soundtrack (default: disabled). - #[arg( - long, - short = 'D', - help = "Disable soundtrack (default: disable)", - default_value_t = false - )] - pub disable_audio: bool, + /// Disable soundtrack (default: disabled). + #[arg( + long, + short = 'D', + help = "Disable soundtrack (default: disable)", + default_value_t = false + )] + pub disable_audio: bool, - /// Mute soundtrack (default: disabled). - #[arg( - long, - short = 'm', - help = "Mute soundtrack (default: disabled)", - default_value_t = false - )] - pub mute: bool, + /// Mute soundtrack (default: disabled). + #[arg( + long, + short = 'm', + help = "Mute soundtrack (default: disabled)", + default_value_t = false + )] + pub mute: bool, - /// Enable logging to a file (default: disabled). - #[arg( - long, - short = 'l', - help = "Enable logging to file (default: disabled)", - default_value_t = false - )] - pub log: bool, + /// Enable logging to a file (default: disabled). + #[arg( + long, + short = 'l', + help = "Enable logging to file (default: disabled)", + default_value_t = false + )] + pub log: bool, } /// Parses the command line arguments and returns a fully populated `Cli` @@ -197,13 +197,13 @@ pub struct Cli { /// handles argument validation and displays helpful error messages if /// the user supplies invalid input. pub fn get_args() -> Cli { - let args: Cli = Cli::parse(); + let args: Cli = Cli::parse(); - if args.log { - init_logger(); - } + if args.log { + init_logger(); + } - args + args } /// Parses a string into a floating‑point XP modifier and validates that it @@ -218,15 +218,15 @@ pub fn get_args() -> Cli { /// * Returns an error if the string cannot be parsed as `f32`. /// * Returns an error if the parsed value is outside the allowed range. fn parse_xp(s: &str) -> Result { - let v: f32 = s - .parse() - .map_err(|e: ParseFloatError| Error::raw(ErrorKind::InvalidValue, e.to_string()))?; - if (0.5..=2.0).contains(&v) { - Ok(v) - } else { - Err(Error::raw( - ErrorKind::InvalidValue, - format!("Value {} is out of range 0.5..=2.0", v), - )) - } + let v: f32 = s + .parse() + .map_err(|e: ParseFloatError| Error::raw(ErrorKind::InvalidValue, e.to_string()))?; + if (0.5..=2.0).contains(&v) { + Ok(v) + } else { + Err(Error::raw( + ErrorKind::InvalidValue, + format!("Value {} is out of range 0.5..=2.0", v), + )) + } } diff --git a/src/logs.rs b/src/logs.rs index 05f3511..842582b 100644 --- a/src/logs.rs +++ b/src/logs.rs @@ -28,10 +28,10 @@ use std::fs::File; /// } /// ``` pub fn init_logger() { - WriteLogger::init( - LevelFilter::Info, - Config::default(), - File::create("war_in_tunnels.log").expect("Failed to create log file"), - ) - .expect("Failed to initialize logger"); + WriteLogger::init( + LevelFilter::Info, + Config::default(), + File::create("war_in_tunnels.log").expect("Failed to create log file"), + ) + .expect("Failed to initialize logger"); } diff --git a/src/main.rs b/src/main.rs index c8cdcbc..389daba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,15 @@ use ratatui::{Terminal, crossterm::event::KeyEvent, prelude::CrosstermBackend}; use std::{ - io::{Result, Stdout}, - sync::mpsc::channel, - thread, + io::{Result, Stdout}, + sync::mpsc::channel, + thread, }; use war_in_tunnels::{ - app::{ - App, - threads::{AudioCmd, handle_audio, handle_ct_events, handle_tick_event}, - }, - cli::{Cli, get_args}, + app::{ + App, + threads::{AudioCmd, handle_audio, handle_ct_events, handle_tick_event}, + }, + cli::{Cli, get_args}, }; /// Starts the terminal UI application. @@ -18,25 +18,25 @@ use war_in_tunnels::{ /// It returns any `std::io::Error` encountered while initializing or restoring the /// terminal, or while running the `App`. fn main() -> Result<()> { - let args: Cli = get_args(); + let args: Cli = get_args(); - let (input_tx, input_rx) = channel::(); - let (resize_tx, resize_rx) = channel::<(u16, u16)>(); - let (tick_tx, tick_rx) = channel::<()>(); - let (audio_tx, audio_rx) = channel::(); + let (input_tx, input_rx) = channel::(); + let (resize_tx, resize_rx) = channel::<(u16, u16)>(); + let (tick_tx, tick_rx) = channel::<()>(); + let (audio_tx, audio_rx) = channel::(); - thread::spawn(move || handle_ct_events(input_tx, resize_tx)); - thread::spawn(move || handle_tick_event(tick_tx)); - if !args.disable_audio { - thread::spawn(move || handle_audio(audio_rx, args.mute, args.soundtrack)); - } + thread::spawn(move || handle_ct_events(input_tx, resize_tx)); + thread::spawn(move || handle_tick_event(tick_tx)); + if !args.disable_audio { + thread::spawn(move || handle_audio(audio_rx, args.mute, args.soundtrack)); + } - let mut terminal: Terminal> = ratatui::init(); - let mut app: App = App::new(args, audio_tx); + let mut terminal: Terminal> = ratatui::init(); + let mut app: App = App::new(args, audio_tx); - let app_result: Result<()> = app.run(&mut terminal, input_rx, resize_rx, tick_rx); + let app_result: Result<()> = app.run(&mut terminal, input_rx, resize_rx, tick_rx); - ratatui::restore(); + ratatui::restore(); - app_result + app_result } diff --git a/tests/app.rs b/tests/app.rs index 0041d50..2cde209 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -1,162 +1,161 @@ use clap::Parser; use ratatui::{ - Terminal, - backend::TestBackend, - crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, - layout::Rect, + Terminal, + backend::TestBackend, + crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}, + layout::Rect, }; use std::sync::mpsc::{Receiver, channel}; use war_in_tunnels::{ - app::{App, GameStates, View, handle_keybindings, threads::AudioCmd}, - cli::Cli, + app::{App, GameStates, View, handle_keybindings, threads::AudioCmd}, + cli::Cli, }; fn app() -> (App, Receiver) { - let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); - let (tx, rx) = channel(); - let mut app = App::new(args, tx); - app.window_area = Rect::new(0, 0, 120, 40); - app.states = Some(GameStates::new(&app.args, &app.window_area)); - (app, rx) + let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); + let (tx, rx) = channel(); + let mut app = App::new(args, tx); + app.window_area = Rect::new(0, 0, 120, 40); + app.states = Some(GameStates::new(&app.args, &app.window_area)); + (app, rx) } fn press(app: &mut App, code: KeyCode) { - handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE)); + handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE)); } #[test] fn menu_selection_stays_in_bounds_and_opens_each_view() { - let (mut app, _) = app(); - for _ in 0..10 { - press(&mut app, KeyCode::Up); - } - assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1); - for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] { - press(&mut app, KeyCode::Char(' ')); - assert_eq!(app.view, view); - press(&mut app, KeyCode::Esc); - assert_eq!(app.view, View::MainMenu); - press(&mut app, KeyCode::Down); - } - for _ in 0..10 { - press(&mut app, KeyCode::Down); - } - assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3); + let (mut app, _) = app(); + for _ in 0..10 { + press(&mut app, KeyCode::Up); + } + assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1); + for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] { + press(&mut app, KeyCode::Char(' ')); + assert_eq!(app.view, view); + press(&mut app, KeyCode::Esc); + assert_eq!(app.view, View::MainMenu); + press(&mut app, KeyCode::Down); + } + for _ in 0..10 { + press(&mut app, KeyCode::Down); + } + assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3); } #[test] fn quit_keys_work_in_every_view() { - for view in [ - View::MainMenu, - View::Skirmish, - View::PerkDecks, - View::SkillsConfig, - ] { - for key in [ - KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), + for view in [ + View::MainMenu, + View::Skirmish, + View::PerkDecks, + View::SkillsConfig, ] { - let (mut app, _) = app(); - app.view = view; - handle_keybindings(&mut app, key); - assert!(app.exit); + for key in [ + KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE), + KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), + ] { + let (mut app, _) = app(); + app.view = view; + handle_keybindings(&mut app, key); + assert!(app.exit); + } } - } } #[test] fn key_release_does_not_trigger_quit() { - let (mut app, _) = app(); - handle_keybindings( - &mut app, - KeyEvent::new_with_kind( - KeyCode::Char('q'), - KeyModifiers::NONE, - KeyEventKind::Release, - ), - ); - assert!(!app.exit); + let (mut app, _) = app(); + handle_keybindings( + &mut app, + KeyEvent::new_with_kind( + KeyCode::Char('q'), + KeyModifiers::NONE, + KeyEventKind::Release, + ), + ); + assert!(!app.exit); } #[test] fn music_keys_send_commands_without_opening_audio_device() { - let (mut app, rx) = app(); - press(&mut app, KeyCode::Char('m')); - press(&mut app, KeyCode::Char('b')); - press(&mut app, KeyCode::Char('n')); - assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute)); - assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp)); - assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown)); - drop(rx); - press(&mut app, KeyCode::Char('m')); - assert!(!app.exit); + let (mut app, rx) = app(); + press(&mut app, KeyCode::Char('m')); + press(&mut app, KeyCode::Char('b')); + press(&mut app, KeyCode::Char('n')); + assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute)); + assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp)); + assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown)); + drop(rx); + press(&mut app, KeyCode::Char('m')); + assert!(!app.exit); } #[test] fn skirmish_keys_move_focus_toggle_panel_and_limit_zoom() { - let (mut app, _) = app(); - app.view = View::Skirmish; - press(&mut app, KeyCode::Right); - assert_eq!( - app - .states_ref() - .unwrap() - .skirmish - .board - .get_focused_cell() - .get_coords(), - (5, 4) - ); - press(&mut app, KeyCode::Tab); - assert!(app.states_ref().unwrap().skirmish.side_panel); - press(&mut app, KeyCode::Tab); - assert!(!app.states_ref().unwrap().skirmish.side_panel); - for _ in 0..4 { - press(&mut app, KeyCode::Char(',')); - } - assert_eq!( - app.states_ref().unwrap().skirmish.board.zoom_level, - war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn - ); - for _ in 0..4 { - press(&mut app, KeyCode::Char('.')); - } - assert_eq!( - app.states_ref().unwrap().skirmish.board.zoom_level, - war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut - ); + let (mut app, _) = app(); + app.view = View::Skirmish; + press(&mut app, KeyCode::Right); + assert_eq!( + app.states_ref() + .unwrap() + .skirmish + .board + .get_focused_cell() + .get_coords(), + (5, 4) + ); + press(&mut app, KeyCode::Tab); + assert!(app.states_ref().unwrap().skirmish.side_panel); + press(&mut app, KeyCode::Tab); + assert!(!app.states_ref().unwrap().skirmish.side_panel); + for _ in 0..4 { + press(&mut app, KeyCode::Char(',')); + } + assert_eq!( + app.states_ref().unwrap().skirmish.board.zoom_level, + war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn + ); + for _ in 0..4 { + press(&mut app, KeyCode::Char('.')); + } + assert_eq!( + app.states_ref().unwrap().skirmish.board.zoom_level, + war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut + ); } #[test] fn all_views_render_without_real_terminal() { - let (mut app, _) = app(); - let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap(); - for view in [ - View::MainMenu, - View::Skirmish, - View::PerkDecks, - View::SkillsConfig, - ] { - app.view = view; - let before = app.states.clone(); - terminal - .draw(|frame| frame.render_widget(&mut app, frame.area())) - .unwrap(); - let buffer = terminal.backend().buffer(); - let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect(); - assert!(!text.trim().is_empty(), "{view:?}"); - if matches!(view, View::PerkDecks | View::SkillsConfig) { - assert!(text.contains("Work in progress")); + let (mut app, _) = app(); + let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap(); + for view in [ + View::MainMenu, + View::Skirmish, + View::PerkDecks, + View::SkillsConfig, + ] { + app.view = view; + let before = app.states.clone(); + terminal + .draw(|frame| frame.render_widget(&mut app, frame.area())) + .unwrap(); + let buffer = terminal.backend().buffer(); + let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect(); + assert!(!text.trim().is_empty(), "{view:?}"); + if matches!(view, View::PerkDecks | View::SkillsConfig) { + assert!(text.contains("Work in progress")); + } + assert_eq!(app.states, before); } - assert_eq!(app.states, before); - } } #[test] fn common_keys_work_before_states_are_initialized() { - let (mut app, _) = app(); - app.states = None; - press(&mut app, KeyCode::Down); - press(&mut app, KeyCode::Char('q')); - assert!(app.exit); + let (mut app, _) = app(); + app.states = None; + press(&mut app, KeyCode::Down); + press(&mut app, KeyCode::Char('q')); + assert!(app.exit); } diff --git a/tests/audio.rs b/tests/audio.rs index 0b32d0d..f70e7f0 100644 --- a/tests/audio.rs +++ b/tests/audio.rs @@ -1,65 +1,65 @@ use rodio::Decoder; use std::{ - fs::{self, read}, - io::{BufReader, Cursor}, - path::PathBuf, - process::{Command, Output}, + fs::{self, read}, + io::{BufReader, Cursor}, + path::PathBuf, + process::{Command, Output}, }; use war_in_tunnels::app::threads::{SoundrackParts, Soundtrack, load_audio}; #[test] fn audio_repeats_after_end_of_file() { - let data = read("soundtracks/default/calm.ogg").unwrap(); - let sample_count = Decoder::new(BufReader::new(Cursor::new(data))) - .unwrap() - .count(); - assert!(sample_count > 0); + let data = read("soundtracks/default/calm.ogg").unwrap(); + let sample_count = Decoder::new(BufReader::new(Cursor::new(data))) + .unwrap() + .count(); + assert!(sample_count > 0); - let mut source = load_audio(&Soundtrack::Default, &SoundrackParts::Calm); - assert_eq!(source.by_ref().take(sample_count).count(), sample_count); - assert_eq!(source.take(1024).count(), 1024); + let mut source = load_audio(&Soundtrack::Default, &SoundrackParts::Calm); + assert_eq!(source.by_ref().take(sample_count).count(), sample_count); + assert_eq!(source.take(1024).count(), 1024); } #[test] fn packaged_audio_loads_without_project_working_directory() -> () { - if std::env::var_os("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST").is_some() { - let mut source: rodio::source::Amplify< - rodio::decoder::LoopedDecoder>>>, - > = load_audio(&Soundtrack::Default, &SoundrackParts::Calm); - assert_eq!(source.by_ref().take(1024).count(), 1024); - return; - } + if std::env::var_os("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST").is_some() { + let mut source: rodio::source::Amplify< + rodio::decoder::LoopedDecoder>>>, + > = load_audio(&Soundtrack::Default, &SoundrackParts::Calm); + assert_eq!(source.by_ref().take(1024).count(), 1024); + return; + } - let directory: PathBuf = std::env::temp_dir().join(format!( - "war-in-tunnels-packaged-audio-{}", - std::process::id() - )); - fs::create_dir_all(directory.join("soundtracks/default")).unwrap(); - fs::create_dir_all(directory.join("working-directory")).unwrap(); - let executable: PathBuf = directory.join("audio-test"); - fs::copy(std::env::current_exe().unwrap(), &executable).unwrap(); - for filename in ["calm.ogg", "test.ogg"] { - fs::copy( - PathBuf::from("soundtracks/default").join(filename), - directory.join("soundtracks/default").join(filename), - ) - .unwrap(); - } - let output: Output = Command::new(executable) - .args([ - "--exact", - "packaged_audio_loads_without_project_working_directory", - "--nocapture", - ]) - .env("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST", "1") - .current_dir(directory.join("working-directory")) - .output() - .unwrap(); - fs::remove_dir_all(directory).unwrap(); - assert!( - output.status.success(), - "{}{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + let directory: PathBuf = std::env::temp_dir().join(format!( + "war-in-tunnels-packaged-audio-{}", + std::process::id() + )); + fs::create_dir_all(directory.join("soundtracks/default")).unwrap(); + fs::create_dir_all(directory.join("working-directory")).unwrap(); + let executable: PathBuf = directory.join("audio-test"); + fs::copy(std::env::current_exe().unwrap(), &executable).unwrap(); + for filename in ["calm.ogg", "test.ogg"] { + fs::copy( + PathBuf::from("soundtracks/default").join(filename), + directory.join("soundtracks/default").join(filename), + ) + .unwrap(); + } + let output: Output = Command::new(executable) + .args([ + "--exact", + "packaged_audio_loads_without_project_working_directory", + "--nocapture", + ]) + .env("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST", "1") + .current_dir(directory.join("working-directory")) + .output() + .unwrap(); + fs::remove_dir_all(directory).unwrap(); + assert!( + output.status.success(), + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); } diff --git a/tests/board.rs b/tests/board.rs index 8bc283e..190987d 100644 --- a/tests/board.rs +++ b/tests/board.rs @@ -1,155 +1,155 @@ use ratatui::layout::Rect; use war_in_tunnels::app::states::skirmish_states::{ - BoardState, FocusedCell, MoveFocusedCell, Offset, Players, ZoomLevel, - structures::{Structure, Structures}, - tasks::Task, - units::Unit, + BoardState, FocusedCell, MoveFocusedCell, Offset, Players, ZoomLevel, + structures::{Structure, Structures}, + tasks::Task, + units::Unit, }; fn board() -> BoardState { - BoardState::new(&Rect::new(0, 0, 120, 40), 42, 11, ZoomLevel::Default, false) + BoardState::new(&Rect::new(0, 0, 120, 40), 42, 11, ZoomLevel::Default, false) } #[test] fn map_has_opposing_bases_ore_and_two_miners() { - let board = board(); - assert_eq!(board.cells.len(), 11); - assert!(board.cells.iter().all(|row| row.len() == 42)); - for (col, owner, ore_col) in [(2, Players::Player, 1), (39, Players::Enemy, 40)] { - let cell = board.get_ref_cell(5, col); - assert!(matches!(cell.get_structure(), Structures::Base(_))); - assert_eq!(cell.get_structure().get_owner(), owner); - let unit = cell.get_ref_option_unit().as_ref().unwrap(); - assert_eq!(unit.get_coords(), (5, col)); - assert_eq!(unit.get_owner(), owner); - assert!(matches!( - board.get_ref_cell(5, ore_col).get_structure(), - Structures::Ore(_) - )); + let board = board(); + assert_eq!(board.cells.len(), 11); + assert!(board.cells.iter().all(|row| row.len() == 42)); + for (col, owner, ore_col) in [(2, Players::Player, 1), (39, Players::Enemy, 40)] { + let cell = board.get_ref_cell(5, col); + assert!(matches!(cell.get_structure(), Structures::Base(_))); + assert_eq!(cell.get_structure().get_owner(), owner); + let unit = cell.get_ref_option_unit().as_ref().unwrap(); + assert_eq!(unit.get_coords(), (5, col)); + assert_eq!(unit.get_owner(), owner); + assert!(matches!( + board.get_ref_cell(5, ore_col).get_structure(), + Structures::Ore(_) + )); + assert_eq!( + board.get_ref_cell(5, ore_col).get_structure().get_owner(), + Players::Unclaimed + ); + } assert_eq!( - board.get_ref_cell(5, ore_col).get_structure().get_owner(), - Players::Unclaimed + board + .cells + .iter() + .flatten() + .filter(|c| c.get_ref_option_unit().is_some()) + .count(), + 2 ); - } - assert_eq!( - board - .cells - .iter() - .flatten() - .filter(|c| c.get_ref_option_unit().is_some()) - .count(), - 2 - ); - assert!(matches!( - board.get_ref_cell(0, 0).get_structure(), - Structures::Stone(_) - )); + assert!(matches!( + board.get_ref_cell(0, 0).get_structure(), + Structures::Stone(_) + )); } #[test] fn focus_cannot_leave_map_even_with_extreme_coordinates() { - let mut focus = FocusedCell::new(0, 0, 11, 42); - assert_eq!(focus.move_focused_cell(MoveFocusedCell::Up), (0, 0)); - assert_eq!(focus.move_focused_cell(MoveFocusedCell::Left), (0, 0)); - focus.set_focused_cell((usize::MAX, usize::MAX)); - assert_eq!(focus.get_coords(), (10, 41)); - assert_eq!(focus.move_focused_cell(MoveFocusedCell::Down), (10, 41)); - assert_eq!(focus.move_focused_cell(MoveFocusedCell::Right), (10, 41)); + let mut focus = FocusedCell::new(0, 0, 11, 42); + assert_eq!(focus.move_focused_cell(MoveFocusedCell::Up), (0, 0)); + assert_eq!(focus.move_focused_cell(MoveFocusedCell::Left), (0, 0)); + focus.set_focused_cell((usize::MAX, usize::MAX)); + assert_eq!(focus.get_coords(), (10, 41)); + assert_eq!(focus.move_focused_cell(MoveFocusedCell::Down), (10, 41)); + assert_eq!(focus.move_focused_cell(MoveFocusedCell::Right), (10, 41)); } #[test] fn offsets_remain_within_bounds_when_limits_change() { - let mut offset = Offset::new(Some(100), Some(5)); - assert_eq!(offset.get_value(), 5); - offset.next(); - assert_eq!(offset.get_value(), 5); - offset.set_max(2); - assert_eq!(offset.get_value(), 2); - offset.set_value(usize::MAX); - assert_eq!(offset.get_value(), 2); - for _ in 0..4 { - offset.prev(); - } - assert_eq!(offset.get_value(), 0); + let mut offset = Offset::new(Some(100), Some(5)); + assert_eq!(offset.get_value(), 5); + offset.next(); + assert_eq!(offset.get_value(), 5); + offset.set_max(2); + assert_eq!(offset.get_value(), 2); + offset.set_value(usize::MAX); + assert_eq!(offset.get_value(), 2); + for _ in 0..4 { + offset.prev(); + } + assert_eq!(offset.get_value(), 0); } #[test] fn confirming_path_queues_task_without_including_start_cell() { - let mut board = board(); - board.change_focused_cell(MoveFocusedCell::Left); - board.toggle_marking(); - board.change_focused_cell(MoveFocusedCell::Right); - board.change_focused_cell(MoveFocusedCell::Down); - board.toggle_marking(); - let unit = board - .get_ref_cell(5, 2) - .get_ref_option_unit() - .as_ref() - .unwrap(); - let tasks = unit.get_ref_tasks(); - assert_eq!(tasks.len(), 1); - assert_eq!( - tasks[0].get_path().iter().copied().collect::>(), - [(5, 3), (6, 3)] - ); - assert!(!board.marked_cells.marking_cells); - assert!(board.get_marked_cells().is_empty()); - assert!(board.cells.iter().flatten().all(|c| !c.get_marked())); + let mut board = board(); + board.change_focused_cell(MoveFocusedCell::Left); + board.toggle_marking(); + board.change_focused_cell(MoveFocusedCell::Right); + board.change_focused_cell(MoveFocusedCell::Down); + board.toggle_marking(); + let unit = board + .get_ref_cell(5, 2) + .get_ref_option_unit() + .as_ref() + .unwrap(); + let tasks = unit.get_ref_tasks(); + assert_eq!(tasks.len(), 1); + assert_eq!( + tasks[0].get_path().iter().copied().collect::>(), + [(5, 3), (6, 3)] + ); + assert!(!board.marked_cells.marking_cells); + assert!(board.get_marked_cells().is_empty()); + assert!(board.cells.iter().flatten().all(|c| !c.get_marked())); } #[test] fn cancelling_path_clears_marks_without_queuing_task() { - let mut board = board(); - board.change_focused_cell(MoveFocusedCell::Left); - board.toggle_marking(); - board.change_focused_cell(MoveFocusedCell::Right); - board.marked_cells.marking_cells = false; - board.end_marking_cells(true); - assert!( - board - .get_ref_cell(5, 2) - .get_ref_option_unit() - .as_ref() - .unwrap() - .get_ref_tasks() - .is_empty() - ); - assert!(board.get_marked_cells().is_empty()); - assert!(board.marked_cells.selected_unit.is_none()); - assert!(board.cells.iter().flatten().all(|c| !c.get_marked())); + let mut board = board(); + board.change_focused_cell(MoveFocusedCell::Left); + board.toggle_marking(); + board.change_focused_cell(MoveFocusedCell::Right); + board.marked_cells.marking_cells = false; + board.end_marking_cells(true); + assert!( + board + .get_ref_cell(5, 2) + .get_ref_option_unit() + .as_ref() + .unwrap() + .get_ref_tasks() + .is_empty() + ); + assert!(board.get_marked_cells().is_empty()); + assert!(board.marked_cells.selected_unit.is_none()); + assert!(board.cells.iter().flatten().all(|c| !c.get_marked())); } #[test] fn backtracking_revisited_cell_preserves_earlier_mark() { - let mut board = board(); - board.change_focused_cell(MoveFocusedCell::Left); - board.toggle_marking(); - board.change_focused_cell(MoveFocusedCell::Right); - board.change_focused_cell(MoveFocusedCell::Left); - board.pop_marked_cell(); - assert_eq!(board.get_focused_cell().get_coords(), (5, 3)); - assert!(board.get_ref_cell(5, 2).get_marked()); - board.pop_marked_cell(); - board.pop_marked_cell(); - assert_eq!(board.get_focused_cell().get_coords(), (5, 2)); - assert_eq!(board.get_marked_cells().len(), 1); - assert!(!board.get_ref_cell(5, 3).get_marked()); + let mut board = board(); + board.change_focused_cell(MoveFocusedCell::Left); + board.toggle_marking(); + board.change_focused_cell(MoveFocusedCell::Right); + board.change_focused_cell(MoveFocusedCell::Left); + board.pop_marked_cell(); + assert_eq!(board.get_focused_cell().get_coords(), (5, 3)); + assert!(board.get_ref_cell(5, 2).get_marked()); + board.pop_marked_cell(); + board.pop_marked_cell(); + assert_eq!(board.get_focused_cell().get_coords(), (5, 2)); + assert_eq!(board.get_marked_cells().len(), 1); + assert!(!board.get_ref_cell(5, 3).get_marked()); } #[test] fn large_viewport_and_zoom_keep_camera_inside_map() { - for zoom in [ - ZoomLevel::ZoomedIn, - ZoomLevel::Default, - ZoomLevel::ZoomedOut, - ] { - let mut board = BoardState::new(&Rect::new(0, 0, 240, 120), 36, 11, zoom, false); - assert_eq!(board.vertical_offset.get_value(), 0); - board.horizontal_offset.set_value(usize::MAX); - board.change_resize(&Rect::new(0, 0, 500, 120), false); - assert_eq!(board.horizontal_offset.get_value(), 0); - board.change_zoom(ZoomLevel::ZoomedOut); - assert!(board.is_focused_cell_visible()); - assert_eq!(board.cells.len(), 11); - } + for zoom in [ + ZoomLevel::ZoomedIn, + ZoomLevel::Default, + ZoomLevel::ZoomedOut, + ] { + let mut board = BoardState::new(&Rect::new(0, 0, 240, 120), 36, 11, zoom, false); + assert_eq!(board.vertical_offset.get_value(), 0); + board.horizontal_offset.set_value(usize::MAX); + board.change_resize(&Rect::new(0, 0, 500, 120), false); + assert_eq!(board.horizontal_offset.get_value(), 0); + board.change_zoom(ZoomLevel::ZoomedOut); + assert!(board.is_focused_cell_visible()); + assert_eq!(board.cells.len(), 11); + } } diff --git a/tests/cli.rs b/tests/cli.rs index 71f08fa..bc420f6 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -4,94 +4,94 @@ use war_in_tunnels::{app::View, cli::Cli}; #[test] fn defaults_start_in_menu_without_logging() { - let args = Cli::try_parse_from(["game"]).unwrap(); - assert_eq!(args.view, View::MainMenu); - assert_eq!((args.map_width, args.map_height), (42, 11)); - assert_eq!(args.username, "Player"); - assert!(!args.log); - assert!(!args.disable_audio); - assert!(!args.mute); + let args = Cli::try_parse_from(["game"]).unwrap(); + assert_eq!(args.view, View::MainMenu); + assert_eq!((args.map_width, args.map_height), (42, 11)); + assert_eq!(args.username, "Player"); + assert!(!args.log); + assert!(!args.disable_audio); + assert!(!args.mute); } #[test] fn numeric_options_accept_boundaries_and_reject_out_of_range_values() { - for (flag, min, max) in [ - ("--map-width", 36, 108), - ("--map-height", 11, 39), - ("--starting-wood", 1, 150), - ("--starting-metal", 1, 150), - ("--supply-limit", 49, 149), - ("--skill-points-limit", 90, 690), - ] { - for value in [min, max] { - assert!( - Cli::try_parse_from(["game", flag, &value.to_string()]).is_ok(), - "{flag}={value}" - ); + for (flag, min, max) in [ + ("--map-width", 36, 108), + ("--map-height", 11, 39), + ("--starting-wood", 1, 150), + ("--starting-metal", 1, 150), + ("--supply-limit", 49, 149), + ("--skill-points-limit", 90, 690), + ] { + for value in [min, max] { + assert!( + Cli::try_parse_from(["game", flag, &value.to_string()]).is_ok(), + "{flag}={value}" + ); + } + for value in [min - 1, max + 1] { + assert!( + Cli::try_parse_from(["game", flag, &value.to_string()]).is_err(), + "{flag}={value}" + ); + } } - for value in [min - 1, max + 1] { - assert!( - Cli::try_parse_from(["game", flag, &value.to_string()]).is_err(), - "{flag}={value}" - ); - } - } } #[test] fn xp_modifier_rejects_non_finite_malformed_and_out_of_range_values() { - for value in ["0.5", "1", "2.0"] { - let args = Cli::try_parse_from(["game", "--xp-modifier", value]).unwrap(); - assert_eq!(args.xp_modifier, value.parse::().unwrap()); - } - for value in ["0.49", "2.01", "NaN", "inf", "text"] { - assert!( - Cli::try_parse_from(["game", "--xp-modifier", value]).is_err(), - "{value}" - ); - } + for value in ["0.5", "1", "2.0"] { + let args = Cli::try_parse_from(["game", "--xp-modifier", value]).unwrap(); + assert_eq!(args.xp_modifier, value.parse::().unwrap()); + } + for value in ["0.49", "2.01", "NaN", "inf", "text"] { + assert!( + Cli::try_parse_from(["game", "--xp-modifier", value]).is_err(), + "{value}" + ); + } } #[test] fn enum_options_and_audio_flags_are_parsed() { - let args = - Cli::try_parse_from(["game", "--view", "skirmish", "--disable-audio", "--mute"]).unwrap(); - assert_eq!(args.view, View::Skirmish); - assert!(args.disable_audio && args.mute); - for flag in [ - "--view", - "--game-mode", - "--zoom-level", - "--perk-deck", - "--soundtrack", - ] { - assert_eq!( - Cli::try_parse_from(["game", flag, "unknown"]) - .unwrap_err() - .kind(), - ErrorKind::InvalidValue - ); - } + let args = + Cli::try_parse_from(["game", "--view", "skirmish", "--disable-audio", "--mute"]).unwrap(); + assert_eq!(args.view, View::Skirmish); + assert!(args.disable_audio && args.mute); + for flag in [ + "--view", + "--game-mode", + "--zoom-level", + "--perk-deck", + "--soundtrack", + ] { + assert_eq!( + Cli::try_parse_from(["game", flag, "unknown"]) + .unwrap_err() + .kind(), + ErrorKind::InvalidValue + ); + } } #[test] fn binary_help_and_version_work_without_terminal_or_audio() { - for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] { + for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] { + let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels")) + .arg(flag) + .output() + .unwrap(); + assert!(output.status.success()); + assert!(String::from_utf8(output.stdout).unwrap().contains(expected)); + } let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels")) - .arg(flag) - .output() - .unwrap(); - assert!(output.status.success()); - assert!(String::from_utf8(output.stdout).unwrap().contains(expected)); - } - let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels")) - .args(["--map-width", "0"]) - .output() - .unwrap(); - assert!(!output.status.success()); - assert!( - String::from_utf8(output.stderr) - .unwrap() - .contains("--map-width") - ); + .args(["--map-width", "0"]) + .output() + .unwrap(); + assert!(!output.status.success()); + assert!( + String::from_utf8(output.stderr) + .unwrap() + .contains("--map-width") + ); } diff --git a/tests/construction.rs b/tests/construction.rs index c6e9dba..f5a5e90 100644 --- a/tests/construction.rs +++ b/tests/construction.rs @@ -1,420 +1,423 @@ use clap::Parser; use ratatui::{ - Terminal, - backend::TestBackend, - buffer::Cell, - crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, - layout::Rect, + Terminal, + backend::TestBackend, + buffer::Cell, + crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, + layout::Rect, }; use std::sync::mpsc::{Receiver, Sender, channel}; use war_in_tunnels::{ - app::{ - Action, App, GameStates, View, available_skirmish_actions, handle_keybindings, - states::{ - SkirmishState, - skirmish_states::{ - BoardState, MoveFocusedCell, Players, Resources, ZoomLevel, - resources::MINE_PRODUCTION_TICKS, - structures::{ - BaseBuilding, BuildingTerrain, Mine, MineConstruction, Ore, Structure, Structures, Tunnel, + app::{ + Action, App, GameStates, View, available_skirmish_actions, handle_keybindings, + states::{ + SkirmishState, + skirmish_states::{ + BoardState, MoveFocusedCell, Players, Resources, ZoomLevel, + resources::MINE_PRODUCTION_TICKS, + structures::{ + BaseBuilding, BuildingTerrain, Mine, MineConstruction, Ore, Structure, + Structures, Tunnel, + }, + tasks::Tasks, + units::{MinerUnit, Unit, Units}, + }, }, - tasks::Tasks, - units::{MinerUnit, Unit, Units}, - }, + threads::{AudioCmd, TICK_MS}, }, - threads::{AudioCmd, TICK_MS}, - }, - cli::Cli, + cli::Cli, }; fn board() -> BoardState { - BoardState::new(&Rect::new(0, 0, 160, 50), 42, 11, ZoomLevel::Default, false) + BoardState::new(&Rect::new(0, 0, 160, 50), 42, 11, ZoomLevel::Default, false) } fn complete_mine(board: &mut BoardState, coords: (usize, usize)) -> () { - assert!(board.start_mine_construction(coords, Players::Player)); - for _ in 0..MineConstruction::BUILD_TICKS { - board.advance_construction(); - } - assert!(matches!( - board.cells[coords.0][coords.1].get_structure(), - Structures::Mine(_) - )); + assert!(board.start_mine_construction(coords, Players::Player)); + for _ in 0..MineConstruction::BUILD_TICKS { + board.advance_construction(); + } + assert!(matches!( + board.cells[coords.0][coords.1].get_structure(), + Structures::Mine(_) + )); } fn app() -> App { - let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); - let (tx, _rx): (Sender, Receiver) = channel(); - let mut app: App = App::new(args, tx); - app.view = View::Skirmish; - app.window_area = Rect::new(0, 0, 160, 50); - app.states = Some(GameStates::new(&app.args, &app.window_area)); - app + let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); + let (tx, _rx): (Sender, Receiver) = channel(); + let mut app: App = App::new(args, tx); + app.view = View::Skirmish; + app.window_area = Rect::new(0, 0, 160, 50); + app.states = Some(GameStates::new(&app.args, &app.window_area)); + app } fn travel(board: &mut BoardState, direction: MoveFocusedCell) -> () { - board.toggle_marking(); - board.change_focused_cell(direction); - board.toggle_marking(); - for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) { - board.advance_turn(); - } + board.toggle_marking(); + board.change_focused_cell(direction); + board.toggle_marking(); + for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) { + board.advance_turn(); + } } #[test] fn miner_has_builder_permission_through_all_unit_wrappers() -> () { - let miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2)); - assert!(miner.get_can_build()); - let unit: Units = Units::Miner(miner); - assert!(unit.get_can_build()); - assert!(Some(unit).get_can_build()); - let no_unit: Option = None; - assert!(!no_unit.get_can_build()); + let miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2)); + assert!(miner.get_can_build()); + let unit: Units = Units::Miner(miner); + assert!(unit.get_can_build()); + assert!(Some(unit).get_can_build()); + let no_unit: Option = None; + assert!(!no_unit.get_can_build()); } #[test] fn mine_requires_a_cardinal_friendly_builder_and_preserves_target_on_failure() -> () { - let mut board: BoardState = board(); - let original: Structures = board.cells[5][1].get_structure(); - board.cells[5][2].get_mut_option_unit().take(); - assert!(!board.start_mine_construction((5, 1), Players::Player)); - *board.cells[5][2].get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)))); - assert!(!board.start_mine_construction((5, 1), Players::Player)); - board.cells[5][2].get_mut_option_unit().take(); - *board.cells[4][2].get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(Players::Player, (4, 2)))); - assert!(!board.start_mine_construction((5, 1), Players::Player)); - assert_eq!(board.cells[5][1].get_structure(), original); - assert!(!board.start_mine_construction((usize::MAX, 1), Players::Player)); - assert!(!board.start_mine_construction((5, 1), Players::Unclaimed)); + let mut board: BoardState = board(); + let original: Structures = board.cells[5][1].get_structure(); + board.cells[5][2].get_mut_option_unit().take(); + assert!(!board.start_mine_construction((5, 1), Players::Player)); + *board.cells[5][2].get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)))); + assert!(!board.start_mine_construction((5, 1), Players::Player)); + board.cells[5][2].get_mut_option_unit().take(); + *board.cells[4][2].get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(Players::Player, (4, 2)))); + assert!(!board.start_mine_construction((5, 1), Players::Player)); + assert_eq!(board.cells[5][1].get_structure(), original); + assert!(!board.start_mine_construction((usize::MAX, 1), Players::Player)); + assert!(!board.start_mine_construction((5, 1), Players::Unclaimed)); } #[test] fn ore_exception_does_not_require_a_base_or_tunnel_connection() -> () { - let mut board: BoardState = board(); - board.cells[0][0].set_structure(Structures::Ore(Ore::new(Players::Unclaimed, 1))); - *board.cells[0][1].get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(Players::Player, (0, 1)))); - assert!(board.can_build_at((0, 0), Players::Player, BuildingTerrain::Ore)); - complete_mine(&mut board, (0, 0)); - assert_eq!( - board.cells[0][0].get_structure().get_owner(), - Players::Player - ); - assert!(!board.start_mine_construction((0, 0), Players::Player)); + let mut board: BoardState = board(); + board.cells[0][0].set_structure(Structures::Ore(Ore::new(Players::Unclaimed, 1))); + *board.cells[0][1].get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(Players::Player, (0, 1)))); + assert!(board.can_build_at((0, 0), Players::Player, BuildingTerrain::Ore)); + complete_mine(&mut board, (0, 0)); + assert_eq!( + board.cells[0][0].get_structure().get_owner(), + Players::Player + ); + assert!(!board.start_mine_construction((0, 0), Players::Player)); } #[test] fn ordinary_building_sites_require_stone_connected_to_base_or_tunnel() -> () { - let mut board: BoardState = board(); - assert!(board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone)); - assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Ore)); - *board.cells[5][5].get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(Players::Player, (5, 5)))); - assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone)); - board.cells[5][5].set_structure(Structures::Tunnel(Tunnel::new())); - assert!(board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone)); - board.cells[5][5].set_structure(Structures::Base(BaseBuilding::new(Players::Enemy))); - assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone)); - board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new())); - assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone)); + let mut board: BoardState = board(); + assert!(board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone)); + assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Ore)); + *board.cells[5][5].get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(Players::Player, (5, 5)))); + assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone)); + board.cells[5][5].set_structure(Structures::Tunnel(Tunnel::new())); + assert!(board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone)); + board.cells[5][5].set_structure(Structures::Base(BaseBuilding::new(Players::Enemy))); + assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone)); + board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new())); + assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone)); } #[test] fn occupied_depleted_or_claimed_deposit_cannot_be_built_on() -> () { - let mut board: BoardState = board(); - *board.cells[5][1].get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(Players::Player, (5, 1)))); - assert!(!board.start_mine_construction((5, 1), Players::Player)); - board.cells[5][1].get_mut_option_unit().take(); - let mut ore: Ore = Ore::new(Players::Unclaimed, 1); - ore.extract(5000); - board.cells[5][1].set_structure(Structures::Ore(ore)); - assert!(!board.start_mine_construction((5, 1), Players::Player)); - board.cells[5][1].set_structure(Structures::Ore(Ore::new(Players::Enemy, 1))); - assert!(!board.start_mine_construction((5, 1), Players::Player)); + let mut board: BoardState = board(); + *board.cells[5][1].get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(Players::Player, (5, 1)))); + assert!(!board.start_mine_construction((5, 1), Players::Player)); + board.cells[5][1].get_mut_option_unit().take(); + let mut ore: Ore = Ore::new(Players::Unclaimed, 1); + ore.extract(5000); + board.cells[5][1].set_structure(Structures::Ore(ore)); + assert!(!board.start_mine_construction((5, 1), Players::Player)); + board.cells[5][1].set_structure(Structures::Ore(Ore::new(Players::Enemy, 1))); + assert!(!board.start_mine_construction((5, 1), Players::Player)); } #[test] fn construction_view_starts_paid_build_on_enter_and_stays_open() -> () { - let mut app: App = app(); - for _ in 0..2 { - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)); - } - handle_keybindings( - &mut app, - KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), - ); - assert!(available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)); - let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 50)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Ready to build: Enter")); - assert!(text.contains("Enter - Build Mine")); - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - let state: &SkirmishState = &app.states_ref().unwrap().skirmish; - assert!(state.building_view); - assert!(matches!( - state.board.cells[5][1].get_structure(), - Structures::MineConstruction(_) - )); - assert_eq!(state.player_resources.wood, 30); - assert_eq!(state.player_resources.metal, 15); + let mut app: App = app(); + for _ in 0..2 { + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)); + } + handle_keybindings( + &mut app, + KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), + ); + assert!( + available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) + ); + let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 50)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Ready to build: Enter")); + assert!(text.contains("Enter - Build Mine")); + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + let state: &SkirmishState = &app.states_ref().unwrap().skirmish; + assert!(state.building_view); + assert!(matches!( + state.board.cells[5][1].get_structure(), + Structures::MineConstruction(_) + )); + assert_eq!(state.player_resources.wood, 30); + assert_eq!(state.player_resources.metal, 15); } #[test] fn miner_can_travel_into_and_out_of_friendly_mine_with_moving_tasks() -> () { - let mut board: BoardState = board(); - complete_mine(&mut board, (5, 1)); - board.change_focused_cell(MoveFocusedCell::Left); - board.toggle_marking(); - board.change_focused_cell(MoveFocusedCell::Left); - board.toggle_marking(); - let unit: &Units = board.cells[5][2].get_ref_option_unit().as_ref().unwrap(); - assert!(matches!( - unit.get_ref_tasks().front(), - Some(Tasks::Moving(_)) - )); - for _ in 1..3000_u16.div_ceil(u16::from(TICK_MS)) { + let mut board: BoardState = board(); + complete_mine(&mut board, (5, 1)); + board.change_focused_cell(MoveFocusedCell::Left); + board.toggle_marking(); + board.change_focused_cell(MoveFocusedCell::Left); + board.toggle_marking(); + let unit: &Units = board.cells[5][2].get_ref_option_unit().as_ref().unwrap(); + assert!(matches!( + unit.get_ref_tasks().front(), + Some(Tasks::Moving(_)) + )); + for _ in 1..3000_u16.div_ceil(u16::from(TICK_MS)) { + board.advance_turn(); + } + assert!(board.cells[5][1].get_ref_option_unit().is_none()); board.advance_turn(); - } - assert!(board.cells[5][1].get_ref_option_unit().is_none()); - board.advance_turn(); - assert_eq!(board.cells[5][1].get_ref_option_unit().get_coords(), (5, 1)); - assert!(board.cells[5][2].get_ref_option_unit().is_none()); - travel(&mut board, MoveFocusedCell::Right); - assert!(board.cells[5][1].get_ref_option_unit().is_none()); - assert_eq!(board.cells[5][2].get_ref_option_unit().get_coords(), (5, 2)); + assert_eq!(board.cells[5][1].get_ref_option_unit().get_coords(), (5, 1)); + assert!(board.cells[5][2].get_ref_option_unit().is_none()); + travel(&mut board, MoveFocusedCell::Right); + assert!(board.cells[5][1].get_ref_option_unit().is_none()); + assert_eq!(board.cells[5][2].get_ref_option_unit().get_coords(), (5, 2)); } #[test] fn staffed_mine_doubles_production_and_bonus_stops_after_departure() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - complete_mine(&mut game.board, (5, 1)); - game.turn_counter = MINE_PRODUCTION_TICKS - 1; - game.tick_update(); - assert_eq!(game.player_resources.metal, 26); - game.board.change_focused_cell(MoveFocusedCell::Left); - travel(&mut game.board, MoveFocusedCell::Left); - game.turn_counter = MINE_PRODUCTION_TICKS - 1; - game.tick_update(); - assert_eq!(game.player_resources.metal, 28); - travel(&mut game.board, MoveFocusedCell::Right); - game.turn_counter = MINE_PRODUCTION_TICKS - 1; - game.tick_update(); - assert_eq!(game.player_resources.metal, 29); - let Structures::Mine(mine) = game.board.cells[5][1].get_structure() else { - panic!("mine missing"); - }; - assert_eq!(mine.get_ore_amount(), 4996); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + complete_mine(&mut game.board, (5, 1)); + game.turn_counter = MINE_PRODUCTION_TICKS - 1; + game.tick_update(); + assert_eq!(game.player_resources.metal, 26); + game.board.change_focused_cell(MoveFocusedCell::Left); + travel(&mut game.board, MoveFocusedCell::Left); + game.turn_counter = MINE_PRODUCTION_TICKS - 1; + game.tick_update(); + assert_eq!(game.player_resources.metal, 28); + travel(&mut game.board, MoveFocusedCell::Right); + game.turn_counter = MINE_PRODUCTION_TICKS - 1; + game.tick_update(); + assert_eq!(game.player_resources.metal, 29); + let Structures::Mine(mine) = game.board.cells[5][1].get_structure() else { + panic!("mine missing"); + }; + assert_eq!(mine.get_ore_amount(), 4996); } #[test] fn mine_rejects_enemy_miners_and_staffing_does_not_overdraw_ore() -> () { - let mut ore: Ore = Ore::new(Players::Unclaimed, 1); - ore.extract(4999); - let mut mine: Mine = Mine::new(Players::Player, ore).unwrap(); - let player: Units = Units::Miner(MinerUnit::new(Players::Player, (5, 2))); - let enemy: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))); - assert!(mine.accepts_unit(&player)); - assert!(!mine.accepts_unit(&enemy)); - assert!(!Structures::Mine(mine).can_enter(&enemy)); - assert_eq!(mine.produce_with_miner(true), 1); - assert_eq!(mine.produce_with_miner(true), 0); + let mut ore: Ore = Ore::new(Players::Unclaimed, 1); + ore.extract(4999); + let mut mine: Mine = Mine::new(Players::Player, ore).unwrap(); + let player: Units = Units::Miner(MinerUnit::new(Players::Player, (5, 2))); + let enemy: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))); + assert!(mine.accepts_unit(&player)); + assert!(!mine.accepts_unit(&enemy)); + assert!(!Structures::Mine(mine).can_enter(&enemy)); + assert_eq!(mine.produce_with_miner(true), 1); + assert_eq!(mine.produce_with_miner(true), 0); } #[test] fn remote_ore_cannot_start_construction_or_consume_resources() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - let ore: Structures = Structures::Ore(Ore::new(Players::Unclaimed, 1)); - game.board.cells[5][21].set_structure(ore); - let before: Resources = game.player_resources; - assert!(!game.start_mine_construction((5, 21), Players::Player)); - assert_eq!(game.player_resources, before); - assert_eq!(game.board.cells[5][21].get_structure(), ore); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + let ore: Structures = Structures::Ore(Ore::new(Players::Unclaimed, 1)); + game.board.cells[5][21].set_structure(ore); + let before: Resources = game.player_resources; + assert!(!game.start_mine_construction((5, 21), Players::Player)); + assert_eq!(game.player_resources, before); + assert_eq!(game.board.cells[5][21].get_structure(), ore); } #[test] fn construction_payment_is_atomic_and_exact_funds_are_enough() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - for resources in [ - Resources { - wood: 19, - metal: 10, - }, - Resources { wood: 20, metal: 9 }, - ] { - game.player_resources = resources; - assert!(!game.can_start_mine_construction((5, 1), Players::Player)); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + for resources in [ + Resources { + wood: 19, + metal: 10, + }, + Resources { wood: 20, metal: 9 }, + ] { + game.player_resources = resources; + assert!(!game.can_start_mine_construction((5, 1), Players::Player)); + assert!(!game.start_mine_construction((5, 1), Players::Player)); + assert_eq!(game.player_resources, resources); + assert!(matches!( + game.board.cells[5][1].get_structure(), + Structures::Ore(_) + )); + } + game.player_resources = Mine::COST; + assert!(game.start_mine_construction((5, 1), Players::Player)); + assert_eq!(game.player_resources, Resources { wood: 0, metal: 0 }); assert!(!game.start_mine_construction((5, 1), Players::Player)); - assert_eq!(game.player_resources, resources); - assert!(matches!( - game.board.cells[5][1].get_structure(), - Structures::Ore(_) - )); - } - game.player_resources = Mine::COST; - assert!(game.start_mine_construction((5, 1), Players::Player)); - assert_eq!(game.player_resources, Resources { wood: 0, metal: 0 }); - assert!(!game.start_mine_construction((5, 1), Players::Player)); } #[test] fn construction_takes_ten_seconds_and_cannot_produce_or_accept_miners_early() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - assert!(game.start_mine_construction((5, 1), Players::Player)); - for _ in 1..MineConstruction::BUILD_TICKS { + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + assert!(game.start_mine_construction((5, 1), Players::Player)); + for _ in 1..MineConstruction::BUILD_TICKS { + game.tick_update(); + } + let structure: Structures = game.board.cells[5][1].get_structure(); + let Structures::MineConstruction(site) = structure else { + panic!("completed too early"); + }; + assert_eq!(site.progress_percent(), 99); + assert!(site.into_mine().is_none()); + assert_eq!(site.cancel().get_ore_amount(), 5000); + assert!( + !structure.can_enter( + game.board.cells[5][2] + .get_ref_option_unit() + .as_ref() + .unwrap() + ) + ); + assert_eq!(game.player_resources.metal, 15); game.tick_update(); - } - let structure: Structures = game.board.cells[5][1].get_structure(); - let Structures::MineConstruction(site) = structure else { - panic!("completed too early"); - }; - assert_eq!(site.progress_percent(), 99); - assert!(site.into_mine().is_none()); - assert_eq!(site.cancel().get_ore_amount(), 5000); - assert!( - !structure.can_enter( - game.board.cells[5][2] - .get_ref_option_unit() - .as_ref() - .unwrap() - ) - ); - assert_eq!(game.player_resources.metal, 15); - game.tick_update(); - assert!(matches!( - game.board.cells[5][1].get_structure(), - Structures::Mine(_) - )); - assert_eq!(game.player_resources.metal, 15); + assert!(matches!( + game.board.cells[5][1].get_structure(), + Structures::Mine(_) + )); + assert_eq!(game.player_resources.metal, 15); } #[test] fn construction_pauses_without_nearby_builder_and_resumes_after_return() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - assert!(game.start_mine_construction((5, 1), Players::Player)); - for _ in 0..100 { - game.tick_update(); - } - let before: Structures = game.board.cells[5][1].get_structure(); - let builder: Option = game.board.cells[5][2].get_mut_option_unit().take(); - for _ in 0..MineConstruction::BUILD_TICKS { - game.tick_update(); - } - assert_eq!(game.board.cells[5][1].get_structure(), before); - assert_eq!(game.player_resources.metal, 15); - *game.board.cells[5][2].get_mut_option_unit() = builder; - for _ in 100..MineConstruction::BUILD_TICKS { - game.tick_update(); - } - assert!(matches!( - game.board.cells[5][1].get_structure(), - Structures::Mine(_) - )); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + assert!(game.start_mine_construction((5, 1), Players::Player)); + for _ in 0..100 { + game.tick_update(); + } + let before: Structures = game.board.cells[5][1].get_structure(); + let builder: Option = game.board.cells[5][2].get_mut_option_unit().take(); + for _ in 0..MineConstruction::BUILD_TICKS { + game.tick_update(); + } + assert_eq!(game.board.cells[5][1].get_structure(), before); + assert_eq!(game.player_resources.metal, 15); + *game.board.cells[5][2].get_mut_option_unit() = builder; + for _ in 100..MineConstruction::BUILD_TICKS { + game.tick_update(); + } + assert!(matches!( + game.board.cells[5][1].get_structure(), + Structures::Mine(_) + )); } #[test] fn cancellation_restores_original_ore_and_refunds_cost_exactly_once() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - let mut ore: Ore = Ore::new(Players::Unclaimed, 1); - ore.extract(123); - game.board.cells[5][1].set_structure(Structures::Ore(ore)); - let before: Resources = game.player_resources; - assert!(game.start_mine_construction((5, 1), Players::Player)); - for _ in 0..100 { - game.tick_update(); - } - game.board.cells[5][2].get_mut_option_unit().take(); - assert!(game.cancel_mine_construction((5, 1), Players::Player)); - assert_eq!(game.player_resources, before); - assert_eq!(game.board.cells[5][1].get_structure(), Structures::Ore(ore)); - assert!(!game.cancel_mine_construction((5, 1), Players::Player)); - assert_eq!(game.player_resources, before); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + let mut ore: Ore = Ore::new(Players::Unclaimed, 1); + ore.extract(123); + game.board.cells[5][1].set_structure(Structures::Ore(ore)); + let before: Resources = game.player_resources; + assert!(game.start_mine_construction((5, 1), Players::Player)); + for _ in 0..100 { + game.tick_update(); + } + game.board.cells[5][2].get_mut_option_unit().take(); + assert!(game.cancel_mine_construction((5, 1), Players::Player)); + assert_eq!(game.player_resources, before); + assert_eq!(game.board.cells[5][1].get_structure(), Structures::Ore(ore)); + assert!(!game.cancel_mine_construction((5, 1), Players::Player)); + assert_eq!(game.player_resources, before); } #[test] fn panel_shows_progress_and_delete_cancels_while_escape_only_closes_panel() -> () { - let mut app: App = app(); - for _ in 0..2 { - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)); - } - handle_keybindings( - &mut app, - KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), - ); - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - assert!( - available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Delete) - ); - assert!( - !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) - ); - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - assert_eq!(app.states_ref().unwrap().skirmish.player_resources.wood, 30); - let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 50)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Cost: 20 Wood + 10 Metal")); - assert!(text.contains("Construction: 0%")); - assert!(text.contains("Delete - Cancel construction")); - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); - assert!(matches!( - app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(), - Structures::MineConstruction(_) - )); - handle_keybindings( - &mut app, - KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), - ); - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE)); - assert!(matches!( - app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(), - Structures::Ore(_) - )); - assert_eq!( - app.states_ref().unwrap().skirmish.player_resources, - Resources::new(50, 25) - ); + let mut app: App = app(); + for _ in 0..2 { + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE)); + } + handle_keybindings( + &mut app, + KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), + ); + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert!( + available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Delete) + ); + assert!( + !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) + ); + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + assert_eq!(app.states_ref().unwrap().skirmish.player_resources.wood, 30); + let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 50)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Cost: 20 Wood + 10 Metal")); + assert!(text.contains("Construction: 0%")); + assert!(text.contains("Delete - Cancel construction")); + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + assert!(matches!( + app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(), + Structures::MineConstruction(_) + )); + handle_keybindings( + &mut app, + KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL), + ); + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE)); + assert!(matches!( + app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(), + Structures::Ore(_) + )); + assert_eq!( + app.states_ref().unwrap().skirmish.player_resources, + Resources::new(50, 25) + ); } #[test] fn player_cannot_cancel_enemy_site_or_refund_a_completed_mine() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - assert!(game.start_mine_construction((5, 40), Players::Enemy)); - assert!(!game.cancel_mine_construction((5, 40), Players::Player)); - assert_eq!(game.player_resources, Resources::new(50, 25)); - for _ in 0..MineConstruction::BUILD_TICKS { - game.tick_update(); - } - assert!(!game.cancel_mine_construction((5, 40), Players::Enemy)); - assert_eq!(game.enemy_resources.metal, 15); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + assert!(game.start_mine_construction((5, 40), Players::Enemy)); + assert!(!game.cancel_mine_construction((5, 40), Players::Player)); + assert_eq!(game.player_resources, Resources::new(50, 25)); + for _ in 0..MineConstruction::BUILD_TICKS { + game.tick_update(); + } + assert!(!game.cancel_mine_construction((5, 40), Players::Enemy)); + assert_eq!(game.enemy_resources.metal, 15); } diff --git a/tests/context_keybindings.rs b/tests/context_keybindings.rs index 98a1e97..2cd58e9 100644 --- a/tests/context_keybindings.rs +++ b/tests/context_keybindings.rs @@ -1,160 +1,160 @@ use clap::Parser; use ratatui::{ - Terminal, - backend::TestBackend, - buffer::Cell, - crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, - layout::Rect, + Terminal, + backend::TestBackend, + buffer::Cell, + crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, + layout::Rect, }; use std::sync::mpsc::{Receiver, Sender, channel}; use war_in_tunnels::{ - app::{ - Action, App, GameStates, View, available_skirmish_actions, handle_keybindings, - states::skirmish_states::structures::Structures, threads::AudioCmd, - }, - cli::Cli, + app::{ + Action, App, GameStates, View, available_skirmish_actions, handle_keybindings, + states::skirmish_states::structures::Structures, threads::AudioCmd, + }, + cli::Cli, }; fn app() -> App { - let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); - let (tx, _rx): (Sender, Receiver) = channel(); - let mut app: App = App::new(args, tx); - app.view = View::Skirmish; - app.window_area = Rect::new(0, 0, 160, 40); - app.states = Some(GameStates::new(&app.args, &app.window_area)); - app + let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); + let (tx, _rx): (Sender, Receiver) = channel(); + let mut app: App = App::new(args, tx); + app.view = View::Skirmish; + app.window_area = Rect::new(0, 0, 160, 40); + app.states = Some(GameStates::new(&app.args, &app.window_area)); + app } fn press(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> () { - handle_keybindings(app, KeyEvent::new(code, modifiers)); + handle_keybindings(app, KeyEvent::new(code, modifiers)); } #[test] fn selecting_miner_switches_help_and_escape_cancels_only_the_route() -> () { - let mut app: App = app(); - assert!( - !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space) - ); - press(&mut app, KeyCode::Left, KeyModifiers::NONE); - assert!(available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space)); - press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); - let actions: Vec = available_skirmish_actions(&app.states_ref().unwrap().skirmish); - assert!(actions.contains(&Action::Backspace)); - assert!(actions.contains(&Action::Delete)); - assert!(!actions.contains(&Action::Building)); - assert!(!actions.contains(&Action::ZoomIn)); - press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); - assert!(!app.states_ref().unwrap().skirmish.building_view); - press(&mut app, KeyCode::Esc, KeyModifiers::NONE); - assert_eq!(app.view, View::Skirmish); - assert!( - !app - .states_ref() - .unwrap() - .skirmish - .board - .marked_cells - .marking_cells - ); - assert!( - app - .states_ref() - .unwrap() - .skirmish - .board - .get_marked_cells() - .is_empty() - ); + let mut app: App = app(); + assert!( + !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space) + ); + press(&mut app, KeyCode::Left, KeyModifiers::NONE); + assert!( + available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space) + ); + press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); + let actions: Vec = available_skirmish_actions(&app.states_ref().unwrap().skirmish); + assert!(actions.contains(&Action::Backspace)); + assert!(actions.contains(&Action::Delete)); + assert!(!actions.contains(&Action::Building)); + assert!(!actions.contains(&Action::ZoomIn)); + press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); + assert!(!app.states_ref().unwrap().skirmish.building_view); + press(&mut app, KeyCode::Esc, KeyModifiers::NONE); + assert_eq!(app.view, View::Skirmish); + assert!( + !app.states_ref() + .unwrap() + .skirmish + .board + .marked_cells + .marking_cells + ); + assert!( + app.states_ref() + .unwrap() + .skirmish + .board + .get_marked_cells() + .is_empty() + ); } #[test] fn building_view_has_its_own_help_and_does_not_build_on_invalid_tile() -> () { - let mut app: App = app(); - let before: war_in_tunnels::app::states::skirmish_states::BoardState = - app.states_ref().unwrap().skirmish.board.clone(); - press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); - assert!(app.states_ref().unwrap().skirmish.building_view); - assert_eq!( - available_skirmish_actions(&app.states_ref().unwrap().skirmish), - [Action::Building, Action::Esc, Action::Quit2] - ); - press(&mut app, KeyCode::Left, KeyModifiers::NONE); - press(&mut app, KeyCode::Enter, KeyModifiers::NONE); - press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); - assert_eq!(app.states_ref().unwrap().skirmish.board, before); - let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 40)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Construction unavailable")); - assert!(text.contains("Close buildings")); - assert!(!text.contains("Marking cells")); - press(&mut app, KeyCode::Esc, KeyModifiers::NONE); - assert!(!app.states_ref().unwrap().skirmish.building_view); - assert_eq!(app.view, View::Skirmish); - for row in &app.states_ref().unwrap().skirmish.board.cells { - for cell in row { - assert!(!matches!(cell.get_structure(), Structures::Mine(_))); + let mut app: App = app(); + let before: war_in_tunnels::app::states::skirmish_states::BoardState = + app.states_ref().unwrap().skirmish.board.clone(); + press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); + assert!(app.states_ref().unwrap().skirmish.building_view); + assert_eq!( + available_skirmish_actions(&app.states_ref().unwrap().skirmish), + [Action::Building, Action::Esc, Action::Quit2] + ); + press(&mut app, KeyCode::Left, KeyModifiers::NONE); + press(&mut app, KeyCode::Enter, KeyModifiers::NONE); + press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); + assert_eq!(app.states_ref().unwrap().skirmish.board, before); + let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 40)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Construction unavailable")); + assert!(text.contains("Close buildings")); + assert!(!text.contains("Marking cells")); + press(&mut app, KeyCode::Esc, KeyModifiers::NONE); + assert!(!app.states_ref().unwrap().skirmish.building_view); + assert_eq!(app.view, View::Skirmish); + for row in &app.states_ref().unwrap().skirmish.board.cells { + for cell in row { + assert!(!matches!(cell.get_structure(), Structures::Mine(_))); + } } - } } #[test] fn legacy_control_space_and_quit_work_in_building_view() -> () { - let mut app: App = app(); - press(&mut app, KeyCode::Null, KeyModifiers::NONE); - assert!(app.states_ref().unwrap().skirmish.building_view); - press(&mut app, KeyCode::Null, KeyModifiers::CONTROL); - assert!(!app.states_ref().unwrap().skirmish.building_view); - press(&mut app, KeyCode::Null, KeyModifiers::NONE); - press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL); - assert!(app.exit); + let mut app: App = app(); + press(&mut app, KeyCode::Null, KeyModifiers::NONE); + assert!(app.states_ref().unwrap().skirmish.building_view); + press(&mut app, KeyCode::Null, KeyModifiers::CONTROL); + assert!(!app.states_ref().unwrap().skirmish.building_view); + press(&mut app, KeyCode::Null, KeyModifiers::NONE); + press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL); + assert!(app.exit); } #[test] fn contextual_help_fits_normal_terminal_and_changes_when_route_is_selected() -> () { - let mut app: App = app(); - app.window_area = Rect::new(0, 0, 120, 40); - press(&mut app, KeyCode::Left, KeyModifiers::NONE); - let mut terminal: Terminal = Terminal::new(TestBackend::new(120, 40)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Ctrl + Space - Build")); - assert!(text.contains("Space - Select miner")); - press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Space - Confirm route")); - assert!(text.contains("Esc - Cancel route")); - assert!(!text.contains("Ctrl + Space")); - assert!(!text.contains("Zoom in")); + let mut app: App = app(); + app.window_area = Rect::new(0, 0, 120, 40); + press(&mut app, KeyCode::Left, KeyModifiers::NONE); + let mut terminal: Terminal = Terminal::new(TestBackend::new(120, 40)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Ctrl + Space - Build")); + assert!(text.contains("Space - Select miner")); + press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Space - Confirm route")); + assert!(text.contains("Esc - Cancel route")); + assert!(!text.contains("Ctrl + Space")); + assert!(!text.contains("Zoom in")); } diff --git a/tests/digging.rs b/tests/digging.rs index e40444c..722fecd 100644 --- a/tests/digging.rs +++ b/tests/digging.rs @@ -1,534 +1,534 @@ use clap::Parser; use ratatui::{ - Terminal, - backend::TestBackend, - buffer::Buffer, - crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, - layout::Rect, - widgets::Widget, + Terminal, + backend::TestBackend, + buffer::Buffer, + crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, + layout::Rect, + widgets::Widget, }; use std::sync::mpsc::{Receiver, Sender, channel}; use war_in_tunnels::{ - app::{ - App, GameStates, View, handle_keybindings, - states::{ - SkirmishState, - skirmish_states::{ - BoardState, CellSizes, MoveFocusedCell, Players, ZoomLevel, - structures::{Structure, Structures, Tunnel}, - tasks::{DiggingTask, MovingTask, Task, Tasks}, - units::{MinerUnit, Unit, Units}, - }, + app::{ + App, GameStates, View, handle_keybindings, + states::{ + SkirmishState, + skirmish_states::{ + BoardState, CellSizes, MoveFocusedCell, Players, ZoomLevel, + structures::{Structure, Structures, Tunnel}, + tasks::{DiggingTask, MovingTask, Task, Tasks}, + units::{MinerUnit, Unit, Units}, + }, + }, + threads::{AudioCmd, TICK_MS}, }, - threads::{AudioCmd, TICK_MS}, - }, - cli::Cli, + cli::Cli, }; fn game() -> SkirmishState { - let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); - GameStates::new(&args, &Rect::new(0, 0, 120, 40)).skirmish + let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); + GameStates::new(&args, &Rect::new(0, 0, 120, 40)).skirmish } fn queue(board: &mut BoardState, origin: (usize, usize), path: &[(usize, usize)]) { - let unit = board.cells[origin.0][origin.1] - .get_mut_option_unit() - .as_mut() - .unwrap(); - let power = unit.get_digging_power(); - unit.set_task(Tasks::Digging(DiggingTask::new( - path.iter().copied().collect(), - power, - ))); + let unit = board.cells[origin.0][origin.1] + .get_mut_option_unit() + .as_mut() + .unwrap(); + let power = unit.get_digging_power(); + unit.set_task(Tasks::Digging(DiggingTask::new( + path.iter().copied().collect(), + power, + ))); } fn tunnel(board: &mut BoardState, row: usize, col: usize) { - board.cells[row][col].set_structure(Structures::Tunnel(Tunnel::new())); + board.cells[row][col].set_structure(Structures::Tunnel(Tunnel::new())); } fn unit(board: &BoardState, row: usize, col: usize) -> &Units { - board.cells[row][col] - .get_ref_option_unit() - .as_ref() - .unwrap() + board.cells[row][col] + .get_ref_option_unit() + .as_ref() + .unwrap() } fn travel_ticks() -> u16 { - 3000_u16.div_ceil(u16::from(TICK_MS)) + 3000_u16.div_ceil(u16::from(TICK_MS)) } fn advance(game: &mut SkirmishState, ticks: u16) { - for _ in 0..ticks { - game.tick_update(); - } + for _ in 0..ticks { + game.tick_update(); + } } #[test] fn tick_damages_stone_before_moving_and_finishes_entire_route() { - let mut game = game(); - queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]); - game.tick_update(); - assert_eq!(game.turn_counter, 1); - let durability = game.board.cells[5][3].get_structure().get_durability(); - assert!(durability < 1000 && durability > 0); - assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); - assert!(game.board.cells[5][3].get_ref_option_unit().is_none()); - for _ in 0..500 { + let mut game = game(); + queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]); game.tick_update(); - } - for col in [3, 4] { + assert_eq!(game.turn_counter, 1); + let durability = game.board.cells[5][3].get_structure().get_durability(); + assert!(durability < 1000 && durability > 0); + assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); + assert!(game.board.cells[5][3].get_ref_option_unit().is_none()); + for _ in 0..500 { + game.tick_update(); + } + for col in [3, 4] { + assert!(matches!( + game.board.cells[5][col].get_structure(), + Structures::Tunnel(_) + )); + } assert!(matches!( - game.board.cells[5][col].get_structure(), - Structures::Tunnel(_) + game.board.cells[5][2].get_structure(), + Structures::Base(_) )); - } - assert!(matches!( - game.board.cells[5][2].get_structure(), - Structures::Base(_) - )); - assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); - assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); - assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); - assert!(game.board.cells[5][3].get_ref_option_unit().is_none()); + assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); + assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); + assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); + assert!(game.board.cells[5][3].get_ref_option_unit().is_none()); } #[test] fn each_tunnel_step_takes_three_seconds_in_both_directions() { - for path in [[(5, 3), (5, 4)], [(4, 2), (3, 2)]] { - let mut game = game(); - for (row, col) in path { - tunnel(&mut game.board, row, col); + for path in [[(5, 3), (5, 4)], [(4, 2), (3, 2)]] { + let mut game = game(); + for (row, col) in path { + tunnel(&mut game.board, row, col); + } + queue(&mut game.board, (5, 2), &path); + advance(&mut game, travel_ticks() - 1); + assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); + game.tick_update(); + assert_eq!( + unit(&game.board, path[0].0, path[0].1).get_coords(), + path[0] + ); + assert_eq!( + unit(&game.board, path[0].0, path[0].1).get_ref_tasks()[0] + .get_path() + .len(), + 1 + ); + advance(&mut game, travel_ticks() - 1); + assert_eq!( + unit(&game.board, path[0].0, path[0].1).get_coords(), + path[0] + ); + game.tick_update(); + assert!( + unit(&game.board, path[1].0, path[1].1) + .get_ref_tasks() + .is_empty() + ); } - queue(&mut game.board, (5, 2), &path); - advance(&mut game, travel_ticks() - 1); - assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); - game.tick_update(); - assert_eq!( - unit(&game.board, path[0].0, path[0].1).get_coords(), - path[0] - ); - assert_eq!( - unit(&game.board, path[0].0, path[0].1).get_ref_tasks()[0] - .get_path() - .len(), - 1 - ); - advance(&mut game, travel_ticks() - 1); - assert_eq!( - unit(&game.board, path[0].0, path[0].1).get_coords(), - path[0] - ); - game.tick_update(); - assert!( - unit(&game.board, path[1].0, path[1].1) - .get_ref_tasks() - .is_empty() - ); - } } #[test] fn queued_tasks_execute_in_order_and_keep_unit_coordinates_current() { - let mut game = game(); - tunnel(&mut game.board, 5, 3); - tunnel(&mut game.board, 5, 4); - queue(&mut game.board, (5, 2), &[(5, 3)]); - queue(&mut game.board, (5, 2), &[(5, 4)]); - advance(&mut game, travel_ticks()); - assert_eq!(unit(&game.board, 5, 3).get_ref_tasks().len(), 1); - advance(&mut game, travel_ticks()); - assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); - assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); - queue(&mut game.board, (5, 4), &[(5, 3), (5, 2)]); - advance(&mut game, 2 * travel_ticks() - 1); - assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); - game.tick_update(); - assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); - assert!(matches!( - game.board.cells[5][2].get_structure(), - Structures::Base(_) - )); + let mut game = game(); + tunnel(&mut game.board, 5, 3); + tunnel(&mut game.board, 5, 4); + queue(&mut game.board, (5, 2), &[(5, 3)]); + queue(&mut game.board, (5, 2), &[(5, 4)]); + advance(&mut game, travel_ticks()); + assert_eq!(unit(&game.board, 5, 3).get_ref_tasks().len(), 1); + advance(&mut game, travel_ticks()); + assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); + assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); + queue(&mut game.board, (5, 4), &[(5, 3), (5, 2)]); + advance(&mut game, 2 * travel_ticks() - 1); + assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); + game.tick_update(); + assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); + assert!(matches!( + game.board.cells[5][2].get_structure(), + Structures::Base(_) + )); } #[test] fn occupied_tunnel_waits_without_losing_units_or_task() { - let mut game = game(); - tunnel(&mut game.board, 5, 3); - *game.board.cells[5][3].get_mut_option_unit() = - Some(Units::Miner(MinerUnit::new(Players::Player, (5, 3)))); - queue(&mut game.board, (5, 2), &[(5, 3)]); - for _ in 0..3 { + let mut game = game(); + tunnel(&mut game.board, 5, 3); + *game.board.cells[5][3].get_mut_option_unit() = + Some(Units::Miner(MinerUnit::new(Players::Player, (5, 3)))); + queue(&mut game.board, (5, 2), &[(5, 3)]); + for _ in 0..3 { + game.tick_update(); + } + assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 1); + assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); + game.board.cells[5][3].get_mut_option_unit().take(); + advance(&mut game, travel_ticks() - 1); + assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); game.tick_update(); - } - assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 1); - assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); - game.board.cells[5][3].get_mut_option_unit().take(); - advance(&mut game, travel_ticks() - 1); - assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); - game.tick_update(); - assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); - assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); + assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); + assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); } #[test] fn ore_is_not_destroyed_by_digging_task() { - let mut game = game(); - let ore = game.board.cells[5][1].get_structure(); - queue(&mut game.board, (5, 2), &[(5, 1)]); - for _ in 0..200 { - game.tick_update(); - } - assert_eq!(game.board.cells[5][1].get_structure(), ore); - assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); + let mut game = game(); + let ore = game.board.cells[5][1].get_structure(); + queue(&mut game.board, (5, 2), &[(5, 1)]); + for _ in 0..200 { + game.tick_update(); + } + assert_eq!(game.board.cells[5][1].get_structure(), ore); + assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); } #[test] fn invalid_or_empty_routes_do_not_teleport_or_remove_miner() { - for path in [vec![], vec![(usize::MAX, usize::MAX)], vec![(0, 0)]] { - let mut game = game(); - queue(&mut game.board, (5, 2), &path); - game.tick_update(); - assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); - assert!(unit(&game.board, 5, 2).get_ref_tasks().is_empty()); - } + for path in [vec![], vec![(usize::MAX, usize::MAX)], vec![(0, 0)]] { + let mut game = game(); + queue(&mut game.board, (5, 2), &path); + game.tick_update(); + assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2)); + assert!(unit(&game.board, 5, 2).get_ref_tasks().is_empty()); + } } #[test] fn only_player_miner_can_start_drawing_route() { - let mut game = game(); - game.board.toggle_marking(); - assert!(!game.board.marked_cells.marking_cells); - for _ in 0..36 { - game.board.change_focused_cell(MoveFocusedCell::Right); - } - assert_eq!(game.board.get_focused_cell().get_coords(), (5, 39)); - game.board.toggle_marking(); - assert!(!game.board.marked_cells.marking_cells); - assert!(game.board.get_marked_cells().is_empty()); + let mut game = game(); + game.board.toggle_marking(); + assert!(!game.board.marked_cells.marking_cells); + for _ in 0..36 { + game.board.change_focused_cell(MoveFocusedCell::Right); + } + assert_eq!(game.board.get_focused_cell().get_coords(), (5, 39)); + game.board.toggle_marking(); + assert!(!game.board.marked_cells.marking_cells); + assert!(game.board.get_marked_cells().is_empty()); } #[test] fn drawing_route_pauses_selected_miner_and_boundary_moves_do_not_repeat_cells() { - let mut game = game(); - game.board.change_focused_cell(MoveFocusedCell::Left); - queue(&mut game.board, (5, 2), &[(5, 3)]); - game.board.toggle_marking(); - game.tick_update(); - assert_eq!( - game.board.cells[5][3].get_structure().get_durability(), - 1000 - ); - for _ in 0..10 { - game.board.change_focused_cell(MoveFocusedCell::Up); - } - let coords: Vec<_> = game.board.get_marked_cells().iter().copied().collect(); - assert!(coords.windows(2).all(|pair| pair[0] != pair[1])); - game.board.end_marking_cells(true); - game.tick_update(); - assert!(game.board.cells[5][3].get_structure().get_durability() < 1000); + let mut game = game(); + game.board.change_focused_cell(MoveFocusedCell::Left); + queue(&mut game.board, (5, 2), &[(5, 3)]); + game.board.toggle_marking(); + game.tick_update(); + assert_eq!( + game.board.cells[5][3].get_structure().get_durability(), + 1000 + ); + for _ in 0..10 { + game.board.change_focused_cell(MoveFocusedCell::Up); + } + let coords: Vec<_> = game.board.get_marked_cells().iter().copied().collect(); + assert!(coords.windows(2).all(|pair| pair[0] != pair[1])); + game.board.end_marking_cells(true); + game.tick_update(); + assert!(game.board.cells[5][3].get_structure().get_durability() < 1000); } #[test] fn drawing_another_task_extends_the_end_of_queued_work() { - let mut game = game(); - for col in [3, 4, 5] { - tunnel(&mut game.board, 5, col); - } - game.board.change_focused_cell(MoveFocusedCell::Left); - game.board.toggle_marking(); - game.board.change_focused_cell(MoveFocusedCell::Right); - game.board.change_focused_cell(MoveFocusedCell::Right); - game.board.toggle_marking(); - game.board.change_focused_cell(MoveFocusedCell::Left); - game.board.change_focused_cell(MoveFocusedCell::Left); - game.board.toggle_marking(); - assert_eq!(game.board.get_focused_cell().get_coords(), (5, 4)); - game.board.change_focused_cell(MoveFocusedCell::Right); - game.board.toggle_marking(); - assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 2); - advance(&mut game, 3 * travel_ticks()); - assert_eq!(unit(&game.board, 5, 5).get_coords(), (5, 5)); - assert!(unit(&game.board, 5, 5).get_ref_tasks().is_empty()); + let mut game = game(); + for col in [3, 4, 5] { + tunnel(&mut game.board, 5, col); + } + game.board.change_focused_cell(MoveFocusedCell::Left); + game.board.toggle_marking(); + game.board.change_focused_cell(MoveFocusedCell::Right); + game.board.change_focused_cell(MoveFocusedCell::Right); + game.board.toggle_marking(); + game.board.change_focused_cell(MoveFocusedCell::Left); + game.board.change_focused_cell(MoveFocusedCell::Left); + game.board.toggle_marking(); + assert_eq!(game.board.get_focused_cell().get_coords(), (5, 4)); + game.board.change_focused_cell(MoveFocusedCell::Right); + game.board.toggle_marking(); + assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 2); + advance(&mut game, 3 * travel_ticks()); + assert_eq!(unit(&game.board, 5, 5).get_coords(), (5, 5)); + assert!(unit(&game.board, 5, 5).get_ref_tasks().is_empty()); } #[test] fn freshly_dug_cell_is_entered_immediately_then_tunnel_travel_takes_three_seconds() { - let mut game = game(); - tunnel(&mut game.board, 5, 4); - queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]); - for _ in 0..200 { - game.tick_update(); - if matches!( - game.board.cells[5][3].get_structure(), - Structures::Tunnel(_) - ) { - assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); - advance(&mut game, travel_ticks() - 1); - assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); - game.tick_update(); - assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); - return; + let mut game = game(); + tunnel(&mut game.board, 5, 4); + queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]); + for _ in 0..200 { + game.tick_update(); + if matches!( + game.board.cells[5][3].get_structure(), + Structures::Tunnel(_) + ) { + assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); + advance(&mut game, travel_ticks() - 1); + assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3)); + game.tick_update(); + assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); + return; + } } - } - panic!("miner did not finish digging the stone"); + panic!("miner did not finish digging the stone"); } #[test] fn miner_symbol_in_tunnel_uses_owner_color_even_when_selected_or_marked() { - for owner in [Players::Player, Players::Enemy] { - for zoom in [ZoomLevel::Default, ZoomLevel::ZoomedIn] { - for (selected, marked) in [(false, false), (true, false), (false, true), (true, true)] { - let mut game = game(); - let cell = &mut game.board.cells[5][3]; - cell.set_structure(Structures::Tunnel(Tunnel::new())); - cell - .set_selected(selected) - .set_marked(marked) - .set_zoom_level(zoom); - *cell.get_mut_option_unit() = Some(Units::Miner(MinerUnit::new(owner, (5, 3)))); - let area = Rect::new( - 0, - 0, - zoom.get_cell_size(CellSizes::Width) as u16, - zoom.get_cell_size(CellSizes::Height) as u16, - ); - let mut buffer = Buffer::empty(area); - cell.clone().render(area, &mut buffer); - let marker = buffer - .content - .iter() - .find(|cell| cell.symbol() == "M") - .unwrap(); - assert_eq!(marker.fg, owner.get_color(), "{owner:?} {zoom:?}"); - assert!(buffer.content.iter().any(|cell| cell.symbol() == "T")); - } + for owner in [Players::Player, Players::Enemy] { + for zoom in [ZoomLevel::Default, ZoomLevel::ZoomedIn] { + for (selected, marked) in [(false, false), (true, false), (false, true), (true, true)] { + let mut game = game(); + let cell = &mut game.board.cells[5][3]; + cell.set_structure(Structures::Tunnel(Tunnel::new())); + cell.set_selected(selected) + .set_marked(marked) + .set_zoom_level(zoom); + *cell.get_mut_option_unit() = Some(Units::Miner(MinerUnit::new(owner, (5, 3)))); + let area = Rect::new( + 0, + 0, + zoom.get_cell_size(CellSizes::Width) as u16, + zoom.get_cell_size(CellSizes::Height) as u16, + ); + let mut buffer = Buffer::empty(area); + cell.clone().render(area, &mut buffer); + let marker = buffer + .content + .iter() + .find(|cell| cell.symbol() == "M") + .unwrap(); + assert_eq!(marker.fg, owner.get_color(), "{owner:?} {zoom:?}"); + assert!(buffer.content.iter().any(|cell| cell.symbol() == "T")); + } + } } - } } #[test] fn tui_shows_movement_percentage_and_clears_it_after_each_step() { - for zoom in [ - ZoomLevel::ZoomedIn, - ZoomLevel::Default, - ZoomLevel::ZoomedOut, - ] { - let mut game = game(); - for col in [2, 3, 4] { - tunnel(&mut game.board, 5, col); + for zoom in [ + ZoomLevel::ZoomedIn, + ZoomLevel::Default, + ZoomLevel::ZoomedOut, + ] { + let mut game = game(); + for col in [2, 3, 4] { + tunnel(&mut game.board, 5, col); + } + queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]); + let render = |board: &BoardState, col: usize| { + let area = Rect::new( + 0, + 0, + zoom.get_cell_size(CellSizes::Width) as u16, + zoom.get_cell_size(CellSizes::Height) as u16, + ); + let mut buffer = Buffer::empty(area); + let mut cell = board.get_ref_cell(5, col).clone(); + cell.set_zoom_level(zoom); + cell.render(area, &mut buffer); + buffer + }; + let text = |buffer: &Buffer| -> String { + buffer.content.iter().map(|cell| cell.symbol()).collect() + }; + assert!(!text(&render(&game.board, 2)).contains('%')); + advance(&mut game, 46); + let buffer = render(&game.board, 2); + assert!(text(&buffer).contains("50%"), "{zoom:?}"); + if zoom != ZoomLevel::ZoomedOut { + let marker = buffer + .content + .iter() + .find(|cell| cell.symbol() == "M") + .unwrap(); + assert_eq!(marker.fg, Players::Player.get_color()); + } + advance(&mut game, travel_ticks() - 46); + assert!(!text(&render(&game.board, 2)).contains('%')); + assert!(!text(&render(&game.board, 3)).contains('%')); + game.tick_update(); + assert!(text(&render(&game.board, 3)).contains("1%")); + advance(&mut game, travel_ticks() - 1); + assert!(!text(&render(&game.board, 4)).contains('%')); + assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); } - queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]); - let render = |board: &BoardState, col: usize| { - let area = Rect::new( - 0, - 0, - zoom.get_cell_size(CellSizes::Width) as u16, - zoom.get_cell_size(CellSizes::Height) as u16, - ); - let mut buffer = Buffer::empty(area); - let mut cell = board.get_ref_cell(5, col).clone(); - cell.set_zoom_level(zoom); - cell.render(area, &mut buffer); - buffer - }; - let text = - |buffer: &Buffer| -> String { buffer.content.iter().map(|cell| cell.symbol()).collect() }; - assert!(!text(&render(&game.board, 2)).contains('%')); - advance(&mut game, 46); - let buffer = render(&game.board, 2); - assert!(text(&buffer).contains("50%"), "{zoom:?}"); - if zoom != ZoomLevel::ZoomedOut { - let marker = buffer - .content - .iter() - .find(|cell| cell.symbol() == "M") - .unwrap(); - assert_eq!(marker.fg, Players::Player.get_color()); - } - advance(&mut game, travel_ticks() - 46); - assert!(!text(&render(&game.board, 2)).contains('%')); - assert!(!text(&render(&game.board, 3)).contains('%')); - game.tick_update(); - assert!(text(&render(&game.board, 3)).contains("1%")); - advance(&mut game, travel_ticks() - 1); - assert!(!text(&render(&game.board, 4)).contains('%')); - assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty()); - } } #[test] fn confirmed_route_splits_into_moving_and_digging_segments() { - let mut game = game(); - for col in [3, 4, 7] { - tunnel(&mut game.board, 5, col); - } - game.board.change_focused_cell(MoveFocusedCell::Left); - game.board.toggle_marking(); - for _ in 0..5 { - game.board.change_focused_cell(MoveFocusedCell::Right); - } - game.board.toggle_marking(); - let tasks = unit(&game.board, 5, 2).get_ref_tasks(); - assert_eq!(tasks.len(), 3); - assert!(matches!(tasks[0], Tasks::Moving(_))); - assert_eq!( - tasks[0].get_path().iter().copied().collect::>(), - [(5, 3), (5, 4)] - ); - assert!(matches!(tasks[1], Tasks::Digging(_))); - assert_eq!( - tasks[1].get_path().iter().copied().collect::>(), - [(5, 5), (5, 6)] - ); - assert!(matches!(tasks[2], Tasks::Moving(_))); - assert_eq!(tasks[0].get_icon(), '→'); - assert_eq!(tasks[1].get_icon(), '⛏'); - advance(&mut game, 2 * travel_ticks()); - assert!(matches!( - unit(&game.board, 5, 4).get_ref_tasks()[0], - Tasks::Digging(_) - )); - advance(&mut game, 2 * 182); - assert!(matches!( - unit(&game.board, 5, 6).get_ref_tasks()[0], - Tasks::Moving(_) - )); - advance(&mut game, travel_ticks()); - assert!(unit(&game.board, 5, 7).get_ref_tasks().is_empty()); + let mut game = game(); + for col in [3, 4, 7] { + tunnel(&mut game.board, 5, col); + } + game.board.change_focused_cell(MoveFocusedCell::Left); + game.board.toggle_marking(); + for _ in 0..5 { + game.board.change_focused_cell(MoveFocusedCell::Right); + } + game.board.toggle_marking(); + let tasks = unit(&game.board, 5, 2).get_ref_tasks(); + assert_eq!(tasks.len(), 3); + assert!(matches!(tasks[0], Tasks::Moving(_))); + assert_eq!( + tasks[0].get_path().iter().copied().collect::>(), + [(5, 3), (5, 4)] + ); + assert!(matches!(tasks[1], Tasks::Digging(_))); + assert_eq!( + tasks[1].get_path().iter().copied().collect::>(), + [(5, 5), (5, 6)] + ); + assert!(matches!(tasks[2], Tasks::Moving(_))); + assert_eq!(tasks[0].get_icon(), '→'); + assert_eq!(tasks[1].get_icon(), '⛏'); + advance(&mut game, 2 * travel_ticks()); + assert!(matches!( + unit(&game.board, 5, 4).get_ref_tasks()[0], + Tasks::Digging(_) + )); + advance(&mut game, 2 * 182); + assert!(matches!( + unit(&game.board, 5, 6).get_ref_tasks()[0], + Tasks::Moving(_) + )); + advance(&mut game, travel_ticks()); + assert!(unit(&game.board, 5, 7).get_ref_tasks().is_empty()); } #[test] fn moving_task_does_not_damage_walkable_structure() { - let mut game = game(); - tunnel(&mut game.board, 5, 3); - let structure = game.board.cells[5][3].get_structure(); - game.board.cells[5][2] - .get_mut_option_unit() - .set_task(Tasks::Moving(MovingTask::new([(5, 3)].into()))); - advance(&mut game, travel_ticks()); - assert_eq!(game.board.cells[5][3].get_structure(), structure); - assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); + let mut game = game(); + tunnel(&mut game.board, 5, 3); + let structure = game.board.cells[5][3].get_structure(); + game.board.cells[5][2] + .get_mut_option_unit() + .set_task(Tasks::Moving(MovingTask::new([(5, 3)].into()))); + advance(&mut game, travel_ticks()); + assert_eq!(game.board.cells[5][3].get_structure(), structure); + assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); } #[test] fn previously_dug_route_becomes_moving_without_instant_steps() { - let mut game = game(); - queue(&mut game.board, (5, 2), &[(5, 3), (5, 4), (5, 3)]); - advance(&mut game, 2 * 182); - assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); - game.tick_update(); - assert!(matches!( - unit(&game.board, 5, 4).get_ref_tasks()[0], - Tasks::Moving(_) - )); - advance(&mut game, travel_ticks() - 2); - assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); - game.tick_update(); - assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); + let mut game = game(); + queue(&mut game.board, (5, 2), &[(5, 3), (5, 4), (5, 3)]); + advance(&mut game, 2 * 182); + assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); + game.tick_update(); + assert!(matches!( + unit(&game.board, 5, 4).get_ref_tasks()[0], + Tasks::Moving(_) + )); + advance(&mut game, travel_ticks() - 2); + assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4)); + game.tick_update(); + assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); } #[test] fn route_reclassifies_as_digging_if_target_is_stone() { - let mut game = game(); - game.board.cells[5][2] - .get_mut_option_unit() - .set_task(Tasks::Moving(MovingTask::new([(5, 3)].into()))); - game.tick_update(); - assert!(matches!( - unit(&game.board, 5, 2).get_ref_tasks()[0], - Tasks::Digging(_) - )); - assert!(game.board.cells[5][3].get_structure().get_durability() < 1000); - advance(&mut game, 181); - assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); + let mut game = game(); + game.board.cells[5][2] + .get_mut_option_unit() + .set_task(Tasks::Moving(MovingTask::new([(5, 3)].into()))); + game.tick_update(); + assert!(matches!( + unit(&game.board, 5, 2).get_ref_tasks()[0], + Tasks::Digging(_) + )); + assert!(game.board.cells[5][3].get_structure().get_durability() < 1000); + advance(&mut game, 181); + assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); } #[test] fn tab_panel_shows_current_step_and_total_for_active_and_pending_tasks() -> () { - let args: Cli = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap(); - let channels: (Sender, Receiver) = channel(); - let mut app: App = App::new(args, channels.0); - app.window_area = Rect::new(0, 0, 120, 60); - app.states = Some(GameStates::new(&app.args, &app.window_area)); - let state: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - tunnel(&mut state.board, 5, 3); - tunnel(&mut state.board, 5, 4); - state.board.cells[5][2] - .get_mut_option_unit() - .set_task(Tasks::Moving(MovingTask::new([(5, 3), (5, 4)].into()))); - state.board.cells[5][2] - .get_mut_option_unit() - .set_task(Tasks::Digging(DiggingTask::new([(5, 5)].into(), 0.55))); - advance(state, travel_ticks() + 46); - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); - let mut terminal: Terminal = Terminal::new(TestBackend::new(120, 60)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &ratatui::buffer::Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Step: 50% | Total: 75%")); - assert!(text.contains("Step: 0% | Total: 0%")); + let args: Cli = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap(); + let channels: (Sender, Receiver) = channel(); + let mut app: App = App::new(args, channels.0); + app.window_area = Rect::new(0, 0, 120, 60); + app.states = Some(GameStates::new(&app.args, &app.window_area)); + let state: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + tunnel(&mut state.board, 5, 3); + tunnel(&mut state.board, 5, 4); + state.board.cells[5][2] + .get_mut_option_unit() + .set_task(Tasks::Moving(MovingTask::new([(5, 3), (5, 4)].into()))); + state.board.cells[5][2] + .get_mut_option_unit() + .set_task(Tasks::Digging(DiggingTask::new([(5, 5)].into(), 0.55))); + advance(state, travel_ticks() + 46); + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); + let mut terminal: Terminal = Terminal::new(TestBackend::new(120, 60)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &ratatui::buffer::Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Step: 50% | Total: 75%")); + assert!(text.contains("Step: 0% | Total: 0%")); } #[test] fn space_route_tick_and_tui_show_remaining_durability_at_every_zoom() { - let args = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap(); - let (tx, _) = channel(); - let mut app = App::new(args, tx); - app.window_area = Rect::new(0, 0, 120, 40); - app.states = Some(GameStates::new(&app.args, &app.window_area)); - for code in [ - KeyCode::Left, - KeyCode::Char(' '), - KeyCode::Right, - KeyCode::Char(' '), - ] { - handle_keybindings(&mut app, KeyEvent::new(code, KeyModifiers::NONE)); - } - app.states_mut().unwrap().skirmish.tick_update(); - let durability = app.states_ref().unwrap().skirmish.board.cells[5][3] - .get_structure() - .get_durability(); - let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap(); - for zoom in [ - ZoomLevel::ZoomedIn, - ZoomLevel::Default, - ZoomLevel::ZoomedOut, - ] { - let state = &mut app.states_mut().unwrap().skirmish; - state.board.change_zoom(zoom); - state.board.vertical_offset.set_value(5); + let args = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap(); + let (tx, _) = channel(); + let mut app = App::new(args, tx); + app.window_area = Rect::new(0, 0, 120, 40); + app.states = Some(GameStates::new(&app.args, &app.window_area)); + for code in [ + KeyCode::Left, + KeyCode::Char(' '), + KeyCode::Right, + KeyCode::Char(' '), + ] { + handle_keybindings(&mut app, KeyEvent::new(code, KeyModifiers::NONE)); + } + app.states_mut().unwrap().skirmish.tick_update(); + let durability = app.states_ref().unwrap().skirmish.board.cells[5][3] + .get_structure() + .get_durability(); + let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap(); + for zoom in [ + ZoomLevel::ZoomedIn, + ZoomLevel::Default, + ZoomLevel::ZoomedOut, + ] { + let state = &mut app.states_mut().unwrap().skirmish; + state.board.change_zoom(zoom); + state.board.vertical_offset.set_value(5); + terminal + .draw(|frame| frame.render_widget(&mut app, frame.area())) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell| cell.symbol()) + .collect(); + assert!(text.contains("99%"), "{zoom:?}"); + } + handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); terminal - .draw(|frame| frame.render_widget(&mut app, frame.area())) - .unwrap(); + .draw(|frame| frame.render_widget(&mut app, frame.area())) + .unwrap(); let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell| cell.symbol()) - .collect(); - assert!(text.contains("99%"), "{zoom:?}"); - } - handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); - terminal - .draw(|frame| frame.render_widget(&mut app, frame.area())) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell| cell.symbol()) - .collect(); - assert!(text.contains(&format!("{durability}/1000"))); - assert_eq!(app.view, View::Skirmish); + .backend() + .buffer() + .content + .iter() + .map(|cell| cell.symbol()) + .collect(); + assert!(text.contains(&format!("{durability}/1000"))); + assert_eq!(app.view, View::Skirmish); } diff --git a/tests/recruitment.rs b/tests/recruitment.rs index 1e5854b..5dcd119 100644 --- a/tests/recruitment.rs +++ b/tests/recruitment.rs @@ -1,338 +1,337 @@ use clap::Parser; use ratatui::{ - Terminal, - backend::TestBackend, - buffer::Cell, - crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, - layout::Rect, + Terminal, + backend::TestBackend, + buffer::Cell, + crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, + layout::Rect, }; use std::sync::mpsc::{Receiver, Sender, channel}; use war_in_tunnels::{ - app::{ - Action, App, GameStates, View, available_skirmish_actions, handle_keybindings, - states::{ - SkirmishState, - skirmish_states::{ - MoveFocusedCell, Players, Resources, - structures::{BaseBuilding, Structures, Tunnel}, - units::{MinerUnit, Unit, Units}, - }, + app::{ + Action, App, GameStates, View, available_skirmish_actions, handle_keybindings, + states::{ + SkirmishState, + skirmish_states::{ + MoveFocusedCell, Players, Resources, + structures::{BaseBuilding, Structures, Tunnel}, + units::{MinerUnit, Unit, Units}, + }, + }, + threads::{AudioCmd, TICK_MS}, }, - threads::{AudioCmd, TICK_MS}, - }, - cli::Cli, + cli::Cli, }; fn app() -> App { - let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); - let (tx, _rx): (Sender, Receiver) = channel(); - let mut app: App = App::new(args, tx); - app.view = View::Skirmish; - app.window_area = Rect::new(0, 0, 160, 50); - app.states = Some(GameStates::new(&app.args, &app.window_area)); - app + let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); + let (tx, _rx): (Sender, Receiver) = channel(); + let mut app: App = App::new(args, tx); + app.view = View::Skirmish; + app.window_area = Rect::new(0, 0, 160, 50); + app.states = Some(GameStates::new(&app.args, &app.window_area)); + app } fn base(game: &SkirmishState, coords: (usize, usize)) -> BaseBuilding { - let Structures::Base(base) = game.board.cells[coords.0][coords.1].get_structure() else { - panic!("base missing"); - }; - base + let Structures::Base(base) = game.board.cells[coords.0][coords.1].get_structure() else { + panic!("base missing"); + }; + base } fn press(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> () { - handle_keybindings(app, KeyEvent::new(code, modifiers)); + handle_keybindings(app, KeyEvent::new(code, modifiers)); } #[test] fn recruitment_is_owned_by_base_and_invalid_requests_do_not_charge() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - let before: Resources = game.player_resources; - for coords in [(5, 39), (0, 0), (usize::MAX, 2)] { - assert!(!game.recruit_miner(coords, Players::Player)); - assert_eq!(game.player_resources, before); - } - assert!(!game.recruit_miner((5, 2), Players::Unclaimed)); - assert!(game.recruit_miner((5, 2), Players::Player)); - assert_eq!(game.player_resources, Resources::new(40, 10)); - assert_eq!(game.enemy_resources, Resources::new(50, 25)); - assert_eq!(base(game, (5, 2)).queued_miners(), 1); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + let before: Resources = game.player_resources; + for coords in [(5, 39), (0, 0), (usize::MAX, 2)] { + assert!(!game.recruit_miner(coords, Players::Player)); + assert_eq!(game.player_resources, before); + } + assert!(!game.recruit_miner((5, 2), Players::Unclaimed)); + assert!(game.recruit_miner((5, 2), Players::Player)); + assert_eq!(game.player_resources, Resources::new(40, 10)); + assert_eq!(game.enemy_resources, Resources::new(50, 25)); + assert_eq!(base(game, (5, 2)).queued_miners(), 1); } #[test] fn recruitment_requires_both_resources_and_charges_each_queue_entry() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - for resources in [ - Resources { wood: 9, metal: 15 }, - Resources { - wood: 10, - metal: 14, - }, - ] { - game.player_resources = resources; - assert!(!game.can_recruit_miner((5, 2), Players::Player)); + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + for resources in [ + Resources { wood: 9, metal: 15 }, + Resources { + wood: 10, + metal: 14, + }, + ] { + game.player_resources = resources; + assert!(!game.can_recruit_miner((5, 2), Players::Player)); + assert!(!game.recruit_miner((5, 2), Players::Player)); + assert_eq!(game.player_resources, resources); + assert_eq!(base(game, (5, 2)).queued_miners(), 0); + } + game.player_resources = Resources::new(20, 30); + assert!(game.recruit_miner((5, 2), Players::Player)); + assert!(game.recruit_miner((5, 2), Players::Player)); + assert_eq!(base(game, (5, 2)).queued_miners(), 2); + assert_eq!(game.player_resources, Resources::new(0, 0)); assert!(!game.recruit_miner((5, 2), Players::Player)); - assert_eq!(game.player_resources, resources); - assert_eq!(base(game, (5, 2)).queued_miners(), 0); - } - game.player_resources = Resources::new(20, 30); - assert!(game.recruit_miner((5, 2), Players::Player)); - assert!(game.recruit_miner((5, 2), Players::Player)); - assert_eq!(base(game, (5, 2)).queued_miners(), 2); - assert_eq!(game.player_resources, Resources::new(0, 0)); - assert!(!game.recruit_miner((5, 2), Players::Player)); } #[test] fn first_recruit_waits_at_full_progress_and_following_recruits_pause_until_departure() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - game.player_resources = Resources::new(100, 100); - assert!(game.recruit_miner((5, 2), Players::Player)); - assert!(game.recruit_miner((5, 2), Players::Player)); - let original: Option = game.board.cells[5][2].get_option_unit(); - for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS { + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + game.player_resources = Resources::new(100, 100); + assert!(game.recruit_miner((5, 2), Players::Player)); + assert!(game.recruit_miner((5, 2), Players::Player)); + let original: Option = game.board.cells[5][2].get_option_unit(); + for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS { + game.tick_update(); + } + assert_eq!(base(game, (5, 2)).queued_miners(), 2); + assert_eq!(base(game, (5, 2)).recruitment_progress(), 100); + assert_eq!(game.board.cells[5][2].get_option_unit(), original); + game.board.cells[5][2].get_mut_option_unit().take(); game.tick_update(); - } - assert_eq!(base(game, (5, 2)).queued_miners(), 2); - assert_eq!(base(game, (5, 2)).recruitment_progress(), 100); - assert_eq!(game.board.cells[5][2].get_option_unit(), original); - game.board.cells[5][2].get_mut_option_unit().take(); - game.tick_update(); - let spawned: Option = game.board.cells[5][2].get_option_unit(); - assert_eq!(spawned.get_owner(), Players::Player); - assert_eq!(spawned.get_coords(), (5, 2)); - assert!(spawned.get_ref_tasks().is_empty()); - assert_eq!(base(game, (5, 2)).queued_miners(), 1); - for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS { + let spawned: Option = game.board.cells[5][2].get_option_unit(); + assert_eq!(spawned.get_owner(), Players::Player); + assert_eq!(spawned.get_coords(), (5, 2)); + assert!(spawned.get_ref_tasks().is_empty()); + assert_eq!(base(game, (5, 2)).queued_miners(), 1); + for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS { + game.tick_update(); + } + assert_eq!(base(game, (5, 2)).queued_miners(), 1); + assert_eq!(base(game, (5, 2)).recruitment_progress(), 0); + assert_eq!(game.board.cells[5][2].get_option_unit(), spawned); + game.board.cells[5][2].get_mut_option_unit().take(); + for _ in 1..BaseBuilding::RECRUITMENT_TICKS { + game.tick_update(); + } + assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); + assert_eq!(base(game, (5, 2)).recruitment_progress(), 99); game.tick_update(); - } - assert_eq!(base(game, (5, 2)).queued_miners(), 1); - assert_eq!(base(game, (5, 2)).recruitment_progress(), 0); - assert_eq!(game.board.cells[5][2].get_option_unit(), spawned); - game.board.cells[5][2].get_mut_option_unit().take(); - for _ in 1..BaseBuilding::RECRUITMENT_TICKS { - game.tick_update(); - } - assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); - assert_eq!(base(game, (5, 2)).recruitment_progress(), 99); - game.tick_update(); - assert_eq!(base(game, (5, 2)).queued_miners(), 0); - assert_eq!( - game.board.cells[5][2].get_ref_option_unit().get_owner(), - Players::Player - ); + assert_eq!(base(game, (5, 2)).queued_miners(), 0); + assert_eq!( + game.board.cells[5][2].get_ref_option_unit().get_owner(), + Players::Player + ); } #[test] fn walking_out_of_base_resumes_recruitment_and_new_miner_can_receive_tasks() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - assert!(game.recruit_miner((5, 2), Players::Player)); - game.board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new())); - game.board.change_focused_cell(MoveFocusedCell::Left); - game.board.toggle_marking(); - game.board.change_focused_cell(MoveFocusedCell::Right); - game.board.toggle_marking(); - for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) { + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + assert!(game.recruit_miner((5, 2), Players::Player)); + game.board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new())); + game.board.change_focused_cell(MoveFocusedCell::Left); + game.board.toggle_marking(); + game.board.change_focused_cell(MoveFocusedCell::Right); + game.board.toggle_marking(); + for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) { + game.tick_update(); + } + assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); + for _ in 3000_u16.div_ceil(u16::from(TICK_MS))..BaseBuilding::RECRUITMENT_TICKS - 1 { + game.tick_update(); + } + assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); + assert_eq!(base(game, (5, 2)).recruitment_progress(), 99); game.tick_update(); - } - assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); - for _ in 3000_u16.div_ceil(u16::from(TICK_MS))..BaseBuilding::RECRUITMENT_TICKS - 1 { - game.tick_update(); - } - assert!(game.board.cells[5][2].get_ref_option_unit().is_none()); - assert_eq!(base(game, (5, 2)).recruitment_progress(), 99); - game.tick_update(); - assert_eq!(base(game, (5, 2)).queued_miners(), 0); - assert_eq!( - game.board.cells[5][2].get_ref_option_unit().get_coords(), - (5, 2) - ); - assert_eq!( - game.board.cells[5][3].get_ref_option_unit().get_coords(), - (5, 3) - ); - game.board.change_focused_cell(MoveFocusedCell::Left); - game.board.toggle_marking(); - game.board.change_focused_cell(MoveFocusedCell::Down); - game.board.toggle_marking(); - assert_eq!( - game.board.cells[5][2] - .get_ref_option_unit() - .get_ref_tasks() - .len(), - 1 - ); + assert_eq!(base(game, (5, 2)).queued_miners(), 0); + assert_eq!( + game.board.cells[5][2].get_ref_option_unit().get_coords(), + (5, 2) + ); + assert_eq!( + game.board.cells[5][3].get_ref_option_unit().get_coords(), + (5, 3) + ); + game.board.change_focused_cell(MoveFocusedCell::Left); + game.board.toggle_marking(); + game.board.change_focused_cell(MoveFocusedCell::Down); + game.board.toggle_marking(); + assert_eq!( + game.board.cells[5][2] + .get_ref_option_unit() + .get_ref_tasks() + .len(), + 1 + ); } #[test] fn arrival_during_training_does_not_overwrite_visitor_and_ready_recruit_spawns_after_departure() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - game.board.cells[5][2].get_mut_option_unit().take(); - assert!(game.recruit_miner((5, 2), Players::Player)); - for _ in 0..100 { + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + game.board.cells[5][2].get_mut_option_unit().take(); + assert!(game.recruit_miner((5, 2), Players::Player)); + for _ in 0..100 { + game.tick_update(); + } + let visitor: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))); + *game.board.cells[5][2].get_mut_option_unit() = Some(visitor.clone()); + for _ in 0..BaseBuilding::RECRUITMENT_TICKS { + game.tick_update(); + } + assert_eq!(base(game, (5, 2)).recruitment_progress(), 100); + assert_eq!(game.board.cells[5][2].get_option_unit(), Some(visitor)); + game.board.cells[5][2].get_mut_option_unit().take(); game.tick_update(); - } - let visitor: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))); - *game.board.cells[5][2].get_mut_option_unit() = Some(visitor.clone()); - for _ in 0..BaseBuilding::RECRUITMENT_TICKS { - game.tick_update(); - } - assert_eq!(base(game, (5, 2)).recruitment_progress(), 100); - assert_eq!(game.board.cells[5][2].get_option_unit(), Some(visitor)); - game.board.cells[5][2].get_mut_option_unit().take(); - game.tick_update(); - assert_eq!(base(game, (5, 2)).queued_miners(), 0); - assert_eq!( - game.board.cells[5][2].get_ref_option_unit().get_owner(), - Players::Player - ); + assert_eq!(base(game, (5, 2)).queued_miners(), 0); + assert_eq!( + game.board.cells[5][2].get_ref_option_unit().get_owner(), + Players::Player + ); } #[test] fn bases_have_independent_queues_costs_and_spawn_ownership() -> () { - let mut app: App = app(); - let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; - for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] { - game.board.cells[coords.0][coords.1] - .get_mut_option_unit() - .take(); - assert!(game.recruit_miner(coords, owner)); - } - assert_eq!(game.player_resources.metal, 10); - assert_eq!(game.enemy_resources.metal, 10); - for _ in 0..BaseBuilding::RECRUITMENT_TICKS { - game.tick_update(); - } - for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] { - assert_eq!(base(game, coords).queued_miners(), 0); - assert_eq!( - game.board.cells[coords.0][coords.1] - .get_ref_option_unit() - .get_owner(), - owner - ); - assert_eq!( - game.board.cells[coords.0][coords.1] - .get_ref_option_unit() - .get_coords(), - coords - ); - } + let mut app: App = app(); + let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish; + for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] { + game.board.cells[coords.0][coords.1] + .get_mut_option_unit() + .take(); + assert!(game.recruit_miner(coords, owner)); + } + assert_eq!(game.player_resources.metal, 10); + assert_eq!(game.enemy_resources.metal, 10); + for _ in 0..BaseBuilding::RECRUITMENT_TICKS { + game.tick_update(); + } + for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] { + assert_eq!(base(game, coords).queued_miners(), 0); + assert_eq!( + game.board.cells[coords.0][coords.1] + .get_ref_option_unit() + .get_owner(), + owner + ); + assert_eq!( + game.board.cells[coords.0][coords.1] + .get_ref_option_unit() + .get_coords(), + coords + ); + } } #[test] fn recruitment_panel_queues_with_enter_shows_ready_state_and_closes_without_losing_queue() -> () { - let mut app: App = app(); - press(&mut app, KeyCode::Left, KeyModifiers::NONE); - assert!( - available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Recruitment) - ); - press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); - assert!(app.states_ref().unwrap().skirmish.recruitment_view); - press(&mut app, KeyCode::Enter, KeyModifiers::NONE); - press(&mut app, KeyCode::Enter, KeyModifiers::NONE); - assert_eq!( - base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), - 1 - ); - assert!( - !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) - ); - press(&mut app, KeyCode::Right, KeyModifiers::NONE); - press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); - assert_eq!( - app - .states_ref() - .unwrap() - .skirmish - .board - .get_focused_cell() - .get_coords(), - (5, 2) - ); - assert!(!app.states_ref().unwrap().skirmish.building_view); - for _ in 0..BaseBuilding::RECRUITMENT_TICKS { - app.states_mut().unwrap().skirmish.tick_update(); - } - let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 50)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Cost: 10 Wood + 15 Metal")); - assert!(text.contains("Queued Miners: 1")); - assert!(text.contains("Current progress: 100%")); - assert!(text.contains("Ready: waiting for free base cell")); - assert!(text.contains("Close recruitment")); - press(&mut app, KeyCode::Esc, KeyModifiers::NONE); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - assert_eq!(app.view, View::Skirmish); - press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); - press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - assert_eq!( - base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), - 1 - ); + let mut app: App = app(); + press(&mut app, KeyCode::Left, KeyModifiers::NONE); + assert!( + available_skirmish_actions(&app.states_ref().unwrap().skirmish) + .contains(&Action::Recruitment) + ); + press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); + assert!(app.states_ref().unwrap().skirmish.recruitment_view); + press(&mut app, KeyCode::Enter, KeyModifiers::NONE); + press(&mut app, KeyCode::Enter, KeyModifiers::NONE); + assert_eq!( + base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), + 1 + ); + assert!( + !available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter) + ); + press(&mut app, KeyCode::Right, KeyModifiers::NONE); + press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL); + assert_eq!( + app.states_ref() + .unwrap() + .skirmish + .board + .get_focused_cell() + .get_coords(), + (5, 2) + ); + assert!(!app.states_ref().unwrap().skirmish.building_view); + for _ in 0..BaseBuilding::RECRUITMENT_TICKS { + app.states_mut().unwrap().skirmish.tick_update(); + } + let mut terminal: Terminal = Terminal::new(TestBackend::new(160, 50)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Cost: 10 Wood + 15 Metal")); + assert!(text.contains("Queued Miners: 1")); + assert!(text.contains("Current progress: 100%")); + assert!(text.contains("Ready: waiting for free base cell")); + assert!(text.contains("Close recruitment")); + press(&mut app, KeyCode::Esc, KeyModifiers::NONE); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + assert_eq!(app.view, View::Skirmish); + press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); + press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + assert_eq!( + base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), + 1 + ); } #[test] fn recruitment_panel_is_unavailable_off_base_and_during_route_selection() -> () { - let mut app: App = app(); - press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - press(&mut app, KeyCode::Left, KeyModifiers::NONE); - press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); - press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - assert!( - app - .states_ref() - .unwrap() - .skirmish - .board - .marked_cells - .marking_cells - ); - press(&mut app, KeyCode::Esc, KeyModifiers::NONE); - press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); - press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL); - assert!(app.exit); + let mut app: App = app(); + press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + press(&mut app, KeyCode::Left, KeyModifiers::NONE); + press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE); + press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + assert!( + app.states_ref() + .unwrap() + .skirmish + .board + .marked_cells + .marking_cells + ); + press(&mut app, KeyCode::Esc, KeyModifiers::NONE); + press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT); + press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL); + assert!(app.exit); } #[test] fn recruitment_fallback_opens_queues_and_closes_on_own_base() -> () { - let mut app: App = app(); - press(&mut app, KeyCode::Left, KeyModifiers::NONE); - press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); - assert!(app.states_ref().unwrap().skirmish.recruitment_view); - press(&mut app, KeyCode::Enter, KeyModifiers::NONE); - press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); - assert!(!app.states_ref().unwrap().skirmish.recruitment_view); - assert_eq!( - base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), - 1 - ); - assert_eq!( - app.states_ref().unwrap().skirmish.player_resources, - Resources::new(40, 10) - ); + let mut app: App = app(); + press(&mut app, KeyCode::Left, KeyModifiers::NONE); + press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); + assert!(app.states_ref().unwrap().skirmish.recruitment_view); + press(&mut app, KeyCode::Enter, KeyModifiers::NONE); + press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE); + assert!(!app.states_ref().unwrap().skirmish.recruitment_view); + assert_eq!( + base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(), + 1 + ); + assert_eq!( + app.states_ref().unwrap().skirmish.player_resources, + Resources::new(40, 10) + ); } diff --git a/tests/resources.rs b/tests/resources.rs index 94c9127..5e5883c 100644 --- a/tests/resources.rs +++ b/tests/resources.rs @@ -2,160 +2,162 @@ use clap::Parser; use ratatui::{Terminal, backend::TestBackend, buffer::Cell, layout::Rect}; use std::sync::mpsc::{Receiver, Sender, channel}; use war_in_tunnels::{ - app::{ - App, GameStates, View, - states::{ - SkirmishState, - skirmish_states::{ - Players, Resources, - resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS}, - structures::{Mine, Ore, Stone, Structure, Structures}, - }, + app::{ + App, GameStates, View, + states::{ + SkirmishState, + skirmish_states::{ + Players, Resources, + resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS}, + structures::{Mine, Ore, Stone, Structure, Structures}, + }, + }, + threads::AudioCmd, }, - threads::AudioCmd, - }, - cli::Cli, + cli::Cli, }; fn game() -> SkirmishState { - let args: Cli = - Cli::try_parse_from(["game", "--starting-wood", "70", "--starting-metal", "40"]).unwrap(); - GameStates::new(&args, &Rect::new(0, 0, 160, 40)).skirmish + let args: Cli = + Cli::try_parse_from(["game", "--starting-wood", "70", "--starting-metal", "40"]).unwrap(); + GameStates::new(&args, &Rect::new(0, 0, 160, 40)).skirmish } #[test] fn resources_start_from_cli_and_legacy_iron_flag_remains_an_alias() -> () { - let game: SkirmishState = game(); - assert_eq!(game.player_resources, Resources::new(70, 40)); - assert_eq!(game.enemy_resources, Resources::new(70, 40)); - let args: Cli = Cli::try_parse_from(["game", "--starting-iron", "33"]).unwrap(); - assert_eq!(args.starting_metal, 33); + let game: SkirmishState = game(); + assert_eq!(game.player_resources, Resources::new(70, 40)); + assert_eq!(game.enemy_resources, Resources::new(70, 40)); + let args: Cli = Cli::try_parse_from(["game", "--starting-iron", "33"]).unwrap(); + assert_eq!(args.starting_metal, 33); } #[test] fn passive_wood_arrives_once_per_interval_without_free_metal() -> () { - let mut game: SkirmishState = game(); - for _ in 1..WOOD_DELIVERY_TICKS { + let mut game: SkirmishState = game(); + for _ in 1..WOOD_DELIVERY_TICKS { + game.tick_update(); + } + assert_eq!(game.player_resources.wood, 70); game.tick_update(); - } - assert_eq!(game.player_resources.wood, 70); - game.tick_update(); - assert_eq!(game.player_resources.wood, 70 + WOOD_DELIVERY_AMOUNT); - for _ in 0..WOOD_DELIVERY_TICKS { - game.tick_update(); - } - assert_eq!(game.player_resources.wood, 70 + 2 * WOOD_DELIVERY_AMOUNT); - assert_eq!(game.player_resources.metal, 40); - assert_eq!(game.enemy_resources, Resources::new(70, 40)); + assert_eq!(game.player_resources.wood, 70 + WOOD_DELIVERY_AMOUNT); + for _ in 0..WOOD_DELIVERY_TICKS { + game.tick_update(); + } + assert_eq!(game.player_resources.wood, 70 + 2 * WOOD_DELIVERY_AMOUNT); + assert_eq!(game.player_resources.metal, 40); + assert_eq!(game.enemy_resources, Resources::new(70, 40)); } #[test] fn ore_is_neutral_and_no_mines_spawn_on_the_board() -> () { - let game: SkirmishState = game(); - let mut deposits: usize = 0; - for row in &game.board.cells { - for cell in row { - let structure: Structures = cell.get_structure(); - assert!(!matches!(structure, Structures::Mine(_))); - if matches!(structure, Structures::Ore(_)) { - deposits += 1; - assert_eq!(structure.get_owner(), Players::Unclaimed); - } + let game: SkirmishState = game(); + let mut deposits: usize = 0; + for row in &game.board.cells { + for cell in row { + let structure: Structures = cell.get_structure(); + assert!(!matches!(structure, Structures::Mine(_))); + if matches!(structure, Structures::Ore(_)) { + deposits += 1; + assert_eq!(structure.get_owner(), Players::Unclaimed); + } + } } - } - assert_eq!(deposits, 2); + assert_eq!(deposits, 2); } #[test] fn mine_can_only_replace_neutral_ore_and_preserves_its_reserve() -> () { - let mut ore: Ore = Ore::new(Players::Unclaimed, 1); - assert_eq!(ore.extract(4999), 4999); - let mut structure: Structures = Structures::Ore(ore); - assert!(structure.build_mine(Players::Player)); - let Structures::Mine(mut mine) = structure else { - panic!("expected mine"); - }; - assert_eq!(mine.get_owner(), Players::Player); - assert_eq!(mine.get_ore_amount(), 1); - assert_eq!(mine.produce(), 1); - assert_eq!(mine.produce(), 0); - assert_eq!(mine.get_ore_amount(), 0); - assert!(!structure.build_mine(Players::Enemy)); - let mut stone: Structures = Structures::Stone(Stone::new()); - assert!(!stone.build_mine(Players::Player)); - assert_eq!(stone, Structures::Stone(Stone::new())); - assert!(Mine::new(Players::Unclaimed, ore).is_none()); - assert!(Mine::new(Players::Player, Ore::new(Players::Enemy, 1)).is_none()); + let mut ore: Ore = Ore::new(Players::Unclaimed, 1); + assert_eq!(ore.extract(4999), 4999); + let mut structure: Structures = Structures::Ore(ore); + assert!(structure.build_mine(Players::Player)); + let Structures::Mine(mut mine) = structure else { + panic!("expected mine"); + }; + assert_eq!(mine.get_owner(), Players::Player); + assert_eq!(mine.get_ore_amount(), 1); + assert_eq!(mine.produce(), 1); + assert_eq!(mine.produce(), 0); + assert_eq!(mine.get_ore_amount(), 0); + assert!(!structure.build_mine(Players::Enemy)); + let mut stone: Structures = Structures::Stone(Stone::new()); + assert!(!stone.build_mine(Players::Player)); + assert_eq!(stone, Structures::Stone(Stone::new())); + assert!(Mine::new(Players::Unclaimed, ore).is_none()); + assert!(Mine::new(Players::Player, Ore::new(Players::Enemy, 1)).is_none()); } #[test] fn mining_credits_each_owner_and_stops_when_deposit_is_empty() -> () { - let mut game: SkirmishState = game(); - for (col, owner) in [(1, Players::Player), (40, Players::Enemy)] { - let mut ore: Ore = Ore::new(Players::Unclaimed, 1); - ore.extract(4999); - let mut structure: Structures = Structures::Ore(ore); - assert!(structure.build_mine(owner)); - game.board.cells[5][col].set_structure(structure); - } - for _ in 1..MINE_PRODUCTION_TICKS { + let mut game: SkirmishState = game(); + for (col, owner) in [(1, Players::Player), (40, Players::Enemy)] { + let mut ore: Ore = Ore::new(Players::Unclaimed, 1); + ore.extract(4999); + let mut structure: Structures = Structures::Ore(ore); + assert!(structure.build_mine(owner)); + game.board.cells[5][col].set_structure(structure); + } + for _ in 1..MINE_PRODUCTION_TICKS { + game.tick_update(); + } + assert_eq!(game.player_resources.metal, 40); game.tick_update(); - } - assert_eq!(game.player_resources.metal, 40); - game.tick_update(); - assert_eq!(game.player_resources.metal, 41); - assert_eq!(game.enemy_resources.metal, 41); - for _ in 0..MINE_PRODUCTION_TICKS { - game.tick_update(); - } - assert_eq!(game.player_resources.metal, 41); - assert_eq!(game.enemy_resources.metal, 41); - let structure: Structures = game.board.cells[5][1].get_structure(); - assert!( - structure - .text() - .iter() - .any(|line: &ratatui::text::Line<'_>| -> bool { line.to_string().contains("Ore: 0/5000") }) - ); + assert_eq!(game.player_resources.metal, 41); + assert_eq!(game.enemy_resources.metal, 41); + for _ in 0..MINE_PRODUCTION_TICKS { + game.tick_update(); + } + assert_eq!(game.player_resources.metal, 41); + assert_eq!(game.enemy_resources.metal, 41); + let structure: Structures = game.board.cells[5][1].get_structure(); + assert!( + structure + .text() + .iter() + .any(|line: &ratatui::text::Line<'_>| -> bool { + line.to_string().contains("Ore: 0/5000") + }) + ); } #[test] fn resource_additions_do_not_overflow() -> () { - let mut resources: Resources = Resources { - wood: u32::MAX - 1, - metal: u32::MAX - 1, - }; - resources.add_wood(5); - resources.add_metal(5); - assert_eq!(resources.wood, u32::MAX); - assert_eq!(resources.metal, u32::MAX); + let mut resources: Resources = Resources { + wood: u32::MAX - 1, + metal: u32::MAX - 1, + }; + resources.add_wood(5); + resources.add_metal(5); + assert_eq!(resources.wood, u32::MAX); + assert_eq!(resources.metal, u32::MAX); } #[test] fn tui_reads_current_resources_instead_of_starting_settings() -> () { - let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); - let (tx, _rx): (Sender, Receiver) = channel(); - let mut app: App = App::new(args, tx); - app.view = View::Skirmish; - app.states = Some(GameStates::new(&app.args, &Rect::new(0, 0, 200, 40))); - app.states_mut().unwrap().skirmish.player_resources = Resources { - wood: 777, - metal: 888, - }; - let mut terminal: Terminal = Terminal::new(TestBackend::new(200, 40)).unwrap(); - terminal - .draw(|frame: &mut ratatui::Frame<'_>| -> () { - frame.render_widget(&mut app, frame.area()); - }) - .unwrap(); - let text: String = terminal - .backend() - .buffer() - .content - .iter() - .map(|cell: &Cell| -> &str { cell.symbol() }) - .collect(); - assert!(text.contains("Wood: 777 (+5/5s)")); - assert!(text.contains("Metal: 888")); - assert!(!text.contains("Iron:")); + let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap(); + let (tx, _rx): (Sender, Receiver) = channel(); + let mut app: App = App::new(args, tx); + app.view = View::Skirmish; + app.states = Some(GameStates::new(&app.args, &Rect::new(0, 0, 200, 40))); + app.states_mut().unwrap().skirmish.player_resources = Resources { + wood: 777, + metal: 888, + }; + let mut terminal: Terminal = Terminal::new(TestBackend::new(200, 40)).unwrap(); + terminal + .draw(|frame: &mut ratatui::Frame<'_>| -> () { + frame.render_widget(&mut app, frame.area()); + }) + .unwrap(); + let text: String = terminal + .backend() + .buffer() + .content + .iter() + .map(|cell: &Cell| -> &str { cell.symbol() }) + .collect(); + assert!(text.contains("Wood: 777 (+5/5s)")); + assert!(text.contains("Metal: 888")); + assert!(!text.contains("Iron:")); } diff --git a/tests/tasks.rs b/tests/tasks.rs index e7af361..068d24a 100644 --- a/tests/tasks.rs +++ b/tests/tasks.rs @@ -1,123 +1,123 @@ use war_in_tunnels::app::states::skirmish_states::{ - Players, - tasks::{DiggingTask, MovingTask, Task, Tasks}, - units::{MinerUnit, Unit}, + Players, + tasks::{DiggingTask, MovingTask, Task, Tasks}, + units::{MinerUnit, Unit}, }; #[test] fn digging_progress_accumulates_and_stops_at_completion() { - let mut task = DiggingTask::new([(5, 3)].into(), 40.0); - assert!(!task.is_finished()); - task.progress(); - assert_eq!(task.get_progress(), 40.0); - assert!(!task.is_finished()); - for _ in 0..3 { + let mut task = DiggingTask::new([(5, 3)].into(), 40.0); + assert!(!task.is_finished()); task.progress(); - } - assert_eq!(task.get_progress(), 100.0); - assert!(task.is_finished()); + assert_eq!(task.get_progress(), 40.0); + assert!(!task.is_finished()); + for _ in 0..3 { + task.progress(); + } + assert_eq!(task.get_progress(), 100.0); + assert!(task.is_finished()); } #[test] fn advancing_path_resets_progress_and_handles_empty_path() { - let mut task = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); - task.progress(); - task.pop_front(); - assert_eq!(task.current_target(), Some((5, 4))); - assert_eq!(task.get_progress(), 0.0); - task.pop_front(); - task.pop_front(); - assert_eq!(task.current_target(), None); - assert!(task.get_path().is_empty()); + let mut task = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); + task.progress(); + task.pop_front(); + assert_eq!(task.current_target(), Some((5, 4))); + assert_eq!(task.get_progress(), 0.0); + task.pop_front(); + task.pop_front(); + assert_eq!(task.current_target(), None); + assert!(task.get_path().is_empty()); } #[test] fn miners_keep_tasks_in_submission_order() { - let mut miner = MinerUnit::new(Players::Player, (5, 2)); - for target in [(5, 3), (6, 3)] { - miner.set_task(Tasks::Digging(DiggingTask::new([target].into(), 1.0))); - } - assert_eq!(miner.get_ref_tasks().len(), 2); - assert_eq!( - *miner - .get_mut_tasks() - .pop_front() - .unwrap() - .get_path_front_coords(), - (5, 3) - ); - assert_eq!(*miner.get_ref_tasks()[0].get_path_front_coords(), (6, 3)); + let mut miner = MinerUnit::new(Players::Player, (5, 2)); + for target in [(5, 3), (6, 3)] { + miner.set_task(Tasks::Digging(DiggingTask::new([target].into(), 1.0))); + } + assert_eq!(miner.get_ref_tasks().len(), 2); + assert_eq!( + *miner + .get_mut_tasks() + .pop_front() + .unwrap() + .get_path_front_coords(), + (5, 3) + ); + assert_eq!(*miner.get_ref_tasks()[0].get_path_front_coords(), (6, 3)); } #[test] fn movement_progress_is_separate_from_digging_and_resets_for_next_cell() { - let mut task = MovingTask::new([(5, 3), (5, 4)].into()); - assert_eq!(task.get_movement_progress(), None); - assert_eq!(task.get_progress(), 0.0); - assert!(!task.is_finished()); - for _ in 0..46 { + let mut task = MovingTask::new([(5, 3), (5, 4)].into()); + assert_eq!(task.get_movement_progress(), None); + assert_eq!(task.get_progress(), 0.0); + assert!(!task.is_finished()); + for _ in 0..46 { + task.progress(); + } + assert_eq!(task.get_movement_progress(), Some(50)); + for _ in 46..91 { + task.progress(); + } + assert_eq!(task.get_movement_progress(), Some(100)); + assert_eq!(task.get_progress(), 100.0); + assert!(task.is_finished()); task.progress(); - } - assert_eq!(task.get_movement_progress(), Some(50)); - for _ in 46..91 { - task.progress(); - } - assert_eq!(task.get_movement_progress(), Some(100)); - assert_eq!(task.get_progress(), 100.0); - assert!(task.is_finished()); - task.progress(); - assert_eq!(task.get_progress(), 100.0); - task.pop_front(); - assert_eq!(task.get_movement_progress(), None); - assert_eq!(task.get_progress(), 0.0); - assert!(!task.is_finished()); + assert_eq!(task.get_progress(), 100.0); + task.pop_front(); + assert_eq!(task.get_movement_progress(), None); + assert_eq!(task.get_progress(), 0.0); + assert!(!task.is_finished()); } #[test] fn overall_digging_progress_counts_completed_cells_and_current_step() -> () { - let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); - assert_eq!(task.get_total_progress(), 0.0); - task.progress(); - assert_eq!(task.get_progress(), 25.0); - assert_eq!(task.get_total_progress(), 12.5); - task.pop_front(); - assert_eq!(task.get_progress(), 0.0); - assert_eq!(task.get_total_progress(), 50.0); - task.progress(); - assert_eq!(task.get_total_progress(), 62.5); - task.pop_front(); - assert_eq!(task.get_total_progress(), 100.0); - let empty: DiggingTask = DiggingTask::new([].into(), 1.0); - assert_eq!(empty.get_total_progress(), 100.0); + let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); + assert_eq!(task.get_total_progress(), 0.0); + task.progress(); + assert_eq!(task.get_progress(), 25.0); + assert_eq!(task.get_total_progress(), 12.5); + task.pop_front(); + assert_eq!(task.get_progress(), 0.0); + assert_eq!(task.get_total_progress(), 50.0); + task.progress(); + assert_eq!(task.get_total_progress(), 62.5); + task.pop_front(); + assert_eq!(task.get_total_progress(), 100.0); + let empty: DiggingTask = DiggingTask::new([].into(), 1.0); + assert_eq!(empty.get_total_progress(), 100.0); } #[test] fn task_conversion_keeps_completed_steps_and_original_length() -> () { - let mut digging: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); - digging.pop_front(); - let mut moving: MovingTask = MovingTask::from_digging(digging); - assert_eq!(moving.get_total_steps(), 2); - assert_eq!(moving.get_total_progress(), 50.0); - for _ in 0..91 { - moving.progress(); - } - assert_eq!(moving.get_total_progress(), 100.0); - let mut digging: DiggingTask = DiggingTask::from_moving(moving, 25.0); - assert_eq!(digging.get_total_steps(), 2); - assert_eq!(digging.get_total_progress(), 50.0); - digging.progress(); - assert_eq!(digging.get_total_progress(), 62.5); + let mut digging: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); + digging.pop_front(); + let mut moving: MovingTask = MovingTask::from_digging(digging); + assert_eq!(moving.get_total_steps(), 2); + assert_eq!(moving.get_total_progress(), 50.0); + for _ in 0..91 { + moving.progress(); + } + assert_eq!(moving.get_total_progress(), 100.0); + let mut digging: DiggingTask = DiggingTask::from_moving(moving, 25.0); + assert_eq!(digging.get_total_steps(), 2); + assert_eq!(digging.get_total_progress(), 50.0); + digging.progress(); + assert_eq!(digging.get_total_progress(), 62.5); } #[test] fn digging_queue_formats_step_total_and_pending_progress() -> () { - let mut miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2)); - let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); - task.pop_front(); - task.progress(); - miner.set_task(Tasks::Digging(task)); - miner.set_task(Tasks::Moving(MovingTask::new([(5, 5)].into()))); - let lines: Vec> = miner.get_tasks_formatted(); - assert!(lines[1].to_string().contains("Step: 25% | Total: 62%")); - assert!(lines[3].to_string().contains("Step: 0% | Total: 0%")); + let mut miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2)); + let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0); + task.pop_front(); + task.progress(); + miner.set_task(Tasks::Digging(task)); + miner.set_task(Tasks::Moving(MovingTask::new([(5, 5)].into()))); + let lines: Vec> = miner.get_tasks_formatted(); + assert!(lines[1].to_string().contains("Step: 25% | Total: 62%")); + assert!(lines[3].to_string().contains("Step: 0% | Total: 0%")); }