generated from GarandPLG/rust-flake-template
Update formatting to use 4‑space indentation
- Adjust rustfmt.toml to set tab_spaces = 4. - Reformat imports, struct definitions, function bodies, and match arms across the project to match the new style (aligned fields, added spacing, etc.). - Ensure all code complies with the updated rustfmt settings.
This commit is contained in:
1 parent
a981663152
commit
73f544d5d6
66 files changed
+5378
-5373
No files matched your search
+1
-1
@@ -112,7 +112,7 @@ Zaktualizowano instrukcje agentów, mapę i opis testów. Nie zmieniano mechanik
|
||||
|
||||
## 2026-10-19 — formatowanie Rust z wcięciami po 2 spacje
|
||||
|
||||
Dodano `rustfmt.toml` z uzgodnionymi opcjami: edycja i styl 2024, `max_width = 100`, `hard_tabs = false`, `tab_spaces = 2`, zakończenia Unix, sortowanie importów i modułów oraz domyślne heurystyki krótkich konstrukcji. Zastosowano `nix develop -c cargo fmt --all` do źródeł i testów (56 zmienionych plików Rust). Zaktualizowano instrukcje agentów i mapę konfiguracji projektu.
|
||||
Dodano `rustfmt.toml` z uzgodnionymi opcjami: edycja i styl 2024, `max_width = 100`, `hard_tabs = false`, `tab_spaces = 4`, zakończenia Unix, sortowanie importów i modułów oraz domyślne heurystyki krótkich konstrukcji. Zastosowano `nix develop -c cargo fmt --all` do źródeł i testów (56 zmienionych plików Rust). Zaktualizowano instrukcje agentów i mapę konfiguracji projektu.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo fmt --all -- --check`, `cargo check --all-targets` i `cargo test --all-targets` zakończone sukcesem; 45 testów zaliczonych. Nie wprowadzano zmian mechanik ani zależności. Nie powtarzano Clippy ani ręcznej sesji TUI przy zmianie formatowania.
|
||||
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ style_edition = "2024"
|
||||
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
tab_spaces = 4
|
||||
newline_style = "Unix"
|
||||
|
||||
reorder_imports = true
|
||||
|
||||
+85
-84
@@ -1,102 +1,103 @@
|
||||
use crate::{
|
||||
app::{GameStates, handle_keybindings, threads::AudioCmd, view::View},
|
||||
cli::Cli,
|
||||
app::{GameStates, handle_keybindings, threads::AudioCmd, view::View},
|
||||
cli::Cli,
|
||||
};
|
||||
use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect};
|
||||
use std::{
|
||||
io::Result,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
io::Result,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct App {
|
||||
pub exit: bool,
|
||||
pub view: View,
|
||||
pub window_area: Rect,
|
||||
pub args: Cli,
|
||||
pub states: Option<GameStates>,
|
||||
pub audio_tx: Sender<AudioCmd>,
|
||||
pub exit: bool,
|
||||
pub view: View,
|
||||
pub window_area: Rect,
|
||||
pub args: Cli,
|
||||
pub states: Option<GameStates>,
|
||||
pub audio_tx: Sender<AudioCmd>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(args: Cli, audio_tx: Sender<AudioCmd>) -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
view: args.view,
|
||||
window_area: Rect::default(),
|
||||
args: args,
|
||||
states: None,
|
||||
audio_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn states_ref(&self) -> Option<&GameStates> {
|
||||
self.states.as_ref()
|
||||
}
|
||||
|
||||
pub fn states_mut(&mut self) -> Option<&mut GameStates> {
|
||||
self.states.as_mut()
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
&mut self,
|
||||
terminal: &mut DefaultTerminal,
|
||||
input_rx: Receiver<KeyEvent>,
|
||||
resize_rx: Receiver<(u16, u16)>,
|
||||
tick_rx: Receiver<()>,
|
||||
) -> Result<()> {
|
||||
while !self.exit {
|
||||
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
|
||||
|
||||
if let Ok(key) = input_rx.try_recv() {
|
||||
self.handle_key_event(key)?;
|
||||
}
|
||||
|
||||
if let Ok((_, _)) = resize_rx.try_recv() {
|
||||
let window_area: Rect = self.window_area;
|
||||
if let Some(state) = self.states_mut() {
|
||||
state
|
||||
.skirmish
|
||||
.board
|
||||
.change_resize(&window_area, state.skirmish.side_panel);
|
||||
pub fn new(args: Cli, audio_tx: Sender<AudioCmd>) -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
view: args.view,
|
||||
window_area: Rect::default(),
|
||||
args: args,
|
||||
states: None,
|
||||
audio_tx,
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(()) = tick_rx.try_recv() {
|
||||
self.update()?;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame<'_>) {
|
||||
let window_area: Rect = frame.area();
|
||||
|
||||
if window_area.width != self.window_area.width || window_area.height != self.window_area.height
|
||||
{
|
||||
self.window_area = window_area;
|
||||
if self.states.is_none() {
|
||||
self.states = Some(GameStates::new(&self.args, &self.window_area))
|
||||
}
|
||||
pub fn states_ref(&self) -> Option<&GameStates> {
|
||||
self.states.as_ref()
|
||||
}
|
||||
|
||||
frame.render_widget(self, window_area);
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
|
||||
handle_keybindings(self, key_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> Result<()> {
|
||||
if let Some(state) = self.states_mut() {
|
||||
state.skirmish.tick_update();
|
||||
pub fn states_mut(&mut self) -> Option<&mut GameStates> {
|
||||
self.states.as_mut()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn run(
|
||||
&mut self,
|
||||
terminal: &mut DefaultTerminal,
|
||||
input_rx: Receiver<KeyEvent>,
|
||||
resize_rx: Receiver<(u16, u16)>,
|
||||
tick_rx: Receiver<()>,
|
||||
) -> Result<()> {
|
||||
while !self.exit {
|
||||
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
|
||||
|
||||
if let Ok(key) = input_rx.try_recv() {
|
||||
self.handle_key_event(key)?;
|
||||
}
|
||||
|
||||
if let Ok((_, _)) = resize_rx.try_recv() {
|
||||
let window_area: Rect = self.window_area;
|
||||
if let Some(state) = self.states_mut() {
|
||||
state
|
||||
.skirmish
|
||||
.board
|
||||
.change_resize(&window_area, state.skirmish.side_panel);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(()) = tick_rx.try_recv() {
|
||||
self.update()?;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame<'_>) {
|
||||
let window_area: Rect = frame.area();
|
||||
|
||||
if window_area.width != self.window_area.width
|
||||
|| window_area.height != self.window_area.height
|
||||
{
|
||||
self.window_area = window_area;
|
||||
if self.states.is_none() {
|
||||
self.states = Some(GameStates::new(&self.args, &self.window_area))
|
||||
}
|
||||
}
|
||||
|
||||
frame.render_widget(self, window_area);
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
|
||||
handle_keybindings(self, key_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> Result<()> {
|
||||
if let Some(state) = self.states_mut() {
|
||||
state.skirmish.tick_update();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use ratatui::{
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
/// Creates a styled title `Line` for a UI block.
|
||||
@@ -9,27 +9,27 @@ use ratatui::{
|
||||
/// the spans, so the returned `Line` owns all its data and does not borrow from
|
||||
/// the caller.
|
||||
pub fn block_title_helper(
|
||||
texts: &[(String, Color)],
|
||||
separator: Option<&'static str>,
|
||||
texts: &[(String, Color)],
|
||||
separator: Option<&'static str>,
|
||||
) -> Line<'static> {
|
||||
let mut line: Line<'static> = Line::default();
|
||||
let mut line: Line<'static> = Line::default();
|
||||
|
||||
line.spans.push(Span::from("[ ").gray());
|
||||
line.spans.push(Span::from("[ ").gray());
|
||||
|
||||
for (i, (text, color)) in texts.iter().enumerate() {
|
||||
let span: Span<'_> = Span::styled(text.clone(), Style::new().fg(*color));
|
||||
line.spans.push(span);
|
||||
for (i, (text, color)) in texts.iter().enumerate() {
|
||||
let span: Span<'_> = Span::styled(text.clone(), Style::new().fg(*color));
|
||||
line.spans.push(span);
|
||||
|
||||
if let Some(sep) = separator {
|
||||
if i + 1 < texts.len() {
|
||||
line.spans.push(Span::from(sep).gray());
|
||||
}
|
||||
if let Some(sep) = separator {
|
||||
if i + 1 < texts.len() {
|
||||
line.spans.push(Span::from(sep).gray());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line.spans.push(Span::from(" ]").gray());
|
||||
line.spans.push(Span::from(" ]").gray());
|
||||
|
||||
line
|
||||
line
|
||||
}
|
||||
|
||||
/// Convenience wrapper for a single‑title line.
|
||||
@@ -38,5 +38,5 @@ pub fn block_title_helper(
|
||||
/// forwards it to `block_title_helper`. Because `block_title_helper` clones the
|
||||
/// string, the temporary slice is safe.
|
||||
pub fn block_single_title_helper(text: String, color: Color) -> Line<'static> {
|
||||
block_title_helper(&[(text, color)], None)
|
||||
block_title_helper(&[(text, color)], None)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+345
-345
@@ -9,80 +9,80 @@ use std::collections::HashMap;
|
||||
/// character key and carries the actual character pressed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Action {
|
||||
/// Request the application to quit.
|
||||
Quit,
|
||||
/// Alternate quit action.
|
||||
Quit2,
|
||||
/// Move the selection cursor up.
|
||||
Up,
|
||||
/// Move the selection cursor down.
|
||||
Down,
|
||||
/// Move the selection cursor left.
|
||||
Left,
|
||||
/// Move the selection cursor right.
|
||||
Right,
|
||||
/// Scroll up without moving the cursor.
|
||||
ScrollUp,
|
||||
/// Scroll down without moving the cursor.
|
||||
ScrollDown,
|
||||
/// Scroll left without moving the cursor.
|
||||
ScrollLeft,
|
||||
/// Scroll right without moving the cursor.
|
||||
ScrollRight,
|
||||
/// Toggle selecting cell.
|
||||
Space,
|
||||
/// Open or close the construction view.
|
||||
Building,
|
||||
/// Open or close the recruitment view.
|
||||
Recruitment,
|
||||
/// Unmark last selected cell.
|
||||
Backspace,
|
||||
/// Cancel marking cell.
|
||||
Delete,
|
||||
/// Submit or confirm the current choice.
|
||||
Enter,
|
||||
/// Return to the main menu or previous screen.
|
||||
Esc,
|
||||
/// Zoom the view in.
|
||||
ZoomIn,
|
||||
/// Zoom the view out.
|
||||
ZoomOut,
|
||||
/// Open side panel.
|
||||
Tab,
|
||||
/// Toggle skirmish board/skills view.
|
||||
ShiftTab,
|
||||
/// Mute music.
|
||||
Mute,
|
||||
/// Volume up.
|
||||
VolumeUp,
|
||||
/// Volume down.
|
||||
VolumeDown,
|
||||
/// Matches any character key; the inner `char` is the actual key pressed.
|
||||
WildCard(char),
|
||||
/// Request the application to quit.
|
||||
Quit,
|
||||
/// Alternate quit action.
|
||||
Quit2,
|
||||
/// Move the selection cursor up.
|
||||
Up,
|
||||
/// Move the selection cursor down.
|
||||
Down,
|
||||
/// Move the selection cursor left.
|
||||
Left,
|
||||
/// Move the selection cursor right.
|
||||
Right,
|
||||
/// Scroll up without moving the cursor.
|
||||
ScrollUp,
|
||||
/// Scroll down without moving the cursor.
|
||||
ScrollDown,
|
||||
/// Scroll left without moving the cursor.
|
||||
ScrollLeft,
|
||||
/// Scroll right without moving the cursor.
|
||||
ScrollRight,
|
||||
/// Toggle selecting cell.
|
||||
Space,
|
||||
/// Open or close the construction view.
|
||||
Building,
|
||||
/// Open or close the recruitment view.
|
||||
Recruitment,
|
||||
/// Unmark last selected cell.
|
||||
Backspace,
|
||||
/// Cancel marking cell.
|
||||
Delete,
|
||||
/// Submit or confirm the current choice.
|
||||
Enter,
|
||||
/// Return to the main menu or previous screen.
|
||||
Esc,
|
||||
/// Zoom the view in.
|
||||
ZoomIn,
|
||||
/// Zoom the view out.
|
||||
ZoomOut,
|
||||
/// Open side panel.
|
||||
Tab,
|
||||
/// Toggle skirmish board/skills view.
|
||||
ShiftTab,
|
||||
/// Mute music.
|
||||
Mute,
|
||||
/// Volume up.
|
||||
VolumeUp,
|
||||
/// Volume down.
|
||||
VolumeDown,
|
||||
/// Matches any character key; the inner `char` is the actual key pressed.
|
||||
WildCard(char),
|
||||
}
|
||||
|
||||
/// Logical groups of key bindings used for organising help screens or
|
||||
/// configuration sections.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum Group {
|
||||
/// Bindings that combine `Ctrl` with movement keys.
|
||||
CtrlMovement,
|
||||
/// Plain movement bindings.
|
||||
Movement,
|
||||
/// Scroll bindings.
|
||||
Scroll,
|
||||
/// Selection bindings.
|
||||
Select,
|
||||
/// Text input related bindings.
|
||||
Input,
|
||||
/// Zoom related bindings.
|
||||
Zoom,
|
||||
/// Music related bindings.
|
||||
Music,
|
||||
/// Opens/Closes/Toggles elements in current view.
|
||||
Opener,
|
||||
/// Quit related bindings.
|
||||
Quit,
|
||||
/// Bindings that combine `Ctrl` with movement keys.
|
||||
CtrlMovement,
|
||||
/// Plain movement bindings.
|
||||
Movement,
|
||||
/// Scroll bindings.
|
||||
Scroll,
|
||||
/// Selection bindings.
|
||||
Select,
|
||||
/// Text input related bindings.
|
||||
Input,
|
||||
/// Zoom related bindings.
|
||||
Zoom,
|
||||
/// Music related bindings.
|
||||
Music,
|
||||
/// Opens/Closes/Toggles elements in current view.
|
||||
Opener,
|
||||
/// Quit related bindings.
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Description of a single key binding.
|
||||
@@ -91,20 +91,20 @@ pub enum Group {
|
||||
/// together with metadata used for displaying help information.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct KeyBinding {
|
||||
/// The logical action performed when this binding is triggered.
|
||||
pub action: Action,
|
||||
/// The raw `crossterm::event::KeyCode` that identifies the key.
|
||||
pub code: KeyCode,
|
||||
/// The kind of key event (crossterm::event::KeyEventKind).
|
||||
pub kind: KeyEventKind,
|
||||
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
|
||||
pub modifiers: KeyModifiers,
|
||||
/// The group this binding belongs to, useful for categorising help.
|
||||
pub group: Group,
|
||||
/// Human‑readable symbol shown in UI/help screens.
|
||||
pub symbol: &'static str,
|
||||
/// Short description of what the binding does.
|
||||
pub description: &'static str,
|
||||
/// The logical action performed when this binding is triggered.
|
||||
pub action: Action,
|
||||
/// The raw `crossterm::event::KeyCode` that identifies the key.
|
||||
pub code: KeyCode,
|
||||
/// The kind of key event (crossterm::event::KeyEventKind).
|
||||
pub kind: KeyEventKind,
|
||||
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
|
||||
pub modifiers: KeyModifiers,
|
||||
/// The group this binding belongs to, useful for categorising help.
|
||||
pub group: Group,
|
||||
/// Human‑readable symbol shown in UI/help screens.
|
||||
pub symbol: &'static str,
|
||||
/// Short description of what the binding does.
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
/// Complete list of all key bindings used by the application.
|
||||
@@ -112,245 +112,245 @@ pub struct KeyBinding {
|
||||
/// The slice is ordered primarily for readability; the runtime lookup scans
|
||||
/// this slice linearly, which is acceptable given the modest size of the list.
|
||||
static KEYBINDINGS: &[KeyBinding] = &[
|
||||
KeyBinding {
|
||||
action: Action::Recruitment,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::ALT,
|
||||
group: Group::Opener,
|
||||
symbol: "Alt + Space / r",
|
||||
description: "Recruit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Recruitment,
|
||||
code: KeyCode::Char('r'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Opener,
|
||||
symbol: "r",
|
||||
description: "Recruit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Building,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::Opener,
|
||||
symbol: "Ctrl + Space",
|
||||
description: "Buildings",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit,
|
||||
code: KeyCode::Char('q'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "q",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit2,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::Quit,
|
||||
symbol: "Ctrl + c",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Up,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↑",
|
||||
description: "Up",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Down,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↓",
|
||||
description: "Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Left,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "←",
|
||||
description: "Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Right,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "→",
|
||||
description: "Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollUp,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↑",
|
||||
description: "Scroll Up ",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollDown,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↓",
|
||||
description: "Scroll Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollLeft,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ←",
|
||||
description: "Scroll Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollRight,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + →",
|
||||
description: "Scroll Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Space,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Space",
|
||||
description: "Marking cells",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Backspace,
|
||||
code: KeyCode::Backspace,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Backspace",
|
||||
description: "Unmark cell",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Delete,
|
||||
code: KeyCode::Delete,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Delete",
|
||||
description: "Cancel marking",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Enter,
|
||||
code: KeyCode::Enter,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Enter",
|
||||
description: "Submit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Esc,
|
||||
code: KeyCode::Esc,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "Esc",
|
||||
description: "Main menu",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomIn,
|
||||
code: KeyCode::Char(','),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ",",
|
||||
description: "Zoom in",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomOut,
|
||||
code: KeyCode::Char('.'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ".",
|
||||
description: "Zoom out",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Mute,
|
||||
code: KeyCode::Char('m'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "m",
|
||||
description: "🔈",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeUp,
|
||||
code: KeyCode::Char('b'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "b",
|
||||
description: "🔊",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeDown,
|
||||
code: KeyCode::Char('n'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "n",
|
||||
description: "🔉",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Tab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Opener,
|
||||
symbol: "Tab",
|
||||
description: "Toggle side panel",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ShiftTab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
group: Group::Opener,
|
||||
symbol: "Shift + Tab",
|
||||
description: "Toggle Map/Skills window",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::WildCard('_'),
|
||||
code: KeyCode::Char('_'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Input,
|
||||
symbol: "[All]",
|
||||
description: "All keyboard characters",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Recruitment,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::ALT,
|
||||
group: Group::Opener,
|
||||
symbol: "Alt + Space / r",
|
||||
description: "Recruit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Recruitment,
|
||||
code: KeyCode::Char('r'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Opener,
|
||||
symbol: "r",
|
||||
description: "Recruit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Building,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::Opener,
|
||||
symbol: "Ctrl + Space",
|
||||
description: "Buildings",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit,
|
||||
code: KeyCode::Char('q'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "q",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit2,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::Quit,
|
||||
symbol: "Ctrl + c",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Up,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↑",
|
||||
description: "Up",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Down,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↓",
|
||||
description: "Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Left,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "←",
|
||||
description: "Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Right,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "→",
|
||||
description: "Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollUp,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↑",
|
||||
description: "Scroll Up ",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollDown,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↓",
|
||||
description: "Scroll Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollLeft,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ←",
|
||||
description: "Scroll Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollRight,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + →",
|
||||
description: "Scroll Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Space,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Space",
|
||||
description: "Marking cells",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Backspace,
|
||||
code: KeyCode::Backspace,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Backspace",
|
||||
description: "Unmark cell",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Delete,
|
||||
code: KeyCode::Delete,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Delete",
|
||||
description: "Cancel marking",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Enter,
|
||||
code: KeyCode::Enter,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Enter",
|
||||
description: "Submit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Esc,
|
||||
code: KeyCode::Esc,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "Esc",
|
||||
description: "Main menu",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomIn,
|
||||
code: KeyCode::Char(','),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ",",
|
||||
description: "Zoom in",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomOut,
|
||||
code: KeyCode::Char('.'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ".",
|
||||
description: "Zoom out",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Mute,
|
||||
code: KeyCode::Char('m'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "m",
|
||||
description: "🔈",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeUp,
|
||||
code: KeyCode::Char('b'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "b",
|
||||
description: "🔊",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeDown,
|
||||
code: KeyCode::Char('n'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "n",
|
||||
description: "🔉",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Tab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Opener,
|
||||
symbol: "Tab",
|
||||
description: "Toggle side panel",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ShiftTab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
group: Group::Opener,
|
||||
symbol: "Shift + Tab",
|
||||
description: "Toggle Map/Skills window",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::WildCard('_'),
|
||||
code: KeyCode::Char('_'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Input,
|
||||
symbol: "[All]",
|
||||
description: "All keyboard characters",
|
||||
},
|
||||
];
|
||||
|
||||
/// Returns the `KeyBinding` associated with the given `Action`, if one exists.
|
||||
pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
|
||||
KEYBINDINGS.iter().find(|b| b.action == action)
|
||||
KEYBINDINGS.iter().find(|b| b.action == action)
|
||||
}
|
||||
|
||||
/// Converts a raw `KeyEvent` into an `Action` if a matching binding is found.
|
||||
@@ -361,30 +361,30 @@ pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
|
||||
/// was pressed. Returns `None` when the event does not correspond to any known
|
||||
/// action.
|
||||
pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
|
||||
// Legacy terminals encode Ctrl+Space as NUL instead of a modified space.
|
||||
if event.code == KeyCode::Null
|
||||
&& event.kind == KeyEventKind::Press
|
||||
&& (event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::CONTROL)
|
||||
{
|
||||
return Some(Action::Building);
|
||||
}
|
||||
if let Some(b) = KEYBINDINGS
|
||||
.iter()
|
||||
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
|
||||
{
|
||||
return Some(b.action);
|
||||
}
|
||||
|
||||
if KEYBINDINGS
|
||||
.iter()
|
||||
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
|
||||
{
|
||||
if let KeyCode::Char(c) = event.code {
|
||||
return Some(Action::WildCard(c));
|
||||
// Legacy terminals encode Ctrl+Space as NUL instead of a modified space.
|
||||
if event.code == KeyCode::Null
|
||||
&& event.kind == KeyEventKind::Press
|
||||
&& (event.modifiers == KeyModifiers::NONE || event.modifiers == KeyModifiers::CONTROL)
|
||||
{
|
||||
return Some(Action::Building);
|
||||
}
|
||||
if let Some(b) = KEYBINDINGS
|
||||
.iter()
|
||||
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
|
||||
{
|
||||
return Some(b.action);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
if KEYBINDINGS
|
||||
.iter()
|
||||
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
|
||||
{
|
||||
if let KeyCode::Char(c) = event.code {
|
||||
return Some(Action::WildCard(c));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Determines the size of the largest group among a slice of actions.
|
||||
@@ -393,13 +393,13 @@ pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
|
||||
/// 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<Group, u16> = HashMap::new();
|
||||
let mut group_counts: HashMap<Group, u16> = 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)
|
||||
}
|
||||
@@ -1,36 +1,38 @@
|
||||
use crate::app::{
|
||||
App, View,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
App, View,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
};
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
|
||||
pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
|
||||
if let Some(action) = event_to_action(&event) {
|
||||
common_keybindings(app, action);
|
||||
if let Some(action) = event_to_action(&event) {
|
||||
common_keybindings(app, action);
|
||||
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match action {
|
||||
Action::Up => {
|
||||
states.main_menu.selected_view = states.main_menu.selected_view.saturating_sub(1).max(1);
|
||||
}
|
||||
Action::Down => {
|
||||
states.main_menu.selected_view = states.main_menu.selected_view.saturating_add(1).min(3);
|
||||
}
|
||||
Action::Space => {
|
||||
let selected_view: usize = states.main_menu.selected_view;
|
||||
match action {
|
||||
Action::Up => {
|
||||
states.main_menu.selected_view =
|
||||
states.main_menu.selected_view.saturating_sub(1).max(1);
|
||||
}
|
||||
Action::Down => {
|
||||
states.main_menu.selected_view =
|
||||
states.main_menu.selected_view.saturating_add(1).min(3);
|
||||
}
|
||||
Action::Space => {
|
||||
let selected_view: usize = states.main_menu.selected_view;
|
||||
|
||||
if selected_view == 1 {
|
||||
app.view = View::Skirmish;
|
||||
} else if selected_view == 2 {
|
||||
app.view = View::PerkDecks;
|
||||
} else if selected_view == 3 {
|
||||
app.view = View::SkillsConfig;
|
||||
if selected_view == 1 {
|
||||
app.view = View::Skirmish;
|
||||
} else if selected_view == 2 {
|
||||
app.view = View::PerkDecks;
|
||||
} else if selected_view == 3 {
|
||||
app.view = View::SkillsConfig;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ mod skirmish;
|
||||
|
||||
pub use default::{common_keybindings, default_keybindings};
|
||||
pub use keybindings::{
|
||||
Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action,
|
||||
Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action,
|
||||
};
|
||||
pub use main_menu::main_menu_keybindings;
|
||||
pub use skirmish::{available_skirmish_actions, skirmish_keybindings};
|
||||
+193
-190
@@ -1,213 +1,216 @@
|
||||
use crate::app::{
|
||||
App,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, FocusedCell, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
App,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, FocusedCell, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
use ratatui::{crossterm::event::KeyEvent, layout::Rect};
|
||||
|
||||
pub fn available_skirmish_actions(state: &SkirmishState) -> Vec<Action> {
|
||||
if state.recruitment_view {
|
||||
let mut actions: Vec<Action> = vec![Action::Recruitment, Action::Esc, Action::Quit2];
|
||||
if state.can_recruit_miner(state.board.get_focused_cell().get_coords(), Players::Player) {
|
||||
actions.push(Action::Enter);
|
||||
if state.recruitment_view {
|
||||
let mut actions: Vec<Action> = vec![Action::Recruitment, Action::Esc, Action::Quit2];
|
||||
if state.can_recruit_miner(state.board.get_focused_cell().get_coords(), Players::Player) {
|
||||
actions.push(Action::Enter);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
if state.building_view {
|
||||
let mut actions: Vec<Action> = vec![Action::Building, Action::Esc, Action::Quit2];
|
||||
let coords: (usize, usize) = state.board.get_focused_cell().get_coords();
|
||||
if let Structures::MineConstruction(site) =
|
||||
state.board.get_ref_cell(coords.0, coords.1).get_structure()
|
||||
&& site.get_owner() == Players::Player
|
||||
{
|
||||
actions.push(Action::Delete);
|
||||
} else if state.can_start_mine_construction(coords, Players::Player) {
|
||||
actions.push(Action::Enter);
|
||||
if state.building_view {
|
||||
let mut actions: Vec<Action> = vec![Action::Building, Action::Esc, Action::Quit2];
|
||||
let coords: (usize, usize) = state.board.get_focused_cell().get_coords();
|
||||
if let Structures::MineConstruction(site) =
|
||||
state.board.get_ref_cell(coords.0, coords.1).get_structure()
|
||||
&& site.get_owner() == Players::Player
|
||||
{
|
||||
actions.push(Action::Delete);
|
||||
} else if state.can_start_mine_construction(coords, Players::Player) {
|
||||
actions.push(Action::Enter);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
let mut actions: Vec<Action> = vec![Action::Up, Action::Down, Action::Left, Action::Right];
|
||||
if state.board.marked_cells.marking_cells {
|
||||
actions.extend([
|
||||
Action::Space,
|
||||
Action::Backspace,
|
||||
Action::Delete,
|
||||
Action::Esc,
|
||||
Action::Tab,
|
||||
Action::Quit2,
|
||||
]);
|
||||
return actions;
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
let mut actions: Vec<Action> = vec![Action::Up, Action::Down, Action::Left, Action::Right];
|
||||
if state.board.marked_cells.marking_cells {
|
||||
actions.extend([
|
||||
Action::Space,
|
||||
Action::Backspace,
|
||||
Action::Delete,
|
||||
Action::Esc,
|
||||
Action::Tab,
|
||||
Action::Quit2,
|
||||
Action::ScrollUp,
|
||||
Action::ScrollDown,
|
||||
Action::ScrollLeft,
|
||||
Action::ScrollRight,
|
||||
Action::Tab,
|
||||
Action::Building,
|
||||
Action::Mute,
|
||||
Action::Quit,
|
||||
Action::Esc,
|
||||
]);
|
||||
return actions;
|
||||
}
|
||||
actions.extend([
|
||||
Action::ScrollUp,
|
||||
Action::ScrollDown,
|
||||
Action::ScrollLeft,
|
||||
Action::ScrollRight,
|
||||
Action::Tab,
|
||||
Action::Building,
|
||||
Action::Mute,
|
||||
Action::Quit,
|
||||
Action::Esc,
|
||||
]);
|
||||
if state.board.zoom_level != ZoomLevel::ZoomedIn {
|
||||
actions.push(Action::ZoomIn);
|
||||
}
|
||||
if state.board.zoom_level != ZoomLevel::ZoomedOut {
|
||||
actions.push(Action::ZoomOut);
|
||||
}
|
||||
let cell: &FocusedCell = state.board.get_focused_cell();
|
||||
let (row, col): (usize, usize) = cell.get_coords();
|
||||
if matches!(state.board.get_ref_cell(row, col).get_structure(), Structures::Base(base) if base.get_owner() == Players::Player)
|
||||
{
|
||||
actions.push(Action::Recruitment);
|
||||
}
|
||||
let unit: &Option<Units> = state.board.get_ref_cell(row, col).get_ref_option_unit();
|
||||
if unit.get_owner() == Players::Player && unit.get_can_dig() {
|
||||
actions.push(Action::Space);
|
||||
}
|
||||
actions
|
||||
if state.board.zoom_level != ZoomLevel::ZoomedIn {
|
||||
actions.push(Action::ZoomIn);
|
||||
}
|
||||
if state.board.zoom_level != ZoomLevel::ZoomedOut {
|
||||
actions.push(Action::ZoomOut);
|
||||
}
|
||||
let cell: &FocusedCell = state.board.get_focused_cell();
|
||||
let (row, col): (usize, usize) = cell.get_coords();
|
||||
if matches!(state.board.get_ref_cell(row, col).get_structure(), Structures::Base(base) if base.get_owner() == Players::Player)
|
||||
{
|
||||
actions.push(Action::Recruitment);
|
||||
}
|
||||
let unit: &Option<Units> = state.board.get_ref_cell(row, col).get_ref_option_unit();
|
||||
if unit.get_owner() == Players::Player && unit.get_can_dig() {
|
||||
actions.push(Action::Space);
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) -> () {
|
||||
if let Some(action) = event_to_action(key_event) {
|
||||
let Some(states) = app.states_mut() else {
|
||||
common_keybindings(app, action);
|
||||
return;
|
||||
};
|
||||
if states.skirmish.recruitment_view {
|
||||
match action {
|
||||
Action::Recruitment | Action::Esc => states.skirmish.recruitment_view = false,
|
||||
Action::Enter => {
|
||||
let coords: (usize, usize) = states.skirmish.board.get_focused_cell().get_coords();
|
||||
states.skirmish.recruit_miner(coords, Players::Player);
|
||||
if let Some(action) = event_to_action(key_event) {
|
||||
let Some(states) = app.states_mut() else {
|
||||
common_keybindings(app, action);
|
||||
return;
|
||||
};
|
||||
if states.skirmish.recruitment_view {
|
||||
match action {
|
||||
Action::Recruitment | Action::Esc => states.skirmish.recruitment_view = false,
|
||||
Action::Enter => {
|
||||
let coords: (usize, usize) =
|
||||
states.skirmish.board.get_focused_cell().get_coords();
|
||||
states.skirmish.recruit_miner(coords, Players::Player);
|
||||
}
|
||||
_ => common_keybindings(app, action),
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => common_keybindings(app, action),
|
||||
}
|
||||
return;
|
||||
}
|
||||
if states.skirmish.building_view {
|
||||
match action {
|
||||
Action::Building | Action::Esc => states.skirmish.building_view = false,
|
||||
Action::Enter => {
|
||||
let coords: (usize, usize) = states.skirmish.board.get_focused_cell().get_coords();
|
||||
states
|
||||
.skirmish
|
||||
.start_mine_construction(coords, Players::Player);
|
||||
if states.skirmish.building_view {
|
||||
match action {
|
||||
Action::Building | Action::Esc => states.skirmish.building_view = false,
|
||||
Action::Enter => {
|
||||
let coords: (usize, usize) =
|
||||
states.skirmish.board.get_focused_cell().get_coords();
|
||||
states
|
||||
.skirmish
|
||||
.start_mine_construction(coords, Players::Player);
|
||||
}
|
||||
Action::Delete => {
|
||||
let coords: (usize, usize) =
|
||||
states.skirmish.board.get_focused_cell().get_coords();
|
||||
states
|
||||
.skirmish
|
||||
.cancel_mine_construction(coords, Players::Player);
|
||||
}
|
||||
_ => common_keybindings(app, action),
|
||||
}
|
||||
return;
|
||||
}
|
||||
Action::Delete => {
|
||||
let coords: (usize, usize) = states.skirmish.board.get_focused_cell().get_coords();
|
||||
states
|
||||
.skirmish
|
||||
.cancel_mine_construction(coords, Players::Player);
|
||||
if action == Action::Esc && states.skirmish.board.marked_cells.marking_cells {
|
||||
states.skirmish.board.marked_cells.marking_cells = false;
|
||||
states.skirmish.board.end_marking_cells(true);
|
||||
return;
|
||||
}
|
||||
let available: bool = available_skirmish_actions(&states.skirmish).contains(&action);
|
||||
common_keybindings(app, action);
|
||||
if !available {
|
||||
return;
|
||||
}
|
||||
_ => common_keybindings(app, action),
|
||||
}
|
||||
return;
|
||||
}
|
||||
if action == Action::Esc && states.skirmish.board.marked_cells.marking_cells {
|
||||
states.skirmish.board.marked_cells.marking_cells = false;
|
||||
states.skirmish.board.end_marking_cells(true);
|
||||
return;
|
||||
}
|
||||
let available: bool = available_skirmish_actions(&states.skirmish).contains(&action);
|
||||
common_keybindings(app, action);
|
||||
if !available {
|
||||
return;
|
||||
}
|
||||
|
||||
let window_area: Rect = app.window_area;
|
||||
let window_area: Rect = app.window_area;
|
||||
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let board: &mut BoardState = &mut states.skirmish.board;
|
||||
let board: &mut BoardState = &mut states.skirmish.board;
|
||||
|
||||
match action {
|
||||
Action::Up => {
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.prev();
|
||||
match action {
|
||||
Action::Up => {
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Down => {
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
}
|
||||
Action::Left => {
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Right => {
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.next();
|
||||
}
|
||||
}
|
||||
Action::ScrollUp => {
|
||||
board.vertical_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
Action::ScrollDown => {
|
||||
board.vertical_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
}
|
||||
Action::ScrollLeft => {
|
||||
board.horizontal_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
}
|
||||
Action::ScrollRight => {
|
||||
board.horizontal_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
Action::ZoomIn => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedIn);
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.next();
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
},
|
||||
Action::ZoomOut => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedOut);
|
||||
board.vertical_offset.prev();
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {}
|
||||
},
|
||||
Action::Space => board.toggle_marking(),
|
||||
Action::Building => states.skirmish.building_view = true,
|
||||
Action::Recruitment => states.skirmish.recruitment_view = true,
|
||||
Action::Backspace => board.pop_marked_cell(),
|
||||
Action::Delete => {
|
||||
board.marked_cells.marking_cells = false;
|
||||
board.end_marking_cells(true)
|
||||
}
|
||||
Action::Tab => {
|
||||
states.skirmish.side_panel = !states.skirmish.side_panel;
|
||||
board.change_resize(&window_area, states.skirmish.side_panel);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Action::Down => {
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
}
|
||||
Action::Left => {
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Right => {
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.next();
|
||||
}
|
||||
}
|
||||
Action::ScrollUp => {
|
||||
board.vertical_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
Action::ScrollDown => {
|
||||
board.vertical_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
}
|
||||
Action::ScrollLeft => {
|
||||
board.horizontal_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
}
|
||||
Action::ScrollRight => {
|
||||
board.horizontal_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
Action::ZoomIn => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedIn);
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.next();
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
},
|
||||
Action::ZoomOut => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedOut);
|
||||
board.vertical_offset.prev();
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {}
|
||||
},
|
||||
Action::Space => board.toggle_marking(),
|
||||
Action::Building => states.skirmish.building_view = true,
|
||||
Action::Recruitment => states.skirmish.recruitment_view = true,
|
||||
Action::Backspace => board.pop_marked_cell(),
|
||||
Action::Delete => {
|
||||
board.marked_cells.marking_cells = false;
|
||||
board.end_marking_cells(true)
|
||||
}
|
||||
Action::Tab => {
|
||||
states.skirmish.side_panel = !states.skirmish.side_panel;
|
||||
board.change_resize(&window_area, states.skirmish.side_panel);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-55
@@ -1,66 +1,66 @@
|
||||
use crate::{
|
||||
app::states::{
|
||||
MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState,
|
||||
skirmish_states::{BoardState, Resources},
|
||||
},
|
||||
cli::Cli,
|
||||
app::states::{
|
||||
MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState,
|
||||
skirmish_states::{BoardState, Resources},
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GameStates {
|
||||
pub main_menu: MainMenuState,
|
||||
pub skirmish: SkirmishState,
|
||||
pub perk_decks: PerkDecksState,
|
||||
pub skills_config: SkillsConfigState,
|
||||
pub settings: SettingsState,
|
||||
pub main_menu: MainMenuState,
|
||||
pub skirmish: SkirmishState,
|
||||
pub perk_decks: PerkDecksState,
|
||||
pub skills_config: SkillsConfigState,
|
||||
pub settings: SettingsState,
|
||||
}
|
||||
|
||||
impl GameStates {
|
||||
pub fn new(args: &Cli, area: &Rect) -> Self {
|
||||
Self {
|
||||
main_menu: MainMenuState {
|
||||
id: 0,
|
||||
name: "Main Menu",
|
||||
selected_view: 1,
|
||||
},
|
||||
skirmish: SkirmishState {
|
||||
id: 1,
|
||||
name: "Skirmish",
|
||||
board: BoardState::new(
|
||||
area,
|
||||
args.map_width as usize,
|
||||
args.map_height as usize,
|
||||
args.zoom_level,
|
||||
false,
|
||||
),
|
||||
side_panel: false,
|
||||
turn_counter: 0,
|
||||
player_resources: Resources::new(args.starting_wood, args.starting_metal),
|
||||
enemy_resources: Resources::new(args.starting_wood, args.starting_metal),
|
||||
building_view: false,
|
||||
recruitment_view: false,
|
||||
},
|
||||
perk_decks: PerkDecksState {
|
||||
id: 2,
|
||||
name: "Perk Decks",
|
||||
selected_perk_deck: 0,
|
||||
},
|
||||
skills_config: SkillsConfigState {
|
||||
id: 3,
|
||||
name: "Skills Config",
|
||||
selected_skill: 0,
|
||||
},
|
||||
settings: SettingsState {
|
||||
username: args.username.clone(),
|
||||
game_mode: args.game_mode.clone(),
|
||||
perk_deck: args.perk_deck.clone(),
|
||||
starting_wood: args.starting_wood.clone(),
|
||||
starting_metal: args.starting_metal,
|
||||
supply_limit: args.supply_limit.clone(),
|
||||
xp_modifier: args.xp_modifier.clone(),
|
||||
skill_points_limit: args.skill_points_limit.clone(),
|
||||
},
|
||||
pub fn new(args: &Cli, area: &Rect) -> Self {
|
||||
Self {
|
||||
main_menu: MainMenuState {
|
||||
id: 0,
|
||||
name: "Main Menu",
|
||||
selected_view: 1,
|
||||
},
|
||||
skirmish: SkirmishState {
|
||||
id: 1,
|
||||
name: "Skirmish",
|
||||
board: BoardState::new(
|
||||
area,
|
||||
args.map_width as usize,
|
||||
args.map_height as usize,
|
||||
args.zoom_level,
|
||||
false,
|
||||
),
|
||||
side_panel: false,
|
||||
turn_counter: 0,
|
||||
player_resources: Resources::new(args.starting_wood, args.starting_metal),
|
||||
enemy_resources: Resources::new(args.starting_wood, args.starting_metal),
|
||||
building_view: false,
|
||||
recruitment_view: false,
|
||||
},
|
||||
perk_decks: PerkDecksState {
|
||||
id: 2,
|
||||
name: "Perk Decks",
|
||||
selected_perk_deck: 0,
|
||||
},
|
||||
skills_config: SkillsConfigState {
|
||||
id: 3,
|
||||
name: "Skills Config",
|
||||
selected_skill: 0,
|
||||
},
|
||||
settings: SettingsState {
|
||||
username: args.username.clone(),
|
||||
game_mode: args.game_mode.clone(),
|
||||
perk_deck: args.perk_deck.clone(),
|
||||
starting_wood: args.starting_wood.clone(),
|
||||
starting_metal: args.starting_metal,
|
||||
supply_limit: args.supply_limit.clone(),
|
||||
xp_modifier: args.xp_modifier.clone(),
|
||||
skill_points_limit: args.skill_points_limit.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ use crate::app::states::{PerkDecks, skirmish_states::GameMode};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SettingsState {
|
||||
pub username: String,
|
||||
pub game_mode: GameMode,
|
||||
pub perk_deck: PerkDecks,
|
||||
pub starting_wood: u16,
|
||||
pub starting_metal: u16,
|
||||
pub supply_limit: u8,
|
||||
pub xp_modifier: f32,
|
||||
pub skill_points_limit: u16,
|
||||
pub username: String,
|
||||
pub game_mode: GameMode,
|
||||
pub perk_deck: PerkDecks,
|
||||
pub starting_wood: u16,
|
||||
pub starting_metal: u16,
|
||||
pub supply_limit: u8,
|
||||
pub xp_modifier: f32,
|
||||
pub skill_points_limit: u16,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
+103
-102
@@ -1,127 +1,128 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
BoardState, Players, Resources,
|
||||
resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS},
|
||||
structures::{BuildingTerrain, Mine, Structure, Structures},
|
||||
units::MinerUnit,
|
||||
BoardState, Players, Resources,
|
||||
resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS},
|
||||
structures::{BuildingTerrain, Mine, Structure, Structures},
|
||||
units::MinerUnit,
|
||||
};
|
||||
use crate::app::widgets::CellWidget;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SkirmishState {
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub board: BoardState,
|
||||
pub side_panel: bool,
|
||||
pub turn_counter: u64,
|
||||
pub player_resources: Resources,
|
||||
pub enemy_resources: Resources,
|
||||
pub building_view: bool,
|
||||
pub recruitment_view: bool,
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub board: BoardState,
|
||||
pub side_panel: bool,
|
||||
pub turn_counter: u64,
|
||||
pub player_resources: Resources,
|
||||
pub enemy_resources: Resources,
|
||||
pub building_view: bool,
|
||||
pub recruitment_view: bool,
|
||||
}
|
||||
|
||||
impl SkirmishState {
|
||||
pub fn can_recruit_miner(&self, (row, col): (usize, usize), owner: Players) -> bool {
|
||||
let resources: &Resources = match owner {
|
||||
Players::Player => &self.player_resources,
|
||||
Players::Enemy => &self.enemy_resources,
|
||||
Players::Unclaimed => return false,
|
||||
};
|
||||
if !resources.can_afford(MinerUnit::RECRUIT_COST) {
|
||||
return false;
|
||||
}
|
||||
self.board.cells.get(row).and_then(|cells: &Vec<CellWidget>| -> Option<&CellWidget> { cells.get(col) })
|
||||
pub fn can_recruit_miner(&self, (row, col): (usize, usize), owner: Players) -> bool {
|
||||
let resources: &Resources = match owner {
|
||||
Players::Player => &self.player_resources,
|
||||
Players::Enemy => &self.enemy_resources,
|
||||
Players::Unclaimed => return false,
|
||||
};
|
||||
if !resources.can_afford(MinerUnit::RECRUIT_COST) {
|
||||
return false;
|
||||
}
|
||||
self.board.cells.get(row).and_then(|cells: &Vec<CellWidget>| -> Option<&CellWidget> { cells.get(col) })
|
||||
.is_some_and(|cell: &CellWidget| -> bool {
|
||||
matches!(cell.get_structure(), Structures::Base(base) if base.get_owner() == owner && base.can_queue_miner())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn recruit_miner(&mut self, coords: (usize, usize), owner: Players) -> bool {
|
||||
if !self.can_recruit_miner(coords, owner) {
|
||||
return false;
|
||||
}
|
||||
let cell: &mut CellWidget = &mut self.board.cells[coords.0][coords.1];
|
||||
let Structures::Base(mut base) = cell.get_structure() else {
|
||||
return false;
|
||||
};
|
||||
if !base.queue_miner() {
|
||||
return false;
|
||||
}
|
||||
cell.set_structure(Structures::Base(base));
|
||||
let resources: &mut Resources = if owner == Players::Player {
|
||||
&mut self.player_resources
|
||||
} else {
|
||||
&mut self.enemy_resources
|
||||
};
|
||||
resources.spend(MinerUnit::RECRUIT_COST)
|
||||
}
|
||||
|
||||
pub fn can_start_mine_construction(&self, coords: (usize, usize), owner: Players) -> bool {
|
||||
let resources: &Resources = match owner {
|
||||
Players::Player => &self.player_resources,
|
||||
Players::Enemy => &self.enemy_resources,
|
||||
Players::Unclaimed => return false,
|
||||
};
|
||||
resources.can_afford(Mine::COST) && self.board.can_build_at(coords, owner, BuildingTerrain::Ore)
|
||||
}
|
||||
pub fn recruit_miner(&mut self, coords: (usize, usize), owner: Players) -> bool {
|
||||
if !self.can_recruit_miner(coords, owner) {
|
||||
return false;
|
||||
}
|
||||
let cell: &mut CellWidget = &mut self.board.cells[coords.0][coords.1];
|
||||
let Structures::Base(mut base) = cell.get_structure() else {
|
||||
return false;
|
||||
};
|
||||
if !base.queue_miner() {
|
||||
return false;
|
||||
}
|
||||
cell.set_structure(Structures::Base(base));
|
||||
let resources: &mut Resources = if owner == Players::Player {
|
||||
&mut self.player_resources
|
||||
} else {
|
||||
&mut self.enemy_resources
|
||||
};
|
||||
resources.spend(MinerUnit::RECRUIT_COST)
|
||||
}
|
||||
|
||||
pub fn can_start_mine_construction(&self, coords: (usize, usize), owner: Players) -> bool {
|
||||
let resources: &Resources = match owner {
|
||||
Players::Player => &self.player_resources,
|
||||
Players::Enemy => &self.enemy_resources,
|
||||
Players::Unclaimed => return false,
|
||||
};
|
||||
resources.can_afford(Mine::COST)
|
||||
&& self.board.can_build_at(coords, owner, BuildingTerrain::Ore)
|
||||
}
|
||||
|
||||
pub fn start_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool {
|
||||
if !self.can_start_mine_construction(coords, owner)
|
||||
|| !self.board.start_mine_construction(coords, owner)
|
||||
{
|
||||
return false;
|
||||
pub fn start_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool {
|
||||
if !self.can_start_mine_construction(coords, owner)
|
||||
|| !self.board.start_mine_construction(coords, owner)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let resources: &mut Resources = match owner {
|
||||
Players::Player => &mut self.player_resources,
|
||||
Players::Enemy => &mut self.enemy_resources,
|
||||
Players::Unclaimed => return false,
|
||||
};
|
||||
resources.spend(Mine::COST)
|
||||
}
|
||||
let resources: &mut Resources = match owner {
|
||||
Players::Player => &mut self.player_resources,
|
||||
Players::Enemy => &mut self.enemy_resources,
|
||||
Players::Unclaimed => return false,
|
||||
};
|
||||
resources.spend(Mine::COST)
|
||||
}
|
||||
|
||||
pub fn cancel_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool {
|
||||
if owner == Players::Unclaimed || !self.board.cancel_mine_construction(coords, owner) {
|
||||
return false;
|
||||
pub fn cancel_mine_construction(&mut self, coords: (usize, usize), owner: Players) -> bool {
|
||||
if owner == Players::Unclaimed || !self.board.cancel_mine_construction(coords, owner) {
|
||||
return false;
|
||||
}
|
||||
let resources: &mut Resources = if owner == Players::Player {
|
||||
&mut self.player_resources
|
||||
} else {
|
||||
&mut self.enemy_resources
|
||||
};
|
||||
resources.add_wood(Mine::COST.wood);
|
||||
resources.add_metal(Mine::COST.metal);
|
||||
true
|
||||
}
|
||||
let resources: &mut Resources = if owner == Players::Player {
|
||||
&mut self.player_resources
|
||||
} else {
|
||||
&mut self.enemy_resources
|
||||
};
|
||||
resources.add_wood(Mine::COST.wood);
|
||||
resources.add_metal(Mine::COST.metal);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn tick_update(&mut self) -> () {
|
||||
self.board.advance_turn();
|
||||
self.board.advance_recruitment();
|
||||
self.board.advance_construction();
|
||||
pub fn tick_update(&mut self) -> () {
|
||||
self.board.advance_turn();
|
||||
self.board.advance_recruitment();
|
||||
self.board.advance_construction();
|
||||
|
||||
// if self.board.is_victory() {}
|
||||
// if self.board.is_victory() {}
|
||||
|
||||
self.turn_counter += 1;
|
||||
if self.turn_counter.is_multiple_of(WOOD_DELIVERY_TICKS) {
|
||||
self.player_resources.add_wood(WOOD_DELIVERY_AMOUNT);
|
||||
}
|
||||
if self.turn_counter.is_multiple_of(MINE_PRODUCTION_TICKS) {
|
||||
for row in &mut self.board.cells {
|
||||
for cell in row {
|
||||
if let Structures::Mine(mut mine) = cell.get_structure() {
|
||||
let staffed: bool = cell.get_ref_option_unit().as_ref().is_some_and(
|
||||
|unit: &crate::app::states::skirmish_states::units::Units| -> bool {
|
||||
mine.accepts_unit(unit)
|
||||
},
|
||||
);
|
||||
let amount: u32 = u32::from(mine.produce_with_miner(staffed));
|
||||
match mine.get_owner() {
|
||||
Players::Player => self.player_resources.add_metal(amount),
|
||||
Players::Enemy => self.enemy_resources.add_metal(amount),
|
||||
Players::Unclaimed => (),
|
||||
self.turn_counter += 1;
|
||||
if self.turn_counter.is_multiple_of(WOOD_DELIVERY_TICKS) {
|
||||
self.player_resources.add_wood(WOOD_DELIVERY_AMOUNT);
|
||||
}
|
||||
if self.turn_counter.is_multiple_of(MINE_PRODUCTION_TICKS) {
|
||||
for row in &mut self.board.cells {
|
||||
for cell in row {
|
||||
if let Structures::Mine(mut mine) = cell.get_structure() {
|
||||
let staffed: bool = cell.get_ref_option_unit().as_ref().is_some_and(
|
||||
|unit: &crate::app::states::skirmish_states::units::Units| -> bool {
|
||||
mine.accepts_unit(unit)
|
||||
},
|
||||
);
|
||||
let amount: u32 = u32::from(mine.produce_with_miner(staffed));
|
||||
match mine.get_owner() {
|
||||
Players::Player => self.player_resources.add_metal(amount),
|
||||
Players::Enemy => self.enemy_resources.add_metal(amount),
|
||||
Players::Unclaimed => (),
|
||||
}
|
||||
cell.set_structure(Structures::Mine(mine));
|
||||
}
|
||||
}
|
||||
}
|
||||
cell.set_structure(Structures::Mine(mine));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,53 +1,53 @@
|
||||
pub enum MoveFocusedCell {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FocusedCell {
|
||||
row: usize,
|
||||
col: usize,
|
||||
max_row: usize,
|
||||
max_col: usize,
|
||||
row: usize,
|
||||
col: usize,
|
||||
max_row: usize,
|
||||
max_col: usize,
|
||||
}
|
||||
|
||||
impl FocusedCell {
|
||||
pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
max_row,
|
||||
max_col,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_coords(&self) -> (usize, usize) {
|
||||
(self.row, self.col)
|
||||
}
|
||||
|
||||
pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) {
|
||||
match direction {
|
||||
MoveFocusedCell::Up => {
|
||||
self.row = self.row.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Down => {
|
||||
self.row = self.row.saturating_add(1).min(self.max_row - 1);
|
||||
}
|
||||
MoveFocusedCell::Left => {
|
||||
self.col = self.col.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Right => {
|
||||
self.col = self.col.saturating_add(1).min(self.max_col - 1);
|
||||
}
|
||||
pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
max_row,
|
||||
max_col,
|
||||
}
|
||||
}
|
||||
|
||||
(self.row, self.col)
|
||||
}
|
||||
pub fn get_coords(&self) -> (usize, usize) {
|
||||
(self.row, self.col)
|
||||
}
|
||||
|
||||
pub fn set_focused_cell(&mut self, cell: (usize, usize)) {
|
||||
self.row = cell.0.max(0).min(self.max_row - 1);
|
||||
self.col = cell.1.max(0).min(self.max_col - 1);
|
||||
}
|
||||
pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) {
|
||||
match direction {
|
||||
MoveFocusedCell::Up => {
|
||||
self.row = self.row.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Down => {
|
||||
self.row = self.row.saturating_add(1).min(self.max_row - 1);
|
||||
}
|
||||
MoveFocusedCell::Left => {
|
||||
self.col = self.col.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Right => {
|
||||
self.col = self.col.saturating_add(1).min(self.max_col - 1);
|
||||
}
|
||||
}
|
||||
|
||||
(self.row, self.col)
|
||||
}
|
||||
|
||||
pub fn set_focused_cell(&mut self, cell: (usize, usize)) {
|
||||
self.row = cell.0.max(0).min(self.max_row - 1);
|
||||
self.col = cell.1.max(0).min(self.max_col - 1);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@ use clap::ValueEnum;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum GameMode {
|
||||
LastManStanding,
|
||||
FrontLines,
|
||||
LastManStanding,
|
||||
FrontLines,
|
||||
}
|
||||
@@ -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<Units>,
|
||||
pub marking_cells: bool,
|
||||
pub marked_cells: VecDeque<(usize, usize)>,
|
||||
pub selected_unit: Option<Units>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<usize>, initial_max: Option<usize>) -> 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<usize>, initial_max: Option<usize>) -> 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)
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,35 +7,35 @@ pub const MINE_PRODUCTION_TICKS: u64 = 1000_u64.div_ceil(TICK_MS as u64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Resources {
|
||||
pub wood: u32,
|
||||
pub metal: u32,
|
||||
pub wood: u32,
|
||||
pub metal: u32,
|
||||
}
|
||||
|
||||
impl Resources {
|
||||
pub fn can_afford(&self, cost: Self) -> bool {
|
||||
self.wood >= cost.wood && self.metal >= cost.metal
|
||||
}
|
||||
|
||||
pub fn spend(&mut self, cost: Self) -> bool {
|
||||
if !self.can_afford(cost) {
|
||||
return false;
|
||||
pub fn can_afford(&self, cost: Self) -> bool {
|
||||
self.wood >= cost.wood && self.metal >= cost.metal
|
||||
}
|
||||
self.wood -= cost.wood;
|
||||
self.metal -= cost.metal;
|
||||
true
|
||||
}
|
||||
pub fn new(wood: u16, metal: u16) -> Self {
|
||||
Self {
|
||||
wood: u32::from(wood),
|
||||
metal: u32::from(metal),
|
||||
|
||||
pub fn spend(&mut self, cost: Self) -> bool {
|
||||
if !self.can_afford(cost) {
|
||||
return false;
|
||||
}
|
||||
self.wood -= cost.wood;
|
||||
self.metal -= cost.metal;
|
||||
true
|
||||
}
|
||||
pub fn new(wood: u16, metal: u16) -> Self {
|
||||
Self {
|
||||
wood: u32::from(wood),
|
||||
metal: u32::from(metal),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_wood(&mut self, amount: u32) -> () {
|
||||
self.wood = self.wood.saturating_add(amount);
|
||||
}
|
||||
pub fn add_wood(&mut self, amount: u32) -> () {
|
||||
self.wood = self.wood.saturating_add(amount);
|
||||
}
|
||||
|
||||
pub fn add_metal(&mut self, amount: u32) -> () {
|
||||
self.metal = self.metal.saturating_add(amount);
|
||||
}
|
||||
pub fn add_metal(&mut self, amount: u32) -> () {
|
||||
self.metal = self.metal.saturating_add(amount);
|
||||
}
|
||||
}
|
||||
@@ -4,135 +4,135 @@ use ratatui::{style::Color, text::Line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BaseBuilding {
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
owner: Players,
|
||||
level: u8,
|
||||
queued_miners: u32,
|
||||
recruitment_ticks: u16,
|
||||
waiting_for_departure: bool,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
owner: Players,
|
||||
level: u8,
|
||||
queued_miners: u32,
|
||||
recruitment_ticks: u16,
|
||||
waiting_for_departure: bool,
|
||||
}
|
||||
|
||||
impl BaseBuilding {
|
||||
pub const RECRUITMENT_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16);
|
||||
pub const RECRUITMENT_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16);
|
||||
|
||||
pub fn queued_miners(&self) -> u32 {
|
||||
self.queued_miners
|
||||
}
|
||||
|
||||
pub fn recruitment_progress(&self) -> u16 {
|
||||
(u32::from(self.recruitment_ticks) * 100 / u32::from(Self::RECRUITMENT_TICKS)) as u16
|
||||
}
|
||||
|
||||
pub fn can_queue_miner(&self) -> bool {
|
||||
self.owner != Players::Unclaimed && self.queued_miners < u32::MAX
|
||||
}
|
||||
|
||||
pub fn queue_miner(&mut self) -> bool {
|
||||
if !self.can_queue_miner() {
|
||||
return false;
|
||||
pub fn queued_miners(&self) -> u32 {
|
||||
self.queued_miners
|
||||
}
|
||||
self.queued_miners += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn advance_recruitment(&mut self, occupied: bool) -> bool {
|
||||
if self.waiting_for_departure {
|
||||
if occupied {
|
||||
return false;
|
||||
}
|
||||
self.waiting_for_departure = false;
|
||||
pub fn recruitment_progress(&self) -> u16 {
|
||||
(u32::from(self.recruitment_ticks) * 100 / u32::from(Self::RECRUITMENT_TICKS)) as u16
|
||||
}
|
||||
if self.queued_miners == 0 {
|
||||
return false;
|
||||
}
|
||||
self.recruitment_ticks = self
|
||||
.recruitment_ticks
|
||||
.saturating_add(1)
|
||||
.min(Self::RECRUITMENT_TICKS);
|
||||
if self.recruitment_ticks < Self::RECRUITMENT_TICKS || occupied {
|
||||
return false;
|
||||
}
|
||||
self.queued_miners -= 1;
|
||||
self.recruitment_ticks = 0;
|
||||
self.waiting_for_departure = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn recruitment_status(&self, occupied: bool) -> &'static str {
|
||||
if self.queued_miners == 0 {
|
||||
"Queue empty"
|
||||
} else if self.waiting_for_departure && occupied {
|
||||
"Paused: waiting for Miner to leave base"
|
||||
} else if self.recruitment_ticks == Self::RECRUITMENT_TICKS && occupied {
|
||||
"Ready: waiting for free base cell"
|
||||
} else {
|
||||
"Recruiting"
|
||||
pub fn can_queue_miner(&self) -> bool {
|
||||
self.owner != Players::Unclaimed && self.queued_miners < u32::MAX
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(owner: Players) -> Self {
|
||||
Self {
|
||||
durability: 1500,
|
||||
stress: 0,
|
||||
owner,
|
||||
level: 1,
|
||||
queued_miners: 0,
|
||||
recruitment_ticks: 0,
|
||||
waiting_for_departure: false,
|
||||
pub fn queue_miner(&mut self) -> bool {
|
||||
if !self.can_queue_miner() {
|
||||
return false;
|
||||
}
|
||||
self.queued_miners += 1;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn advance_recruitment(&mut self, occupied: bool) -> bool {
|
||||
if self.waiting_for_departure {
|
||||
if occupied {
|
||||
return false;
|
||||
}
|
||||
self.waiting_for_departure = false;
|
||||
}
|
||||
if self.queued_miners == 0 {
|
||||
return false;
|
||||
}
|
||||
self.recruitment_ticks = self
|
||||
.recruitment_ticks
|
||||
.saturating_add(1)
|
||||
.min(Self::RECRUITMENT_TICKS);
|
||||
if self.recruitment_ticks < Self::RECRUITMENT_TICKS || occupied {
|
||||
return false;
|
||||
}
|
||||
self.queued_miners -= 1;
|
||||
self.recruitment_ticks = 0;
|
||||
self.waiting_for_departure = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn recruitment_status(&self, occupied: bool) -> &'static str {
|
||||
if self.queued_miners == 0 {
|
||||
"Queue empty"
|
||||
} else if self.waiting_for_departure && occupied {
|
||||
"Paused: waiting for Miner to leave base"
|
||||
} else if self.recruitment_ticks == Self::RECRUITMENT_TICKS && occupied {
|
||||
"Ready: waiting for free base cell"
|
||||
} else {
|
||||
"Recruiting"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(owner: Players) -> Self {
|
||||
Self {
|
||||
durability: 1500,
|
||||
stress: 0,
|
||||
owner,
|
||||
level: 1,
|
||||
queued_miners: 0,
|
||||
recruitment_ticks: 0,
|
||||
waiting_for_departure: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for BaseBuilding {
|
||||
fn get_tag(&self) -> char {
|
||||
'B'
|
||||
}
|
||||
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => Color::LightBlue,
|
||||
Players::Enemy => Color::LightRed,
|
||||
Players::Unclaimed => Color::LightGreen,
|
||||
fn get_tag(&self) -> char {
|
||||
'B'
|
||||
}
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
(self.level + b'0') as char
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => Color::LightBlue,
|
||||
Players::Enemy => Color::LightRed,
|
||||
Players::Unclaimed => Color::LightGreen,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
(self.level + b'0') as char
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1500
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1500
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Base"
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Base"
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = self.base_text();
|
||||
lines.push(Line::from(format!(
|
||||
"Recruitment queue: {}",
|
||||
self.queued_miners
|
||||
)));
|
||||
if self.queued_miners > 0 {
|
||||
lines.push(Line::from(format!(
|
||||
"Miner progress: {}%",
|
||||
self.recruitment_progress()
|
||||
)));
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = self.base_text();
|
||||
lines.push(Line::from(format!(
|
||||
"Recruitment queue: {}",
|
||||
self.queued_miners
|
||||
)));
|
||||
if self.queued_miners > 0 {
|
||||
lines.push(Line::from(format!(
|
||||
"Miner progress: {}%",
|
||||
self.recruitment_progress()
|
||||
)));
|
||||
}
|
||||
lines
|
||||
}
|
||||
lines
|
||||
}
|
||||
}
|
||||
@@ -1,96 +1,96 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players, Resources,
|
||||
structures::{Ore, Structure},
|
||||
units::{Unit, Units},
|
||||
Players, Resources,
|
||||
structures::{Ore, Structure},
|
||||
units::{Unit, Units},
|
||||
};
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Mine {
|
||||
owner: Players,
|
||||
ore: Ore,
|
||||
owner: Players,
|
||||
ore: Ore,
|
||||
}
|
||||
|
||||
impl Mine {
|
||||
pub const COST: Resources = Resources {
|
||||
wood: 20,
|
||||
metal: 10,
|
||||
};
|
||||
pub const METAL_PER_CYCLE: u16 = 1;
|
||||
pub const STAFFED_METAL_PER_CYCLE: u16 = 2;
|
||||
|
||||
pub fn new(owner: Players, ore: Ore) -> Option<Self> {
|
||||
if owner == Players::Unclaimed || ore.get_owner() != Players::Unclaimed {
|
||||
return None;
|
||||
}
|
||||
Some(Self { owner, ore })
|
||||
}
|
||||
|
||||
pub fn get_ore_amount(&self) -> u16 {
|
||||
self.ore.get_ore_amount()
|
||||
}
|
||||
|
||||
pub fn into_ore(self) -> Ore {
|
||||
self.ore
|
||||
}
|
||||
|
||||
pub fn produce(&mut self) -> u16 {
|
||||
self.produce_with_miner(false)
|
||||
}
|
||||
|
||||
pub fn accepts_unit(&self, unit: &Units) -> bool {
|
||||
matches!(unit, Units::Miner(_)) && unit.get_owner() == self.owner
|
||||
}
|
||||
|
||||
pub fn production_per_cycle(&self, staffed: bool) -> u16 {
|
||||
let rate: u16 = if staffed {
|
||||
Self::STAFFED_METAL_PER_CYCLE
|
||||
} else {
|
||||
Self::METAL_PER_CYCLE
|
||||
pub const COST: Resources = Resources {
|
||||
wood: 20,
|
||||
metal: 10,
|
||||
};
|
||||
rate.min(self.get_ore_amount())
|
||||
}
|
||||
pub const METAL_PER_CYCLE: u16 = 1;
|
||||
pub const STAFFED_METAL_PER_CYCLE: u16 = 2;
|
||||
|
||||
pub fn produce_with_miner(&mut self, staffed: bool) -> u16 {
|
||||
self.ore.extract(self.production_per_cycle(staffed))
|
||||
}
|
||||
pub fn new(owner: Players, ore: Ore) -> Option<Self> {
|
||||
if owner == Players::Unclaimed || ore.get_owner() != Players::Unclaimed {
|
||||
return None;
|
||||
}
|
||||
Some(Self { owner, ore })
|
||||
}
|
||||
|
||||
pub fn get_ore_amount(&self) -> u16 {
|
||||
self.ore.get_ore_amount()
|
||||
}
|
||||
|
||||
pub fn into_ore(self) -> Ore {
|
||||
self.ore
|
||||
}
|
||||
|
||||
pub fn produce(&mut self) -> u16 {
|
||||
self.produce_with_miner(false)
|
||||
}
|
||||
|
||||
pub fn accepts_unit(&self, unit: &Units) -> bool {
|
||||
matches!(unit, Units::Miner(_)) && unit.get_owner() == self.owner
|
||||
}
|
||||
|
||||
pub fn production_per_cycle(&self, staffed: bool) -> u16 {
|
||||
let rate: u16 = if staffed {
|
||||
Self::STAFFED_METAL_PER_CYCLE
|
||||
} else {
|
||||
Self::METAL_PER_CYCLE
|
||||
};
|
||||
rate.min(self.get_ore_amount())
|
||||
}
|
||||
|
||||
pub fn produce_with_miner(&mut self, staffed: bool) -> u16 {
|
||||
self.ore.extract(self.production_per_cycle(staffed))
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Mine {
|
||||
fn get_tag(&self) -> char {
|
||||
'M'
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
self.owner.get_color()
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
self.ore.get_level()
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.ore.get_stress()
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.ore.get_durability()
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.ore.get_max_durability()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Mine"
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = self.base_text();
|
||||
lines.push(Line::from_iter([
|
||||
"Ore: ".gray(),
|
||||
self.get_ore_amount().to_string().cyan(),
|
||||
format!("/{}", self.ore.get_max_ore_amount()).gray(),
|
||||
]));
|
||||
lines
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
'M'
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
self.owner.get_color()
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
self.ore.get_level()
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.ore.get_stress()
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.ore.get_durability()
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.ore.get_max_durability()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Mine"
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = self.base_text();
|
||||
lines.push(Line::from_iter([
|
||||
"Ore: ".gray(),
|
||||
self.get_ore_amount().to_string().cyan(),
|
||||
format!("/{}", self.ore.get_max_ore_amount()).gray(),
|
||||
]));
|
||||
lines
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,75 @@
|
||||
use crate::app::{
|
||||
states::skirmish_states::{
|
||||
Players,
|
||||
structures::{Mine, Ore, Structure},
|
||||
},
|
||||
threads::TICK_MS,
|
||||
states::skirmish_states::{
|
||||
Players,
|
||||
structures::{Mine, Ore, Structure},
|
||||
},
|
||||
threads::TICK_MS,
|
||||
};
|
||||
use ratatui::{style::Color, text::Line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MineConstruction {
|
||||
mine: Mine,
|
||||
ticks: u16,
|
||||
mine: Mine,
|
||||
ticks: u16,
|
||||
}
|
||||
|
||||
impl MineConstruction {
|
||||
pub const BUILD_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16);
|
||||
pub const BUILD_TICKS: u16 = 10000_u16.div_ceil(TICK_MS as u16);
|
||||
|
||||
pub fn new(owner: Players, ore: Ore) -> Option<Self> {
|
||||
Mine::new(owner, ore).map(|mine: Mine| -> Self { Self { mine, ticks: 0 } })
|
||||
}
|
||||
pub fn new(owner: Players, ore: Ore) -> Option<Self> {
|
||||
Mine::new(owner, ore).map(|mine: Mine| -> Self { Self { mine, ticks: 0 } })
|
||||
}
|
||||
|
||||
pub fn advance(&mut self) -> () {
|
||||
self.ticks = self.ticks.saturating_add(1).min(Self::BUILD_TICKS);
|
||||
}
|
||||
pub fn advance(&mut self) -> () {
|
||||
self.ticks = self.ticks.saturating_add(1).min(Self::BUILD_TICKS);
|
||||
}
|
||||
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.ticks == Self::BUILD_TICKS
|
||||
}
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.ticks == Self::BUILD_TICKS
|
||||
}
|
||||
|
||||
pub fn progress_percent(&self) -> u16 {
|
||||
(u32::from(self.ticks) * 100 / u32::from(Self::BUILD_TICKS)) as u16
|
||||
}
|
||||
pub fn progress_percent(&self) -> u16 {
|
||||
(u32::from(self.ticks) * 100 / u32::from(Self::BUILD_TICKS)) as u16
|
||||
}
|
||||
|
||||
pub fn into_mine(self) -> Option<Mine> {
|
||||
self.is_finished().then_some(self.mine)
|
||||
}
|
||||
pub fn into_mine(self) -> Option<Mine> {
|
||||
self.is_finished().then_some(self.mine)
|
||||
}
|
||||
|
||||
pub fn cancel(self) -> Ore {
|
||||
self.mine.into_ore()
|
||||
}
|
||||
pub fn cancel(self) -> Ore {
|
||||
self.mine.into_ore()
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for MineConstruction {
|
||||
fn get_tag(&self) -> char {
|
||||
'C'
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
self.mine.get_color()
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
self.mine.get_level()
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.mine.get_stress()
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.mine.get_durability()
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.mine.get_max_durability()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Mine construction"
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.mine.get_owner()
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
vec![
|
||||
Line::from(format!("Construction: {}%", self.progress_percent())),
|
||||
Line::from(format!("Ore: {}", self.mine.get_ore_amount())),
|
||||
]
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
'C'
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
self.mine.get_color()
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
self.mine.get_level()
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.mine.get_stress()
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.mine.get_durability()
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.mine.get_max_durability()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Mine construction"
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.mine.get_owner()
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
vec![
|
||||
Line::from(format!("Construction: {}%", self.progress_percent())),
|
||||
Line::from(format!("Ore: {}", self.mine.get_ore_amount())),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,6 @@ pub use tunnel::Tunnel;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BuildingTerrain {
|
||||
Stone,
|
||||
Ore,
|
||||
Stone,
|
||||
Ore,
|
||||
}
|
||||
@@ -1,100 +1,100 @@
|
||||
use crate::app::states::skirmish_states::{Players, structures::Structure};
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Ore {
|
||||
owner: Players,
|
||||
ore_amount: u16,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
level: u8,
|
||||
owner: Players,
|
||||
ore_amount: u16,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
level: u8,
|
||||
}
|
||||
|
||||
impl Ore {
|
||||
pub fn new(owner: Players, level: u8) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
ore_amount: 5000,
|
||||
durability: 1000,
|
||||
stress: 0,
|
||||
level,
|
||||
pub fn new(owner: Players, level: u8) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
ore_amount: 5000,
|
||||
durability: 1000,
|
||||
stress: 0,
|
||||
level,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ore_amount(&self) -> u16 {
|
||||
self.ore_amount
|
||||
}
|
||||
pub fn get_ore_amount(&self) -> u16 {
|
||||
self.ore_amount
|
||||
}
|
||||
|
||||
pub fn get_max_ore_amount(&self) -> u16 {
|
||||
5000
|
||||
}
|
||||
pub fn get_max_ore_amount(&self) -> u16 {
|
||||
5000
|
||||
}
|
||||
|
||||
pub fn extract(&mut self, amount: u16) -> u16 {
|
||||
let extracted: u16 = amount.min(self.ore_amount);
|
||||
self.ore_amount -= extracted;
|
||||
extracted
|
||||
}
|
||||
pub fn extract(&mut self, amount: u16) -> u16 {
|
||||
let extracted: u16 = amount.min(self.ore_amount);
|
||||
self.ore_amount -= extracted;
|
||||
extracted
|
||||
}
|
||||
|
||||
pub fn set_owner(&mut self, new_owner: Players) -> &mut Self {
|
||||
self.owner = new_owner;
|
||||
self
|
||||
}
|
||||
pub fn set_owner(&mut self, new_owner: Players) -> &mut Self {
|
||||
self.owner = new_owner;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Ore {
|
||||
fn get_tag(&self) -> char {
|
||||
'O'
|
||||
}
|
||||
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => self.owner.get_color(),
|
||||
Players::Enemy => self.owner.get_color(),
|
||||
Players::Unclaimed => Color::Indexed(217),
|
||||
fn get_tag(&self) -> char {
|
||||
'O'
|
||||
}
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
if self.level != 0 {
|
||||
(self.level + b'0') as char
|
||||
} else {
|
||||
' '
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => self.owner.get_color(),
|
||||
Players::Enemy => self.owner.get_color(),
|
||||
Players::Unclaimed => Color::Indexed(217),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1000
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Ore"
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
if self.level != 0 {
|
||||
(self.level + b'0') as char
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1000
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Ore"
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = Structure::base_text(self);
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
|
||||
lines.push(Line::from_iter([
|
||||
"Ore: ".gray(),
|
||||
self.get_ore_amount().to_string().cyan(),
|
||||
"/".gray(),
|
||||
self.get_max_ore_amount().to_string().cyan(),
|
||||
]));
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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<Line<'_>> {
|
||||
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<Line<'_>> {
|
||||
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<Line<'_>> = 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<Line<'_>> = vec![
|
||||
Line::from_iter([
|
||||
"Durability: ".gray(),
|
||||
durability.to_string().cyan(),
|
||||
"/".gray(),
|
||||
max_durability.to_string().cyan(),
|
||||
" ( ".gray(),
|
||||
durability_percent.to_string().cyan(),
|
||||
"% )".gray(),
|
||||
]),
|
||||
Line::from_iter(["Stress: ".gray(), stress.to_string().cyan(), "%".gray()]),
|
||||
];
|
||||
|
||||
if level != ' ' {
|
||||
lines.push(Line::from_iter([
|
||||
"Level: ".gray(),
|
||||
level.to_string().cyan(),
|
||||
]));
|
||||
if level != ' ' {
|
||||
lines.push(Line::from_iter([
|
||||
"Level: ".gray(),
|
||||
level.to_string().cyan(),
|
||||
]));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
self.base_text()
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
self.base_text()
|
||||
}
|
||||
}
|
||||
@@ -1,100 +1,100 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players,
|
||||
structures::{BaseBuilding, Mine, MineConstruction, Ore, Stone, Structure, Tunnel},
|
||||
units::Units,
|
||||
Players,
|
||||
structures::{BaseBuilding, Mine, MineConstruction, Ore, Stone, Structure, Tunnel},
|
||||
units::Units,
|
||||
};
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Structures {
|
||||
Base(BaseBuilding),
|
||||
Tunnel(Tunnel),
|
||||
Stone(Stone),
|
||||
Ore(Ore),
|
||||
Mine(Mine),
|
||||
MineConstruction(MineConstruction),
|
||||
Base(BaseBuilding),
|
||||
Tunnel(Tunnel),
|
||||
Stone(Stone),
|
||||
Ore(Ore),
|
||||
Mine(Mine),
|
||||
MineConstruction(MineConstruction),
|
||||
}
|
||||
|
||||
impl Structures {
|
||||
pub fn can_enter(&self, unit: &Units) -> bool {
|
||||
match self {
|
||||
Self::Base(_) | Self::Tunnel(_) => true,
|
||||
Self::Mine(mine) => mine.accepts_unit(unit),
|
||||
_ => false,
|
||||
pub fn can_enter(&self, unit: &Units) -> bool {
|
||||
match self {
|
||||
Self::Base(_) | Self::Tunnel(_) => true,
|
||||
Self::Mine(mine) => mine.accepts_unit(unit),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn build_mine(&mut self, owner: Players) -> bool {
|
||||
if let Self::Ore(ore) = *self
|
||||
&& let Some(mine) = Mine::new(owner, ore)
|
||||
{
|
||||
*self = Self::Mine(mine);
|
||||
return true;
|
||||
pub fn build_mine(&mut self, owner: Players) -> bool {
|
||||
if let Self::Ore(ore) = *self
|
||||
&& let Some(mine) = Mine::new(owner, ore)
|
||||
{
|
||||
*self = Self::Mine(mine);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn s(&self) -> &dyn Structure {
|
||||
match self {
|
||||
Self::Base(b) => b,
|
||||
Self::Tunnel(t) => t,
|
||||
Self::Stone(s) => s,
|
||||
Self::Ore(o) => o,
|
||||
Self::Mine(mine) => mine,
|
||||
Self::MineConstruction(site) => site,
|
||||
#[inline]
|
||||
fn s(&self) -> &dyn Structure {
|
||||
match self {
|
||||
Self::Base(b) => b,
|
||||
Self::Tunnel(t) => t,
|
||||
Self::Stone(s) => s,
|
||||
Self::Ore(o) => o,
|
||||
Self::Mine(mine) => mine,
|
||||
Self::MineConstruction(site) => site,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Structures {
|
||||
fn get_color(&self) -> Color {
|
||||
self.s().get_color()
|
||||
}
|
||||
|
||||
fn get_tag(&self) -> char {
|
||||
self.s().get_tag()
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
self.s().get_level()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.s().get_name()
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.s().get_durability()
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.s().get_max_durability()
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.s().get_stress()
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.s().get_owner()
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
match self.s().get_owner() {
|
||||
Players::Unclaimed => self.s().text(),
|
||||
Players::Player | Players::Enemy => {
|
||||
let mut lines: Vec<Line<'_>> = 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<Line<'_>> {
|
||||
match self.s().get_owner() {
|
||||
Players::Unclaimed => self.s().text(),
|
||||
Players::Player | Players::Enemy => {
|
||||
let mut lines: Vec<Line<'_>> = self.s().text();
|
||||
|
||||
lines.push(Line::from_iter([
|
||||
"Owner: ".gray(),
|
||||
self.s().get_owner().get_span(),
|
||||
]));
|
||||
|
||||
lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -3,71 +3,71 @@ use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DiggingTask {
|
||||
path: VecDeque<(usize, usize)>,
|
||||
progress: f32,
|
||||
tick_increase: f32,
|
||||
total_steps: usize,
|
||||
path: VecDeque<(usize, usize)>,
|
||||
progress: f32,
|
||||
tick_increase: f32,
|
||||
total_steps: usize,
|
||||
}
|
||||
|
||||
impl DiggingTask {
|
||||
pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self {
|
||||
Self {
|
||||
total_steps: path.len(),
|
||||
path,
|
||||
progress: 0.0,
|
||||
tick_increase,
|
||||
pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self {
|
||||
Self {
|
||||
total_steps: path.len(),
|
||||
path,
|
||||
progress: 0.0,
|
||||
tick_increase,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_front(&mut self) {
|
||||
self.path.pop_front();
|
||||
self.progress = 0.0;
|
||||
}
|
||||
|
||||
pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self {
|
||||
Self {
|
||||
path: task.get_path().clone(),
|
||||
progress: 0.0,
|
||||
tick_increase,
|
||||
total_steps: task.get_total_steps(),
|
||||
pub fn pop_front(&mut self) {
|
||||
self.path.pop_front();
|
||||
self.progress = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_target(&self) -> Option<(usize, usize)> {
|
||||
self.path.front().copied()
|
||||
}
|
||||
pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self {
|
||||
Self {
|
||||
path: task.get_path().clone(),
|
||||
progress: 0.0,
|
||||
tick_increase,
|
||||
total_steps: task.get_total_steps(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_target(&self) -> Option<(usize, usize)> {
|
||||
self.path.front().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for DiggingTask {
|
||||
fn get_icon(&self) -> char {
|
||||
'⛏'
|
||||
}
|
||||
fn get_icon(&self) -> char {
|
||||
'⛏'
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)> {
|
||||
&self.path
|
||||
}
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn get_path_front_coords(&self) -> &(usize, usize) {
|
||||
self.path.front().map_or(&(0, 0), |c| c)
|
||||
}
|
||||
fn get_path_front_coords(&self) -> &(usize, usize) {
|
||||
self.path.front().map_or(&(0, 0), |c| c)
|
||||
}
|
||||
|
||||
fn get_path_back_coords(&self) -> &(usize, usize) {
|
||||
self.path.back().map_or(&(0, 0), |c| c)
|
||||
}
|
||||
fn get_path_back_coords(&self) -> &(usize, usize) {
|
||||
self.path.back().map_or(&(0, 0), |c| c)
|
||||
}
|
||||
|
||||
fn get_progress(&self) -> f32 {
|
||||
self.progress
|
||||
}
|
||||
fn get_progress(&self) -> f32 {
|
||||
self.progress
|
||||
}
|
||||
|
||||
fn get_total_steps(&self) -> usize {
|
||||
self.total_steps
|
||||
}
|
||||
fn get_total_steps(&self) -> usize {
|
||||
self.total_steps
|
||||
}
|
||||
|
||||
fn progress(&mut self) {
|
||||
self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0);
|
||||
}
|
||||
fn progress(&mut self) {
|
||||
self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0);
|
||||
}
|
||||
|
||||
fn is_finished(&self) -> bool {
|
||||
self.progress >= 100.0
|
||||
}
|
||||
fn is_finished(&self) -> bool {
|
||||
self.progress >= 100.0
|
||||
}
|
||||
}
|
||||
@@ -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<u16> {
|
||||
(self.ticks > 0).then(|| self.ticks * 100 / MOVEMENT_TICKS)
|
||||
}
|
||||
pub fn get_movement_progress(&self) -> Option<u16> {
|
||||
(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
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub trait Task {
|
||||
fn get_icon(&self) -> char;
|
||||
fn get_icon(&self) -> char;
|
||||
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)>;
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)>;
|
||||
|
||||
fn get_path_front_coords(&self) -> &(usize, usize);
|
||||
fn get_path_front_coords(&self) -> &(usize, usize);
|
||||
|
||||
fn get_path_back_coords(&self) -> &(usize, usize);
|
||||
fn get_path_back_coords(&self) -> &(usize, usize);
|
||||
|
||||
fn get_progress(&self) -> f32;
|
||||
fn get_progress(&self) -> f32;
|
||||
|
||||
fn get_total_steps(&self) -> usize;
|
||||
fn get_total_steps(&self) -> usize;
|
||||
|
||||
fn get_total_progress(&self) -> f32 {
|
||||
let total: usize = self.get_total_steps();
|
||||
if total == 0 {
|
||||
return 100.0;
|
||||
fn get_total_progress(&self) -> f32 {
|
||||
let total: usize = self.get_total_steps();
|
||||
if total == 0 {
|
||||
return 100.0;
|
||||
}
|
||||
let completed: usize = total.saturating_sub(self.get_path().len());
|
||||
((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0)
|
||||
}
|
||||
let completed: usize = total.saturating_sub(self.get_path().len());
|
||||
((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0)
|
||||
}
|
||||
|
||||
fn progress(&mut self);
|
||||
fn progress(&mut self);
|
||||
|
||||
fn is_finished(&self) -> bool;
|
||||
fn is_finished(&self) -> bool;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,133 +1,132 @@
|
||||
use ratatui::{style::Stylize, text::Line};
|
||||
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players, Resources,
|
||||
tasks::{Task, Tasks},
|
||||
units::Unit,
|
||||
Players, Resources,
|
||||
tasks::{Task, Tasks},
|
||||
units::Unit,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MinerUnit {
|
||||
owner: Players,
|
||||
hp: u16,
|
||||
can_dig: bool,
|
||||
digging_power: f32,
|
||||
coords: (usize, usize),
|
||||
tasks: VecDeque<Tasks>,
|
||||
owner: Players,
|
||||
hp: u16,
|
||||
can_dig: bool,
|
||||
digging_power: f32,
|
||||
coords: (usize, usize),
|
||||
tasks: VecDeque<Tasks>,
|
||||
}
|
||||
|
||||
impl MinerUnit {
|
||||
pub const RECRUIT_COST: Resources = Resources {
|
||||
wood: 10,
|
||||
metal: 15,
|
||||
};
|
||||
pub fn new(owner: Players, coords: (usize, usize)) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
hp: 100,
|
||||
can_dig: true,
|
||||
digging_power: 0.55,
|
||||
coords,
|
||||
tasks: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn col_to_letters(&self, col: usize) -> String {
|
||||
let mut col: usize = col + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
pub const RECRUIT_COST: Resources = Resources {
|
||||
wood: 10,
|
||||
metal: 15,
|
||||
};
|
||||
pub fn new(owner: Players, coords: (usize, usize)) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
hp: 100,
|
||||
can_dig: true,
|
||||
digging_power: 0.55,
|
||||
coords,
|
||||
tasks: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
fn col_to_letters(&self, col: usize) -> String {
|
||||
let mut col: usize = col + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
fn display_coords(&self, coords: &(usize, usize)) -> String {
|
||||
format!("{}{}", self.col_to_letters(coords.0), coords.1)
|
||||
}
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
fn display_coords(&self, coords: &(usize, usize)) -> String {
|
||||
format!("{}{}", self.col_to_letters(coords.0), coords.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Unit for MinerUnit {
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
|
||||
fn get_tag(&self) -> char {
|
||||
'M'
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
'M'
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Miner"
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Miner"
|
||||
}
|
||||
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.hp
|
||||
}
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.hp
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
100
|
||||
}
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.can_dig
|
||||
}
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.can_dig
|
||||
}
|
||||
|
||||
fn get_can_build(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn get_can_build(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.digging_power
|
||||
}
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.digging_power
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.coords
|
||||
}
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.coords
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
self.coords = coords;
|
||||
}
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
self.coords = coords;
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
&self.tasks
|
||||
}
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
&self.tasks
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
&mut self.tasks
|
||||
}
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
&mut self.tasks
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
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::<Vec<Line<'_>>>(),
|
||||
)
|
||||
}
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
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::<Vec<Line<'_>>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn set_task(&mut self, task: Tasks) {
|
||||
self.tasks.push_back(task);
|
||||
}
|
||||
fn set_task(&mut self, task: Tasks) {
|
||||
self.tasks.push_back(task);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,90 @@
|
||||
use crate::app::states::skirmish_states::{Players, tasks::Tasks};
|
||||
use ratatui::{
|
||||
style::Stylize,
|
||||
text::{Line, Span},
|
||||
style::Stylize,
|
||||
text::{Line, Span},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub trait Unit {
|
||||
fn is_unit(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players;
|
||||
|
||||
fn get_tag(&self) -> char;
|
||||
|
||||
fn get_name(&self) -> &'static str;
|
||||
|
||||
fn get_hp(&self) -> u16;
|
||||
|
||||
fn get_max_hp(&self) -> u16;
|
||||
|
||||
fn get_can_dig(&self) -> bool;
|
||||
|
||||
fn get_can_build(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32;
|
||||
|
||||
fn get_coords(&self) -> (usize, usize);
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize));
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks>;
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks>;
|
||||
|
||||
fn set_task(&mut self, task: Tasks);
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>>;
|
||||
|
||||
fn base_text(&self) -> Vec<Line<'_>> {
|
||||
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<Line<'_>> = self.get_tasks_formatted();
|
||||
|
||||
let mut lines: Vec<Line<'_>> = Vec::from([
|
||||
Line::from_iter(["Owner: ".gray(), owner]),
|
||||
Line::from_iter([
|
||||
"HP: ".gray(),
|
||||
hp.green(),
|
||||
"/".gray(),
|
||||
max_hp.green(),
|
||||
" ( ".gray(),
|
||||
hp_percent.green(),
|
||||
"% )".gray(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if can_dig {
|
||||
lines.push(Line::from_iter([
|
||||
"Digging power: ".gray(),
|
||||
digging_power.green(),
|
||||
]));
|
||||
fn is_unit(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
if self.get_can_build() {
|
||||
lines.push(Line::from("Can build: Yes").green());
|
||||
fn get_owner(&self) -> Players;
|
||||
|
||||
fn get_tag(&self) -> char;
|
||||
|
||||
fn get_name(&self) -> &'static str;
|
||||
|
||||
fn get_hp(&self) -> u16;
|
||||
|
||||
fn get_max_hp(&self) -> u16;
|
||||
|
||||
fn get_can_dig(&self) -> bool;
|
||||
|
||||
fn get_can_build(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
if !have_any_tasks {
|
||||
lines.push(Line::from("Tasks Queue: ".gray()));
|
||||
for task in tasks {
|
||||
lines.push(task);
|
||||
}
|
||||
fn get_digging_power(&self) -> f32;
|
||||
|
||||
fn get_coords(&self) -> (usize, usize);
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize));
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks>;
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks>;
|
||||
|
||||
fn set_task(&mut self, task: Tasks);
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>>;
|
||||
|
||||
fn base_text(&self) -> Vec<Line<'_>> {
|
||||
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<Line<'_>> = self.get_tasks_formatted();
|
||||
|
||||
let mut lines: Vec<Line<'_>> = Vec::from([
|
||||
Line::from_iter(["Owner: ".gray(), owner]),
|
||||
Line::from_iter([
|
||||
"HP: ".gray(),
|
||||
hp.green(),
|
||||
"/".gray(),
|
||||
max_hp.green(),
|
||||
" ( ".gray(),
|
||||
hp_percent.green(),
|
||||
"% )".gray(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if can_dig {
|
||||
lines.push(Line::from_iter([
|
||||
"Digging power: ".gray(),
|
||||
digging_power.green(),
|
||||
]));
|
||||
}
|
||||
|
||||
if self.get_can_build() {
|
||||
lines.push(Line::from("Can build: Yes").green());
|
||||
}
|
||||
|
||||
if !have_any_tasks {
|
||||
lines.push(Line::from("Tasks Queue: ".gray()));
|
||||
for task in tasks {
|
||||
lines.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
self.base_text()
|
||||
}
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
self.base_text()
|
||||
}
|
||||
}
|
||||
@@ -1,158 +1,154 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players,
|
||||
tasks::Tasks,
|
||||
units::{MinerUnit, Unit},
|
||||
Players,
|
||||
tasks::Tasks,
|
||||
units::{MinerUnit, Unit},
|
||||
};
|
||||
use ratatui::text::Line;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Units {
|
||||
Miner(MinerUnit),
|
||||
Miner(MinerUnit),
|
||||
}
|
||||
|
||||
impl Units {
|
||||
#[inline]
|
||||
pub fn u(&self) -> &dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
#[inline]
|
||||
pub fn u(&self) -> &dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn u_mut(&mut self) -> &mut dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
#[inline]
|
||||
pub fn u_mut(&mut self) -> &mut dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Unit for Units {
|
||||
fn get_can_build(&self) -> bool {
|
||||
self.u().get_can_build()
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
self.u().get_tag()
|
||||
}
|
||||
fn get_can_build(&self) -> bool {
|
||||
self.u().get_can_build()
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
self.u().get_tag()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.u().get_name()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.u().get_name()
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.u().get_owner()
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.u().get_owner()
|
||||
}
|
||||
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.u().get_hp()
|
||||
}
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.u().get_hp()
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
self.u().get_max_hp()
|
||||
}
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
self.u().get_max_hp()
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.u().get_can_dig()
|
||||
}
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.u().get_can_dig()
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.u().get_digging_power()
|
||||
}
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.u().get_digging_power()
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.u().get_coords()
|
||||
}
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.u().get_coords()
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
self.u_mut().set_coords(coords);
|
||||
}
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
self.u_mut().set_coords(coords);
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
self.u().get_ref_tasks()
|
||||
}
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
self.u().get_ref_tasks()
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
self.u_mut().get_mut_tasks()
|
||||
}
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
self.u_mut().get_mut_tasks()
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
self.u().get_tasks_formatted()
|
||||
}
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
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<Units> {
|
||||
fn get_can_build(&self) -> bool {
|
||||
self
|
||||
.as_ref()
|
||||
.is_some_and(|unit: &Units| -> bool { unit.get_can_build() })
|
||||
}
|
||||
fn is_unit(&self) -> bool {
|
||||
self.as_ref().map_or(false, |u| u.is_unit())
|
||||
}
|
||||
|
||||
fn get_tag(&self) -> char {
|
||||
self.as_ref().map_or(' ', |u| u.get_tag())
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.as_ref().map_or("", |u| u.get_name())
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner())
|
||||
}
|
||||
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.as_ref().map_or(0, |u| u.get_hp())
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
self.as_ref().map_or(0, |u| u.get_max_hp())
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.as_ref().map_or(false, |u| u.get_can_dig())
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.as_ref().map_or(0.0, |u| u.get_digging_power())
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.as_ref().map_or((0, 0), |u| u.get_coords())
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
if let Some(unit) = self.as_mut() {
|
||||
unit.set_coords(coords);
|
||||
fn get_can_build(&self) -> bool {
|
||||
self.as_ref()
|
||||
.is_some_and(|unit: &Units| -> bool { unit.get_can_build() })
|
||||
}
|
||||
fn is_unit(&self) -> bool {
|
||||
self.as_ref().map_or(false, |u| u.is_unit())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
self
|
||||
.as_ref()
|
||||
.expect("attempted to get referenced tasks from `None`")
|
||||
.get_ref_tasks()
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
self.as_ref().map_or(' ', |u| u.get_tag())
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
self
|
||||
.as_mut()
|
||||
.expect("attempted to get mutable tasks from `None`")
|
||||
.get_mut_tasks()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.as_ref().map_or("", |u| u.get_name())
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
self
|
||||
.as_ref()
|
||||
.map_or(Vec::new(), |u| u.get_tasks_formatted())
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner())
|
||||
}
|
||||
|
||||
fn set_task(&mut self, task: Tasks) {
|
||||
self.as_mut().map_or((), |u| u.set_task(task))
|
||||
}
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.as_ref().map_or(0, |u| u.get_hp())
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
self.as_ref().map_or(0, |u| u.get_max_hp())
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.as_ref().map_or(false, |u| u.get_can_dig())
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.as_ref().map_or(0.0, |u| u.get_digging_power())
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.as_ref().map_or((0, 0), |u| u.get_coords())
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
if let Some(unit) = self.as_mut() {
|
||||
unit.set_coords(coords);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
self.as_ref()
|
||||
.expect("attempted to get referenced tasks from `None`")
|
||||
.get_ref_tasks()
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
self.as_mut()
|
||||
.expect("attempted to get mutable tasks from `None`")
|
||||
.get_mut_tasks()
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -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<Line<'a>> {
|
||||
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<Line<'a>> {
|
||||
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))],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+135
-135
@@ -1,185 +1,185 @@
|
||||
use clap::ValueEnum;
|
||||
use once_cell::sync::Lazy;
|
||||
use rodio::{
|
||||
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize,
|
||||
decoder::LoopedDecoder, source::Amplify,
|
||||
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize,
|
||||
decoder::LoopedDecoder, source::Amplify,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::read,
|
||||
io::{BufReader, Cursor},
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
|
||||
collections::HashMap,
|
||||
fs::read,
|
||||
io::{BufReader, Cursor},
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AudioCmd {
|
||||
Mute,
|
||||
VolumeUp,
|
||||
VolumeDown,
|
||||
ChangePart(SoundrackParts),
|
||||
ChangeSoundtrack(Soundtrack),
|
||||
Mute,
|
||||
VolumeUp,
|
||||
VolumeDown,
|
||||
ChangePart(SoundrackParts),
|
||||
ChangeSoundtrack(Soundtrack),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, ValueEnum)]
|
||||
pub enum Soundtrack {
|
||||
Default,
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum SoundrackParts {
|
||||
Calm,
|
||||
Buildup,
|
||||
Assault,
|
||||
Outro,
|
||||
Calm,
|
||||
Buildup,
|
||||
Assault,
|
||||
Outro,
|
||||
}
|
||||
|
||||
type SoundtrackCache = HashMap<SoundrackParts, Vec<u8>>;
|
||||
|
||||
static PRELOADED_SOUNDTRACKS: Lazy<RwLock<HashMap<Soundtrack, Arc<SoundtrackCache>>>> =
|
||||
Lazy::new(|| {
|
||||
let mut map: HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>> = HashMap::new();
|
||||
Lazy::new(|| {
|
||||
let mut map: HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>> = HashMap::new();
|
||||
|
||||
map.insert(
|
||||
Soundtrack::Default,
|
||||
Arc::new(load_soundtrack_folder("default")),
|
||||
);
|
||||
map.insert(
|
||||
Soundtrack::Default,
|
||||
Arc::new(load_soundtrack_folder("default")),
|
||||
);
|
||||
|
||||
RwLock::new(map)
|
||||
});
|
||||
RwLock::new(map)
|
||||
});
|
||||
|
||||
fn load_soundtrack_folder(folder: &str) -> SoundtrackCache {
|
||||
let soundtrack_root: PathBuf = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|executable: PathBuf| -> Option<PathBuf> {
|
||||
executable
|
||||
.parent()
|
||||
.map(|directory: &Path| -> PathBuf { directory.join("soundtracks") })
|
||||
})
|
||||
.filter(|directory: &PathBuf| -> bool { directory.is_dir() })
|
||||
.unwrap_or_else(|| -> PathBuf { PathBuf::from("soundtracks") });
|
||||
let base: PathBuf = soundtrack_root.join(folder);
|
||||
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
|
||||
let soundtrack_root: PathBuf = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|executable: PathBuf| -> Option<PathBuf> {
|
||||
executable
|
||||
.parent()
|
||||
.map(|directory: &Path| -> PathBuf { directory.join("soundtracks") })
|
||||
})
|
||||
.filter(|directory: &PathBuf| -> bool { directory.is_dir() })
|
||||
.unwrap_or_else(|| -> PathBuf { PathBuf::from("soundtracks") });
|
||||
let base: PathBuf = soundtrack_root.join(folder);
|
||||
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
|
||||
|
||||
macro_rules! load {
|
||||
($part:ident, $file:expr) => {
|
||||
let path: PathBuf = base.join($file);
|
||||
cache.insert(
|
||||
SoundrackParts::$part,
|
||||
read(&path).expect(&format!("cannot read {}", path.display())),
|
||||
);
|
||||
};
|
||||
}
|
||||
macro_rules! load {
|
||||
($part:ident, $file:expr) => {
|
||||
let path: PathBuf = base.join($file);
|
||||
cache.insert(
|
||||
SoundrackParts::$part,
|
||||
read(&path).expect(&format!("cannot read {}", path.display())),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
load!(Calm, "calm.ogg"); // calm.ogg
|
||||
load!(Buildup, "test.ogg"); // buildup.ogg
|
||||
load!(Assault, "test.ogg"); // assault.ogg
|
||||
load!(Outro, "test.ogg"); // outro.ogg
|
||||
load!(Calm, "calm.ogg"); // calm.ogg
|
||||
load!(Buildup, "test.ogg"); // buildup.ogg
|
||||
load!(Assault, "test.ogg"); // assault.ogg
|
||||
load!(Outro, "test.ogg"); // outro.ogg
|
||||
|
||||
cache
|
||||
cache
|
||||
}
|
||||
|
||||
pub fn load_audio(
|
||||
soundtrack: &Soundtrack,
|
||||
part: &SoundrackParts,
|
||||
soundtrack: &Soundtrack,
|
||||
part: &SoundrackParts,
|
||||
) -> Amplify<LoopedDecoder<BufReader<Cursor<Vec<u8>>>>> {
|
||||
{
|
||||
let guard: RwLockReadGuard<'_, HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>> =
|
||||
PRELOADED_SOUNDTRACKS
|
||||
.read()
|
||||
.map_err(|e| {
|
||||
eprintln!("RwLock poisoned: {}", e);
|
||||
exit(1);
|
||||
})
|
||||
.expect("lock issue");
|
||||
{
|
||||
let guard: RwLockReadGuard<'_, HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>> =
|
||||
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<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||
let data: &Vec<u8> = 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<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>,
|
||||
> = 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<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
|
||||
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
}
|
||||
}
|
||||
|
||||
let folder_name: &str = match soundtrack {
|
||||
Soundtrack::Default => "default",
|
||||
};
|
||||
{
|
||||
let mut guard: RwLockWriteGuard<
|
||||
'_,
|
||||
HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>,
|
||||
> = PRELOADED_SOUNDTRACKS
|
||||
.write()
|
||||
.map_err(|e| {
|
||||
eprintln!("RwLock poisoned: {}", e);
|
||||
exit(1);
|
||||
})
|
||||
.expect("lock issue");
|
||||
|
||||
let new_cache: Arc<HashMap<SoundrackParts, Vec<u8>>> =
|
||||
Arc::new(load_soundtrack_folder(folder_name));
|
||||
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
|
||||
let data: &Vec<u8> = new_cache.get(&part).expect("part missing after load");
|
||||
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
}
|
||||
|
||||
guard.insert(*soundtrack, Arc::clone(&new_cache));
|
||||
let folder_name: &str = match soundtrack {
|
||||
Soundtrack::Default => "default",
|
||||
};
|
||||
|
||||
Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20)
|
||||
}
|
||||
let new_cache: Arc<HashMap<SoundrackParts, Vec<u8>>> =
|
||||
Arc::new(load_soundtrack_folder(folder_name));
|
||||
|
||||
let data: &Vec<u8> = new_cache.get(&part).expect("part missing after load");
|
||||
|
||||
guard.insert(*soundtrack, Arc::clone(&new_cache));
|
||||
|
||||
Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_audio(rx: Receiver<AudioCmd>, 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-14
@@ -2,20 +2,20 @@ use ratatui::crossterm::event::{Event, KeyEvent, read};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub fn handle_ct_events(input_tx: Sender<KeyEvent>, resize_tx: Sender<(u16, u16)>) {
|
||||
loop {
|
||||
match read() {
|
||||
Ok(Event::Key(k)) => {
|
||||
if input_tx.send(k).is_err() {
|
||||
break;
|
||||
loop {
|
||||
match read() {
|
||||
Ok(Event::Key(k)) => {
|
||||
if input_tx.send(k).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(cols, rows)) => {
|
||||
if resize_tx.send((cols, rows)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(cols, rows)) => {
|
||||
if resize_tx.send((cols, rows)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-12
@@ -1,21 +1,21 @@
|
||||
use std::{
|
||||
sync::mpsc::Sender,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
sync::mpsc::Sender,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub const TICK_MS: u8 = 33;
|
||||
|
||||
pub fn handle_tick_event(tx: Sender<()>) {
|
||||
let interval: Duration = Duration::from_millis(TICK_MS as u64);
|
||||
loop {
|
||||
if tx.send(()).is_err() {
|
||||
break;
|
||||
}
|
||||
let interval: Duration = Duration::from_millis(TICK_MS as u64);
|
||||
loop {
|
||||
if tx.send(()).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let elapsed: Duration = Instant::now().elapsed();
|
||||
if interval > elapsed {
|
||||
thread::sleep(interval - elapsed);
|
||||
let elapsed: Duration = Instant::now().elapsed();
|
||||
if interval > elapsed {
|
||||
thread::sleep(interval - elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
+74
-74
@@ -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<Rect> = main_menu_area.layout_vec(&Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]));
|
||||
let main_menu_areas: Vec<Rect> = main_menu_area.layout_vec(&Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]));
|
||||
|
||||
{
|
||||
let title_area: Rect = main_menu_areas[0].centered_vertically(Constraint::Percentage(50));
|
||||
{
|
||||
let title_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<Line<'_>> = 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<Line<'_>> = 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);
|
||||
}
|
||||
}
|
||||
+252
-248
@@ -1,265 +1,269 @@
|
||||
use crate::app::{
|
||||
App,
|
||||
helpers::block_title_helper,
|
||||
keybindings::{Action, available_skirmish_actions},
|
||||
states::skirmish_states::{
|
||||
BoardState, Players,
|
||||
resources::{WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_INTERVAL_MS},
|
||||
structures::{Mine, Structure, Structures},
|
||||
units::MinerUnit,
|
||||
},
|
||||
widgets::{BoardWidget, CellWidget, KeybindingsWidget, SidePanelWidget},
|
||||
App,
|
||||
helpers::block_title_helper,
|
||||
keybindings::{Action, available_skirmish_actions},
|
||||
states::skirmish_states::{
|
||||
BoardState, Players,
|
||||
resources::{WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_INTERVAL_MS},
|
||||
structures::{Mine, Structure, Structures},
|
||||
units::MinerUnit,
|
||||
},
|
||||
widgets::{BoardWidget, CellWidget, KeybindingsWidget, SidePanelWidget},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
pub fn skirmish_layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::vertical([
|
||||
Constraint::Length(4),
|
||||
Constraint::Fill(1),
|
||||
// Four movement rows plus borders keep the board geometry stable across modes.
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.areas(area)
|
||||
Layout::vertical([
|
||||
Constraint::Length(4),
|
||||
Constraint::Fill(1),
|
||||
// Four movement rows plus borders keep the board geometry stable across modes.
|
||||
Constraint::Length(6),
|
||||
])
|
||||
.areas(area)
|
||||
}
|
||||
|
||||
pub fn skirmish_main_area_layout(area: Rect, side_panel: bool) -> [Rect; 2] {
|
||||
let constraint: [Constraint; 2] = if side_panel {
|
||||
[Constraint::Fill(1), Constraint::Length(39)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
};
|
||||
let constraint: [Constraint; 2] = if side_panel {
|
||||
[Constraint::Fill(1), Constraint::Length(39)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
};
|
||||
|
||||
Layout::horizontal(constraint).areas(area)
|
||||
Layout::horizontal(constraint).areas(area)
|
||||
}
|
||||
|
||||
pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
let Some(states) = app.states_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let board: &BoardState = &states.skirmish.board;
|
||||
|
||||
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
|
||||
|
||||
{
|
||||
let lines: Vec<Line<'_>> = Vec::from_iter([
|
||||
Line::raw("War in Tunnels").yellow(),
|
||||
Line::from_iter([
|
||||
format!(
|
||||
"Wood: {} (+{}/{}s) | ",
|
||||
states.skirmish.player_resources.wood,
|
||||
WOOD_DELIVERY_AMOUNT,
|
||||
WOOD_DELIVERY_INTERVAL_MS / 1000
|
||||
),
|
||||
format!("Metal: {} | ", states.skirmish.player_resources.metal),
|
||||
format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit),
|
||||
format!(
|
||||
"Skills points: {} ({}/{}) | ",
|
||||
1, 20, states.settings.skill_points_limit
|
||||
),
|
||||
format!("Perk Deck: {}/9 | ", 5),
|
||||
format!("Tick: {}", states.skirmish.turn_counter),
|
||||
]),
|
||||
]);
|
||||
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.block(
|
||||
Block::new()
|
||||
.gray()
|
||||
.padding(Padding {
|
||||
left: 1,
|
||||
right: 1,
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
})
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
|
||||
)
|
||||
.render(title_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
let main_block: Block = Block::new()
|
||||
.gray()
|
||||
.title(block_title_helper(
|
||||
&[
|
||||
("Skirmish".to_string(), Color::Magenta),
|
||||
("Map".to_string(), Color::Green),
|
||||
],
|
||||
Some(" - "),
|
||||
))
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT);
|
||||
|
||||
let main_inner_area: Rect = main_block.inner(main_area);
|
||||
main_block.render(main_area, buf);
|
||||
|
||||
let [board_area, side_panel_area] =
|
||||
skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel);
|
||||
|
||||
let cells_area: Rect = board_area.inner(Margin {
|
||||
horizontal: 1,
|
||||
vertical: 1,
|
||||
});
|
||||
// .centered(
|
||||
// Constraint::Length(
|
||||
// (board.cell_width * board.cols) as u16,
|
||||
// ),
|
||||
// Constraint::Length(
|
||||
// (board.cell_height * board.rows) as u16,
|
||||
// ),
|
||||
// );
|
||||
|
||||
BoardWidget::new(&board).render(cells_area, buf);
|
||||
|
||||
if states.skirmish.side_panel {
|
||||
let (row, col) = board.get_focused_cell().get_coords();
|
||||
let mut is_unit_marked: bool = false;
|
||||
|
||||
if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) {
|
||||
is_unit_marked = first_row == &row && first_col == &col;
|
||||
}
|
||||
|
||||
SidePanelWidget::new(
|
||||
(row, col),
|
||||
&board.get_ref_cell(row, col).get_structure(),
|
||||
&board.get_ref_cell(row, col).get_ref_option_unit(),
|
||||
is_unit_marked,
|
||||
)
|
||||
.render(side_panel_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let actions: Vec<Action> = available_skirmish_actions(&states.skirmish);
|
||||
let descriptions: [(Action, &'static str); 11] = [
|
||||
(
|
||||
Action::Enter,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Queue Miner"
|
||||
} else {
|
||||
"Build Mine"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Recruitment,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Close recruitment"
|
||||
} else {
|
||||
"Recruit"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Delete,
|
||||
if states.skirmish.building_view {
|
||||
"Cancel construction"
|
||||
} else {
|
||||
"Cancel marking"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Space,
|
||||
if board.marked_cells.marking_cells {
|
||||
"Confirm route"
|
||||
} else {
|
||||
"Select miner"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Esc,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Close recruitment"
|
||||
} else if states.skirmish.building_view {
|
||||
"Close buildings"
|
||||
} else if board.marked_cells.marking_cells {
|
||||
"Cancel route"
|
||||
} else {
|
||||
"Menu"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Building,
|
||||
if states.skirmish.building_view {
|
||||
"Close buildings"
|
||||
} else {
|
||||
"Build"
|
||||
},
|
||||
),
|
||||
(Action::Tab, "Details"),
|
||||
(Action::ScrollUp, "Scroll"),
|
||||
(Action::ScrollDown, "Scroll"),
|
||||
(Action::ScrollLeft, "Scroll"),
|
||||
(Action::ScrollRight, "Scroll"),
|
||||
];
|
||||
KeybindingsWidget::with_descriptions(&actions, keybindings_area.height, &descriptions)
|
||||
.render(keybindings_area, buf);
|
||||
}
|
||||
if states.skirmish.building_view {
|
||||
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
|
||||
let coords: (usize, usize) = board.get_focused_cell().get_coords();
|
||||
let structure: Structures = board.get_ref_cell(coords.0, coords.1).get_structure();
|
||||
let status: String = if let Structures::MineConstruction(site) = structure
|
||||
&& site.get_owner() == Players::Player
|
||||
{
|
||||
format!(
|
||||
"Construction: {}%\n{}\nDelete: cancel and refund full cost",
|
||||
site.progress_percent(),
|
||||
if board.has_adjacent_builder(coords, Players::Player) {
|
||||
"Builder working"
|
||||
} else {
|
||||
"Paused: adjacent friendly builder required"
|
||||
}
|
||||
)
|
||||
} else if matches!(structure, Structures::Mine(_)) && structure.get_owner() == Players::Player {
|
||||
"Mine completed".to_string()
|
||||
} else if states
|
||||
.skirmish
|
||||
.can_start_mine_construction(coords, Players::Player)
|
||||
{
|
||||
"Ready to build: Enter".to_string()
|
||||
} else {
|
||||
"Construction unavailable: check site, builder and resources".to_string()
|
||||
let Some(states) = app.states_ref() else {
|
||||
return;
|
||||
};
|
||||
Clear.render(popup, buf);
|
||||
Paragraph::new(format!("Mine\nCost: {} Wood + {} Metal | Build time: 10s\nRequires: neutral Ore + adjacent friendly builder\nProduces: 1 Metal/s (2 with a Miner inside)\n{status}", Mine::COST.wood, Mine::COST.metal))
|
||||
|
||||
let board: &BoardState = &states.skirmish.board;
|
||||
|
||||
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
|
||||
|
||||
{
|
||||
let lines: Vec<Line<'_>> = Vec::from_iter([
|
||||
Line::raw("War in Tunnels").yellow(),
|
||||
Line::from_iter([
|
||||
format!(
|
||||
"Wood: {} (+{}/{}s) | ",
|
||||
states.skirmish.player_resources.wood,
|
||||
WOOD_DELIVERY_AMOUNT,
|
||||
WOOD_DELIVERY_INTERVAL_MS / 1000
|
||||
),
|
||||
format!("Metal: {} | ", states.skirmish.player_resources.metal),
|
||||
format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit),
|
||||
format!(
|
||||
"Skills points: {} ({}/{}) | ",
|
||||
1, 20, states.settings.skill_points_limit
|
||||
),
|
||||
format!("Perk Deck: {}/9 | ", 5),
|
||||
format!("Tick: {}", states.skirmish.turn_counter),
|
||||
]),
|
||||
]);
|
||||
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.block(
|
||||
Block::new()
|
||||
.gray()
|
||||
.padding(Padding {
|
||||
left: 1,
|
||||
right: 1,
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
})
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
|
||||
)
|
||||
.render(title_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
let main_block: Block = Block::new()
|
||||
.gray()
|
||||
.title(block_title_helper(
|
||||
&[
|
||||
("Skirmish".to_string(), Color::Magenta),
|
||||
("Map".to_string(), Color::Green),
|
||||
],
|
||||
Some(" - "),
|
||||
))
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT);
|
||||
|
||||
let main_inner_area: Rect = main_block.inner(main_area);
|
||||
main_block.render(main_area, buf);
|
||||
|
||||
let [board_area, side_panel_area] =
|
||||
skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel);
|
||||
|
||||
let cells_area: Rect = board_area.inner(Margin {
|
||||
horizontal: 1,
|
||||
vertical: 1,
|
||||
});
|
||||
// .centered(
|
||||
// Constraint::Length(
|
||||
// (board.cell_width * board.cols) as u16,
|
||||
// ),
|
||||
// Constraint::Length(
|
||||
// (board.cell_height * board.rows) as u16,
|
||||
// ),
|
||||
// );
|
||||
|
||||
BoardWidget::new(&board).render(cells_area, buf);
|
||||
|
||||
if states.skirmish.side_panel {
|
||||
let (row, col) = board.get_focused_cell().get_coords();
|
||||
let mut is_unit_marked: bool = false;
|
||||
|
||||
if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) {
|
||||
is_unit_marked = first_row == &row && first_col == &col;
|
||||
}
|
||||
|
||||
SidePanelWidget::new(
|
||||
(row, col),
|
||||
&board.get_ref_cell(row, col).get_structure(),
|
||||
&board.get_ref_cell(row, col).get_ref_option_unit(),
|
||||
is_unit_marked,
|
||||
)
|
||||
.render(side_panel_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let actions: Vec<Action> = available_skirmish_actions(&states.skirmish);
|
||||
let descriptions: [(Action, &'static str); 11] = [
|
||||
(
|
||||
Action::Enter,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Queue Miner"
|
||||
} else {
|
||||
"Build Mine"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Recruitment,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Close recruitment"
|
||||
} else {
|
||||
"Recruit"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Delete,
|
||||
if states.skirmish.building_view {
|
||||
"Cancel construction"
|
||||
} else {
|
||||
"Cancel marking"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Space,
|
||||
if board.marked_cells.marking_cells {
|
||||
"Confirm route"
|
||||
} else {
|
||||
"Select miner"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Esc,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Close recruitment"
|
||||
} else if states.skirmish.building_view {
|
||||
"Close buildings"
|
||||
} else if board.marked_cells.marking_cells {
|
||||
"Cancel route"
|
||||
} else {
|
||||
"Menu"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Building,
|
||||
if states.skirmish.building_view {
|
||||
"Close buildings"
|
||||
} else {
|
||||
"Build"
|
||||
},
|
||||
),
|
||||
(Action::Tab, "Details"),
|
||||
(Action::ScrollUp, "Scroll"),
|
||||
(Action::ScrollDown, "Scroll"),
|
||||
(Action::ScrollLeft, "Scroll"),
|
||||
(Action::ScrollRight, "Scroll"),
|
||||
];
|
||||
KeybindingsWidget::with_descriptions(&actions, keybindings_area.height, &descriptions)
|
||||
.render(keybindings_area, buf);
|
||||
}
|
||||
if states.skirmish.building_view {
|
||||
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
|
||||
let coords: (usize, usize) = board.get_focused_cell().get_coords();
|
||||
let structure: Structures = board.get_ref_cell(coords.0, coords.1).get_structure();
|
||||
let status: String = if let Structures::MineConstruction(site) = structure
|
||||
&& site.get_owner() == Players::Player
|
||||
{
|
||||
format!(
|
||||
"Construction: {}%\n{}\nDelete: cancel and refund full cost",
|
||||
site.progress_percent(),
|
||||
if board.has_adjacent_builder(coords, Players::Player) {
|
||||
"Builder working"
|
||||
} else {
|
||||
"Paused: adjacent friendly builder required"
|
||||
}
|
||||
)
|
||||
} else if matches!(structure, Structures::Mine(_))
|
||||
&& structure.get_owner() == Players::Player
|
||||
{
|
||||
"Mine completed".to_string()
|
||||
} else if states
|
||||
.skirmish
|
||||
.can_start_mine_construction(coords, Players::Player)
|
||||
{
|
||||
"Ready to build: Enter".to_string()
|
||||
} else {
|
||||
"Construction unavailable: check site, builder and resources".to_string()
|
||||
};
|
||||
Clear.render(popup, buf);
|
||||
Paragraph::new(format!("Mine\nCost: {} Wood + {} Metal | Build time: 10s\nRequires: neutral Ore + adjacent friendly builder\nProduces: 1 Metal/s (2 with a Miner inside)\n{status}", Mine::COST.wood, Mine::COST.metal))
|
||||
.block(Block::bordered().title("Buildings").padding(Padding::uniform(1)))
|
||||
.render(popup, buf);
|
||||
}
|
||||
if states.skirmish.recruitment_view {
|
||||
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
|
||||
let coords: (usize, usize) = board.get_focused_cell().get_coords();
|
||||
let cell: &CellWidget = board.get_ref_cell(coords.0, coords.1);
|
||||
let text: String = if let Structures::Base(base) = cell.get_structure()
|
||||
&& base.get_owner() == Players::Player
|
||||
{
|
||||
let status: &'static str = base.recruitment_status(cell.get_ref_option_unit().is_some());
|
||||
let action: &'static str = if states.skirmish.can_recruit_miner(coords, Players::Player) {
|
||||
"Enter: queue Miner"
|
||||
} else {
|
||||
"Insufficient resources or queue full"
|
||||
};
|
||||
format!(
|
||||
"Miner\nCost: {} Wood + {} Metal | Recruit time: 10s\nQueued Miners: {} (includes active)\nCurrent progress: {}%\n{status}\n{action}",
|
||||
MinerUnit::RECRUIT_COST.wood,
|
||||
MinerUnit::RECRUIT_COST.metal,
|
||||
base.queued_miners(),
|
||||
base.recruitment_progress()
|
||||
)
|
||||
} else {
|
||||
"Select your Base to recruit Miners".to_string()
|
||||
};
|
||||
Clear.render(popup, buf);
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title("Recruitment")
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(popup, buf);
|
||||
}
|
||||
}
|
||||
if states.skirmish.recruitment_view {
|
||||
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
|
||||
let coords: (usize, usize) = board.get_focused_cell().get_coords();
|
||||
let cell: &CellWidget = board.get_ref_cell(coords.0, coords.1);
|
||||
let text: String = if let Structures::Base(base) = cell.get_structure()
|
||||
&& base.get_owner() == Players::Player
|
||||
{
|
||||
let status: &'static str =
|
||||
base.recruitment_status(cell.get_ref_option_unit().is_some());
|
||||
let action: &'static str = if states.skirmish.can_recruit_miner(coords, Players::Player)
|
||||
{
|
||||
"Enter: queue Miner"
|
||||
} else {
|
||||
"Insufficient resources or queue full"
|
||||
};
|
||||
format!(
|
||||
"Miner\nCost: {} Wood + {} Metal | Recruit time: 10s\nQueued Miners: {} (includes active)\nCurrent progress: {}%\n{status}\n{action}",
|
||||
MinerUnit::RECRUIT_COST.wood,
|
||||
MinerUnit::RECRUIT_COST.metal,
|
||||
base.queued_miners(),
|
||||
base.recruitment_progress()
|
||||
)
|
||||
} else {
|
||||
"Select your Base to recruit Miners".to_string()
|
||||
};
|
||||
Clear.render(popup, buf);
|
||||
Paragraph::new(text)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title("Recruitment")
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(popup, buf);
|
||||
}
|
||||
}
|
||||
+29
-30
@@ -1,46 +1,45 @@
|
||||
use crate::app::states::skirmish_states::BoardState;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
widgets::Widget,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
widgets::Widget,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct BoardWidget<'a> {
|
||||
state: &'a BoardState,
|
||||
state: &'a BoardState,
|
||||
}
|
||||
|
||||
impl<'a> BoardWidget<'a> {
|
||||
pub fn new(state: &'a BoardState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
pub fn new(state: &'a BoardState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for BoardWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let horizontal: Rc<[Rect]> =
|
||||
Layout::horizontal(vec![
|
||||
Constraint::Length(self.state.cell_width as u16);
|
||||
self.state.cols
|
||||
])
|
||||
.split(area);
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let horizontal: Rc<[Rect]> =
|
||||
Layout::horizontal(vec![
|
||||
Constraint::Length(self.state.cell_width as u16);
|
||||
self.state.cols
|
||||
])
|
||||
.split(area);
|
||||
|
||||
for (col_idx, col_area) in horizontal.iter().enumerate() {
|
||||
let vertical: Rc<[Rect]> =
|
||||
Layout::vertical(vec![
|
||||
Constraint::Length(self.state.cell_height as u16);
|
||||
self.state.rows
|
||||
])
|
||||
.split(*col_area);
|
||||
for (col_idx, col_area) in horizontal.iter().enumerate() {
|
||||
let vertical: Rc<[Rect]> =
|
||||
Layout::vertical(vec![
|
||||
Constraint::Length(self.state.cell_height as u16);
|
||||
self.state.rows
|
||||
])
|
||||
.split(*col_area);
|
||||
|
||||
for (row_idx, cell_area) in vertical.iter().enumerate() {
|
||||
self
|
||||
.state
|
||||
.cells
|
||||
.get(row_idx + self.state.vertical_offset.get_value())
|
||||
.and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value()))
|
||||
.map(|cell| cell.clone().render(*cell_area, buf));
|
||||
}
|
||||
for (row_idx, cell_area) in vertical.iter().enumerate() {
|
||||
self.state
|
||||
.cells
|
||||
.get(row_idx + self.state.vertical_offset.get_value())
|
||||
.and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value()))
|
||||
.map(|cell| cell.clone().render(*cell_area, buf));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+142
-143
@@ -1,166 +1,165 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
ZoomLevel,
|
||||
structures::{Stone, Structure, Structures},
|
||||
tasks::Tasks,
|
||||
units::{Unit, Units},
|
||||
ZoomLevel,
|
||||
structures::{Stone, Structure, Structures},
|
||||
tasks::Tasks,
|
||||
units::{Unit, Units},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span, ToSpan},
|
||||
widgets::{Block, BorderType, Borders, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span, ToSpan},
|
||||
widgets::{Block, BorderType, Borders, Paragraph, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CellWidget {
|
||||
row: usize,
|
||||
col: usize,
|
||||
selected: bool,
|
||||
zoom_level: ZoomLevel,
|
||||
structure: Structures,
|
||||
marked: bool,
|
||||
unit: Option<Units>,
|
||||
row: usize,
|
||||
col: usize,
|
||||
selected: bool,
|
||||
zoom_level: ZoomLevel,
|
||||
structure: Structures,
|
||||
marked: bool,
|
||||
unit: Option<Units>,
|
||||
}
|
||||
|
||||
impl CellWidget {
|
||||
pub fn new(
|
||||
row: usize,
|
||||
col: usize,
|
||||
zoom_level: ZoomLevel,
|
||||
selected: bool,
|
||||
structure: Structures,
|
||||
unit: Option<Units>,
|
||||
) -> 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<Units> {
|
||||
self.unit.clone()
|
||||
}
|
||||
|
||||
pub fn get_ref_option_unit(&self) -> &Option<Units> {
|
||||
&self.unit
|
||||
}
|
||||
|
||||
pub fn get_mut_option_unit(&mut self) -> &mut Option<Units> {
|
||||
&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<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
pub fn new(
|
||||
row: usize,
|
||||
col: usize,
|
||||
zoom_level: ZoomLevel,
|
||||
selected: bool,
|
||||
structure: Structures,
|
||||
unit: Option<Units>,
|
||||
) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
selected,
|
||||
zoom_level,
|
||||
marked: false,
|
||||
structure,
|
||||
unit,
|
||||
}
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
pub fn set_selected(&mut self, selected: bool) -> &mut Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
fn display_coords(&self) -> Span<'_> {
|
||||
if self.selected || self.structure != Structures::Stone(Stone::new()) || self.marked {
|
||||
format!("{}{}", self.col_to_letters(), self.row).green()
|
||||
} else {
|
||||
"".to_span()
|
||||
pub fn set_zoom_level(&mut self, zoom_level: ZoomLevel) -> &mut Self {
|
||||
self.zoom_level = zoom_level;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn fg_color(&self) -> Color {
|
||||
if self.marked && self.selected {
|
||||
Color::Magenta
|
||||
} else if self.marked && !self.selected {
|
||||
Color::LightMagenta
|
||||
} else if !self.marked && self.selected {
|
||||
Color::LightYellow
|
||||
} else {
|
||||
self.structure.get_color()
|
||||
pub fn get_structure(&self) -> Structures {
|
||||
self.structure
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text_area(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = 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<Units> {
|
||||
self.unit.clone()
|
||||
}
|
||||
if let Structures::MineConstruction(site) = self.structure {
|
||||
let middle: usize = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{}%", site.progress_percent())).cyan();
|
||||
}
|
||||
if let Some(unit) = &self.unit
|
||||
&& let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front()
|
||||
&& let Some(percent) = task.get_movement_progress()
|
||||
{
|
||||
let middle = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn get_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(self.fg_color()))
|
||||
.title(self.display_coords())
|
||||
.border_type(BorderType::Rounded)
|
||||
}
|
||||
pub fn get_ref_option_unit(&self) -> &Option<Units> {
|
||||
&self.unit
|
||||
}
|
||||
|
||||
pub fn get_mut_option_unit(&mut self) -> &mut Option<Units> {
|
||||
&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<char> = 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<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = self.zoom_level.get_cell_text_area(
|
||||
self.structure.get_tag(),
|
||||
self.structure.get_level(),
|
||||
self.unit
|
||||
.get_tag()
|
||||
.to_string()
|
||||
.fg(self.unit.get_owner().get_color()),
|
||||
);
|
||||
if let Structures::Stone(stone) = self.structure
|
||||
&& stone.get_durability() < stone.get_max_durability()
|
||||
{
|
||||
let percent: u32 =
|
||||
u32::from(stone.get_durability()) * 100 / u32::from(stone.get_max_durability());
|
||||
let middle = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{percent}%")).cyan();
|
||||
}
|
||||
if let Structures::MineConstruction(site) = self.structure {
|
||||
let middle: usize = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{}%", site.progress_percent())).cyan();
|
||||
}
|
||||
if let Some(unit) = &self.unit
|
||||
&& let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front()
|
||||
&& let Some(percent) = task.get_movement_progress()
|
||||
{
|
||||
let middle = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn get_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(self.fg_color()))
|
||||
.title(self.display_coords())
|
||||
.border_type(BorderType::Rounded)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CellWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new(self.get_text_area())
|
||||
.alignment(Alignment::Center)
|
||||
.block(self.get_block())
|
||||
.render(area, buf);
|
||||
}
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new(self.get_text_area())
|
||||
.alignment(Alignment::Center)
|
||||
.block(self.get_block())
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
+133
-128
@@ -1,14 +1,14 @@
|
||||
use crate::app::{
|
||||
helpers::block_single_title_helper,
|
||||
keybindings::{Action, Group, KeyBinding, binding_for},
|
||||
helpers::block_single_title_helper,
|
||||
keybindings::{Action, Group, KeyBinding, binding_for},
|
||||
};
|
||||
use clap::ValueEnum;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Padding, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Padding, Paragraph, Widget},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -18,136 +18,141 @@ use std::collections::HashSet;
|
||||
/// logical group of keybindings (e.g. navigation, editing, etc.). The paragraphs
|
||||
/// are rendered side‑by‑side inside a bordered container.
|
||||
pub struct KeybindingsWidget {
|
||||
/// Paragraphs, one per used `Group`, containing the formatted keybinding lines.
|
||||
grouped: Vec<Paragraph<'static>>,
|
||||
widths: Vec<u16>,
|
||||
/// Paragraphs, one per used `Group`, containing the formatted keybinding lines.
|
||||
grouped: Vec<Paragraph<'static>>,
|
||||
widths: Vec<u16>,
|
||||
}
|
||||
|
||||
impl KeybindingsWidget {
|
||||
/// Create a new widget from a list of actions.
|
||||
///
|
||||
/// For each action, the corresponding static [`KeyBinding`] is looked up
|
||||
/// using [`binding_for`]. Only groups that have at least one binding are
|
||||
/// displayed. The resulting widget contains a paragraph for each group,
|
||||
/// formatted with the key symbol (red & bold) followed by its description.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `actions` – A slice of actions for which keybindings should be shown.
|
||||
/// * `area_height` – The height of the area in which the widget will be rendered.
|
||||
/// This value is used to calculate vertical padding so that the keybinding rows
|
||||
/// are vertically centered when the widget's allocated height is larger than the
|
||||
/// number of lines needed to display the bindings.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A fully‑initialised `KeybindingsWidget` ready for rendering.
|
||||
pub fn new(actions: &[Action], area_height: u16) -> Self {
|
||||
Self::with_descriptions(actions, area_height, &[])
|
||||
}
|
||||
|
||||
pub fn with_descriptions(
|
||||
actions: &[Action],
|
||||
area_height: u16,
|
||||
descriptions: &[(Action, &'static str)],
|
||||
) -> Self {
|
||||
let keybindings: Vec<Option<&'static KeyBinding>> =
|
||||
actions.iter().map(|a| binding_for(*a)).collect();
|
||||
|
||||
let used_groups: HashSet<Group> = keybindings
|
||||
.iter()
|
||||
.filter_map(|b| b.map(|binding| binding.group))
|
||||
.collect();
|
||||
|
||||
let mut grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
|
||||
let mut widths: Vec<u16> = Vec::new();
|
||||
|
||||
for group in Group::value_variants().iter() {
|
||||
if !used_groups.contains(group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let grouped_lines: Vec<Line<'_>> = keybindings
|
||||
.iter()
|
||||
.filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group))
|
||||
.filter_map(|b| {
|
||||
b.as_ref().map(|kb| {
|
||||
let description: &'static str = descriptions
|
||||
.iter()
|
||||
.find(|(action, _): &&(Action, &'static str)| -> bool { *action == kb.action })
|
||||
.map_or(
|
||||
kb.description,
|
||||
|(_, description): &(Action, &'static str)| -> &'static str { description },
|
||||
);
|
||||
Line::from_iter([kb.symbol.red().bold(), " - ".into(), description.into()])
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let width: usize = grouped_lines.iter().map(Line::width).max().unwrap_or(0);
|
||||
widths.push(u16::try_from(width).unwrap_or(u16::MAX).saturating_add(3));
|
||||
let height_diff: u16 =
|
||||
area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16);
|
||||
|
||||
let padding_top: u16 = if height_diff <= 1 {
|
||||
height_diff
|
||||
} else {
|
||||
height_diff / 2
|
||||
};
|
||||
|
||||
let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0));
|
||||
|
||||
if !grouped_keybindings.is_empty() {
|
||||
block = block.borders(Borders::LEFT);
|
||||
}
|
||||
|
||||
grouped_keybindings.push(
|
||||
Paragraph::new(grouped_lines)
|
||||
.alignment(Alignment::Center)
|
||||
.block(block),
|
||||
);
|
||||
/// Create a new widget from a list of actions.
|
||||
///
|
||||
/// For each action, the corresponding static [`KeyBinding`] is looked up
|
||||
/// using [`binding_for`]. Only groups that have at least one binding are
|
||||
/// displayed. The resulting widget contains a paragraph for each group,
|
||||
/// formatted with the key symbol (red & bold) followed by its description.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `actions` – A slice of actions for which keybindings should be shown.
|
||||
/// * `area_height` – The height of the area in which the widget will be rendered.
|
||||
/// This value is used to calculate vertical padding so that the keybinding rows
|
||||
/// are vertically centered when the widget's allocated height is larger than the
|
||||
/// number of lines needed to display the bindings.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A fully‑initialised `KeybindingsWidget` ready for rendering.
|
||||
pub fn new(actions: &[Action], area_height: u16) -> Self {
|
||||
Self::with_descriptions(actions, area_height, &[])
|
||||
}
|
||||
|
||||
Self {
|
||||
grouped: grouped_keybindings,
|
||||
widths,
|
||||
pub fn with_descriptions(
|
||||
actions: &[Action],
|
||||
area_height: u16,
|
||||
descriptions: &[(Action, &'static str)],
|
||||
) -> Self {
|
||||
let keybindings: Vec<Option<&'static KeyBinding>> =
|
||||
actions.iter().map(|a| binding_for(*a)).collect();
|
||||
|
||||
let used_groups: HashSet<Group> = keybindings
|
||||
.iter()
|
||||
.filter_map(|b| b.map(|binding| binding.group))
|
||||
.collect();
|
||||
|
||||
let mut grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
|
||||
let mut widths: Vec<u16> = Vec::new();
|
||||
|
||||
for group in Group::value_variants().iter() {
|
||||
if !used_groups.contains(group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let grouped_lines: Vec<Line<'_>> = keybindings
|
||||
.iter()
|
||||
.filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group))
|
||||
.filter_map(|b| {
|
||||
b.as_ref().map(|kb| {
|
||||
let description: &'static str = descriptions
|
||||
.iter()
|
||||
.find(|(action, _): &&(Action, &'static str)| -> bool {
|
||||
*action == kb.action
|
||||
})
|
||||
.map_or(
|
||||
kb.description,
|
||||
|(_, description): &(Action, &'static str)| -> &'static str {
|
||||
description
|
||||
},
|
||||
);
|
||||
Line::from_iter([kb.symbol.red().bold(), " - ".into(), description.into()])
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let width: usize = grouped_lines.iter().map(Line::width).max().unwrap_or(0);
|
||||
widths.push(u16::try_from(width).unwrap_or(u16::MAX).saturating_add(3));
|
||||
let height_diff: u16 =
|
||||
area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16);
|
||||
|
||||
let padding_top: u16 = if height_diff <= 1 {
|
||||
height_diff
|
||||
} else {
|
||||
height_diff / 2
|
||||
};
|
||||
|
||||
let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0));
|
||||
|
||||
if !grouped_keybindings.is_empty() {
|
||||
block = block.borders(Borders::LEFT);
|
||||
}
|
||||
|
||||
grouped_keybindings.push(
|
||||
Paragraph::new(grouped_lines)
|
||||
.alignment(Alignment::Center)
|
||||
.block(block),
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
grouped: grouped_keybindings,
|
||||
widths,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for KeybindingsWidget {
|
||||
/// Render the widget inside the given rectangular area.
|
||||
///
|
||||
/// The widget first draws a surrounding border with the title *Keybindings*.
|
||||
/// It then divides the inner area proportionally to each group's text width—one per
|
||||
/// used group—and renders each group's paragraph into its slice.
|
||||
///
|
||||
/// If no groups are present, the function returns early without drawing
|
||||
/// anything.
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let count: u16 = self.grouped.len() as u16;
|
||||
/// Render the widget inside the given rectangular area.
|
||||
///
|
||||
/// The widget first draws a surrounding border with the title *Keybindings*.
|
||||
/// It then divides the inner area proportionally to each group's text width—one per
|
||||
/// used group—and renders each group's paragraph into its slice.
|
||||
///
|
||||
/// If no groups are present, the function returns early without drawing
|
||||
/// anything.
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let count: u16 = self.grouped.len() as u16;
|
||||
|
||||
if count == 0 {
|
||||
return;
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let block: Block<'_> =
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(block_single_title_helper(
|
||||
"Keybindings".to_string(),
|
||||
Color::Magenta,
|
||||
));
|
||||
|
||||
let inner: Rect = block.inner(area);
|
||||
|
||||
block.render(area, buf);
|
||||
|
||||
let constraints: Vec<Constraint> = self.widths.into_iter().map(Constraint::Fill).collect();
|
||||
|
||||
let chunks: Vec<Rect> = Layout::horizontal(constraints).split(inner).to_vec();
|
||||
|
||||
for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) {
|
||||
paragraph.render(chunk, buf);
|
||||
}
|
||||
}
|
||||
|
||||
let block: Block<'_> = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(block_single_title_helper(
|
||||
"Keybindings".to_string(),
|
||||
Color::Magenta,
|
||||
));
|
||||
|
||||
let inner: Rect = block.inner(area);
|
||||
|
||||
block.render(area, buf);
|
||||
|
||||
let constraints: Vec<Constraint> = self.widths.into_iter().map(Constraint::Fill).collect();
|
||||
|
||||
let chunks: Vec<Rect> = Layout::horizontal(constraints).split(inner).to_vec();
|
||||
|
||||
for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) {
|
||||
paragraph.render(chunk, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-102
@@ -1,127 +1,127 @@
|
||||
use crate::app::{
|
||||
helpers::block_single_title_helper,
|
||||
states::skirmish_states::{
|
||||
structures::{Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
},
|
||||
helpers::block_single_title_helper,
|
||||
states::skirmish_states::{
|
||||
structures::{Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::Color,
|
||||
text::Line,
|
||||
widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::Color,
|
||||
text::Line,
|
||||
widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SidePanelWidget<'a> {
|
||||
coords: (usize, usize),
|
||||
structure: &'a Structures,
|
||||
unit: &'a Option<Units>,
|
||||
is_unit_marked: bool,
|
||||
}
|
||||
|
||||
impl<'a> SidePanelWidget<'a> {
|
||||
pub fn new(
|
||||
coords: (usize, usize),
|
||||
structure: &'a Structures,
|
||||
unit: &'a Option<Units>,
|
||||
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<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
impl<'a> SidePanelWidget<'a> {
|
||||
pub fn new(
|
||||
coords: (usize, usize),
|
||||
structure: &'a Structures,
|
||||
unit: &'a Option<Units>,
|
||||
is_unit_marked: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
structure,
|
||||
unit,
|
||||
is_unit_marked,
|
||||
}
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
fn col_to_letters(&self) -> String {
|
||||
let mut col: usize = self.coords.1 + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
fn get_outer_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Double)
|
||||
.title(block_single_title_helper(
|
||||
format!("{}{}", self.col_to_letters(), self.coords.0),
|
||||
Color::Yellow,
|
||||
))
|
||||
.title_alignment(Alignment::Center)
|
||||
}
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
|
||||
fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> {
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.title(block_single_title_helper(title, color))
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::symmetric(1, 1))
|
||||
}
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
fn get_area_constraints(&self) -> [Constraint; 2] {
|
||||
if self.unit.is_unit() {
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
fn get_outer_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Double)
|
||||
.title(block_single_title_helper(
|
||||
format!("{}{}", self.col_to_letters(), self.coords.0),
|
||||
Color::Yellow,
|
||||
))
|
||||
.title_alignment(Alignment::Center)
|
||||
}
|
||||
|
||||
fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> {
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.title(block_single_title_helper(title, color))
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::symmetric(1, 1))
|
||||
}
|
||||
|
||||
fn get_area_constraints(&self) -> [Constraint; 2] {
|
||||
if self.unit.is_unit() {
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SidePanelWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let block: Block<'_> = self.get_outer_block();
|
||||
let inner_area: Rect = block.inner(area).inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
block.render(area, buf);
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let block: Block<'_> = self.get_outer_block();
|
||||
let inner_area: Rect = block.inner(area).inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
block.render(area, buf);
|
||||
|
||||
let [structure_area, unit_area] =
|
||||
Layout::vertical(self.get_area_constraints()).areas(inner_area);
|
||||
let [structure_area, unit_area] =
|
||||
Layout::vertical(self.get_area_constraints()).areas(inner_area);
|
||||
|
||||
let mut structure_text: Vec<Line<'_>> = self.structure.text();
|
||||
if let Structures::Base(base) = self.structure
|
||||
&& base.queued_miners() > 0
|
||||
&& self.unit.is_some()
|
||||
{
|
||||
structure_text.push(Line::from(base.recruitment_status(true)));
|
||||
let mut structure_text: Vec<Line<'_>> = self.structure.text();
|
||||
if let Structures::Base(base) = self.structure
|
||||
&& base.queued_miners() > 0
|
||||
&& self.unit.is_some()
|
||||
{
|
||||
structure_text.push(Line::from(base.recruitment_status(true)));
|
||||
}
|
||||
if let Structures::Mine(mine) = self.structure {
|
||||
let staffed: bool = self
|
||||
.unit
|
||||
.as_ref()
|
||||
.is_some_and(|unit: &Units| -> bool { mine.accepts_unit(unit) });
|
||||
structure_text.push(Line::from(format!(
|
||||
"Metal: {}/s{}",
|
||||
mine.production_per_cycle(staffed),
|
||||
if staffed { " (Miner)" } else { "" }
|
||||
)));
|
||||
}
|
||||
Paragraph::new(structure_text)
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan))
|
||||
.render(structure_area, buf);
|
||||
|
||||
if self.unit.is_unit() {
|
||||
let mut unit_name: String = self.unit.get_name().to_string();
|
||||
|
||||
if self.is_unit_marked {
|
||||
unit_name = format!("> {} <", unit_name);
|
||||
}
|
||||
|
||||
Paragraph::new(self.unit.text())
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(unit_name, Color::Green))
|
||||
.render(unit_area, buf);
|
||||
}
|
||||
}
|
||||
if let Structures::Mine(mine) = self.structure {
|
||||
let staffed: bool = self
|
||||
.unit
|
||||
.as_ref()
|
||||
.is_some_and(|unit: &Units| -> bool { mine.accepts_unit(unit) });
|
||||
structure_text.push(Line::from(format!(
|
||||
"Metal: {}/s{}",
|
||||
mine.production_per_cycle(staffed),
|
||||
if staffed { " (Miner)" } else { "" }
|
||||
)));
|
||||
}
|
||||
Paragraph::new(structure_text)
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan))
|
||||
.render(structure_area, buf);
|
||||
|
||||
if self.unit.is_unit() {
|
||||
let mut unit_name: String = self.unit.get_name().to_string();
|
||||
|
||||
if self.is_unit_marked {
|
||||
unit_name = format!("> {} <", unit_name);
|
||||
}
|
||||
|
||||
Paragraph::new(self.unit.text())
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(unit_name, Color::Green))
|
||||
.render(unit_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
+93
-93
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
app::{
|
||||
states::{
|
||||
PerkDecks,
|
||||
skirmish_states::{GameMode, ZoomLevel},
|
||||
app::{
|
||||
states::{
|
||||
PerkDecks,
|
||||
skirmish_states::{GameMode, ZoomLevel},
|
||||
},
|
||||
threads::Soundtrack,
|
||||
view::View,
|
||||
},
|
||||
threads::Soundtrack,
|
||||
view::View,
|
||||
},
|
||||
logs::init_logger,
|
||||
logs::init_logger,
|
||||
};
|
||||
use clap::{Error, Parser, error::ErrorKind, value_parser};
|
||||
use std::num::ParseFloatError;
|
||||
@@ -21,8 +21,8 @@ use std::num::ParseFloatError;
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(version, about = "War in Tunnels", long_about = "War in Tunnels")]
|
||||
pub struct Cli {
|
||||
/// The initial view/window to display.
|
||||
#[arg(
|
||||
/// The initial view/window to display.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Default window",
|
||||
@@ -30,20 +30,20 @@ pub struct Cli {
|
||||
default_value_t = View::MainMenu,
|
||||
value_enum
|
||||
)]
|
||||
pub view: View,
|
||||
pub view: View,
|
||||
|
||||
/// Player name shown in the UI.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Username",
|
||||
value_name = "String",
|
||||
default_value = "Player"
|
||||
)]
|
||||
pub username: String,
|
||||
/// Player name shown in the UI.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Username",
|
||||
value_name = "String",
|
||||
default_value = "Player"
|
||||
)]
|
||||
pub username: String,
|
||||
|
||||
/// Game mode selection.
|
||||
#[arg(
|
||||
/// Game mode selection.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Game mode",
|
||||
@@ -51,10 +51,10 @@ pub struct Cli {
|
||||
default_value_t = GameMode::LastManStanding,
|
||||
value_enum
|
||||
)]
|
||||
pub game_mode: GameMode,
|
||||
pub game_mode: GameMode,
|
||||
|
||||
/// Width of the generated map (36–100 tiles).
|
||||
#[arg(
|
||||
/// Width of the generated map (36–100 tiles).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'W',
|
||||
help = "Map width",
|
||||
@@ -62,10 +62,10 @@ pub struct Cli {
|
||||
default_value = "42",
|
||||
value_parser = value_parser!(u8).range(36..=108)
|
||||
)]
|
||||
pub map_width: u8,
|
||||
pub map_width: u8,
|
||||
|
||||
/// Height of the generated map (11–50 tiles).
|
||||
#[arg(
|
||||
/// Height of the generated map (11–50 tiles).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'H',
|
||||
help = "Map height",
|
||||
@@ -73,10 +73,10 @@ pub struct Cli {
|
||||
default_value = "11",
|
||||
value_parser = value_parser!(u8).range(11..=39)
|
||||
)]
|
||||
pub map_height: u8,
|
||||
pub map_height: u8,
|
||||
|
||||
/// Zoom level used for the UI.
|
||||
#[arg(
|
||||
/// Zoom level used for the UI.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Zoom level",
|
||||
@@ -84,10 +84,10 @@ pub struct Cli {
|
||||
default_value_t = ZoomLevel::Default,
|
||||
value_enum
|
||||
)]
|
||||
pub zoom_level: ZoomLevel,
|
||||
pub zoom_level: ZoomLevel,
|
||||
|
||||
/// Which perk deck to use.
|
||||
#[arg(
|
||||
/// Which perk deck to use.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Perk Deck",
|
||||
@@ -95,10 +95,10 @@ pub struct Cli {
|
||||
default_value_t = PerkDecks::Silesian,
|
||||
value_enum
|
||||
)]
|
||||
pub perk_deck: PerkDecks,
|
||||
pub perk_deck: PerkDecks,
|
||||
|
||||
/// Starting amount of wood (1–150).
|
||||
#[arg(
|
||||
/// Starting amount of wood (1–150).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'w',
|
||||
help = "Starting wood",
|
||||
@@ -106,10 +106,10 @@ pub struct Cli {
|
||||
default_value = "50",
|
||||
value_parser = value_parser!(u16).range(1..=150)
|
||||
)]
|
||||
pub starting_wood: u16,
|
||||
pub starting_wood: u16,
|
||||
|
||||
/// Starting amount of metal (1–150).
|
||||
#[arg(
|
||||
/// Starting amount of metal (1–150).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'i',
|
||||
help = "Starting metal",
|
||||
@@ -118,10 +118,10 @@ pub struct Cli {
|
||||
default_value = "25",
|
||||
value_parser = value_parser!(u16).range(1..=150)
|
||||
)]
|
||||
pub starting_metal: u16,
|
||||
pub starting_metal: u16,
|
||||
|
||||
/// Maximum amount of units (49–149).
|
||||
#[arg(
|
||||
/// Maximum amount of units (49–149).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'L',
|
||||
help = "Supply limit",
|
||||
@@ -129,10 +129,10 @@ pub struct Cli {
|
||||
default_value = "99",
|
||||
value_parser = value_parser!(u8).range(49..=149)
|
||||
)]
|
||||
pub supply_limit: u8,
|
||||
pub supply_limit: u8,
|
||||
|
||||
/// Modifier applied to experience points (0.5–2.0).
|
||||
#[arg(
|
||||
/// Modifier applied to experience points (0.5–2.0).
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "XP modifier",
|
||||
@@ -140,10 +140,10 @@ pub struct Cli {
|
||||
default_value = "1.0",
|
||||
value_parser = parse_xp
|
||||
)]
|
||||
pub xp_modifier: f32,
|
||||
pub xp_modifier: f32,
|
||||
|
||||
/// Upper bound for skill points that can be earned (90–690).
|
||||
#[arg(
|
||||
/// Upper bound for skill points that can be earned (90–690).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'P',
|
||||
help = "Skill points limit",
|
||||
@@ -151,10 +151,10 @@ pub struct Cli {
|
||||
default_value = "120",
|
||||
value_parser = value_parser!(u16).range(90..=690)
|
||||
)]
|
||||
pub skill_points_limit: u16,
|
||||
pub skill_points_limit: u16,
|
||||
|
||||
/// Which soundtrack to play (default: default)
|
||||
#[arg(
|
||||
/// Which soundtrack to play (default: default)
|
||||
#[arg(
|
||||
long,
|
||||
short = 'S',
|
||||
help = "Soundtrack",
|
||||
@@ -162,34 +162,34 @@ pub struct Cli {
|
||||
default_value_t = Soundtrack::Default,
|
||||
value_enum
|
||||
)]
|
||||
pub soundtrack: Soundtrack,
|
||||
pub soundtrack: Soundtrack,
|
||||
|
||||
/// Disable soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'D',
|
||||
help = "Disable soundtrack (default: disable)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub disable_audio: bool,
|
||||
/// Disable soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'D',
|
||||
help = "Disable soundtrack (default: disable)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub disable_audio: bool,
|
||||
|
||||
/// Mute soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'm',
|
||||
help = "Mute soundtrack (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub mute: bool,
|
||||
/// Mute soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'm',
|
||||
help = "Mute soundtrack (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub mute: bool,
|
||||
|
||||
/// Enable logging to a file (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'l',
|
||||
help = "Enable logging to file (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub log: bool,
|
||||
/// Enable logging to a file (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'l',
|
||||
help = "Enable logging to file (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub log: bool,
|
||||
}
|
||||
|
||||
/// Parses the command line arguments and returns a fully populated `Cli`
|
||||
@@ -197,13 +197,13 @@ pub struct Cli {
|
||||
/// handles argument validation and displays helpful error messages if
|
||||
/// the user supplies invalid input.
|
||||
pub fn get_args() -> Cli {
|
||||
let args: Cli = Cli::parse();
|
||||
let args: Cli = Cli::parse();
|
||||
|
||||
if args.log {
|
||||
init_logger();
|
||||
}
|
||||
if args.log {
|
||||
init_logger();
|
||||
}
|
||||
|
||||
args
|
||||
args
|
||||
}
|
||||
|
||||
/// Parses a string into a floating‑point XP modifier and validates that it
|
||||
@@ -218,15 +218,15 @@ pub fn get_args() -> Cli {
|
||||
/// * Returns an error if the string cannot be parsed as `f32`.
|
||||
/// * Returns an error if the parsed value is outside the allowed range.
|
||||
fn parse_xp(s: &str) -> Result<f32, Error> {
|
||||
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),
|
||||
))
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -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");
|
||||
}
|
||||
+23
-23
@@ -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::<KeyEvent>();
|
||||
let (resize_tx, resize_rx) = channel::<(u16, u16)>();
|
||||
let (tick_tx, tick_rx) = channel::<()>();
|
||||
let (audio_tx, audio_rx) = channel::<AudioCmd>();
|
||||
let (input_tx, input_rx) = channel::<KeyEvent>();
|
||||
let (resize_tx, resize_rx) = channel::<(u16, u16)>();
|
||||
let (tick_tx, tick_rx) = channel::<()>();
|
||||
let (audio_tx, audio_rx) = channel::<AudioCmd>();
|
||||
|
||||
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<CrosstermBackend<Stdout>> = ratatui::init();
|
||||
let mut app: App = App::new(args, audio_tx);
|
||||
let mut terminal: Terminal<CrosstermBackend<Stdout>> = 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
|
||||
}
|
||||
+118
-119
@@ -1,162 +1,161 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
layout::Rect,
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{App, GameStates, View, handle_keybindings, threads::AudioCmd},
|
||||
cli::Cli,
|
||||
app::{App, GameStates, View, handle_keybindings, threads::AudioCmd},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn app() -> (App, Receiver<AudioCmd>) {
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, rx) = channel();
|
||||
let mut app = App::new(args, tx);
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
(app, rx)
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, rx) = channel();
|
||||
let mut app = App::new(args, tx);
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
(app, rx)
|
||||
}
|
||||
|
||||
fn press(app: &mut App, code: KeyCode) {
|
||||
handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE));
|
||||
handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_selection_stays_in_bounds_and_opens_each_view() {
|
||||
let (mut app, _) = app();
|
||||
for _ in 0..10 {
|
||||
press(&mut app, KeyCode::Up);
|
||||
}
|
||||
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1);
|
||||
for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] {
|
||||
press(&mut app, KeyCode::Char(' '));
|
||||
assert_eq!(app.view, view);
|
||||
press(&mut app, KeyCode::Esc);
|
||||
assert_eq!(app.view, View::MainMenu);
|
||||
press(&mut app, KeyCode::Down);
|
||||
}
|
||||
for _ in 0..10 {
|
||||
press(&mut app, KeyCode::Down);
|
||||
}
|
||||
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3);
|
||||
let (mut app, _) = app();
|
||||
for _ in 0..10 {
|
||||
press(&mut app, KeyCode::Up);
|
||||
}
|
||||
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1);
|
||||
for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] {
|
||||
press(&mut app, KeyCode::Char(' '));
|
||||
assert_eq!(app.view, view);
|
||||
press(&mut app, KeyCode::Esc);
|
||||
assert_eq!(app.view, View::MainMenu);
|
||||
press(&mut app, KeyCode::Down);
|
||||
}
|
||||
for _ in 0..10 {
|
||||
press(&mut app, KeyCode::Down);
|
||||
}
|
||||
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quit_keys_work_in_every_view() {
|
||||
for view in [
|
||||
View::MainMenu,
|
||||
View::Skirmish,
|
||||
View::PerkDecks,
|
||||
View::SkillsConfig,
|
||||
] {
|
||||
for key in [
|
||||
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
|
||||
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
|
||||
for view in [
|
||||
View::MainMenu,
|
||||
View::Skirmish,
|
||||
View::PerkDecks,
|
||||
View::SkillsConfig,
|
||||
] {
|
||||
let (mut app, _) = app();
|
||||
app.view = view;
|
||||
handle_keybindings(&mut app, key);
|
||||
assert!(app.exit);
|
||||
for key in [
|
||||
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
|
||||
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
|
||||
] {
|
||||
let (mut app, _) = app();
|
||||
app.view = view;
|
||||
handle_keybindings(&mut app, key);
|
||||
assert!(app.exit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_release_does_not_trigger_quit() {
|
||||
let (mut app, _) = app();
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new_with_kind(
|
||||
KeyCode::Char('q'),
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Release,
|
||||
),
|
||||
);
|
||||
assert!(!app.exit);
|
||||
let (mut app, _) = app();
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new_with_kind(
|
||||
KeyCode::Char('q'),
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Release,
|
||||
),
|
||||
);
|
||||
assert!(!app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn music_keys_send_commands_without_opening_audio_device() {
|
||||
let (mut app, rx) = app();
|
||||
press(&mut app, KeyCode::Char('m'));
|
||||
press(&mut app, KeyCode::Char('b'));
|
||||
press(&mut app, KeyCode::Char('n'));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown));
|
||||
drop(rx);
|
||||
press(&mut app, KeyCode::Char('m'));
|
||||
assert!(!app.exit);
|
||||
let (mut app, rx) = app();
|
||||
press(&mut app, KeyCode::Char('m'));
|
||||
press(&mut app, KeyCode::Char('b'));
|
||||
press(&mut app, KeyCode::Char('n'));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown));
|
||||
drop(rx);
|
||||
press(&mut app, KeyCode::Char('m'));
|
||||
assert!(!app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skirmish_keys_move_focus_toggle_panel_and_limit_zoom() {
|
||||
let (mut app, _) = app();
|
||||
app.view = View::Skirmish;
|
||||
press(&mut app, KeyCode::Right);
|
||||
assert_eq!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_focused_cell()
|
||||
.get_coords(),
|
||||
(5, 4)
|
||||
);
|
||||
press(&mut app, KeyCode::Tab);
|
||||
assert!(app.states_ref().unwrap().skirmish.side_panel);
|
||||
press(&mut app, KeyCode::Tab);
|
||||
assert!(!app.states_ref().unwrap().skirmish.side_panel);
|
||||
for _ in 0..4 {
|
||||
press(&mut app, KeyCode::Char(','));
|
||||
}
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.board.zoom_level,
|
||||
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn
|
||||
);
|
||||
for _ in 0..4 {
|
||||
press(&mut app, KeyCode::Char('.'));
|
||||
}
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.board.zoom_level,
|
||||
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut
|
||||
);
|
||||
let (mut app, _) = app();
|
||||
app.view = View::Skirmish;
|
||||
press(&mut app, KeyCode::Right);
|
||||
assert_eq!(
|
||||
app.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_focused_cell()
|
||||
.get_coords(),
|
||||
(5, 4)
|
||||
);
|
||||
press(&mut app, KeyCode::Tab);
|
||||
assert!(app.states_ref().unwrap().skirmish.side_panel);
|
||||
press(&mut app, KeyCode::Tab);
|
||||
assert!(!app.states_ref().unwrap().skirmish.side_panel);
|
||||
for _ in 0..4 {
|
||||
press(&mut app, KeyCode::Char(','));
|
||||
}
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.board.zoom_level,
|
||||
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn
|
||||
);
|
||||
for _ in 0..4 {
|
||||
press(&mut app, KeyCode::Char('.'));
|
||||
}
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.board.zoom_level,
|
||||
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_views_render_without_real_terminal() {
|
||||
let (mut app, _) = app();
|
||||
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
for view in [
|
||||
View::MainMenu,
|
||||
View::Skirmish,
|
||||
View::PerkDecks,
|
||||
View::SkillsConfig,
|
||||
] {
|
||||
app.view = view;
|
||||
let before = app.states.clone();
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect();
|
||||
assert!(!text.trim().is_empty(), "{view:?}");
|
||||
if matches!(view, View::PerkDecks | View::SkillsConfig) {
|
||||
assert!(text.contains("Work in progress"));
|
||||
let (mut app, _) = app();
|
||||
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
for view in [
|
||||
View::MainMenu,
|
||||
View::Skirmish,
|
||||
View::PerkDecks,
|
||||
View::SkillsConfig,
|
||||
] {
|
||||
app.view = view;
|
||||
let before = app.states.clone();
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect();
|
||||
assert!(!text.trim().is_empty(), "{view:?}");
|
||||
if matches!(view, View::PerkDecks | View::SkillsConfig) {
|
||||
assert!(text.contains("Work in progress"));
|
||||
}
|
||||
assert_eq!(app.states, before);
|
||||
}
|
||||
assert_eq!(app.states, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_keys_work_before_states_are_initialized() {
|
||||
let (mut app, _) = app();
|
||||
app.states = None;
|
||||
press(&mut app, KeyCode::Down);
|
||||
press(&mut app, KeyCode::Char('q'));
|
||||
assert!(app.exit);
|
||||
let (mut app, _) = app();
|
||||
app.states = None;
|
||||
press(&mut app, KeyCode::Down);
|
||||
press(&mut app, KeyCode::Char('q'));
|
||||
assert!(app.exit);
|
||||
}
|
||||
+51
-51
@@ -1,65 +1,65 @@
|
||||
use rodio::Decoder;
|
||||
use std::{
|
||||
fs::{self, read},
|
||||
io::{BufReader, Cursor},
|
||||
path::PathBuf,
|
||||
process::{Command, Output},
|
||||
fs::{self, read},
|
||||
io::{BufReader, Cursor},
|
||||
path::PathBuf,
|
||||
process::{Command, Output},
|
||||
};
|
||||
use war_in_tunnels::app::threads::{SoundrackParts, Soundtrack, load_audio};
|
||||
|
||||
#[test]
|
||||
fn audio_repeats_after_end_of_file() {
|
||||
let data = read("soundtracks/default/calm.ogg").unwrap();
|
||||
let sample_count = Decoder::new(BufReader::new(Cursor::new(data)))
|
||||
.unwrap()
|
||||
.count();
|
||||
assert!(sample_count > 0);
|
||||
let data = read("soundtracks/default/calm.ogg").unwrap();
|
||||
let sample_count = Decoder::new(BufReader::new(Cursor::new(data)))
|
||||
.unwrap()
|
||||
.count();
|
||||
assert!(sample_count > 0);
|
||||
|
||||
let mut source = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
|
||||
assert_eq!(source.by_ref().take(sample_count).count(), sample_count);
|
||||
assert_eq!(source.take(1024).count(), 1024);
|
||||
let mut source = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
|
||||
assert_eq!(source.by_ref().take(sample_count).count(), sample_count);
|
||||
assert_eq!(source.take(1024).count(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_audio_loads_without_project_working_directory() -> () {
|
||||
if std::env::var_os("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST").is_some() {
|
||||
let mut source: rodio::source::Amplify<
|
||||
rodio::decoder::LoopedDecoder<BufReader<Cursor<Vec<u8>>>>,
|
||||
> = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
|
||||
assert_eq!(source.by_ref().take(1024).count(), 1024);
|
||||
return;
|
||||
}
|
||||
if std::env::var_os("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST").is_some() {
|
||||
let mut source: rodio::source::Amplify<
|
||||
rodio::decoder::LoopedDecoder<BufReader<Cursor<Vec<u8>>>>,
|
||||
> = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
|
||||
assert_eq!(source.by_ref().take(1024).count(), 1024);
|
||||
return;
|
||||
}
|
||||
|
||||
let directory: PathBuf = std::env::temp_dir().join(format!(
|
||||
"war-in-tunnels-packaged-audio-{}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir_all(directory.join("soundtracks/default")).unwrap();
|
||||
fs::create_dir_all(directory.join("working-directory")).unwrap();
|
||||
let executable: PathBuf = directory.join("audio-test");
|
||||
fs::copy(std::env::current_exe().unwrap(), &executable).unwrap();
|
||||
for filename in ["calm.ogg", "test.ogg"] {
|
||||
fs::copy(
|
||||
PathBuf::from("soundtracks/default").join(filename),
|
||||
directory.join("soundtracks/default").join(filename),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let output: Output = Command::new(executable)
|
||||
.args([
|
||||
"--exact",
|
||||
"packaged_audio_loads_without_project_working_directory",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST", "1")
|
||||
.current_dir(directory.join("working-directory"))
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
let directory: PathBuf = std::env::temp_dir().join(format!(
|
||||
"war-in-tunnels-packaged-audio-{}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir_all(directory.join("soundtracks/default")).unwrap();
|
||||
fs::create_dir_all(directory.join("working-directory")).unwrap();
|
||||
let executable: PathBuf = directory.join("audio-test");
|
||||
fs::copy(std::env::current_exe().unwrap(), &executable).unwrap();
|
||||
for filename in ["calm.ogg", "test.ogg"] {
|
||||
fs::copy(
|
||||
PathBuf::from("soundtracks/default").join(filename),
|
||||
directory.join("soundtracks/default").join(filename),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let output: Output = Command::new(executable)
|
||||
.args([
|
||||
"--exact",
|
||||
"packaged_audio_loads_without_project_working_directory",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST", "1")
|
||||
.current_dir(directory.join("working-directory"))
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
+119
-119
@@ -1,155 +1,155 @@
|
||||
use ratatui::layout::Rect;
|
||||
use war_in_tunnels::app::states::skirmish_states::{
|
||||
BoardState, FocusedCell, MoveFocusedCell, Offset, Players, ZoomLevel,
|
||||
structures::{Structure, Structures},
|
||||
tasks::Task,
|
||||
units::Unit,
|
||||
BoardState, FocusedCell, MoveFocusedCell, Offset, Players, ZoomLevel,
|
||||
structures::{Structure, Structures},
|
||||
tasks::Task,
|
||||
units::Unit,
|
||||
};
|
||||
|
||||
fn board() -> BoardState {
|
||||
BoardState::new(&Rect::new(0, 0, 120, 40), 42, 11, ZoomLevel::Default, false)
|
||||
BoardState::new(&Rect::new(0, 0, 120, 40), 42, 11, ZoomLevel::Default, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_has_opposing_bases_ore_and_two_miners() {
|
||||
let board = board();
|
||||
assert_eq!(board.cells.len(), 11);
|
||||
assert!(board.cells.iter().all(|row| row.len() == 42));
|
||||
for (col, owner, ore_col) in [(2, Players::Player, 1), (39, Players::Enemy, 40)] {
|
||||
let cell = board.get_ref_cell(5, col);
|
||||
assert!(matches!(cell.get_structure(), Structures::Base(_)));
|
||||
assert_eq!(cell.get_structure().get_owner(), owner);
|
||||
let unit = cell.get_ref_option_unit().as_ref().unwrap();
|
||||
assert_eq!(unit.get_coords(), (5, col));
|
||||
assert_eq!(unit.get_owner(), owner);
|
||||
assert!(matches!(
|
||||
board.get_ref_cell(5, ore_col).get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
let board = board();
|
||||
assert_eq!(board.cells.len(), 11);
|
||||
assert!(board.cells.iter().all(|row| row.len() == 42));
|
||||
for (col, owner, ore_col) in [(2, Players::Player, 1), (39, Players::Enemy, 40)] {
|
||||
let cell = board.get_ref_cell(5, col);
|
||||
assert!(matches!(cell.get_structure(), Structures::Base(_)));
|
||||
assert_eq!(cell.get_structure().get_owner(), owner);
|
||||
let unit = cell.get_ref_option_unit().as_ref().unwrap();
|
||||
assert_eq!(unit.get_coords(), (5, col));
|
||||
assert_eq!(unit.get_owner(), owner);
|
||||
assert!(matches!(
|
||||
board.get_ref_cell(5, ore_col).get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
assert_eq!(
|
||||
board.get_ref_cell(5, ore_col).get_structure().get_owner(),
|
||||
Players::Unclaimed
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
board.get_ref_cell(5, ore_col).get_structure().get_owner(),
|
||||
Players::Unclaimed
|
||||
board
|
||||
.cells
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|c| c.get_ref_option_unit().is_some())
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
board
|
||||
.cells
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|c| c.get_ref_option_unit().is_some())
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(matches!(
|
||||
board.get_ref_cell(0, 0).get_structure(),
|
||||
Structures::Stone(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
board.get_ref_cell(0, 0).get_structure(),
|
||||
Structures::Stone(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_cannot_leave_map_even_with_extreme_coordinates() {
|
||||
let mut focus = FocusedCell::new(0, 0, 11, 42);
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Up), (0, 0));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Left), (0, 0));
|
||||
focus.set_focused_cell((usize::MAX, usize::MAX));
|
||||
assert_eq!(focus.get_coords(), (10, 41));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Down), (10, 41));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Right), (10, 41));
|
||||
let mut focus = FocusedCell::new(0, 0, 11, 42);
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Up), (0, 0));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Left), (0, 0));
|
||||
focus.set_focused_cell((usize::MAX, usize::MAX));
|
||||
assert_eq!(focus.get_coords(), (10, 41));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Down), (10, 41));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Right), (10, 41));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offsets_remain_within_bounds_when_limits_change() {
|
||||
let mut offset = Offset::new(Some(100), Some(5));
|
||||
assert_eq!(offset.get_value(), 5);
|
||||
offset.next();
|
||||
assert_eq!(offset.get_value(), 5);
|
||||
offset.set_max(2);
|
||||
assert_eq!(offset.get_value(), 2);
|
||||
offset.set_value(usize::MAX);
|
||||
assert_eq!(offset.get_value(), 2);
|
||||
for _ in 0..4 {
|
||||
offset.prev();
|
||||
}
|
||||
assert_eq!(offset.get_value(), 0);
|
||||
let mut offset = Offset::new(Some(100), Some(5));
|
||||
assert_eq!(offset.get_value(), 5);
|
||||
offset.next();
|
||||
assert_eq!(offset.get_value(), 5);
|
||||
offset.set_max(2);
|
||||
assert_eq!(offset.get_value(), 2);
|
||||
offset.set_value(usize::MAX);
|
||||
assert_eq!(offset.get_value(), 2);
|
||||
for _ in 0..4 {
|
||||
offset.prev();
|
||||
}
|
||||
assert_eq!(offset.get_value(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirming_path_queues_task_without_including_start_cell() {
|
||||
let mut board = board();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
board.toggle_marking();
|
||||
let unit = board
|
||||
.get_ref_cell(5, 2)
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let tasks = unit.get_ref_tasks();
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(
|
||||
tasks[0].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(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::<Vec<_>>(),
|
||||
[(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);
|
||||
}
|
||||
}
|
||||
+72
-72
@@ -4,94 +4,94 @@ use war_in_tunnels::{app::View, cli::Cli};
|
||||
|
||||
#[test]
|
||||
fn defaults_start_in_menu_without_logging() {
|
||||
let args = Cli::try_parse_from(["game"]).unwrap();
|
||||
assert_eq!(args.view, View::MainMenu);
|
||||
assert_eq!((args.map_width, args.map_height), (42, 11));
|
||||
assert_eq!(args.username, "Player");
|
||||
assert!(!args.log);
|
||||
assert!(!args.disable_audio);
|
||||
assert!(!args.mute);
|
||||
let args = Cli::try_parse_from(["game"]).unwrap();
|
||||
assert_eq!(args.view, View::MainMenu);
|
||||
assert_eq!((args.map_width, args.map_height), (42, 11));
|
||||
assert_eq!(args.username, "Player");
|
||||
assert!(!args.log);
|
||||
assert!(!args.disable_audio);
|
||||
assert!(!args.mute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_options_accept_boundaries_and_reject_out_of_range_values() {
|
||||
for (flag, min, max) in [
|
||||
("--map-width", 36, 108),
|
||||
("--map-height", 11, 39),
|
||||
("--starting-wood", 1, 150),
|
||||
("--starting-metal", 1, 150),
|
||||
("--supply-limit", 49, 149),
|
||||
("--skill-points-limit", 90, 690),
|
||||
] {
|
||||
for value in [min, max] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", flag, &value.to_string()]).is_ok(),
|
||||
"{flag}={value}"
|
||||
);
|
||||
for (flag, min, max) in [
|
||||
("--map-width", 36, 108),
|
||||
("--map-height", 11, 39),
|
||||
("--starting-wood", 1, 150),
|
||||
("--starting-metal", 1, 150),
|
||||
("--supply-limit", 49, 149),
|
||||
("--skill-points-limit", 90, 690),
|
||||
] {
|
||||
for value in [min, max] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", flag, &value.to_string()]).is_ok(),
|
||||
"{flag}={value}"
|
||||
);
|
||||
}
|
||||
for value in [min - 1, max + 1] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", flag, &value.to_string()]).is_err(),
|
||||
"{flag}={value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for value in [min - 1, max + 1] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", flag, &value.to_string()]).is_err(),
|
||||
"{flag}={value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xp_modifier_rejects_non_finite_malformed_and_out_of_range_values() {
|
||||
for value in ["0.5", "1", "2.0"] {
|
||||
let args = Cli::try_parse_from(["game", "--xp-modifier", value]).unwrap();
|
||||
assert_eq!(args.xp_modifier, value.parse::<f32>().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::<f32>().unwrap());
|
||||
}
|
||||
for value in ["0.49", "2.01", "NaN", "inf", "text"] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", "--xp-modifier", value]).is_err(),
|
||||
"{value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_options_and_audio_flags_are_parsed() {
|
||||
let args =
|
||||
Cli::try_parse_from(["game", "--view", "skirmish", "--disable-audio", "--mute"]).unwrap();
|
||||
assert_eq!(args.view, View::Skirmish);
|
||||
assert!(args.disable_audio && args.mute);
|
||||
for flag in [
|
||||
"--view",
|
||||
"--game-mode",
|
||||
"--zoom-level",
|
||||
"--perk-deck",
|
||||
"--soundtrack",
|
||||
] {
|
||||
assert_eq!(
|
||||
Cli::try_parse_from(["game", flag, "unknown"])
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ErrorKind::InvalidValue
|
||||
);
|
||||
}
|
||||
let args =
|
||||
Cli::try_parse_from(["game", "--view", "skirmish", "--disable-audio", "--mute"]).unwrap();
|
||||
assert_eq!(args.view, View::Skirmish);
|
||||
assert!(args.disable_audio && args.mute);
|
||||
for flag in [
|
||||
"--view",
|
||||
"--game-mode",
|
||||
"--zoom-level",
|
||||
"--perk-deck",
|
||||
"--soundtrack",
|
||||
] {
|
||||
assert_eq!(
|
||||
Cli::try_parse_from(["game", flag, "unknown"])
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ErrorKind::InvalidValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_help_and_version_work_without_terminal_or_audio() {
|
||||
for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] {
|
||||
for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
|
||||
.arg(flag)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success());
|
||||
assert!(String::from_utf8(output.stdout).unwrap().contains(expected));
|
||||
}
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
|
||||
.arg(flag)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success());
|
||||
assert!(String::from_utf8(output.stdout).unwrap().contains(expected));
|
||||
}
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
|
||||
.args(["--map-width", "0"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(
|
||||
String::from_utf8(output.stderr)
|
||||
.unwrap()
|
||||
.contains("--map-width")
|
||||
);
|
||||
.args(["--map-width", "0"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(
|
||||
String::from_utf8(output.stderr)
|
||||
.unwrap()
|
||||
.contains("--map-width")
|
||||
);
|
||||
}
|
||||
+336
-333
@@ -1,420 +1,423 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, MoveFocusedCell, Players, Resources, ZoomLevel,
|
||||
resources::MINE_PRODUCTION_TICKS,
|
||||
structures::{
|
||||
BaseBuilding, BuildingTerrain, Mine, MineConstruction, Ore, Structure, Structures, Tunnel,
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, MoveFocusedCell, Players, Resources, ZoomLevel,
|
||||
resources::MINE_PRODUCTION_TICKS,
|
||||
structures::{
|
||||
BaseBuilding, BuildingTerrain, Mine, MineConstruction, Ore, Structure,
|
||||
Structures, Tunnel,
|
||||
},
|
||||
tasks::Tasks,
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
},
|
||||
tasks::Tasks,
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
cli::Cli,
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn board() -> BoardState {
|
||||
BoardState::new(&Rect::new(0, 0, 160, 50), 42, 11, ZoomLevel::Default, false)
|
||||
BoardState::new(&Rect::new(0, 0, 160, 50), 42, 11, ZoomLevel::Default, false)
|
||||
}
|
||||
|
||||
fn complete_mine(board: &mut BoardState, coords: (usize, usize)) -> () {
|
||||
assert!(board.start_mine_construction(coords, Players::Player));
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
board.advance_construction();
|
||||
}
|
||||
assert!(matches!(
|
||||
board.cells[coords.0][coords.1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
assert!(board.start_mine_construction(coords, Players::Player));
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
board.advance_construction();
|
||||
}
|
||||
assert!(matches!(
|
||||
board.cells[coords.0][coords.1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
}
|
||||
|
||||
fn app() -> App {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 50);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 50);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
}
|
||||
|
||||
fn travel(board: &mut BoardState, direction: MoveFocusedCell) -> () {
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(direction);
|
||||
board.toggle_marking();
|
||||
for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
board.advance_turn();
|
||||
}
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(direction);
|
||||
board.toggle_marking();
|
||||
for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
board.advance_turn();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_has_builder_permission_through_all_unit_wrappers() -> () {
|
||||
let miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2));
|
||||
assert!(miner.get_can_build());
|
||||
let unit: Units = Units::Miner(miner);
|
||||
assert!(unit.get_can_build());
|
||||
assert!(Some(unit).get_can_build());
|
||||
let no_unit: Option<Units> = None;
|
||||
assert!(!no_unit.get_can_build());
|
||||
let miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2));
|
||||
assert!(miner.get_can_build());
|
||||
let unit: Units = Units::Miner(miner);
|
||||
assert!(unit.get_can_build());
|
||||
assert!(Some(unit).get_can_build());
|
||||
let no_unit: Option<Units> = None;
|
||||
assert!(!no_unit.get_can_build());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mine_requires_a_cardinal_friendly_builder_and_preserves_target_on_failure() -> () {
|
||||
let mut board: BoardState = board();
|
||||
let original: Structures = board.cells[5][1].get_structure();
|
||||
board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
*board.cells[5][2].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][2].get_mut_option_unit().take();
|
||||
*board.cells[4][2].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (4, 2))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(board.cells[5][1].get_structure(), original);
|
||||
assert!(!board.start_mine_construction((usize::MAX, 1), Players::Player));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Unclaimed));
|
||||
let mut board: BoardState = board();
|
||||
let original: Structures = board.cells[5][1].get_structure();
|
||||
board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
*board.cells[5][2].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Enemy, (5, 2))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][2].get_mut_option_unit().take();
|
||||
*board.cells[4][2].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (4, 2))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(board.cells[5][1].get_structure(), original);
|
||||
assert!(!board.start_mine_construction((usize::MAX, 1), Players::Player));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Unclaimed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ore_exception_does_not_require_a_base_or_tunnel_connection() -> () {
|
||||
let mut board: BoardState = board();
|
||||
board.cells[0][0].set_structure(Structures::Ore(Ore::new(Players::Unclaimed, 1)));
|
||||
*board.cells[0][1].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (0, 1))));
|
||||
assert!(board.can_build_at((0, 0), Players::Player, BuildingTerrain::Ore));
|
||||
complete_mine(&mut board, (0, 0));
|
||||
assert_eq!(
|
||||
board.cells[0][0].get_structure().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
assert!(!board.start_mine_construction((0, 0), Players::Player));
|
||||
let mut board: BoardState = board();
|
||||
board.cells[0][0].set_structure(Structures::Ore(Ore::new(Players::Unclaimed, 1)));
|
||||
*board.cells[0][1].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (0, 1))));
|
||||
assert!(board.can_build_at((0, 0), Players::Player, BuildingTerrain::Ore));
|
||||
complete_mine(&mut board, (0, 0));
|
||||
assert_eq!(
|
||||
board.cells[0][0].get_structure().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
assert!(!board.start_mine_construction((0, 0), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_building_sites_require_stone_connected_to_base_or_tunnel() -> () {
|
||||
let mut board: BoardState = board();
|
||||
assert!(board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone));
|
||||
assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Ore));
|
||||
*board.cells[5][5].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 5))));
|
||||
assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][5].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
assert!(board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][5].set_structure(Structures::Base(BaseBuilding::new(Players::Enemy)));
|
||||
assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone));
|
||||
let mut board: BoardState = board();
|
||||
assert!(board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone));
|
||||
assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Ore));
|
||||
*board.cells[5][5].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 5))));
|
||||
assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][5].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
assert!(board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][5].set_structure(Structures::Base(BaseBuilding::new(Players::Enemy)));
|
||||
assert!(!board.can_build_at((5, 4), Players::Player, BuildingTerrain::Stone));
|
||||
board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
assert!(!board.can_build_at((5, 3), Players::Player, BuildingTerrain::Stone));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupied_depleted_or_claimed_deposit_cannot_be_built_on() -> () {
|
||||
let mut board: BoardState = board();
|
||||
*board.cells[5][1].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 1))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][1].get_mut_option_unit().take();
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(5000);
|
||||
board.cells[5][1].set_structure(Structures::Ore(ore));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][1].set_structure(Structures::Ore(Ore::new(Players::Enemy, 1)));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
let mut board: BoardState = board();
|
||||
*board.cells[5][1].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 1))));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][1].get_mut_option_unit().take();
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(5000);
|
||||
board.cells[5][1].set_structure(Structures::Ore(ore));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
board.cells[5][1].set_structure(Structures::Ore(Ore::new(Players::Enemy, 1)));
|
||||
assert!(!board.start_mine_construction((5, 1), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_view_starts_paid_build_on_enter_and_stays_open() -> () {
|
||||
let mut app: App = app();
|
||||
for _ in 0..2 {
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
}
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
assert!(available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter));
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Ready to build: Enter"));
|
||||
assert!(text.contains("Enter - Build Mine"));
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
let state: &SkirmishState = &app.states_ref().unwrap().skirmish;
|
||||
assert!(state.building_view);
|
||||
assert!(matches!(
|
||||
state.board.cells[5][1].get_structure(),
|
||||
Structures::MineConstruction(_)
|
||||
));
|
||||
assert_eq!(state.player_resources.wood, 30);
|
||||
assert_eq!(state.player_resources.metal, 15);
|
||||
let mut app: App = app();
|
||||
for _ in 0..2 {
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
}
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Ready to build: Enter"));
|
||||
assert!(text.contains("Enter - Build Mine"));
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
let state: &SkirmishState = &app.states_ref().unwrap().skirmish;
|
||||
assert!(state.building_view);
|
||||
assert!(matches!(
|
||||
state.board.cells[5][1].get_structure(),
|
||||
Structures::MineConstruction(_)
|
||||
));
|
||||
assert_eq!(state.player_resources.wood, 30);
|
||||
assert_eq!(state.player_resources.metal, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_can_travel_into_and_out_of_friendly_mine_with_moving_tasks() -> () {
|
||||
let mut board: BoardState = board();
|
||||
complete_mine(&mut board, (5, 1));
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
let unit: &Units = board.cells[5][2].get_ref_option_unit().as_ref().unwrap();
|
||||
assert!(matches!(
|
||||
unit.get_ref_tasks().front(),
|
||||
Some(Tasks::Moving(_))
|
||||
));
|
||||
for _ in 1..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
let mut board: BoardState = board();
|
||||
complete_mine(&mut board, (5, 1));
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
let unit: &Units = board.cells[5][2].get_ref_option_unit().as_ref().unwrap();
|
||||
assert!(matches!(
|
||||
unit.get_ref_tasks().front(),
|
||||
Some(Tasks::Moving(_))
|
||||
));
|
||||
for _ in 1..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
board.advance_turn();
|
||||
}
|
||||
assert!(board.cells[5][1].get_ref_option_unit().is_none());
|
||||
board.advance_turn();
|
||||
}
|
||||
assert!(board.cells[5][1].get_ref_option_unit().is_none());
|
||||
board.advance_turn();
|
||||
assert_eq!(board.cells[5][1].get_ref_option_unit().get_coords(), (5, 1));
|
||||
assert!(board.cells[5][2].get_ref_option_unit().is_none());
|
||||
travel(&mut board, MoveFocusedCell::Right);
|
||||
assert!(board.cells[5][1].get_ref_option_unit().is_none());
|
||||
assert_eq!(board.cells[5][2].get_ref_option_unit().get_coords(), (5, 2));
|
||||
assert_eq!(board.cells[5][1].get_ref_option_unit().get_coords(), (5, 1));
|
||||
assert!(board.cells[5][2].get_ref_option_unit().is_none());
|
||||
travel(&mut board, MoveFocusedCell::Right);
|
||||
assert!(board.cells[5][1].get_ref_option_unit().is_none());
|
||||
assert_eq!(board.cells[5][2].get_ref_option_unit().get_coords(), (5, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staffed_mine_doubles_production_and_bonus_stops_after_departure() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
complete_mine(&mut game.board, (5, 1));
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 26);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
travel(&mut game.board, MoveFocusedCell::Left);
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 28);
|
||||
travel(&mut game.board, MoveFocusedCell::Right);
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 29);
|
||||
let Structures::Mine(mine) = game.board.cells[5][1].get_structure() else {
|
||||
panic!("mine missing");
|
||||
};
|
||||
assert_eq!(mine.get_ore_amount(), 4996);
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
complete_mine(&mut game.board, (5, 1));
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 26);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
travel(&mut game.board, MoveFocusedCell::Left);
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 28);
|
||||
travel(&mut game.board, MoveFocusedCell::Right);
|
||||
game.turn_counter = MINE_PRODUCTION_TICKS - 1;
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 29);
|
||||
let Structures::Mine(mine) = game.board.cells[5][1].get_structure() else {
|
||||
panic!("mine missing");
|
||||
};
|
||||
assert_eq!(mine.get_ore_amount(), 4996);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mine_rejects_enemy_miners_and_staffing_does_not_overdraw_ore() -> () {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(4999);
|
||||
let mut mine: Mine = Mine::new(Players::Player, ore).unwrap();
|
||||
let player: Units = Units::Miner(MinerUnit::new(Players::Player, (5, 2)));
|
||||
let enemy: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)));
|
||||
assert!(mine.accepts_unit(&player));
|
||||
assert!(!mine.accepts_unit(&enemy));
|
||||
assert!(!Structures::Mine(mine).can_enter(&enemy));
|
||||
assert_eq!(mine.produce_with_miner(true), 1);
|
||||
assert_eq!(mine.produce_with_miner(true), 0);
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(4999);
|
||||
let mut mine: Mine = Mine::new(Players::Player, ore).unwrap();
|
||||
let player: Units = Units::Miner(MinerUnit::new(Players::Player, (5, 2)));
|
||||
let enemy: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)));
|
||||
assert!(mine.accepts_unit(&player));
|
||||
assert!(!mine.accepts_unit(&enemy));
|
||||
assert!(!Structures::Mine(mine).can_enter(&enemy));
|
||||
assert_eq!(mine.produce_with_miner(true), 1);
|
||||
assert_eq!(mine.produce_with_miner(true), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_ore_cannot_start_construction_or_consume_resources() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let ore: Structures = Structures::Ore(Ore::new(Players::Unclaimed, 1));
|
||||
game.board.cells[5][21].set_structure(ore);
|
||||
let before: Resources = game.player_resources;
|
||||
assert!(!game.start_mine_construction((5, 21), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
assert_eq!(game.board.cells[5][21].get_structure(), ore);
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let ore: Structures = Structures::Ore(Ore::new(Players::Unclaimed, 1));
|
||||
game.board.cells[5][21].set_structure(ore);
|
||||
let before: Resources = game.player_resources;
|
||||
assert!(!game.start_mine_construction((5, 21), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
assert_eq!(game.board.cells[5][21].get_structure(), ore);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_payment_is_atomic_and_exact_funds_are_enough() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for resources in [
|
||||
Resources {
|
||||
wood: 19,
|
||||
metal: 10,
|
||||
},
|
||||
Resources { wood: 20, metal: 9 },
|
||||
] {
|
||||
game.player_resources = resources;
|
||||
assert!(!game.can_start_mine_construction((5, 1), Players::Player));
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for resources in [
|
||||
Resources {
|
||||
wood: 19,
|
||||
metal: 10,
|
||||
},
|
||||
Resources { wood: 20, metal: 9 },
|
||||
] {
|
||||
game.player_resources = resources;
|
||||
assert!(!game.can_start_mine_construction((5, 1), Players::Player));
|
||||
assert!(!game.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, resources);
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
}
|
||||
game.player_resources = Mine::COST;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources { wood: 0, metal: 0 });
|
||||
assert!(!game.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, resources);
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
}
|
||||
game.player_resources = Mine::COST;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources { wood: 0, metal: 0 });
|
||||
assert!(!game.start_mine_construction((5, 1), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_takes_ten_seconds_and_cannot_produce_or_accept_miners_early() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 1..MineConstruction::BUILD_TICKS {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 1..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
let structure: Structures = game.board.cells[5][1].get_structure();
|
||||
let Structures::MineConstruction(site) = structure else {
|
||||
panic!("completed too early");
|
||||
};
|
||||
assert_eq!(site.progress_percent(), 99);
|
||||
assert!(site.into_mine().is_none());
|
||||
assert_eq!(site.cancel().get_ore_amount(), 5000);
|
||||
assert!(
|
||||
!structure.can_enter(
|
||||
game.board.cells[5][2]
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
game.tick_update();
|
||||
}
|
||||
let structure: Structures = game.board.cells[5][1].get_structure();
|
||||
let Structures::MineConstruction(site) = structure else {
|
||||
panic!("completed too early");
|
||||
};
|
||||
assert_eq!(site.progress_percent(), 99);
|
||||
assert!(site.into_mine().is_none());
|
||||
assert_eq!(site.cancel().get_ore_amount(), 5000);
|
||||
assert!(
|
||||
!structure.can_enter(
|
||||
game.board.cells[5][2]
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
)
|
||||
);
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
game.tick_update();
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_pauses_without_nearby_builder_and_resumes_after_return() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
let before: Structures = game.board.cells[5][1].get_structure();
|
||||
let builder: Option<Units> = game.board.cells[5][2].get_mut_option_unit().take();
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), before);
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
*game.board.cells[5][2].get_mut_option_unit() = builder;
|
||||
for _ in 100..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
let before: Structures = game.board.cells[5][1].get_structure();
|
||||
let builder: Option<Units> = game.board.cells[5][2].get_mut_option_unit().take();
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), before);
|
||||
assert_eq!(game.player_resources.metal, 15);
|
||||
*game.board.cells[5][2].get_mut_option_unit() = builder;
|
||||
for _ in 100..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(matches!(
|
||||
game.board.cells[5][1].get_structure(),
|
||||
Structures::Mine(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_restores_original_ore_and_refunds_cost_exactly_once() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(123);
|
||||
game.board.cells[5][1].set_structure(Structures::Ore(ore));
|
||||
let before: Resources = game.player_resources;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(game.cancel_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), Structures::Ore(ore));
|
||||
assert!(!game.cancel_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(123);
|
||||
game.board.cells[5][1].set_structure(Structures::Ore(ore));
|
||||
let before: Resources = game.player_resources;
|
||||
assert!(game.start_mine_construction((5, 1), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(game.cancel_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), Structures::Ore(ore));
|
||||
assert!(!game.cancel_mine_construction((5, 1), Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_shows_progress_and_delete_cancels_while_escape_only_closes_panel() -> () {
|
||||
let mut app: App = app();
|
||||
for _ in 0..2 {
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
}
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Delete)
|
||||
);
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(app.states_ref().unwrap().skirmish.player_resources.wood, 30);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Cost: 20 Wood + 10 Metal"));
|
||||
assert!(text.contains("Construction: 0%"));
|
||||
assert!(text.contains("Delete - Cancel construction"));
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(),
|
||||
Structures::MineConstruction(_)
|
||||
));
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.player_resources,
|
||||
Resources::new(50, 25)
|
||||
);
|
||||
let mut app: App = app();
|
||||
for _ in 0..2 {
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
|
||||
}
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Delete)
|
||||
);
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
assert_eq!(app.states_ref().unwrap().skirmish.player_resources.wood, 30);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Cost: 20 Wood + 10 Metal"));
|
||||
assert!(text.contains("Construction: 0%"));
|
||||
assert!(text.contains("Delete - Cancel construction"));
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(),
|
||||
Structures::MineConstruction(_)
|
||||
));
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
|
||||
);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE));
|
||||
assert!(matches!(
|
||||
app.states_ref().unwrap().skirmish.board.cells[5][1].get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.player_resources,
|
||||
Resources::new(50, 25)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_cannot_cancel_enemy_site_or_refund_a_completed_mine() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 40), Players::Enemy));
|
||||
assert!(!game.cancel_mine_construction((5, 40), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources::new(50, 25));
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(!game.cancel_mine_construction((5, 40), Players::Enemy));
|
||||
assert_eq!(game.enemy_resources.metal, 15);
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.start_mine_construction((5, 40), Players::Enemy));
|
||||
assert!(!game.cancel_mine_construction((5, 40), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources::new(50, 25));
|
||||
for _ in 0..MineConstruction::BUILD_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(!game.cancel_mine_construction((5, 40), Players::Enemy));
|
||||
assert_eq!(game.enemy_resources.metal, 15);
|
||||
}
|
||||
+131
-131
@@ -1,160 +1,160 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::skirmish_states::structures::Structures, threads::AudioCmd,
|
||||
},
|
||||
cli::Cli,
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::skirmish_states::structures::Structures, threads::AudioCmd,
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn app() -> App {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
}
|
||||
|
||||
fn press(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> () {
|
||||
handle_keybindings(app, KeyEvent::new(code, modifiers));
|
||||
handle_keybindings(app, KeyEvent::new(code, modifiers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_miner_switches_help_and_escape_cancels_only_the_route() -> () {
|
||||
let mut app: App = app();
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space)
|
||||
);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
assert!(available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
let actions: Vec<Action> = available_skirmish_actions(&app.states_ref().unwrap().skirmish);
|
||||
assert!(actions.contains(&Action::Backspace));
|
||||
assert!(actions.contains(&Action::Delete));
|
||||
assert!(!actions.contains(&Action::Building));
|
||||
assert!(!actions.contains(&Action::ZoomIn));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
assert!(
|
||||
!app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.marked_cells
|
||||
.marking_cells
|
||||
);
|
||||
assert!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_marked_cells()
|
||||
.is_empty()
|
||||
);
|
||||
let mut app: App = app();
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space)
|
||||
);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Space)
|
||||
);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
let actions: Vec<Action> = available_skirmish_actions(&app.states_ref().unwrap().skirmish);
|
||||
assert!(actions.contains(&Action::Backspace));
|
||||
assert!(actions.contains(&Action::Delete));
|
||||
assert!(!actions.contains(&Action::Building));
|
||||
assert!(!actions.contains(&Action::ZoomIn));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
assert!(
|
||||
!app.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.marked_cells
|
||||
.marking_cells
|
||||
);
|
||||
assert!(
|
||||
app.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_marked_cells()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn building_view_has_its_own_help_and_does_not_build_on_invalid_tile() -> () {
|
||||
let mut app: App = app();
|
||||
let before: war_in_tunnels::app::states::skirmish_states::BoardState =
|
||||
app.states_ref().unwrap().skirmish.board.clone();
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert!(app.states_ref().unwrap().skirmish.building_view);
|
||||
assert_eq!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish),
|
||||
[Action::Building, Action::Esc, Action::Quit2]
|
||||
);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
assert_eq!(app.states_ref().unwrap().skirmish.board, before);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Construction unavailable"));
|
||||
assert!(text.contains("Close buildings"));
|
||||
assert!(!text.contains("Marking cells"));
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
for row in &app.states_ref().unwrap().skirmish.board.cells {
|
||||
for cell in row {
|
||||
assert!(!matches!(cell.get_structure(), Structures::Mine(_)));
|
||||
let mut app: App = app();
|
||||
let before: war_in_tunnels::app::states::skirmish_states::BoardState =
|
||||
app.states_ref().unwrap().skirmish.board.clone();
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert!(app.states_ref().unwrap().skirmish.building_view);
|
||||
assert_eq!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish),
|
||||
[Action::Building, Action::Esc, Action::Quit2]
|
||||
);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
assert_eq!(app.states_ref().unwrap().skirmish.board, before);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Construction unavailable"));
|
||||
assert!(text.contains("Close buildings"));
|
||||
assert!(!text.contains("Marking cells"));
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
for row in &app.states_ref().unwrap().skirmish.board.cells {
|
||||
for cell in row {
|
||||
assert!(!matches!(cell.get_structure(), Structures::Mine(_)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_control_space_and_quit_work_in_building_view() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::NONE);
|
||||
assert!(app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::CONTROL);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert!(app.exit);
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::NONE);
|
||||
assert!(app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::CONTROL);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
press(&mut app, KeyCode::Null, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert!(app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contextual_help_fits_normal_terminal_and_changes_when_route_is_selected() -> () {
|
||||
let mut app: App = app();
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Ctrl + Space - Build"));
|
||||
assert!(text.contains("Space - Select miner"));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Space - Confirm route"));
|
||||
assert!(text.contains("Esc - Cancel route"));
|
||||
assert!(!text.contains("Ctrl + Space"));
|
||||
assert!(!text.contains("Zoom in"));
|
||||
let mut app: App = app();
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Ctrl + Space - Build"));
|
||||
assert!(text.contains("Space - Select miner"));
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Space - Confirm route"));
|
||||
assert!(text.contains("Esc - Cancel route"));
|
||||
assert!(!text.contains("Ctrl + Space"));
|
||||
assert!(!text.contains("Zoom in"));
|
||||
}
|
||||
+427
-427
@@ -1,534 +1,534 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
widgets::Widget,
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
widgets::Widget,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
App, GameStates, View, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, CellSizes, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{Structure, Structures, Tunnel},
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
app::{
|
||||
App, GameStates, View, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, CellSizes, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{Structure, Structures, Tunnel},
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
cli::Cli,
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn game() -> SkirmishState {
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
GameStates::new(&args, &Rect::new(0, 0, 120, 40)).skirmish
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
GameStates::new(&args, &Rect::new(0, 0, 120, 40)).skirmish
|
||||
}
|
||||
|
||||
fn queue(board: &mut BoardState, origin: (usize, usize), path: &[(usize, usize)]) {
|
||||
let unit = board.cells[origin.0][origin.1]
|
||||
.get_mut_option_unit()
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
let power = unit.get_digging_power();
|
||||
unit.set_task(Tasks::Digging(DiggingTask::new(
|
||||
path.iter().copied().collect(),
|
||||
power,
|
||||
)));
|
||||
let unit = board.cells[origin.0][origin.1]
|
||||
.get_mut_option_unit()
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
let power = unit.get_digging_power();
|
||||
unit.set_task(Tasks::Digging(DiggingTask::new(
|
||||
path.iter().copied().collect(),
|
||||
power,
|
||||
)));
|
||||
}
|
||||
|
||||
fn tunnel(board: &mut BoardState, row: usize, col: usize) {
|
||||
board.cells[row][col].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
board.cells[row][col].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
}
|
||||
|
||||
fn unit(board: &BoardState, row: usize, col: usize) -> &Units {
|
||||
board.cells[row][col]
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
board.cells[row][col]
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn travel_ticks() -> u16 {
|
||||
3000_u16.div_ceil(u16::from(TICK_MS))
|
||||
3000_u16.div_ceil(u16::from(TICK_MS))
|
||||
}
|
||||
|
||||
fn advance(game: &mut SkirmishState, ticks: u16) {
|
||||
for _ in 0..ticks {
|
||||
game.tick_update();
|
||||
}
|
||||
for _ in 0..ticks {
|
||||
game.tick_update();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_damages_stone_before_moving_and_finishes_entire_route() {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
game.tick_update();
|
||||
assert_eq!(game.turn_counter, 1);
|
||||
let durability = game.board.cells[5][3].get_structure().get_durability();
|
||||
assert!(durability < 1000 && durability > 0);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(game.board.cells[5][3].get_ref_option_unit().is_none());
|
||||
for _ in 0..500 {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
game.tick_update();
|
||||
}
|
||||
for col in [3, 4] {
|
||||
assert_eq!(game.turn_counter, 1);
|
||||
let durability = game.board.cells[5][3].get_structure().get_durability();
|
||||
assert!(durability < 1000 && durability > 0);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(game.board.cells[5][3].get_ref_option_unit().is_none());
|
||||
for _ in 0..500 {
|
||||
game.tick_update();
|
||||
}
|
||||
for col in [3, 4] {
|
||||
assert!(matches!(
|
||||
game.board.cells[5][col].get_structure(),
|
||||
Structures::Tunnel(_)
|
||||
));
|
||||
}
|
||||
assert!(matches!(
|
||||
game.board.cells[5][col].get_structure(),
|
||||
Structures::Tunnel(_)
|
||||
game.board.cells[5][2].get_structure(),
|
||||
Structures::Base(_)
|
||||
));
|
||||
}
|
||||
assert!(matches!(
|
||||
game.board.cells[5][2].get_structure(),
|
||||
Structures::Base(_)
|
||||
));
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert!(game.board.cells[5][3].get_ref_option_unit().is_none());
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert!(game.board.cells[5][3].get_ref_option_unit().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_tunnel_step_takes_three_seconds_in_both_directions() {
|
||||
for path in [[(5, 3), (5, 4)], [(4, 2), (3, 2)]] {
|
||||
let mut game = game();
|
||||
for (row, col) in path {
|
||||
tunnel(&mut game.board, row, col);
|
||||
for path in [[(5, 3), (5, 4)], [(4, 2), (3, 2)]] {
|
||||
let mut game = game();
|
||||
for (row, col) in path {
|
||||
tunnel(&mut game.board, row, col);
|
||||
}
|
||||
queue(&mut game.board, (5, 2), &path);
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
game.tick_update();
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_coords(),
|
||||
path[0]
|
||||
);
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_ref_tasks()[0]
|
||||
.get_path()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_coords(),
|
||||
path[0]
|
||||
);
|
||||
game.tick_update();
|
||||
assert!(
|
||||
unit(&game.board, path[1].0, path[1].1)
|
||||
.get_ref_tasks()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
queue(&mut game.board, (5, 2), &path);
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
game.tick_update();
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_coords(),
|
||||
path[0]
|
||||
);
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_ref_tasks()[0]
|
||||
.get_path()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_coords(),
|
||||
path[0]
|
||||
);
|
||||
game.tick_update();
|
||||
assert!(
|
||||
unit(&game.board, path[1].0, path[1].1)
|
||||
.get_ref_tasks()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_tasks_execute_in_order_and_keep_unit_coordinates_current() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
tunnel(&mut game.board, 5, 4);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
queue(&mut game.board, (5, 2), &[(5, 4)]);
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 3).get_ref_tasks().len(), 1);
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
queue(&mut game.board, (5, 4), &[(5, 3), (5, 2)]);
|
||||
advance(&mut game, 2 * travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(matches!(
|
||||
game.board.cells[5][2].get_structure(),
|
||||
Structures::Base(_)
|
||||
));
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
tunnel(&mut game.board, 5, 4);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
queue(&mut game.board, (5, 2), &[(5, 4)]);
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 3).get_ref_tasks().len(), 1);
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
queue(&mut game.board, (5, 4), &[(5, 3), (5, 2)]);
|
||||
advance(&mut game, 2 * travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(matches!(
|
||||
game.board.cells[5][2].get_structure(),
|
||||
Structures::Base(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupied_tunnel_waits_without_losing_units_or_task() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
*game.board.cells[5][3].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 3))));
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
for _ in 0..3 {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
*game.board.cells[5][3].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 3))));
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
for _ in 0..3 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.board.cells[5][3].get_mut_option_unit().take();
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.board.cells[5][3].get_mut_option_unit().take();
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty());
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ore_is_not_destroyed_by_digging_task() {
|
||||
let mut game = game();
|
||||
let ore = game.board.cells[5][1].get_structure();
|
||||
queue(&mut game.board, (5, 2), &[(5, 1)]);
|
||||
for _ in 0..200 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), ore);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
let mut game = game();
|
||||
let ore = game.board.cells[5][1].get_structure();
|
||||
queue(&mut game.board, (5, 2), &[(5, 1)]);
|
||||
for _ in 0..200 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), ore);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_or_empty_routes_do_not_teleport_or_remove_miner() {
|
||||
for path in [vec![], vec![(usize::MAX, usize::MAX)], vec![(0, 0)]] {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &path);
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(unit(&game.board, 5, 2).get_ref_tasks().is_empty());
|
||||
}
|
||||
for path in [vec![], vec![(usize::MAX, usize::MAX)], vec![(0, 0)]] {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &path);
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(unit(&game.board, 5, 2).get_ref_tasks().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_player_miner_can_start_drawing_route() {
|
||||
let mut game = game();
|
||||
game.board.toggle_marking();
|
||||
assert!(!game.board.marked_cells.marking_cells);
|
||||
for _ in 0..36 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
assert_eq!(game.board.get_focused_cell().get_coords(), (5, 39));
|
||||
game.board.toggle_marking();
|
||||
assert!(!game.board.marked_cells.marking_cells);
|
||||
assert!(game.board.get_marked_cells().is_empty());
|
||||
let mut game = game();
|
||||
game.board.toggle_marking();
|
||||
assert!(!game.board.marked_cells.marking_cells);
|
||||
for _ in 0..36 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
assert_eq!(game.board.get_focused_cell().get_coords(), (5, 39));
|
||||
game.board.toggle_marking();
|
||||
assert!(!game.board.marked_cells.marking_cells);
|
||||
assert!(game.board.get_marked_cells().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_route_pauses_selected_miner_and_boundary_moves_do_not_repeat_cells() {
|
||||
let mut game = game();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
game.board.toggle_marking();
|
||||
game.tick_update();
|
||||
assert_eq!(
|
||||
game.board.cells[5][3].get_structure().get_durability(),
|
||||
1000
|
||||
);
|
||||
for _ in 0..10 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
let coords: Vec<_> = game.board.get_marked_cells().iter().copied().collect();
|
||||
assert!(coords.windows(2).all(|pair| pair[0] != pair[1]));
|
||||
game.board.end_marking_cells(true);
|
||||
game.tick_update();
|
||||
assert!(game.board.cells[5][3].get_structure().get_durability() < 1000);
|
||||
let mut game = game();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
game.board.toggle_marking();
|
||||
game.tick_update();
|
||||
assert_eq!(
|
||||
game.board.cells[5][3].get_structure().get_durability(),
|
||||
1000
|
||||
);
|
||||
for _ in 0..10 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
let coords: Vec<_> = game.board.get_marked_cells().iter().copied().collect();
|
||||
assert!(coords.windows(2).all(|pair| pair[0] != pair[1]));
|
||||
game.board.end_marking_cells(true);
|
||||
game.tick_update();
|
||||
assert!(game.board.cells[5][3].get_structure().get_durability() < 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_another_task_extends_the_end_of_queued_work() {
|
||||
let mut game = game();
|
||||
for col in [3, 4, 5] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(game.board.get_focused_cell().get_coords(), (5, 4));
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 2);
|
||||
advance(&mut game, 3 * travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 5).get_coords(), (5, 5));
|
||||
assert!(unit(&game.board, 5, 5).get_ref_tasks().is_empty());
|
||||
let mut game = game();
|
||||
for col in [3, 4, 5] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(game.board.get_focused_cell().get_coords(), (5, 4));
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 2);
|
||||
advance(&mut game, 3 * travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 5).get_coords(), (5, 5));
|
||||
assert!(unit(&game.board, 5, 5).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freshly_dug_cell_is_entered_immediately_then_tunnel_travel_takes_three_seconds() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 4);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
for _ in 0..200 {
|
||||
game.tick_update();
|
||||
if matches!(
|
||||
game.board.cells[5][3].get_structure(),
|
||||
Structures::Tunnel(_)
|
||||
) {
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.tick_update();
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
return;
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 4);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
for _ in 0..200 {
|
||||
game.tick_update();
|
||||
if matches!(
|
||||
game.board.cells[5][3].get_structure(),
|
||||
Structures::Tunnel(_)
|
||||
) {
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.tick_update();
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("miner did not finish digging the stone");
|
||||
panic!("miner did not finish digging the stone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_symbol_in_tunnel_uses_owner_color_even_when_selected_or_marked() {
|
||||
for owner in [Players::Player, Players::Enemy] {
|
||||
for zoom in [ZoomLevel::Default, ZoomLevel::ZoomedIn] {
|
||||
for (selected, marked) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let mut game = game();
|
||||
let cell = &mut game.board.cells[5][3];
|
||||
cell.set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
cell
|
||||
.set_selected(selected)
|
||||
.set_marked(marked)
|
||||
.set_zoom_level(zoom);
|
||||
*cell.get_mut_option_unit() = Some(Units::Miner(MinerUnit::new(owner, (5, 3))));
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
zoom.get_cell_size(CellSizes::Width) as u16,
|
||||
zoom.get_cell_size(CellSizes::Height) as u16,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
cell.clone().render(area, &mut buffer);
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.find(|cell| cell.symbol() == "M")
|
||||
.unwrap();
|
||||
assert_eq!(marker.fg, owner.get_color(), "{owner:?} {zoom:?}");
|
||||
assert!(buffer.content.iter().any(|cell| cell.symbol() == "T"));
|
||||
}
|
||||
for owner in [Players::Player, Players::Enemy] {
|
||||
for zoom in [ZoomLevel::Default, ZoomLevel::ZoomedIn] {
|
||||
for (selected, marked) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let mut game = game();
|
||||
let cell = &mut game.board.cells[5][3];
|
||||
cell.set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
cell.set_selected(selected)
|
||||
.set_marked(marked)
|
||||
.set_zoom_level(zoom);
|
||||
*cell.get_mut_option_unit() = Some(Units::Miner(MinerUnit::new(owner, (5, 3))));
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
zoom.get_cell_size(CellSizes::Width) as u16,
|
||||
zoom.get_cell_size(CellSizes::Height) as u16,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
cell.clone().render(area, &mut buffer);
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.find(|cell| cell.symbol() == "M")
|
||||
.unwrap();
|
||||
assert_eq!(marker.fg, owner.get_color(), "{owner:?} {zoom:?}");
|
||||
assert!(buffer.content.iter().any(|cell| cell.symbol() == "T"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_shows_movement_percentage_and_clears_it_after_each_step() {
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let mut game = game();
|
||||
for col in [2, 3, 4] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let mut game = game();
|
||||
for col in [2, 3, 4] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
let render = |board: &BoardState, col: usize| {
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
zoom.get_cell_size(CellSizes::Width) as u16,
|
||||
zoom.get_cell_size(CellSizes::Height) as u16,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
let mut cell = board.get_ref_cell(5, col).clone();
|
||||
cell.set_zoom_level(zoom);
|
||||
cell.render(area, &mut buffer);
|
||||
buffer
|
||||
};
|
||||
let text = |buffer: &Buffer| -> String {
|
||||
buffer.content.iter().map(|cell| cell.symbol()).collect()
|
||||
};
|
||||
assert!(!text(&render(&game.board, 2)).contains('%'));
|
||||
advance(&mut game, 46);
|
||||
let buffer = render(&game.board, 2);
|
||||
assert!(text(&buffer).contains("50%"), "{zoom:?}");
|
||||
if zoom != ZoomLevel::ZoomedOut {
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.find(|cell| cell.symbol() == "M")
|
||||
.unwrap();
|
||||
assert_eq!(marker.fg, Players::Player.get_color());
|
||||
}
|
||||
advance(&mut game, travel_ticks() - 46);
|
||||
assert!(!text(&render(&game.board, 2)).contains('%'));
|
||||
assert!(!text(&render(&game.board, 3)).contains('%'));
|
||||
game.tick_update();
|
||||
assert!(text(&render(&game.board, 3)).contains("1%"));
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert!(!text(&render(&game.board, 4)).contains('%'));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
}
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
let render = |board: &BoardState, col: usize| {
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
zoom.get_cell_size(CellSizes::Width) as u16,
|
||||
zoom.get_cell_size(CellSizes::Height) as u16,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
let mut cell = board.get_ref_cell(5, col).clone();
|
||||
cell.set_zoom_level(zoom);
|
||||
cell.render(area, &mut buffer);
|
||||
buffer
|
||||
};
|
||||
let text =
|
||||
|buffer: &Buffer| -> String { buffer.content.iter().map(|cell| cell.symbol()).collect() };
|
||||
assert!(!text(&render(&game.board, 2)).contains('%'));
|
||||
advance(&mut game, 46);
|
||||
let buffer = render(&game.board, 2);
|
||||
assert!(text(&buffer).contains("50%"), "{zoom:?}");
|
||||
if zoom != ZoomLevel::ZoomedOut {
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.find(|cell| cell.symbol() == "M")
|
||||
.unwrap();
|
||||
assert_eq!(marker.fg, Players::Player.get_color());
|
||||
}
|
||||
advance(&mut game, travel_ticks() - 46);
|
||||
assert!(!text(&render(&game.board, 2)).contains('%'));
|
||||
assert!(!text(&render(&game.board, 3)).contains('%'));
|
||||
game.tick_update();
|
||||
assert!(text(&render(&game.board, 3)).contains("1%"));
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert!(!text(&render(&game.board, 4)).contains('%'));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_route_splits_into_moving_and_digging_segments() {
|
||||
let mut game = game();
|
||||
for col in [3, 4, 7] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
for _ in 0..5 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
game.board.toggle_marking();
|
||||
let tasks = unit(&game.board, 5, 2).get_ref_tasks();
|
||||
assert_eq!(tasks.len(), 3);
|
||||
assert!(matches!(tasks[0], Tasks::Moving(_)));
|
||||
assert_eq!(
|
||||
tasks[0].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(5, 3), (5, 4)]
|
||||
);
|
||||
assert!(matches!(tasks[1], Tasks::Digging(_)));
|
||||
assert_eq!(
|
||||
tasks[1].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(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::<Vec<_>>(),
|
||||
[(5, 3), (5, 4)]
|
||||
);
|
||||
assert!(matches!(tasks[1], Tasks::Digging(_)));
|
||||
assert_eq!(
|
||||
tasks[1].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(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<AudioCmd>, Receiver<AudioCmd>) = 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<TestBackend> = 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<AudioCmd>, Receiver<AudioCmd>) = 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<TestBackend> = Terminal::new(TestBackend::new(120, 60)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &ratatui::buffer::Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Step: 50% | Total: 75%"));
|
||||
assert!(text.contains("Step: 0% | Total: 0%"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_route_tick_and_tui_show_remaining_durability_at_every_zoom() {
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap();
|
||||
let (tx, _) = channel();
|
||||
let mut app = App::new(args, tx);
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
for code in [
|
||||
KeyCode::Left,
|
||||
KeyCode::Char(' '),
|
||||
KeyCode::Right,
|
||||
KeyCode::Char(' '),
|
||||
] {
|
||||
handle_keybindings(&mut app, KeyEvent::new(code, KeyModifiers::NONE));
|
||||
}
|
||||
app.states_mut().unwrap().skirmish.tick_update();
|
||||
let durability = app.states_ref().unwrap().skirmish.board.cells[5][3]
|
||||
.get_structure()
|
||||
.get_durability();
|
||||
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let state = &mut app.states_mut().unwrap().skirmish;
|
||||
state.board.change_zoom(zoom);
|
||||
state.board.vertical_offset.set_value(5);
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap();
|
||||
let (tx, _) = channel();
|
||||
let mut app = App::new(args, tx);
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
for code in [
|
||||
KeyCode::Left,
|
||||
KeyCode::Char(' '),
|
||||
KeyCode::Right,
|
||||
KeyCode::Char(' '),
|
||||
] {
|
||||
handle_keybindings(&mut app, KeyEvent::new(code, KeyModifiers::NONE));
|
||||
}
|
||||
app.states_mut().unwrap().skirmish.tick_update();
|
||||
let durability = app.states_ref().unwrap().skirmish.board.cells[5][3]
|
||||
.get_structure()
|
||||
.get_durability();
|
||||
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let state = &mut app.states_mut().unwrap().skirmish;
|
||||
state.board.change_zoom(zoom);
|
||||
state.board.vertical_offset.set_value(5);
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| cell.symbol())
|
||||
.collect();
|
||||
assert!(text.contains("99%"), "{zoom:?}");
|
||||
}
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| cell.symbol())
|
||||
.collect();
|
||||
assert!(text.contains("99%"), "{zoom:?}");
|
||||
}
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| cell.symbol())
|
||||
.collect();
|
||||
assert!(text.contains(&format!("{durability}/1000")));
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| cell.symbol())
|
||||
.collect();
|
||||
assert!(text.contains(&format!("{durability}/1000")));
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
}
|
||||
+279
-280
@@ -1,338 +1,337 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Cell,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
MoveFocusedCell, Players, Resources,
|
||||
structures::{BaseBuilding, Structures, Tunnel},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
app::{
|
||||
Action, App, GameStates, View, available_skirmish_actions, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
MoveFocusedCell, Players, Resources,
|
||||
structures::{BaseBuilding, Structures, Tunnel},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
cli::Cli,
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn app() -> App {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 50);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.window_area = Rect::new(0, 0, 160, 50);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
app
|
||||
}
|
||||
|
||||
fn base(game: &SkirmishState, coords: (usize, usize)) -> BaseBuilding {
|
||||
let Structures::Base(base) = game.board.cells[coords.0][coords.1].get_structure() else {
|
||||
panic!("base missing");
|
||||
};
|
||||
base
|
||||
let Structures::Base(base) = game.board.cells[coords.0][coords.1].get_structure() else {
|
||||
panic!("base missing");
|
||||
};
|
||||
base
|
||||
}
|
||||
|
||||
fn press(app: &mut App, code: KeyCode, modifiers: KeyModifiers) -> () {
|
||||
handle_keybindings(app, KeyEvent::new(code, modifiers));
|
||||
handle_keybindings(app, KeyEvent::new(code, modifiers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_is_owned_by_base_and_invalid_requests_do_not_charge() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let before: Resources = game.player_resources;
|
||||
for coords in [(5, 39), (0, 0), (usize::MAX, 2)] {
|
||||
assert!(!game.recruit_miner(coords, Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
}
|
||||
assert!(!game.recruit_miner((5, 2), Players::Unclaimed));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources::new(40, 10));
|
||||
assert_eq!(game.enemy_resources, Resources::new(50, 25));
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
let before: Resources = game.player_resources;
|
||||
for coords in [(5, 39), (0, 0), (usize::MAX, 2)] {
|
||||
assert!(!game.recruit_miner(coords, Players::Player));
|
||||
assert_eq!(game.player_resources, before);
|
||||
}
|
||||
assert!(!game.recruit_miner((5, 2), Players::Unclaimed));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(game.player_resources, Resources::new(40, 10));
|
||||
assert_eq!(game.enemy_resources, Resources::new(50, 25));
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_requires_both_resources_and_charges_each_queue_entry() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for resources in [
|
||||
Resources { wood: 9, metal: 15 },
|
||||
Resources {
|
||||
wood: 10,
|
||||
metal: 14,
|
||||
},
|
||||
] {
|
||||
game.player_resources = resources;
|
||||
assert!(!game.can_recruit_miner((5, 2), Players::Player));
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for resources in [
|
||||
Resources { wood: 9, metal: 15 },
|
||||
Resources {
|
||||
wood: 10,
|
||||
metal: 14,
|
||||
},
|
||||
] {
|
||||
game.player_resources = resources;
|
||||
assert!(!game.can_recruit_miner((5, 2), Players::Player));
|
||||
assert!(!game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(game.player_resources, resources);
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
}
|
||||
game.player_resources = Resources::new(20, 30);
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 2);
|
||||
assert_eq!(game.player_resources, Resources::new(0, 0));
|
||||
assert!(!game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(game.player_resources, resources);
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
}
|
||||
game.player_resources = Resources::new(20, 30);
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 2);
|
||||
assert_eq!(game.player_resources, Resources::new(0, 0));
|
||||
assert!(!game.recruit_miner((5, 2), Players::Player));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_recruit_waits_at_full_progress_and_following_recruits_pause_until_departure() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
game.player_resources = Resources::new(100, 100);
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
let original: Option<Units> = game.board.cells[5][2].get_option_unit();
|
||||
for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
game.player_resources = Resources::new(100, 100);
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
let original: Option<Units> = game.board.cells[5][2].get_option_unit();
|
||||
for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 2);
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 100);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), original);
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 2);
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 100);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), original);
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
game.tick_update();
|
||||
let spawned: Option<Units> = game.board.cells[5][2].get_option_unit();
|
||||
assert_eq!(spawned.get_owner(), Players::Player);
|
||||
assert_eq!(spawned.get_coords(), (5, 2));
|
||||
assert!(spawned.get_ref_tasks().is_empty());
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS {
|
||||
let spawned: Option<Units> = game.board.cells[5][2].get_option_unit();
|
||||
assert_eq!(spawned.get_owner(), Players::Player);
|
||||
assert_eq!(spawned.get_coords(), (5, 2));
|
||||
assert!(spawned.get_ref_tasks().is_empty());
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
for _ in 0..2 * BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 0);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), spawned);
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
for _ in 1..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 99);
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 1);
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 0);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), spawned);
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
for _ in 1..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 99);
|
||||
game.tick_update();
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walking_out_of_base_resumes_recruitment_and_new_miner_can_receive_tasks() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
game.board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
game.board.cells[5][3].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
for _ in 0..3000_u16.div_ceil(u16::from(TICK_MS)) {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
for _ in 3000_u16.div_ceil(u16::from(TICK_MS))..BaseBuilding::RECRUITMENT_TICKS - 1 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 99);
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
for _ in 3000_u16.div_ceil(u16::from(TICK_MS))..BaseBuilding::RECRUITMENT_TICKS - 1 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 99);
|
||||
game.tick_update();
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_coords(),
|
||||
(5, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
game.board.cells[5][3].get_ref_option_unit().get_coords(),
|
||||
(5, 3)
|
||||
);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Down);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(
|
||||
game.board.cells[5][2]
|
||||
.get_ref_option_unit()
|
||||
.get_ref_tasks()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_coords(),
|
||||
(5, 2)
|
||||
);
|
||||
assert_eq!(
|
||||
game.board.cells[5][3].get_ref_option_unit().get_coords(),
|
||||
(5, 3)
|
||||
);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Down);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(
|
||||
game.board.cells[5][2]
|
||||
.get_ref_option_unit()
|
||||
.get_ref_tasks()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrival_during_training_does_not_overwrite_visitor_and_ready_recruit_spawns_after_departure()
|
||||
-> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
for _ in 0..100 {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
assert!(game.recruit_miner((5, 2), Players::Player));
|
||||
for _ in 0..100 {
|
||||
game.tick_update();
|
||||
}
|
||||
let visitor: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)));
|
||||
*game.board.cells[5][2].get_mut_option_unit() = Some(visitor.clone());
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 100);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), Some(visitor));
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
game.tick_update();
|
||||
}
|
||||
let visitor: Units = Units::Miner(MinerUnit::new(Players::Enemy, (5, 2)));
|
||||
*game.board.cells[5][2].get_mut_option_unit() = Some(visitor.clone());
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(base(game, (5, 2)).recruitment_progress(), 100);
|
||||
assert_eq!(game.board.cells[5][2].get_option_unit(), Some(visitor));
|
||||
game.board.cells[5][2].get_mut_option_unit().take();
|
||||
game.tick_update();
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
assert_eq!(base(game, (5, 2)).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[5][2].get_ref_option_unit().get_owner(),
|
||||
Players::Player
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bases_have_independent_queues_costs_and_spawn_ownership() -> () {
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] {
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_mut_option_unit()
|
||||
.take();
|
||||
assert!(game.recruit_miner(coords, owner));
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 10);
|
||||
assert_eq!(game.enemy_resources.metal, 10);
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] {
|
||||
assert_eq!(base(game, coords).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_ref_option_unit()
|
||||
.get_owner(),
|
||||
owner
|
||||
);
|
||||
assert_eq!(
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_ref_option_unit()
|
||||
.get_coords(),
|
||||
coords
|
||||
);
|
||||
}
|
||||
let mut app: App = app();
|
||||
let game: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] {
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_mut_option_unit()
|
||||
.take();
|
||||
assert!(game.recruit_miner(coords, owner));
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 10);
|
||||
assert_eq!(game.enemy_resources.metal, 10);
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
for (coords, owner) in [((5, 2), Players::Player), ((5, 39), Players::Enemy)] {
|
||||
assert_eq!(base(game, coords).queued_miners(), 0);
|
||||
assert_eq!(
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_ref_option_unit()
|
||||
.get_owner(),
|
||||
owner
|
||||
);
|
||||
assert_eq!(
|
||||
game.board.cells[coords.0][coords.1]
|
||||
.get_ref_option_unit()
|
||||
.get_coords(),
|
||||
coords
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_panel_queues_with_enter_shows_ready_state_and_closes_without_losing_queue() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Recruitment)
|
||||
);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
press(&mut app, KeyCode::Right, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert_eq!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_focused_cell()
|
||||
.get_coords(),
|
||||
(5, 2)
|
||||
);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
app.states_mut().unwrap().skirmish.tick_update();
|
||||
}
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Cost: 10 Wood + 15 Metal"));
|
||||
assert!(text.contains("Queued Miners: 1"));
|
||||
assert!(text.contains("Current progress: 100%"));
|
||||
assert!(text.contains("Ready: waiting for free base cell"));
|
||||
assert!(text.contains("Close recruitment"));
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
assert!(
|
||||
available_skirmish_actions(&app.states_ref().unwrap().skirmish)
|
||||
.contains(&Action::Recruitment)
|
||||
);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
!available_skirmish_actions(&app.states_ref().unwrap().skirmish).contains(&Action::Enter)
|
||||
);
|
||||
press(&mut app, KeyCode::Right, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::CONTROL);
|
||||
assert_eq!(
|
||||
app.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_focused_cell()
|
||||
.get_coords(),
|
||||
(5, 2)
|
||||
);
|
||||
assert!(!app.states_ref().unwrap().skirmish.building_view);
|
||||
for _ in 0..BaseBuilding::RECRUITMENT_TICKS {
|
||||
app.states_mut().unwrap().skirmish.tick_update();
|
||||
}
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(160, 50)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Cost: 10 Wood + 15 Metal"));
|
||||
assert!(text.contains("Queued Miners: 1"));
|
||||
assert!(text.contains("Current progress: 100%"));
|
||||
assert!(text.contains("Ready: waiting for free base cell"));
|
||||
assert!(text.contains("Close recruitment"));
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_panel_is_unavailable_off_base_and_during_route_selection() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.marked_cells
|
||||
.marking_cells
|
||||
);
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert!(app.exit);
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert!(
|
||||
app.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.marked_cells
|
||||
.marking_cells
|
||||
);
|
||||
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char(' '), KeyModifiers::ALT);
|
||||
press(&mut app, KeyCode::Char('c'), KeyModifiers::CONTROL);
|
||||
assert!(app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recruitment_fallback_opens_queues_and_closes_on_own_base() -> () {
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.player_resources,
|
||||
Resources::new(40, 10)
|
||||
);
|
||||
let mut app: App = app();
|
||||
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
|
||||
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
|
||||
assert!(!app.states_ref().unwrap().skirmish.recruitment_view);
|
||||
assert_eq!(
|
||||
base(&app.states_ref().unwrap().skirmish, (5, 2)).queued_miners(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.player_resources,
|
||||
Resources::new(40, 10)
|
||||
);
|
||||
}
|
||||
+123
-121
@@ -2,160 +2,162 @@ use clap::Parser;
|
||||
use ratatui::{Terminal, backend::TestBackend, buffer::Cell, layout::Rect};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
App, GameStates, View,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
Players, Resources,
|
||||
resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS},
|
||||
structures::{Mine, Ore, Stone, Structure, Structures},
|
||||
},
|
||||
app::{
|
||||
App, GameStates, View,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
Players, Resources,
|
||||
resources::{MINE_PRODUCTION_TICKS, WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_TICKS},
|
||||
structures::{Mine, Ore, Stone, Structure, Structures},
|
||||
},
|
||||
},
|
||||
threads::AudioCmd,
|
||||
},
|
||||
threads::AudioCmd,
|
||||
},
|
||||
cli::Cli,
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn game() -> SkirmishState {
|
||||
let args: Cli =
|
||||
Cli::try_parse_from(["game", "--starting-wood", "70", "--starting-metal", "40"]).unwrap();
|
||||
GameStates::new(&args, &Rect::new(0, 0, 160, 40)).skirmish
|
||||
let args: Cli =
|
||||
Cli::try_parse_from(["game", "--starting-wood", "70", "--starting-metal", "40"]).unwrap();
|
||||
GameStates::new(&args, &Rect::new(0, 0, 160, 40)).skirmish
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resources_start_from_cli_and_legacy_iron_flag_remains_an_alias() -> () {
|
||||
let game: SkirmishState = game();
|
||||
assert_eq!(game.player_resources, Resources::new(70, 40));
|
||||
assert_eq!(game.enemy_resources, Resources::new(70, 40));
|
||||
let args: Cli = Cli::try_parse_from(["game", "--starting-iron", "33"]).unwrap();
|
||||
assert_eq!(args.starting_metal, 33);
|
||||
let game: SkirmishState = game();
|
||||
assert_eq!(game.player_resources, Resources::new(70, 40));
|
||||
assert_eq!(game.enemy_resources, Resources::new(70, 40));
|
||||
let args: Cli = Cli::try_parse_from(["game", "--starting-iron", "33"]).unwrap();
|
||||
assert_eq!(args.starting_metal, 33);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passive_wood_arrives_once_per_interval_without_free_metal() -> () {
|
||||
let mut game: SkirmishState = game();
|
||||
for _ in 1..WOOD_DELIVERY_TICKS {
|
||||
let mut game: SkirmishState = game();
|
||||
for _ in 1..WOOD_DELIVERY_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.wood, 70);
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.wood, 70);
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.wood, 70 + WOOD_DELIVERY_AMOUNT);
|
||||
for _ in 0..WOOD_DELIVERY_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.wood, 70 + 2 * WOOD_DELIVERY_AMOUNT);
|
||||
assert_eq!(game.player_resources.metal, 40);
|
||||
assert_eq!(game.enemy_resources, Resources::new(70, 40));
|
||||
assert_eq!(game.player_resources.wood, 70 + WOOD_DELIVERY_AMOUNT);
|
||||
for _ in 0..WOOD_DELIVERY_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.wood, 70 + 2 * WOOD_DELIVERY_AMOUNT);
|
||||
assert_eq!(game.player_resources.metal, 40);
|
||||
assert_eq!(game.enemy_resources, Resources::new(70, 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ore_is_neutral_and_no_mines_spawn_on_the_board() -> () {
|
||||
let game: SkirmishState = game();
|
||||
let mut deposits: usize = 0;
|
||||
for row in &game.board.cells {
|
||||
for cell in row {
|
||||
let structure: Structures = cell.get_structure();
|
||||
assert!(!matches!(structure, Structures::Mine(_)));
|
||||
if matches!(structure, Structures::Ore(_)) {
|
||||
deposits += 1;
|
||||
assert_eq!(structure.get_owner(), Players::Unclaimed);
|
||||
}
|
||||
let game: SkirmishState = game();
|
||||
let mut deposits: usize = 0;
|
||||
for row in &game.board.cells {
|
||||
for cell in row {
|
||||
let structure: Structures = cell.get_structure();
|
||||
assert!(!matches!(structure, Structures::Mine(_)));
|
||||
if matches!(structure, Structures::Ore(_)) {
|
||||
deposits += 1;
|
||||
assert_eq!(structure.get_owner(), Players::Unclaimed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(deposits, 2);
|
||||
assert_eq!(deposits, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mine_can_only_replace_neutral_ore_and_preserves_its_reserve() -> () {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
assert_eq!(ore.extract(4999), 4999);
|
||||
let mut structure: Structures = Structures::Ore(ore);
|
||||
assert!(structure.build_mine(Players::Player));
|
||||
let Structures::Mine(mut mine) = structure else {
|
||||
panic!("expected mine");
|
||||
};
|
||||
assert_eq!(mine.get_owner(), Players::Player);
|
||||
assert_eq!(mine.get_ore_amount(), 1);
|
||||
assert_eq!(mine.produce(), 1);
|
||||
assert_eq!(mine.produce(), 0);
|
||||
assert_eq!(mine.get_ore_amount(), 0);
|
||||
assert!(!structure.build_mine(Players::Enemy));
|
||||
let mut stone: Structures = Structures::Stone(Stone::new());
|
||||
assert!(!stone.build_mine(Players::Player));
|
||||
assert_eq!(stone, Structures::Stone(Stone::new()));
|
||||
assert!(Mine::new(Players::Unclaimed, ore).is_none());
|
||||
assert!(Mine::new(Players::Player, Ore::new(Players::Enemy, 1)).is_none());
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
assert_eq!(ore.extract(4999), 4999);
|
||||
let mut structure: Structures = Structures::Ore(ore);
|
||||
assert!(structure.build_mine(Players::Player));
|
||||
let Structures::Mine(mut mine) = structure else {
|
||||
panic!("expected mine");
|
||||
};
|
||||
assert_eq!(mine.get_owner(), Players::Player);
|
||||
assert_eq!(mine.get_ore_amount(), 1);
|
||||
assert_eq!(mine.produce(), 1);
|
||||
assert_eq!(mine.produce(), 0);
|
||||
assert_eq!(mine.get_ore_amount(), 0);
|
||||
assert!(!structure.build_mine(Players::Enemy));
|
||||
let mut stone: Structures = Structures::Stone(Stone::new());
|
||||
assert!(!stone.build_mine(Players::Player));
|
||||
assert_eq!(stone, Structures::Stone(Stone::new()));
|
||||
assert!(Mine::new(Players::Unclaimed, ore).is_none());
|
||||
assert!(Mine::new(Players::Player, Ore::new(Players::Enemy, 1)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mining_credits_each_owner_and_stops_when_deposit_is_empty() -> () {
|
||||
let mut game: SkirmishState = game();
|
||||
for (col, owner) in [(1, Players::Player), (40, Players::Enemy)] {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(4999);
|
||||
let mut structure: Structures = Structures::Ore(ore);
|
||||
assert!(structure.build_mine(owner));
|
||||
game.board.cells[5][col].set_structure(structure);
|
||||
}
|
||||
for _ in 1..MINE_PRODUCTION_TICKS {
|
||||
let mut game: SkirmishState = game();
|
||||
for (col, owner) in [(1, Players::Player), (40, Players::Enemy)] {
|
||||
let mut ore: Ore = Ore::new(Players::Unclaimed, 1);
|
||||
ore.extract(4999);
|
||||
let mut structure: Structures = Structures::Ore(ore);
|
||||
assert!(structure.build_mine(owner));
|
||||
game.board.cells[5][col].set_structure(structure);
|
||||
}
|
||||
for _ in 1..MINE_PRODUCTION_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 40);
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 40);
|
||||
game.tick_update();
|
||||
assert_eq!(game.player_resources.metal, 41);
|
||||
assert_eq!(game.enemy_resources.metal, 41);
|
||||
for _ in 0..MINE_PRODUCTION_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 41);
|
||||
assert_eq!(game.enemy_resources.metal, 41);
|
||||
let structure: Structures = game.board.cells[5][1].get_structure();
|
||||
assert!(
|
||||
structure
|
||||
.text()
|
||||
.iter()
|
||||
.any(|line: &ratatui::text::Line<'_>| -> bool { line.to_string().contains("Ore: 0/5000") })
|
||||
);
|
||||
assert_eq!(game.player_resources.metal, 41);
|
||||
assert_eq!(game.enemy_resources.metal, 41);
|
||||
for _ in 0..MINE_PRODUCTION_TICKS {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.player_resources.metal, 41);
|
||||
assert_eq!(game.enemy_resources.metal, 41);
|
||||
let structure: Structures = game.board.cells[5][1].get_structure();
|
||||
assert!(
|
||||
structure
|
||||
.text()
|
||||
.iter()
|
||||
.any(|line: &ratatui::text::Line<'_>| -> bool {
|
||||
line.to_string().contains("Ore: 0/5000")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_additions_do_not_overflow() -> () {
|
||||
let mut resources: Resources = Resources {
|
||||
wood: u32::MAX - 1,
|
||||
metal: u32::MAX - 1,
|
||||
};
|
||||
resources.add_wood(5);
|
||||
resources.add_metal(5);
|
||||
assert_eq!(resources.wood, u32::MAX);
|
||||
assert_eq!(resources.metal, u32::MAX);
|
||||
let mut resources: Resources = Resources {
|
||||
wood: u32::MAX - 1,
|
||||
metal: u32::MAX - 1,
|
||||
};
|
||||
resources.add_wood(5);
|
||||
resources.add_metal(5);
|
||||
assert_eq!(resources.wood, u32::MAX);
|
||||
assert_eq!(resources.metal, u32::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_reads_current_resources_instead_of_starting_settings() -> () {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.states = Some(GameStates::new(&app.args, &Rect::new(0, 0, 200, 40)));
|
||||
app.states_mut().unwrap().skirmish.player_resources = Resources {
|
||||
wood: 777,
|
||||
metal: 888,
|
||||
};
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(200, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Wood: 777 (+5/5s)"));
|
||||
assert!(text.contains("Metal: 888"));
|
||||
assert!(!text.contains("Iron:"));
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, _rx): (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, tx);
|
||||
app.view = View::Skirmish;
|
||||
app.states = Some(GameStates::new(&app.args, &Rect::new(0, 0, 200, 40)));
|
||||
app.states_mut().unwrap().skirmish.player_resources = Resources {
|
||||
wood: 777,
|
||||
metal: 888,
|
||||
};
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(200, 40)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Wood: 777 (+5/5s)"));
|
||||
assert!(text.contains("Metal: 888"));
|
||||
assert!(!text.contains("Iron:"));
|
||||
}
|
||||
+91
-91
@@ -1,123 +1,123 @@
|
||||
use war_in_tunnels::app::states::skirmish_states::{
|
||||
Players,
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit},
|
||||
Players,
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn digging_progress_accumulates_and_stops_at_completion() {
|
||||
let mut task = DiggingTask::new([(5, 3)].into(), 40.0);
|
||||
assert!(!task.is_finished());
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 40.0);
|
||||
assert!(!task.is_finished());
|
||||
for _ in 0..3 {
|
||||
let mut task = DiggingTask::new([(5, 3)].into(), 40.0);
|
||||
assert!(!task.is_finished());
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
assert!(task.is_finished());
|
||||
assert_eq!(task.get_progress(), 40.0);
|
||||
assert!(!task.is_finished());
|
||||
for _ in 0..3 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
assert!(task.is_finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advancing_path_resets_progress_and_handles_empty_path() {
|
||||
let mut task = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
task.progress();
|
||||
task.pop_front();
|
||||
assert_eq!(task.current_target(), Some((5, 4)));
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
task.pop_front();
|
||||
task.pop_front();
|
||||
assert_eq!(task.current_target(), None);
|
||||
assert!(task.get_path().is_empty());
|
||||
let mut task = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
task.progress();
|
||||
task.pop_front();
|
||||
assert_eq!(task.current_target(), Some((5, 4)));
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
task.pop_front();
|
||||
task.pop_front();
|
||||
assert_eq!(task.current_target(), None);
|
||||
assert!(task.get_path().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miners_keep_tasks_in_submission_order() {
|
||||
let mut miner = MinerUnit::new(Players::Player, (5, 2));
|
||||
for target in [(5, 3), (6, 3)] {
|
||||
miner.set_task(Tasks::Digging(DiggingTask::new([target].into(), 1.0)));
|
||||
}
|
||||
assert_eq!(miner.get_ref_tasks().len(), 2);
|
||||
assert_eq!(
|
||||
*miner
|
||||
.get_mut_tasks()
|
||||
.pop_front()
|
||||
.unwrap()
|
||||
.get_path_front_coords(),
|
||||
(5, 3)
|
||||
);
|
||||
assert_eq!(*miner.get_ref_tasks()[0].get_path_front_coords(), (6, 3));
|
||||
let mut miner = MinerUnit::new(Players::Player, (5, 2));
|
||||
for target in [(5, 3), (6, 3)] {
|
||||
miner.set_task(Tasks::Digging(DiggingTask::new([target].into(), 1.0)));
|
||||
}
|
||||
assert_eq!(miner.get_ref_tasks().len(), 2);
|
||||
assert_eq!(
|
||||
*miner
|
||||
.get_mut_tasks()
|
||||
.pop_front()
|
||||
.unwrap()
|
||||
.get_path_front_coords(),
|
||||
(5, 3)
|
||||
);
|
||||
assert_eq!(*miner.get_ref_tasks()[0].get_path_front_coords(), (6, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movement_progress_is_separate_from_digging_and_resets_for_next_cell() {
|
||||
let mut task = MovingTask::new([(5, 3), (5, 4)].into());
|
||||
assert_eq!(task.get_movement_progress(), None);
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert!(!task.is_finished());
|
||||
for _ in 0..46 {
|
||||
let mut task = MovingTask::new([(5, 3), (5, 4)].into());
|
||||
assert_eq!(task.get_movement_progress(), None);
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert!(!task.is_finished());
|
||||
for _ in 0..46 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_movement_progress(), Some(50));
|
||||
for _ in 46..91 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_movement_progress(), Some(100));
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
assert!(task.is_finished());
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_movement_progress(), Some(50));
|
||||
for _ in 46..91 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_movement_progress(), Some(100));
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
assert!(task.is_finished());
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_movement_progress(), None);
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert!(!task.is_finished());
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_movement_progress(), None);
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert!(!task.is_finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_digging_progress_counts_completed_cells_and_current_step() -> () {
|
||||
let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
assert_eq!(task.get_total_progress(), 0.0);
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 25.0);
|
||||
assert_eq!(task.get_total_progress(), 12.5);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert_eq!(task.get_total_progress(), 50.0);
|
||||
task.progress();
|
||||
assert_eq!(task.get_total_progress(), 62.5);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_total_progress(), 100.0);
|
||||
let empty: DiggingTask = DiggingTask::new([].into(), 1.0);
|
||||
assert_eq!(empty.get_total_progress(), 100.0);
|
||||
let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
assert_eq!(task.get_total_progress(), 0.0);
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 25.0);
|
||||
assert_eq!(task.get_total_progress(), 12.5);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert_eq!(task.get_total_progress(), 50.0);
|
||||
task.progress();
|
||||
assert_eq!(task.get_total_progress(), 62.5);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_total_progress(), 100.0);
|
||||
let empty: DiggingTask = DiggingTask::new([].into(), 1.0);
|
||||
assert_eq!(empty.get_total_progress(), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_conversion_keeps_completed_steps_and_original_length() -> () {
|
||||
let mut digging: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
digging.pop_front();
|
||||
let mut moving: MovingTask = MovingTask::from_digging(digging);
|
||||
assert_eq!(moving.get_total_steps(), 2);
|
||||
assert_eq!(moving.get_total_progress(), 50.0);
|
||||
for _ in 0..91 {
|
||||
moving.progress();
|
||||
}
|
||||
assert_eq!(moving.get_total_progress(), 100.0);
|
||||
let mut digging: DiggingTask = DiggingTask::from_moving(moving, 25.0);
|
||||
assert_eq!(digging.get_total_steps(), 2);
|
||||
assert_eq!(digging.get_total_progress(), 50.0);
|
||||
digging.progress();
|
||||
assert_eq!(digging.get_total_progress(), 62.5);
|
||||
let mut digging: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
digging.pop_front();
|
||||
let mut moving: MovingTask = MovingTask::from_digging(digging);
|
||||
assert_eq!(moving.get_total_steps(), 2);
|
||||
assert_eq!(moving.get_total_progress(), 50.0);
|
||||
for _ in 0..91 {
|
||||
moving.progress();
|
||||
}
|
||||
assert_eq!(moving.get_total_progress(), 100.0);
|
||||
let mut digging: DiggingTask = DiggingTask::from_moving(moving, 25.0);
|
||||
assert_eq!(digging.get_total_steps(), 2);
|
||||
assert_eq!(digging.get_total_progress(), 50.0);
|
||||
digging.progress();
|
||||
assert_eq!(digging.get_total_progress(), 62.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn digging_queue_formats_step_total_and_pending_progress() -> () {
|
||||
let mut miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2));
|
||||
let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
task.pop_front();
|
||||
task.progress();
|
||||
miner.set_task(Tasks::Digging(task));
|
||||
miner.set_task(Tasks::Moving(MovingTask::new([(5, 5)].into())));
|
||||
let lines: Vec<ratatui::text::Line<'_>> = 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<ratatui::text::Line<'_>> = miner.get_tasks_formatted();
|
||||
assert!(lines[1].to_string().contains("Step: 25% | Total: 62%"));
|
||||
assert!(lines[3].to_string().contains("Step: 0% | Total: 0%"));
|
||||
}
|
||||
Reference in new issue
Block a user