diff --git a/docs/AGENTS.md b/docs/AGENTS.md index a0bda57..9b4245e 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -31,6 +31,7 @@ Audio wymaga ALSA i urządzenia odtwarzania; do pracy nad TUI używaj `--disable ## Konwencje kodu +- Formatowanie określa `rustfmt.toml`: styl Rust 2024, wcięcia po 2 spacje, szerokość docelowa 100 znaków i zakończenia LF. Stosuj `nix develop -c cargo fmt --all`; kontroluj przez `nix develop -c cargo fmt --all -- --check`. - Jawne adnotacje typów są OBOWIĄZKOWE w nowym i zmienianym kodzie: przy zmiennych i stałych, polach struktur, parametrach funkcji i domknięć oraz typach zwracanych (także `-> ()` dla funkcji bez wyniku). Nie polegaj na inferencji typu tam, gdzie można podać typ jawnie. Dotyczy to również testów. Jest to zasada jawnego typowania; rzutowania wartości (`as`, `From`, `TryFrom`) stosuj tylko wtedy, gdy potrzebna jest rzeczywista konwersja. - Rust 2024, jedna biblioteka i jeden binarny punkt wejścia; bez workspace. - Wykorzystuj istniejące `App`, `GameStates`, `BoardState`, akcje i widgety. diff --git a/docs/CODE_MAP.md b/docs/CODE_MAP.md index 51a242c..85757c9 100644 --- a/docs/CODE_MAP.md +++ b/docs/CODE_MAP.md @@ -21,6 +21,7 @@ Wszystkie ścieżki poniżej są względem katalogu głównego repozytorium. | Pomocniczy layout | `src/app/helpers/` | Obszar komórek, tytuły bloków i tekst opcji menu | | Wątki | `src/app/threads/{events,tick,audio}.rs` | Crossterm, zegar 33 ms, Rodio i komendy audio | | Środowisko | `Cargo.toml`, `flake.nix`, `default.nix`, `flake.lock` | Zależności, toolchain Fenix, ALSA, pakowanie przez Naersk lub buildRustPackage | +| Formatowanie | `rustfmt.toml` | Rustfmt, styl 2024, wcięcia po 2 spacje, docelowo 100 znaków i zakończenia LF | | Zasoby | `soundtracks/default/`, `ui_draft/` | Dwa pliki OGG i tekstowe szkice ekranów | Ścieżki skrócone `skirmish_states/` oznaczają `src/app/states/skirmish_states/`. Nie ma osobnego silnika gry, bazy danych ani warstwy sieciowej. Zależności Cargo: Ratatui/Crossterm, Clap, Rodio, log/simplelog i once_cell. diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 12207f3..c34ac43 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1,5 +1,11 @@ # Dziennik prac +## 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. + +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. + ## 2026-10-18 — przekazanie kontekstu na kolejną sesję Użytkownik zaakceptował aktualny stan. Nie ma ustalonego kolejnego zadania. Na start kolejnej sesji przeczytaj `docs/AGENTS.md`, `docs/CODE_MAP.md` i `docs/TESTING.md`; instrukcje w `docs/` mogą wymagać jawnego wskazania agentowi. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..59a1c1c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,11 @@ +edition = "2024" +style_edition = "2024" + +max_width = 100 +hard_tabs = false +tab_spaces = 2 +newline_style = "Unix" + +reorder_imports = true +reorder_modules = true +use_small_heuristics = "Default" diff --git a/src/app/app.rs b/src/app/app.rs index 7939a6c..318d497 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -1,103 +1,102 @@ 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 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_ref(&self) -> Option<&GameStates> { + self.states.as_ref() + } - pub fn states_mut(&mut self) -> Option<&mut GameStates> { - self.states.as_mut() - } + 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))?; + 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(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 Ok((_, _)) = resize_rx.try_recv() { + let window_area: Rect = self.window_area; if let Some(state) = self.states_mut() { - state.skirmish.tick_update(); + state + .skirmish + .board + .change_resize(&window_area, state.skirmish.side_panel); } + } - Ok(()) + 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 ab320d1..d18421a 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::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 1598c90..7571357 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 85311aa..8513d65 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 4ff35eb..1f1ba82 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 15fe734..14430ac 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 f9c223c..6fb58d1 100644 --- a/src/app/keybindings/keybindings.rs +++ b/src/app/keybindings/keybindings.rs @@ -9,76 +9,76 @@ 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, - /// 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, + /// 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. @@ -87,20 +87,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. @@ -108,218 +108,218 @@ 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::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::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. @@ -330,23 +330,23 @@ 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 { - 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 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)); - } + 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 + None } /// Determines the size of the largest group among a slice of actions. @@ -355,13 +355,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 7f1c895..d8274e2 100644 --- a/src/app/keybindings/main_menu.rs +++ b/src/app/keybindings/main_menu.rs @@ -1,38 +1,36 @@ 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 7c7ec9b..fdf2478 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::skirmish_keybindings; diff --git a/src/app/keybindings/skirmish.rs b/src/app/keybindings/skirmish.rs index e929202..d992625 100644 --- a/src/app/keybindings/skirmish.rs +++ b/src/app/keybindings/skirmish.rs @@ -1,98 +1,98 @@ use crate::app::{ - App, - keybindings::{Action, common_keybindings, event_to_action}, - states::skirmish_states::{BoardState, MoveFocusedCell, ZoomLevel}, + App, + keybindings::{Action, common_keybindings, event_to_action}, + states::skirmish_states::{BoardState, MoveFocusedCell, ZoomLevel}, }; use ratatui::{crossterm::event::KeyEvent, layout::Rect}; pub fn skirmish_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); - 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(); - } - } - 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::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); - } - _ => (), + 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::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 8eed5ba..44f755c 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -1,62 +1,62 @@ use crate::{ - app::states::{ - MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState, - skirmish_states::BoardState, - }, - cli::Cli, + app::states::{ + MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState, + skirmish_states::BoardState, + }, + 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, - }, - 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_iron: args.starting_iron.clone(), - 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, + }, + 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_iron: args.starting_iron.clone(), + 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 d603b59..597d06a 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 ff3fd18..bda9d1d 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 db7fb5e..3491acf 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_iron: 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_iron: 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 7af21bf..15e885e 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 ff63a09..f9aad8f 100644 --- a/src/app/states/skirmish.rs +++ b/src/app/states/skirmish.rs @@ -2,19 +2,19 @@ use crate::app::states::skirmish_states::BoardState; #[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 id: usize, + pub name: &'static str, + pub board: BoardState, + pub side_panel: bool, + pub turn_counter: u64, } impl SkirmishState { - pub fn tick_update(&mut self) { - self.board.advance_turn(); + pub fn tick_update(&mut self) { + self.board.advance_turn(); - // if self.board.is_victory() {} + // if self.board.is_victory() {} - self.turn_counter += 1; - } + self.turn_counter += 1; + } } diff --git a/src/app/states/skirmish_states/board.rs b/src/app/states/skirmish_states/board.rs index 29aabcc..8635090 100644 --- a/src/app/states/skirmish_states/board.rs +++ b/src/app/states/skirmish_states/board.rs @@ -1,463 +1,467 @@ use crate::app::{ - helpers::cells_area_helper, - states::{ - FocusedCell, Offset, - skirmish_states::{ - CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel, - structures::{BaseBuilding, 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, 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 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); + 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 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 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 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 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 focused_cell: FocusedCell = FocusedCell::new((map_height) / 2, 3, map_height, map_width); - let mut cells: Vec> = Vec::new(); + 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); + 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 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; + 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::Player, 1)) - } else if enemy_ore { - Structures::Ore(Ore::new(Players::Enemy, 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(); + 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::Player, 1)) + } else if enemy_ore { + Structures::Ore(Ore::new(Players::Enemy, 1)) } else { - self.end_marking_cells(false); - } + 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); } - 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); + let marked_cells: MarkedCells = MarkedCells::new(); - if !cell.get_marked() { - cell.set_marked(true); - } + Self { + cells_area, + cell_width, + cell_height, + map_width, + map_height, + cols, + rows, + vertical_offset, + horizontal_offset, + cells, + zoom_level, + focused_cell, + marked_cells, + } + } - self.marked_cells - .marked_cells - .push_back((new_cell.0, new_cell.1)); + 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); } - pub fn pop_marked_cell(&mut self) { - if self.marked_cells.marked_cells.len() < 2 { - return; - } + self + .marked_cells + .marked_cells + .push_back((new_cell.0, new_cell.1)); + } - 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 pop_marked_cell(&mut self) { + if self.marked_cells.marked_cells.len() < 2 { + return; } - pub fn start_marking_cells(&mut self) { - let (row, col) = self.focused_cell.get_coords(); + 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 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); + 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); } - 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); + 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| { + matches!( + cell.get_structure(), + Structures::Tunnel(_) | Structures::Base(_) + ) + }) + } + + 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; } - 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); + 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(); + // Previously planned stone may already be a tunnel when this task starts. + task = match (task, structure) { + (Tasks::Digging(dig), Structures::Tunnel(_) | Structures::Base(_)) => { + Tasks::Moving(MovingTask::from_digging(dig)) } - } - - 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| { - matches!( - cell.get_structure(), - Structures::Tunnel(_) | Structures::Base(_) - ) - }) - } - - 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); + (Tasks::Moving(moving), Structures::Stone(_)) => { + Tasks::Digging(DiggingTask::from_moving(moving, unit.get_digging_power())) } - } - } - - 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; + (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)); + } } - 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(); - // Previously planned stone may already be a tunnel when this task starts. - task = match (task, structure) { - (Tasks::Digging(dig), Structures::Tunnel(_) | Structures::Base(_)) => { - 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), Structures::Tunnel(_) | Structures::Base(_)) => { - 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); - } + (Tasks::Moving(moving), Structures::Tunnel(_) | Structures::Base(_)) => { + moving.progress(); + if moving.is_finished() { + destination = (target_row, target_col); + moving.pop_front(); + } } - unit.set_coords(destination); - *self - .get_mut_cell(destination.0, destination.1) - .get_mut_option_unit() = Some(unit); + _ => {} + } } + 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); } + } } diff --git a/src/app/states/skirmish_states/focused_cell.rs b/src/app/states/skirmish_states/focused_cell.rs index 1bcef2a..ebc0cb5 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 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 get_coords(&self) -> (usize, usize) { - (self.row, self.col) - } + (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); - } - } - - (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 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 03c8ff5..66d312f 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 1b0d6c2..b035125 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 1d5c008..8eb008f 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 b7895d2..f6c1270 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/structures/base.rs b/src/app/states/skirmish_states/structures/base.rs index d91b2f1..f2a6154 100644 --- a/src/app/states/skirmish_states/structures/base.rs +++ b/src/app/states/skirmish_states/structures/base.rs @@ -3,57 +3,57 @@ use ratatui::style::Color; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct BaseBuilding { - durability: u16, - stress: u8, - owner: Players, - level: u8, + durability: u16, + stress: u8, + owner: Players, + level: u8, } impl BaseBuilding { - pub fn new(owner: Players) -> Self { - Self { - durability: 1500, - stress: 0, - owner, - level: 1, - } + pub fn new(owner: Players) -> Self { + Self { + durability: 1500, + stress: 0, + owner, + level: 1, } + } } impl Structure for BaseBuilding { - fn get_tag(&self) -> char { - 'B' - } + 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_color(&self) -> Color { + match self.owner { + Players::Player => Color::LightBlue, + Players::Enemy => Color::LightRed, + Players::Unclaimed => Color::LightGreen, } + } - fn get_level(&self) -> char { - (self.level + b'0') as char - } + fn get_level(&self) -> char { + (self.level + b'0') as char + } - fn get_durability(&self) -> u16 { - self.durability - } + fn get_durability(&self) -> u16 { + self.durability + } - fn get_max_durability(&self) -> u16 { - 1500 - } + fn get_max_durability(&self) -> u16 { + 1500 + } - fn get_stress(&self) -> u8 { - self.stress - } + fn get_stress(&self) -> u8 { + self.stress + } - fn get_name(&self) -> &'static str { - "Base" - } + fn get_name(&self) -> &'static str { + "Base" + } - fn get_owner(&self) -> Players { - self.owner - } + fn get_owner(&self) -> Players { + self.owner + } } diff --git a/src/app/states/skirmish_states/structures/ore.rs b/src/app/states/skirmish_states/structures/ore.rs index fb84ee6..bfdac64 100644 --- a/src/app/states/skirmish_states/structures/ore.rs +++ b/src/app/states/skirmish_states/structures/ore.rs @@ -1,94 +1,94 @@ 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 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_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_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_durability(&self) -> u16 { + self.durability + } + + fn get_level(&self) -> char { + if self.level != 0 { + (self.level + b'0') as char + } else { + ' ' } + } - fn get_durability(&self) -> u16 { - self.durability - } + fn get_max_durability(&self) -> u16 { + 1000 + } - fn get_level(&self) -> char { - if self.level != 0 { - (self.level + b'0') as char - } else { - ' ' - } - } + fn get_name(&self) -> &'static str { + "Ore" + } - fn get_max_durability(&self) -> u16 { - 1000 - } + fn get_owner(&self) -> Players { + self.owner + } - fn get_name(&self) -> &'static str { - "Ore" - } + fn get_stress(&self) -> u8 { + self.stress + } - fn get_owner(&self) -> Players { - self.owner - } + fn text(&self) -> Vec> { + let mut lines: Vec> = Structure::base_text(self); - fn get_stress(&self) -> u8 { - self.stress - } + lines.push(Line::from_iter([ + "Ore: ".gray(), + self.get_ore_amount().to_string().cyan(), + "/".gray(), + self.get_max_ore_amount().to_string().cyan(), + ])); - 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 - } + lines + } } diff --git a/src/app/states/skirmish_states/structures/stone.rs b/src/app/states/skirmish_states/structures/stone.rs index ffb9fb8..5f6583a 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 db4843d..3dc2b57 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(), - ])); - } - - lines + if level != ' ' { + lines.push(Line::from_iter([ + "Level: ".gray(), + level.to_string().cyan(), + ])); } - fn text(&self) -> Vec> { - self.base_text() - } + lines + } + + 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 de90e74..2beb86b 100644 --- a/src/app/states/skirmish_states/structures/structures_enum.rs +++ b/src/app/states/skirmish_states/structures/structures_enum.rs @@ -1,78 +1,78 @@ use crate::app::states::skirmish_states::{ - Players, - structures::{BaseBuilding, Ore, Stone, Structure, Tunnel}, + Players, + structures::{BaseBuilding, Ore, Stone, Structure, Tunnel}, }; 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), + Base(BaseBuilding), + Tunnel(Tunnel), + Stone(Stone), + Ore(Ore), } impl Structures { - #[inline] - fn s(&self) -> &dyn Structure { - match self { - Self::Base(b) => b, - Self::Tunnel(t) => t, - Self::Stone(s) => s, - Self::Ore(o) => o, - } + #[inline] + fn s(&self) -> &dyn Structure { + match self { + Self::Base(b) => b, + Self::Tunnel(t) => t, + Self::Stone(s) => s, + Self::Ore(o) => o, } + } } 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 b4ae309..b741c43 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 71cc00e..a0022c1 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 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 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() - } + 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 91b3161..74cc164 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 c69965d..44e0242 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; - } - 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 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) + } - 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 ebc5c56..8f21085 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 ccc5ddd..314883e 100644 --- a/src/app/states/skirmish_states/units/miner.rs +++ b/src/app/states/skirmish_states/units/miner.rs @@ -1,124 +1,125 @@ use ratatui::{style::Stylize, text::Line}; use crate::app::states::skirmish_states::{ - Players, - tasks::{Task, Tasks}, - units::Unit, + Players, + 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 fn new(owner: Players, coords: (usize, usize)) -> Self { - Self { - owner, - hp: 100, - can_dig: true, - digging_power: 0.55, - coords, - tasks: VecDeque::new(), - } + 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; } - fn col_to_letters(&self, col: usize) -> String { - let mut col: usize = col + 1; - let mut letters: Vec = Vec::new(); + letters.iter().rev().collect() + } - 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) - } + 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_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 14f669a..613b53a 100644 --- a/src/app/states/skirmish_states/units/unit_trait.rs +++ b/src/app/states/skirmish_states/units/unit_trait.rs @@ -1,82 +1,82 @@ 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 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_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 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_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 !have_any_tasks { - lines.push(Line::from("Tasks Queue: ".gray())); - for task in tasks { - lines.push(task); - } - } - - lines + if !have_any_tasks { + lines.push(Line::from("Tasks Queue: ".gray())); + for task in tasks { + lines.push(task); + } } - fn text(&self) -> Vec> { - self.base_text() - } + lines + } + + 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 3af5e3a..8f34803 100644 --- a/src/app/states/skirmish_states/units/units_enum.rs +++ b/src/app/states/skirmish_states/units/units_enum.rs @@ -1,147 +1,150 @@ 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_tag(&self) -> char { - self.u().get_tag() - } + 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 is_unit(&self) -> bool { - self.as_ref().map_or(false, |u| u.is_unit()) - } + 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_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_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_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_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_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_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_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 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 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_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_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 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)) - } + 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 e9df983..e954293 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 32f9030..ffa2147 100644 --- a/src/app/threads/audio.rs +++ b/src/app/threads/audio.rs @@ -1,175 +1,175 @@ 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}, - process::exit, - sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver}, + collections::HashMap, + fs::read, + io::{BufReader, Cursor}, + 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 base: String = format!("soundtracks/{}", folder); - let mut cache: HashMap> = HashMap::new(); + let base: String = format!("soundtracks/{}", folder); + let mut cache: HashMap> = HashMap::new(); - macro_rules! load { - ($part:ident, $file:expr) => { - let path = format!("{}/{}", base, $file); - cache.insert( - SoundrackParts::$part, - read(&path).expect(&format!("cannot read {}", path)), - ); - }; - } + macro_rules! load { + ($part:ident, $file:expr) => { + let path = format!("{}/{}", base, $file); + cache.insert( + SoundrackParts::$part, + read(&path).expect(&format!("cannot read {}", path)), + ); + }; + } - 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); - } + 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); } - { - let mut guard: RwLockWriteGuard< - '_, - HashMap>>>, - > = PRELOADED_SOUNDTRACKS - .write() - .map_err(|e| { - eprintln!("RwLock poisoned: {}", e); - exit(1); - }) - .expect("lock issue"); + let folder_name: &str = match soundtrack { + Soundtrack::Default => "default", + }; - if let Some(soundtrack_cache) = guard.get(soundtrack) { - let data: &Vec = soundtrack_cache.get(part).expect("part missing in cache"); + let new_cache: Arc>> = + Arc::new(load_soundtrack_folder(folder_name)); - return Decoder::new_looped(BufReader::new(Cursor::new(data.clone()))) - .expect("decoder issue") - .amplify(0.20); - } + let data: &Vec = new_cache.get(&part).expect("part missing after load"); - let folder_name: &str = match soundtrack { - Soundtrack::Default => "default", - }; + guard.insert(*soundtrack, Arc::clone(&new_cache)); - 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) - } + 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 e9d8781..0aab851 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; - } - } - Ok(Event::Resize(cols, rows)) => { - if resize_tx.send((cols, rows)).is_err() { - break; - } - } - Ok(_) => {} - Err(_) => continue, + 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, } + } } diff --git a/src/app/threads/tick.rs b/src/app/threads/tick.rs index 58a457c..4137a6f 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 elapsed: Duration = Instant::now().elapsed(); - if interval > elapsed { - thread::sleep(interval - elapsed); - } + 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); + } + } } diff --git a/src/app/view.rs b/src/app/view.rs index b7d29b5..4235991 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 129b387..790c9bd 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 a3a63b6..ddc2f18 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 64d7d06..52e3dbf 100644 --- a/src/app/views/skirmish.rs +++ b/src/app/views/skirmish.rs @@ -1,153 +1,153 @@ use crate::app::{ - App, - helpers::block_title_helper, - keybindings::{Action, count_largest_group}, - states::skirmish_states::BoardState, - widgets::{BoardWidget, KeybindingsWidget, SidePanelWidget}, + App, + helpers::block_title_helper, + keybindings::{Action, count_largest_group}, + states::skirmish_states::BoardState, + widgets::{BoardWidget, KeybindingsWidget, SidePanelWidget}, }; use ratatui::{ - buffer::Buffer, - layout::{Alignment, Constraint, Layout, Margin, Rect}, - style::{Color, Stylize}, - text::Line, - widgets::{Block, Borders, Padding, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Margin, Rect}, + style::{Color, Stylize}, + text::Line, + widgets::{Block, Borders, Padding, Paragraph, Widget}, }; const ACTIONS: &[Action] = &[ - Action::Up, - Action::Down, - Action::Left, - Action::Right, - Action::ScrollUp, - Action::ScrollDown, - Action::ScrollLeft, - Action::ScrollRight, - Action::ZoomIn, - Action::ZoomOut, - Action::VolumeUp, - Action::VolumeDown, - Action::Space, - Action::Backspace, - Action::Delete, - Action::Mute, - Action::Tab, - Action::Quit, - Action::Quit2, - Action::Esc, + Action::Up, + Action::Down, + Action::Left, + Action::Right, + Action::ScrollUp, + Action::ScrollDown, + Action::ScrollLeft, + Action::ScrollRight, + Action::ZoomIn, + Action::ZoomOut, + Action::VolumeUp, + Action::VolumeDown, + Action::Space, + Action::Backspace, + Action::Delete, + Action::Mute, + Action::Tab, + Action::Quit, + Action::Quit2, + Action::Esc, ]; pub fn skirmish_layout(area: Rect) -> [Rect; 3] { - Layout::vertical([ - Constraint::Length(4), - Constraint::Fill(1), - Constraint::Length(count_largest_group(&ACTIONS) + 2), - ]) - .areas(area) + Layout::vertical([ + Constraint::Length(4), + Constraint::Fill(1), + Constraint::Length(count_largest_group(&ACTIONS) + 2), + ]) + .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 Some(states) = app.states_ref() else { + return; + }; - let board: &BoardState = &states.skirmish.board; + let board: &BoardState = &states.skirmish.board; - let [title_area, main_area, keybindings_area] = skirmish_layout(area); + 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: {} (+{}) | ", states.settings.starting_wood, 5), - format!("Iron: {} (+{}) | ", states.settings.starting_iron, 1), - 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), - ]), - ]); + { + let lines: Vec> = Vec::from_iter([ + Line::raw("War in Tunnels").yellow(), + Line::from_iter([ + format!("Wood: {} (+{}) | ", states.settings.starting_wood, 5), + format!("Iron: {} (+{}) | ", states.settings.starting_iron, 1), + 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); + 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 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); - } - } - - { - 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/widgets/board.rs b/src/app/widgets/board.rs index d4bdb85..884b4b4 100644 --- a/src/app/widgets/board.rs +++ b/src/app/widgets/board.rs @@ -1,45 +1,46 @@ 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 1846251..6bdf0c1 100644 --- a/src/app/widgets/cell.rs +++ b/src/app/widgets/cell.rs @@ -1,161 +1,162 @@ 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 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 set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self + 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() } + } - 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 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 + } - 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 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) - } + 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 6829b1e..b64407e 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,123 +18,118 @@ 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>, + /// Paragraphs, one per used `Group`, containing the formatted keybinding lines. + grouped: 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 { - let keybindings: Vec> = - actions.iter().map(|a| binding_for(*a)).collect(); + /// 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 { + 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 used_groups: HashSet = keybindings + .iter() + .filter_map(|b| b.map(|binding| binding.group)) + .collect(); - let mut grouped_keybindings: Vec> = Vec::new(); + let mut grouped_keybindings: Vec> = Vec::new(); - for group in Group::value_variants().iter() { - if !used_groups.contains(group) { - continue; - } + 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| { - Line::from_iter([ - kb.symbol.red().bold(), - " - ".into(), - kb.description.into(), - ]) - }) - }) - .collect(); + 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| { + Line::from_iter([kb.symbol.red().bold(), " - ".into(), kb.description.into()]) + }) + }) + .collect(); - let height_diff: u16 = - area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16); + 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 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)); + let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0)); - if !grouped_keybindings.is_empty() { - block = block.borders(Borders::LEFT); - } + 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, - } + grouped_keybindings.push( + Paragraph::new(grouped_lines) + .alignment(Alignment::Center) + .block(block), + ); } + + Self { + grouped: grouped_keybindings, + } + } } 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 into equal horizontal slices—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 into equal horizontal slices—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; - } - - 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 base: u16 = if count == 0 { 0 } else { 100u16 / count }; - - let constraints: Vec = vec![Constraint::Percentage(base); count as usize]; - - 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); - } + 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 base: u16 = if count == 0 { 0 } else { 100u16 / count }; + + let constraints: Vec = vec![Constraint::Percentage(base); count as usize]; + + 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 7163049..91d13ca 100644 --- a/src/app/widgets/side_panel.rs +++ b/src/app/widgets/side_panel.rs @@ -1,108 +1,108 @@ 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, - widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget}, + buffer::Buffer, + layout::{Alignment, Constraint, Layout, Margin, Rect}, + style::Color, + 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, + } + } -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; } - fn col_to_letters(&self) -> String { - let mut col: usize = self.coords.1 + 1; - let mut letters: Vec = Vec::new(); + letters.iter().rev().collect() + } - while col > 0 { - letters.push((b'A' + ((col - 1) % 26) as u8) as char); - col = (col - 1) / 26; - } + 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) + } - letters.iter().rev().collect() - } + 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_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)] - } + 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); - Paragraph::new(self.structure.text()) - .alignment(Alignment::Left) - .block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan)) - .render(structure_area, buf); + Paragraph::new(self.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.unit.is_unit() { + let mut unit_name: String = self.unit.get_name().to_string(); - if self.is_unit_marked { - unit_name = format!("> {} <", unit_name); - } + 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); - } + 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 65a1fe7..1533713 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,13 @@ use crate::{ - app::{ - states::{ - PerkDecks, - skirmish_states::{GameMode, ZoomLevel}, - }, - threads::Soundtrack, - view::View, + app::{ + states::{ + PerkDecks, + skirmish_states::{GameMode, ZoomLevel}, }, - logs::init_logger, + threads::Soundtrack, + view::View, + }, + 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 iron (1–150). - #[arg( + /// Starting amount of iron (1–150). + #[arg( long, short = 'i', help = "Starting iron", @@ -117,10 +117,10 @@ pub struct Cli { default_value = "25", value_parser = value_parser!(u16).range(1..=150) )] - pub starting_iron: u16, + pub starting_iron: u16, - /// Maximum amount of units (49–149). - #[arg( + /// Maximum amount of units (49–149). + #[arg( long, short = 'L', help = "Supply limit", @@ -128,10 +128,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", @@ -139,10 +139,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", @@ -150,10 +150,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", @@ -161,34 +161,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` @@ -196,13 +196,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 @@ -217,15 +217,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 842582b..05f3511 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 389daba..c8cdcbc 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 2cde209..0041d50 100644 --- a/tests/app.rs +++ b/tests/app.rs @@ -1,161 +1,162 @@ 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 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 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); - } + 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")); - } - assert_eq!(app.states, before); + 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); + } } #[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 5d26b10..3be0db7 100644 --- a/tests/audio.rs +++ b/tests/audio.rs @@ -1,19 +1,19 @@ use rodio::Decoder; use std::{ - fs::read, - io::{BufReader, Cursor}, + fs::read, + io::{BufReader, Cursor}, }; 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); } diff --git a/tests/board.rs b/tests/board.rs index 0581ae4..26c03b8 100644 --- a/tests/board.rs +++ b/tests/board.rs @@ -1,151 +1,151 @@ 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(_) - )); - } - assert_eq!( - board - .cells - .iter() - .flatten() - .filter(|c| c.get_ref_option_unit().is_some()) - .count(), - 2 - ); + 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(0, 0).get_structure(), - Structures::Stone(_) + board.get_ref_cell(5, ore_col).get_structure(), + Structures::Ore(_) )); + } + 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(_) + )); } #[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 e8189ae..7fbd2ac 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-iron", 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 (flag, min, max) in [ + ("--map-width", 36, 108), + ("--map-height", 11, 39), + ("--starting-wood", 1, 150), + ("--starting-iron", 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}" + ); + } + } } #[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")] { - 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)); - } + for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] { 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") - ); + .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") + ); } diff --git a/tests/digging.rs b/tests/digging.rs index 722fecd..e40444c 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}, - }, - }, - threads::{AudioCmd, TICK_MS}, + 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}, + }, }, - cli::Cli, + threads::{AudioCmd, TICK_MS}, + }, + 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)]); + 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 { 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 { - game.tick_update(); - } - for col in [3, 4] { - assert!(matches!( - game.board.cells[5][col].get_structure(), - Structures::Tunnel(_) - )); - } + } + for col in [3, 4] { assert!(matches!( - game.board.cells[5][2].get_structure(), - Structures::Base(_) + game.board.cells[5][col].get_structure(), + Structures::Tunnel(_) )); - 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!(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()); } #[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); - } - 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() - ); + 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() + ); + } } #[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 { - 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)); + 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, 3).get_coords(), (5, 3)); - assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty()); + } + 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()); } #[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); - } - 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()); + 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()); + } } #[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); - 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)); + 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(); + .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("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); } diff --git a/tests/tasks.rs b/tests/tasks.rs index 068d24a..e7af361 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()); + 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 { task.progress(); - 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()); + } + 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 { - 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()); + 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_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_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()); } #[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%")); }