Add rustfmt configuration and reformat code

- Document new Rust formatting standards in AGENTS, CODE_MAP, and
  PROGRESS.
- Add rustfmt.toml with style 2024, 2‑space indents, 100‑char width, LF
  endings.
- Apply consistent 2‑space indentation and ordering across source files.
- Minor clean‑up of imports and whitespace to match the new style.
This commit is contained in:
GarandPLG committed 2026-10-19 12:21:20 +02:00
1 parent 9e09aaf75e
commit c4313b4117
60 files changed
+3632 -3610

No files matched your search

+81 -82
View File
@@ -1,103 +1,102 @@
use crate::{
app::{GameStates, handle_keybindings, threads::AudioCmd, view::View},
cli::Cli,
app::{GameStates, handle_keybindings, threads::AudioCmd, view::View},
cli::Cli,
};
use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect};
use std::{
io::Result,
sync::mpsc::{Receiver, Sender},
thread::sleep,
time::Duration,
io::Result,
sync::mpsc::{Receiver, Sender},
thread::sleep,
time::Duration,
};
pub struct App {
pub exit: bool,
pub view: View,
pub window_area: Rect,
pub args: Cli,
pub states: Option<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 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_ref(&self) -> Option<&GameStates> {
self.states.as_ref()
}
pub fn states_mut(&mut self) -> Option<&mut GameStates> {
self.states.as_mut()
}
pub fn states_mut(&mut self) -> Option<&mut GameStates> {
self.states.as_mut()
}
pub fn run(
&mut self,
terminal: &mut DefaultTerminal,
input_rx: Receiver<KeyEvent>,
resize_rx: Receiver<(u16, u16)>,
tick_rx: Receiver<()>,
) -> Result<()> {
while !self.exit {
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
pub fn run(
&mut self,
terminal: &mut DefaultTerminal,
input_rx: Receiver<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(key) = input_rx.try_recv() {
self.handle_key_event(key)?;
}
if let Ok((_, _)) = resize_rx.try_recv() {
let window_area: Rect = self.window_area;
if let Some(state) = self.states_mut() {
state
.skirmish
.board
.change_resize(&window_area, state.skirmish.side_panel);
}
}
if let Ok(()) = tick_rx.try_recv() {
self.update()?;
}
sleep(Duration::from_millis(10));
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame<'_>) {
let window_area: Rect = frame.area();
if window_area.width != self.window_area.width
|| window_area.height != self.window_area.height
{
self.window_area = window_area;
if self.states.is_none() {
self.states = Some(GameStates::new(&self.args, &self.window_area))
}
}
frame.render_widget(self, window_area);
}
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
handle_keybindings(self, key_event);
Ok(())
}
fn update(&mut self) -> Result<()> {
if let Ok((_, _)) = resize_rx.try_recv() {
let window_area: Rect = self.window_area;
if let Some(state) = self.states_mut() {
state.skirmish.tick_update();
state
.skirmish
.board
.change_resize(&window_area, state.skirmish.side_panel);
}
}
Ok(())
if let Ok(()) = tick_rx.try_recv() {
self.update()?;
}
sleep(Duration::from_millis(10));
}
Ok(())
}
fn draw(&mut self, frame: &mut Frame<'_>) {
let window_area: Rect = frame.area();
if window_area.width != self.window_area.width || window_area.height != self.window_area.height
{
self.window_area = window_area;
if self.states.is_none() {
self.states = Some(GameStates::new(&self.args, &self.window_area))
}
}
frame.render_widget(self, window_area);
}
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
handle_keybindings(self, key_event);
Ok(())
}
fn update(&mut self) -> Result<()> {
if let Some(state) = self.states_mut() {
state.skirmish.tick_update();
}
Ok(())
}
}
+17 -17
View File
@@ -1,6 +1,6 @@
use ratatui::{
style::{Color, Style, Stylize},
text::{Line, Span},
style::{Color, Style, Stylize},
text::{Line, Span},
};
/// Creates a styled title `Line` for a UI block.
@@ -9,27 +9,27 @@ use ratatui::{
/// the spans, so the returned `Line` owns all its data and does not borrow from
/// the caller.
pub fn block_title_helper(
texts: &[(String, Color)],
separator: Option<&'static str>,
texts: &[(String, Color)],
separator: Option<&'static str>,
) -> Line<'static> {
let mut line: Line<'static> = Line::default();
let mut line: Line<'static> = Line::default();
line.spans.push(Span::from("[ ").gray());
line.spans.push(Span::from("[ ").gray());
for (i, (text, color)) in texts.iter().enumerate() {
let span = Span::styled(text.clone(), Style::new().fg(*color));
line.spans.push(span);
for (i, (text, color)) in texts.iter().enumerate() {
let span: Span<'_> = Span::styled(text.clone(), Style::new().fg(*color));
line.spans.push(span);
if let Some(sep) = separator {
if i + 1 < texts.len() {
line.spans.push(Span::from(sep).gray());
}
}
if let Some(sep) = separator {
if i + 1 < texts.len() {
line.spans.push(Span::from(sep).gray());
}
}
}
line.spans.push(Span::from(" ]").gray());
line.spans.push(Span::from(" ]").gray());
line
line
}
/// Convenience wrapper for a single‑title line.
@@ -38,5 +38,5 @@ pub fn block_title_helper(
/// forwards it to `block_title_helper`. Because `block_title_helper` clones the
/// string, the temporary slice is safe.
pub fn block_single_title_helper(text: String, color: Color) -> Line<'static> {
block_title_helper(&[(text, color)], None)
block_title_helper(&[(text, color)], None)
}
+10 -10
View File
@@ -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,
})
}
+11 -11
View File
@@ -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
View File
@@ -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),
}
}
+14 -14
View File
@@ -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);
}
}
+306 -306
View File
@@ -9,76 +9,76 @@ use std::collections::HashMap;
/// character key and carries the actual character pressed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action {
/// Request the application to quit.
Quit,
/// Alternate quit action.
Quit2,
/// Move the selection cursor up.
Up,
/// Move the selection cursor down.
Down,
/// Move the selection cursor left.
Left,
/// Move the selection cursor right.
Right,
/// Scroll up without moving the cursor.
ScrollUp,
/// Scroll down without moving the cursor.
ScrollDown,
/// Scroll left without moving the cursor.
ScrollLeft,
/// Scroll right without moving the cursor.
ScrollRight,
/// Toggle selecting cell.
Space,
/// Unmark last selected cell.
Backspace,
/// Cancel marking cell.
Delete,
/// Submit or confirm the current choice.
Enter,
/// Return to the main menu or previous screen.
Esc,
/// Zoom the view in.
ZoomIn,
/// Zoom the view out.
ZoomOut,
/// Open side panel.
Tab,
/// Toggle skirmish board/skills view.
ShiftTab,
/// Mute music.
Mute,
/// Volume up.
VolumeUp,
/// Volume down.
VolumeDown,
/// Matches any character key; the inner `char` is the actual key pressed.
WildCard(char),
/// Request the application to quit.
Quit,
/// Alternate quit action.
Quit2,
/// Move the selection cursor up.
Up,
/// Move the selection cursor down.
Down,
/// Move the selection cursor left.
Left,
/// Move the selection cursor right.
Right,
/// Scroll up without moving the cursor.
ScrollUp,
/// Scroll down without moving the cursor.
ScrollDown,
/// Scroll left without moving the cursor.
ScrollLeft,
/// Scroll right without moving the cursor.
ScrollRight,
/// Toggle selecting cell.
Space,
/// Unmark last selected cell.
Backspace,
/// Cancel marking cell.
Delete,
/// Submit or confirm the current choice.
Enter,
/// Return to the main menu or previous screen.
Esc,
/// Zoom the view in.
ZoomIn,
/// Zoom the view out.
ZoomOut,
/// Open side panel.
Tab,
/// Toggle skirmish board/skills view.
ShiftTab,
/// Mute music.
Mute,
/// Volume up.
VolumeUp,
/// Volume down.
VolumeDown,
/// Matches any character key; the inner `char` is the actual key pressed.
WildCard(char),
}
/// Logical groups of key bindings used for organising help screens or
/// configuration sections.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
pub enum Group {
/// Bindings that combine `Ctrl` with movement keys.
CtrlMovement,
/// Plain movement bindings.
Movement,
/// Scroll bindings.
Scroll,
/// Selection bindings.
Select,
/// Text input related bindings.
Input,
/// Zoom related bindings.
Zoom,
/// Music related bindings.
Music,
/// Opens/Closes/Toggles elements in current view.
Opener,
/// Quit related bindings.
Quit,
/// Bindings that combine `Ctrl` with movement keys.
CtrlMovement,
/// Plain movement bindings.
Movement,
/// Scroll bindings.
Scroll,
/// Selection bindings.
Select,
/// Text input related bindings.
Input,
/// Zoom related bindings.
Zoom,
/// Music related bindings.
Music,
/// Opens/Closes/Toggles elements in current view.
Opener,
/// Quit related bindings.
Quit,
}
/// Description of a single key binding.
@@ -87,20 +87,20 @@ pub enum Group {
/// together with metadata used for displaying help information.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyBinding {
/// The logical action performed when this binding is triggered.
pub action: Action,
/// The raw `crossterm::event::KeyCode` that identifies the key.
pub code: KeyCode,
/// The kind of key event (crossterm::event::KeyEventKind).
pub kind: KeyEventKind,
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
pub modifiers: KeyModifiers,
/// The group this binding belongs to, useful for categorising help.
pub group: Group,
/// Human‑readable symbol shown in UI/help screens.
pub symbol: &'static str,
/// Short description of what the binding does.
pub description: &'static str,
/// The logical action performed when this binding is triggered.
pub action: Action,
/// The raw `crossterm::event::KeyCode` that identifies the key.
pub code: KeyCode,
/// The kind of key event (crossterm::event::KeyEventKind).
pub kind: KeyEventKind,
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
pub modifiers: KeyModifiers,
/// The group this binding belongs to, useful for categorising help.
pub group: Group,
/// Human‑readable symbol shown in UI/help screens.
pub symbol: &'static str,
/// Short description of what the binding does.
pub description: &'static str,
}
/// Complete list of all key bindings used by the application.
@@ -108,218 +108,218 @@ pub struct KeyBinding {
/// The slice is ordered primarily for readability; the runtime lookup scans
/// this slice linearly, which is acceptable given the modest size of the list.
static KEYBINDINGS: &[KeyBinding] = &[
KeyBinding {
action: Action::Quit,
code: KeyCode::Char('q'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "q",
description: "Quit",
},
KeyBinding {
action: Action::Quit2,
code: KeyCode::Char('c'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Quit,
symbol: "Ctrl + c",
description: "Quit",
},
KeyBinding {
action: Action::Up,
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "↑",
description: "Up",
},
KeyBinding {
action: Action::Down,
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "↓",
description: "Down",
},
KeyBinding {
action: Action::Left,
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "←",
description: "Left",
},
KeyBinding {
action: Action::Right,
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "→",
description: "Right",
},
KeyBinding {
action: Action::ScrollUp,
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ↑",
description: "Scroll Up ",
},
KeyBinding {
action: Action::ScrollDown,
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ↓",
description: "Scroll Down",
},
KeyBinding {
action: Action::ScrollLeft,
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ←",
description: "Scroll Left",
},
KeyBinding {
action: Action::ScrollRight,
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + →",
description: "Scroll Right",
},
KeyBinding {
action: Action::Space,
code: KeyCode::Char(' '),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Space",
description: "Marking cells",
},
KeyBinding {
action: Action::Backspace,
code: KeyCode::Backspace,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Backspace",
description: "Unmark cell",
},
KeyBinding {
action: Action::Delete,
code: KeyCode::Delete,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Delete",
description: "Cancel marking",
},
KeyBinding {
action: Action::Enter,
code: KeyCode::Enter,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Enter",
description: "Submit",
},
KeyBinding {
action: Action::Esc,
code: KeyCode::Esc,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "Esc",
description: "Main menu",
},
KeyBinding {
action: Action::ZoomIn,
code: KeyCode::Char(','),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Zoom,
symbol: ",",
description: "Zoom in",
},
KeyBinding {
action: Action::ZoomOut,
code: KeyCode::Char('.'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Zoom,
symbol: ".",
description: "Zoom out",
},
KeyBinding {
action: Action::Mute,
code: KeyCode::Char('m'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "m",
description: "🔈",
},
KeyBinding {
action: Action::VolumeUp,
code: KeyCode::Char('b'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "b",
description: "🔊",
},
KeyBinding {
action: Action::VolumeDown,
code: KeyCode::Char('n'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "n",
description: "🔉",
},
KeyBinding {
action: Action::Tab,
code: KeyCode::Tab,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Opener,
symbol: "Tab",
description: "Toggle side panel",
},
KeyBinding {
action: Action::ShiftTab,
code: KeyCode::Tab,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::SHIFT,
group: Group::Opener,
symbol: "Shift + Tab",
description: "Toggle Map/Skills window",
},
KeyBinding {
action: Action::WildCard('_'),
code: KeyCode::Char('_'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Input,
symbol: "[All]",
description: "All keyboard characters",
},
KeyBinding {
action: Action::Quit,
code: KeyCode::Char('q'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "q",
description: "Quit",
},
KeyBinding {
action: Action::Quit2,
code: KeyCode::Char('c'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::Quit,
symbol: "Ctrl + c",
description: "Quit",
},
KeyBinding {
action: Action::Up,
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "↑",
description: "Up",
},
KeyBinding {
action: Action::Down,
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "↓",
description: "Down",
},
KeyBinding {
action: Action::Left,
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "←",
description: "Left",
},
KeyBinding {
action: Action::Right,
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "→",
description: "Right",
},
KeyBinding {
action: Action::ScrollUp,
code: KeyCode::Up,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ↑",
description: "Scroll Up ",
},
KeyBinding {
action: Action::ScrollDown,
code: KeyCode::Down,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ↓",
description: "Scroll Down",
},
KeyBinding {
action: Action::ScrollLeft,
code: KeyCode::Left,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + ←",
description: "Scroll Left",
},
KeyBinding {
action: Action::ScrollRight,
code: KeyCode::Right,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::CONTROL,
group: Group::CtrlMovement,
symbol: "Ctrl + →",
description: "Scroll Right",
},
KeyBinding {
action: Action::Space,
code: KeyCode::Char(' '),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Space",
description: "Marking cells",
},
KeyBinding {
action: Action::Backspace,
code: KeyCode::Backspace,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Backspace",
description: "Unmark cell",
},
KeyBinding {
action: Action::Delete,
code: KeyCode::Delete,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Delete",
description: "Cancel marking",
},
KeyBinding {
action: Action::Enter,
code: KeyCode::Enter,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Enter",
description: "Submit",
},
KeyBinding {
action: Action::Esc,
code: KeyCode::Esc,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "Esc",
description: "Main menu",
},
KeyBinding {
action: Action::ZoomIn,
code: KeyCode::Char(','),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Zoom,
symbol: ",",
description: "Zoom in",
},
KeyBinding {
action: Action::ZoomOut,
code: KeyCode::Char('.'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Zoom,
symbol: ".",
description: "Zoom out",
},
KeyBinding {
action: Action::Mute,
code: KeyCode::Char('m'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "m",
description: "🔈",
},
KeyBinding {
action: Action::VolumeUp,
code: KeyCode::Char('b'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "b",
description: "🔊",
},
KeyBinding {
action: Action::VolumeDown,
code: KeyCode::Char('n'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Music,
symbol: "n",
description: "🔉",
},
KeyBinding {
action: Action::Tab,
code: KeyCode::Tab,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Opener,
symbol: "Tab",
description: "Toggle side panel",
},
KeyBinding {
action: Action::ShiftTab,
code: KeyCode::Tab,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::SHIFT,
group: Group::Opener,
symbol: "Shift + Tab",
description: "Toggle Map/Skills window",
},
KeyBinding {
action: Action::WildCard('_'),
code: KeyCode::Char('_'),
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Input,
symbol: "[All]",
description: "All keyboard characters",
},
];
/// Returns the `KeyBinding` associated with the given `Action`, if one exists.
pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
KEYBINDINGS.iter().find(|b| b.action == action)
KEYBINDINGS.iter().find(|b| b.action == action)
}
/// Converts a raw `KeyEvent` into an `Action` if a matching binding is found.
@@ -330,23 +330,23 @@ pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
/// was pressed. Returns `None` when the event does not correspond to any known
/// action.
pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
if let Some(b) = KEYBINDINGS
.iter()
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
{
return Some(b.action);
}
if let Some(b) = KEYBINDINGS
.iter()
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
{
return Some(b.action);
}
if KEYBINDINGS
.iter()
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
{
if let KeyCode::Char(c) = event.code {
return Some(Action::WildCard(c));
}
if KEYBINDINGS
.iter()
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
{
if let KeyCode::Char(c) = event.code {
return Some(Action::WildCard(c));
}
}
None
None
}
/// Determines the size of the largest group among a slice of actions.
@@ -355,13 +355,13 @@ pub fn event_to_action(event: &KeyEvent) -> Option<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)
}
+25 -27
View File
@@ -1,38 +1,36 @@
use crate::app::{
App, View,
keybindings::{Action, common_keybindings, event_to_action},
App, View,
keybindings::{Action, common_keybindings, event_to_action},
};
use ratatui::crossterm::event::KeyEvent;
pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
if let Some(action) = event_to_action(&event) {
common_keybindings(app, action);
if let Some(action) = event_to_action(&event) {
common_keybindings(app, action);
let Some(states) = app.states_mut() else {
return;
};
let Some(states) = app.states_mut() else {
return;
};
match action {
Action::Up => {
states.main_menu.selected_view =
states.main_menu.selected_view.saturating_sub(1).max(1);
}
Action::Down => {
states.main_menu.selected_view =
states.main_menu.selected_view.saturating_add(1).min(3);
}
Action::Space => {
let selected_view: usize = states.main_menu.selected_view;
match action {
Action::Up => {
states.main_menu.selected_view = states.main_menu.selected_view.saturating_sub(1).max(1);
}
Action::Down => {
states.main_menu.selected_view = states.main_menu.selected_view.saturating_add(1).min(3);
}
Action::Space => {
let selected_view: usize = states.main_menu.selected_view;
if selected_view == 1 {
app.view = View::Skirmish;
} else if selected_view == 2 {
app.view = View::PerkDecks;
} else if selected_view == 3 {
app.view = View::SkillsConfig;
}
}
_ => (),
if selected_view == 1 {
app.view = View::Skirmish;
} else if selected_view == 2 {
app.view = View::PerkDecks;
} else if selected_view == 3 {
app.view = View::SkillsConfig;
}
}
_ => (),
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ mod skirmish;
pub use default::{common_keybindings, default_keybindings};
pub use keybindings::{
Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action,
Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action,
};
pub use main_menu::main_menu_keybindings;
pub use skirmish::skirmish_keybindings;
+86 -86
View File
@@ -1,98 +1,98 @@
use crate::app::{
App,
keybindings::{Action, common_keybindings, event_to_action},
states::skirmish_states::{BoardState, MoveFocusedCell, ZoomLevel},
App,
keybindings::{Action, common_keybindings, event_to_action},
states::skirmish_states::{BoardState, MoveFocusedCell, ZoomLevel},
};
use ratatui::{crossterm::event::KeyEvent, layout::Rect};
pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) {
if let Some(action) = event_to_action(&key_event) {
common_keybindings(app, action);
if let Some(action) = event_to_action(&key_event) {
common_keybindings(app, action);
let window_area: Rect = app.window_area;
let window_area: Rect = app.window_area;
let Some(states) = app.states_mut() else {
return;
};
let Some(states) = app.states_mut() else {
return;
};
let board: &mut BoardState = &mut states.skirmish.board;
let board: &mut BoardState = &mut states.skirmish.board;
match action {
Action::Up => {
board.change_focused_cell(MoveFocusedCell::Up);
if !board.is_focused_cell_visible() {
board.vertical_offset.prev();
}
}
Action::Down => {
board.change_focused_cell(MoveFocusedCell::Down);
if !board.is_focused_cell_visible() {
board.vertical_offset.next();
}
}
Action::Left => {
board.change_focused_cell(MoveFocusedCell::Left);
if !board.is_focused_cell_visible() {
board.horizontal_offset.prev();
}
}
Action::Right => {
board.change_focused_cell(MoveFocusedCell::Right);
if !board.is_focused_cell_visible() {
board.horizontal_offset.next();
}
}
Action::ScrollUp => {
board.vertical_offset.prev();
board.change_focused_cell(MoveFocusedCell::Up);
}
Action::ScrollDown => {
board.vertical_offset.next();
board.change_focused_cell(MoveFocusedCell::Down);
}
Action::ScrollLeft => {
board.horizontal_offset.prev();
board.change_focused_cell(MoveFocusedCell::Left);
}
Action::ScrollRight => {
board.horizontal_offset.next();
board.change_focused_cell(MoveFocusedCell::Right);
}
Action::ZoomIn => match board.zoom_level {
ZoomLevel::ZoomedIn => {}
ZoomLevel::Default => {
board.change_zoom(ZoomLevel::ZoomedIn);
board.vertical_offset.next();
}
ZoomLevel::ZoomedOut => {
board.change_zoom(ZoomLevel::Default);
board.vertical_offset.next();
board.vertical_offset.next();
}
},
Action::ZoomOut => match board.zoom_level {
ZoomLevel::ZoomedIn => {
board.change_zoom(ZoomLevel::Default);
board.vertical_offset.prev();
}
ZoomLevel::Default => {
board.change_zoom(ZoomLevel::ZoomedOut);
board.vertical_offset.prev();
board.vertical_offset.prev();
}
ZoomLevel::ZoomedOut => {}
},
Action::Space => board.toggle_marking(),
Action::Backspace => board.pop_marked_cell(),
Action::Delete => {
board.marked_cells.marking_cells = false;
board.end_marking_cells(true)
}
Action::Tab => {
states.skirmish.side_panel = !states.skirmish.side_panel;
board.change_resize(&window_area, states.skirmish.side_panel);
}
_ => (),
match action {
Action::Up => {
board.change_focused_cell(MoveFocusedCell::Up);
if !board.is_focused_cell_visible() {
board.vertical_offset.prev();
}
}
Action::Down => {
board.change_focused_cell(MoveFocusedCell::Down);
if !board.is_focused_cell_visible() {
board.vertical_offset.next();
}
}
Action::Left => {
board.change_focused_cell(MoveFocusedCell::Left);
if !board.is_focused_cell_visible() {
board.horizontal_offset.prev();
}
}
Action::Right => {
board.change_focused_cell(MoveFocusedCell::Right);
if !board.is_focused_cell_visible() {
board.horizontal_offset.next();
}
}
Action::ScrollUp => {
board.vertical_offset.prev();
board.change_focused_cell(MoveFocusedCell::Up);
}
Action::ScrollDown => {
board.vertical_offset.next();
board.change_focused_cell(MoveFocusedCell::Down);
}
Action::ScrollLeft => {
board.horizontal_offset.prev();
board.change_focused_cell(MoveFocusedCell::Left);
}
Action::ScrollRight => {
board.horizontal_offset.next();
board.change_focused_cell(MoveFocusedCell::Right);
}
Action::ZoomIn => match board.zoom_level {
ZoomLevel::ZoomedIn => {}
ZoomLevel::Default => {
board.change_zoom(ZoomLevel::ZoomedIn);
board.vertical_offset.next();
}
ZoomLevel::ZoomedOut => {
board.change_zoom(ZoomLevel::Default);
board.vertical_offset.next();
board.vertical_offset.next();
}
},
Action::ZoomOut => match board.zoom_level {
ZoomLevel::ZoomedIn => {
board.change_zoom(ZoomLevel::Default);
board.vertical_offset.prev();
}
ZoomLevel::Default => {
board.change_zoom(ZoomLevel::ZoomedOut);
board.vertical_offset.prev();
board.vertical_offset.prev();
}
ZoomLevel::ZoomedOut => {}
},
Action::Space => board.toggle_marking(),
Action::Backspace => board.pop_marked_cell(),
Action::Delete => {
board.marked_cells.marking_cells = false;
board.end_marking_cells(true)
}
Action::Tab => {
states.skirmish.side_panel = !states.skirmish.side_panel;
board.change_resize(&window_area, states.skirmish.side_panel);
}
_ => (),
}
}
}
+51 -51
View File
@@ -1,62 +1,62 @@
use crate::{
app::states::{
MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState,
skirmish_states::BoardState,
},
cli::Cli,
app::states::{
MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState,
skirmish_states::BoardState,
},
cli::Cli,
};
use ratatui::layout::Rect;
#[derive(Debug, Clone, PartialEq)]
pub struct GameStates {
pub main_menu: MainMenuState,
pub skirmish: SkirmishState,
pub perk_decks: PerkDecksState,
pub skills_config: SkillsConfigState,
pub settings: SettingsState,
pub main_menu: MainMenuState,
pub skirmish: SkirmishState,
pub perk_decks: PerkDecksState,
pub skills_config: SkillsConfigState,
pub settings: SettingsState,
}
impl GameStates {
pub fn new(args: &Cli, area: &Rect) -> Self {
Self {
main_menu: MainMenuState {
id: 0,
name: "Main Menu",
selected_view: 1,
},
skirmish: SkirmishState {
id: 1,
name: "Skirmish",
board: BoardState::new(
area,
args.map_width as usize,
args.map_height as usize,
args.zoom_level,
false,
),
side_panel: false,
turn_counter: 0,
},
perk_decks: PerkDecksState {
id: 2,
name: "Perk Decks",
selected_perk_deck: 0,
},
skills_config: SkillsConfigState {
id: 3,
name: "Skills Config",
selected_skill: 0,
},
settings: SettingsState {
username: args.username.clone(),
game_mode: args.game_mode.clone(),
perk_deck: args.perk_deck.clone(),
starting_wood: args.starting_wood.clone(),
starting_iron: args.starting_iron.clone(),
supply_limit: args.supply_limit.clone(),
xp_modifier: args.xp_modifier.clone(),
skill_points_limit: args.skill_points_limit.clone(),
},
}
pub fn new(args: &Cli, area: &Rect) -> Self {
Self {
main_menu: MainMenuState {
id: 0,
name: "Main Menu",
selected_view: 1,
},
skirmish: SkirmishState {
id: 1,
name: "Skirmish",
board: BoardState::new(
area,
args.map_width as usize,
args.map_height as usize,
args.zoom_level,
false,
),
side_panel: false,
turn_counter: 0,
},
perk_decks: PerkDecksState {
id: 2,
name: "Perk Decks",
selected_perk_deck: 0,
},
skills_config: SkillsConfigState {
id: 3,
name: "Skills Config",
selected_skill: 0,
},
settings: SettingsState {
username: args.username.clone(),
game_mode: args.game_mode.clone(),
perk_deck: args.perk_deck.clone(),
starting_wood: args.starting_wood.clone(),
starting_iron: args.starting_iron.clone(),
supply_limit: args.supply_limit.clone(),
xp_modifier: args.xp_modifier.clone(),
skill_points_limit: args.skill_points_limit.clone(),
},
}
}
}
+3 -3
View File
@@ -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,
}
+12 -12
View File
@@ -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"),
}
}
}
+8 -8
View File
@@ -2,12 +2,12 @@ use crate::app::states::{PerkDecks, skirmish_states::GameMode};
#[derive(Debug, Clone, PartialEq)]
pub struct SettingsState {
pub username: String,
pub game_mode: GameMode,
pub perk_deck: PerkDecks,
pub starting_wood: u16,
pub starting_iron: u16,
pub supply_limit: u8,
pub xp_modifier: f32,
pub skill_points_limit: u16,
pub username: String,
pub game_mode: GameMode,
pub perk_deck: PerkDecks,
pub starting_wood: u16,
pub starting_iron: u16,
pub supply_limit: u8,
pub xp_modifier: f32,
pub skill_points_limit: u16,
}
+3 -3
View File
@@ -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,
}
+10 -10
View File
@@ -2,19 +2,19 @@ use crate::app::states::skirmish_states::BoardState;
#[derive(Debug, Clone, PartialEq)]
pub struct SkirmishState {
pub id: usize,
pub name: &'static str,
pub board: BoardState,
pub side_panel: bool,
pub turn_counter: u64,
pub id: usize,
pub name: &'static str,
pub board: BoardState,
pub side_panel: bool,
pub turn_counter: u64,
}
impl SkirmishState {
pub fn tick_update(&mut self) {
self.board.advance_turn();
pub fn tick_update(&mut self) {
self.board.advance_turn();
// if self.board.is_victory() {}
// if self.board.is_victory() {}
self.turn_counter += 1;
}
self.turn_counter += 1;
}
}
+424 -420
View File
@@ -1,463 +1,467 @@
use crate::app::{
helpers::cells_area_helper,
states::{
FocusedCell, Offset,
skirmish_states::{
CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel,
structures::{BaseBuilding, Ore, Stone, Structure, Structures, Tunnel},
tasks::{DiggingTask, MovingTask, Task, Tasks},
units::{MinerUnit, Unit, Units},
},
helpers::cells_area_helper,
states::{
FocusedCell, Offset,
skirmish_states::{
CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel,
structures::{BaseBuilding, Ore, Stone, Structure, Structures, Tunnel},
tasks::{DiggingTask, MovingTask, Task, Tasks},
units::{MinerUnit, Unit, Units},
},
widgets::CellWidget,
},
widgets::CellWidget,
};
use ratatui::layout::Rect;
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub struct BoardState {
cells_area: Rect,
pub cell_width: usize,
pub cell_height: usize,
pub map_width: usize,
pub map_height: usize,
pub cols: usize,
pub rows: usize,
pub vertical_offset: Offset,
pub horizontal_offset: Offset,
pub cells: Vec<Vec<CellWidget>>,
pub zoom_level: ZoomLevel,
focused_cell: FocusedCell,
pub marked_cells: MarkedCells,
cells_area: Rect,
pub cell_width: usize,
pub cell_height: usize,
pub map_width: usize,
pub map_height: usize,
pub cols: usize,
pub rows: usize,
pub vertical_offset: Offset,
pub horizontal_offset: Offset,
pub cells: Vec<Vec<CellWidget>>,
pub zoom_level: ZoomLevel,
focused_cell: FocusedCell,
pub marked_cells: MarkedCells,
}
impl BoardState {
pub fn new(
area: &Rect,
map_width: usize,
map_height: usize,
zoom_level: ZoomLevel,
side_panel: bool,
) -> Self {
let cells_area: Rect = cells_area_helper(area, side_panel);
pub fn new(
area: &Rect,
map_width: usize,
map_height: usize,
zoom_level: ZoomLevel,
side_panel: bool,
) -> Self {
let cells_area: Rect = cells_area_helper(area, side_panel);
let cell_width: usize = zoom_level.get_cell_size(CellSizes::Width);
let cell_height: usize = zoom_level.get_cell_size(CellSizes::Height);
let cell_width: usize = zoom_level.get_cell_size(CellSizes::Width);
let cell_height: usize = zoom_level.get_cell_size(CellSizes::Height);
let cols: usize = (cells_area.width / cell_width as u16) as usize;
let rows: usize = (cells_area.height / cell_height as u16) as usize;
let cols: usize = (cells_area.width / cell_width as u16) as usize;
let rows: usize = (cells_area.height / cell_height as u16) as usize;
let v_max_offset: usize = Self::max_offset(map_height, rows);
let h_max_offset: usize = Self::max_offset(map_width, cols);
let v_max_offset: usize = Self::max_offset(map_height, rows);
let h_max_offset: usize = Self::max_offset(map_width, cols);
let vertical_offset: Offset = Offset::new(
Some(map_height.saturating_sub(rows).div_ceil(2)),
Some(v_max_offset),
);
let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset));
let vertical_offset: Offset = Offset::new(
Some(map_height.saturating_sub(rows).div_ceil(2)),
Some(v_max_offset),
);
let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset));
let focused_cell: FocusedCell =
FocusedCell::new((map_height) / 2, 3, map_height, map_width);
let focused_cell: FocusedCell = FocusedCell::new((map_height) / 2, 3, map_height, map_width);
let mut cells: Vec<Vec<CellWidget>> = Vec::new();
let mut cells: Vec<Vec<CellWidget>> = Vec::new();
let player_base_coords: (usize, usize) = ((map_height) / 2, 2);
let enemy_base_coords: (usize, usize) = ((map_height) / 2, map_width - 3);
let player_base_coords: (usize, usize) = ((map_height) / 2, 2);
let enemy_base_coords: (usize, usize) = ((map_height) / 2, map_width - 3);
for row in 0..map_height {
let mut rows: Vec<CellWidget> = Vec::new();
for row in 0..map_height {
let mut rows: Vec<CellWidget> = Vec::new();
for col in 0..map_width {
let (focused_row, focused_col) = focused_cell.get_coords();
let selected: bool = row == focused_row && col == focused_col;
let player_base: bool = row == player_base_coords.0 && col == player_base_coords.1;
let player_ore: bool =
row == player_base_coords.0 && col == player_base_coords.1 - 1;
let enemy_base: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1;
let enemy_ore: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1 + 1;
for col in 0..map_width {
let (focused_row, focused_col) = focused_cell.get_coords();
let selected: bool = row == focused_row && col == focused_col;
let player_base: bool = row == player_base_coords.0 && col == player_base_coords.1;
let player_ore: bool = row == player_base_coords.0 && col == player_base_coords.1 - 1;
let enemy_base: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1;
let enemy_ore: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1 + 1;
let structure: Structures = if player_base {
Structures::Base(BaseBuilding::new(Players::Player))
} else if enemy_base {
Structures::Base(BaseBuilding::new(Players::Enemy))
} else if player_ore {
Structures::Ore(Ore::new(Players::Player, 1))
} else if enemy_ore {
Structures::Ore(Ore::new(Players::Enemy, 1))
} else {
Structures::Stone(Stone::new())
};
let unit: Option<Units> = if player_base {
Some(Units::Miner(MinerUnit::new(
Players::Player,
player_base_coords,
)))
} else if enemy_base {
Some(Units::Miner(MinerUnit::new(
Players::Enemy,
enemy_base_coords,
)))
} else {
None
};
rows.push(CellWidget::new(
row, col, zoom_level, selected, structure, unit,
));
}
cells.push(rows);
}
let marked_cells: MarkedCells = MarkedCells::new();
Self {
cells_area,
cell_width,
cell_height,
map_width,
map_height,
cols,
rows,
vertical_offset,
horizontal_offset,
cells,
zoom_level,
focused_cell,
marked_cells,
}
}
pub fn get_ref_cell(&self, row: usize, col: usize) -> &CellWidget {
&self.cells[row][col]
}
fn get_mut_cell(&mut self, row: usize, col: usize) -> &mut CellWidget {
&mut self.cells[row][col]
}
pub fn get_marked_cells_widgets(&self) -> VecDeque<&CellWidget> {
self.marked_cells
.marked_cells
.iter()
.map(|&(row, col)| &self.cells[row][col])
.collect()
}
pub fn get_marked_cells(&self) -> &VecDeque<(usize, usize)> {
&self.marked_cells.marked_cells
}
pub fn toggle_marking(&mut self) {
self.marked_cells.marking_cells = !self.marked_cells.marking_cells;
if self.marked_cells.marking_cells {
self.start_marking_cells();
let structure: Structures = if player_base {
Structures::Base(BaseBuilding::new(Players::Player))
} else if enemy_base {
Structures::Base(BaseBuilding::new(Players::Enemy))
} else if player_ore {
Structures::Ore(Ore::new(Players::Player, 1))
} else if enemy_ore {
Structures::Ore(Ore::new(Players::Enemy, 1))
} else {
self.end_marking_cells(false);
}
Structures::Stone(Stone::new())
};
let unit: Option<Units> = if player_base {
Some(Units::Miner(MinerUnit::new(
Players::Player,
player_base_coords,
)))
} else if enemy_base {
Some(Units::Miner(MinerUnit::new(
Players::Enemy,
enemy_base_coords,
)))
} else {
None
};
rows.push(CellWidget::new(
row, col, zoom_level, selected, structure, unit,
));
}
cells.push(rows);
}
pub fn push_marked_cell(&mut self, new_cell: (usize, usize)) {
if self.marked_cells.marked_cells.back() == Some(&new_cell) {
return;
}
let cell: &mut CellWidget = self.get_mut_cell(new_cell.0, new_cell.1);
let marked_cells: MarkedCells = MarkedCells::new();
if !cell.get_marked() {
cell.set_marked(true);
}
Self {
cells_area,
cell_width,
cell_height,
map_width,
map_height,
cols,
rows,
vertical_offset,
horizontal_offset,
cells,
zoom_level,
focused_cell,
marked_cells,
}
}
self.marked_cells
.marked_cells
.push_back((new_cell.0, new_cell.1));
pub fn get_ref_cell(&self, row: usize, col: usize) -> &CellWidget {
&self.cells[row][col]
}
fn get_mut_cell(&mut self, row: usize, col: usize) -> &mut CellWidget {
&mut self.cells[row][col]
}
pub fn get_marked_cells_widgets(&self) -> VecDeque<&CellWidget> {
self
.marked_cells
.marked_cells
.iter()
.map(|&(row, col)| &self.cells[row][col])
.collect()
}
pub fn get_marked_cells(&self) -> &VecDeque<(usize, usize)> {
&self.marked_cells.marked_cells
}
pub fn toggle_marking(&mut self) {
self.marked_cells.marking_cells = !self.marked_cells.marking_cells;
if self.marked_cells.marking_cells {
self.start_marking_cells();
} else {
self.end_marking_cells(false);
}
}
pub fn push_marked_cell(&mut self, new_cell: (usize, usize)) {
if self.marked_cells.marked_cells.back() == Some(&new_cell) {
return;
}
let cell: &mut CellWidget = self.get_mut_cell(new_cell.0, new_cell.1);
if !cell.get_marked() {
cell.set_marked(true);
}
pub fn pop_marked_cell(&mut self) {
if self.marked_cells.marked_cells.len() < 2 {
return;
}
self
.marked_cells
.marked_cells
.push_back((new_cell.0, new_cell.1));
}
let old: (usize, usize) =
self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 1];
let new: (usize, usize) =
self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 2];
let old_is_unique: bool = self
.marked_cells
.marked_cells
.iter()
.filter(|x| **x == old)
.count()
== 1;
let old_cell: &mut CellWidget = self.get_mut_cell(old.0, old.1).set_selected(false);
if old_is_unique {
old_cell.set_marked(false);
}
self.get_mut_cell(new.0, new.1).set_selected(true);
self.marked_cells.marked_cells.pop_back();
self.focused_cell.set_focused_cell(new);
pub fn pop_marked_cell(&mut self) {
if self.marked_cells.marked_cells.len() < 2 {
return;
}
pub fn start_marking_cells(&mut self) {
let (row, col) = self.focused_cell.get_coords();
let old: (usize, usize) =
self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 1];
let new: (usize, usize) =
self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 2];
let old_is_unique: bool = self
.marked_cells
.marked_cells
.iter()
.filter(|x| **x == old)
.count()
== 1;
let selected_unit: Option<Units> = self.get_ref_cell(row, col).get_option_unit();
if selected_unit.get_owner() != Players::Player || !selected_unit.get_can_dig() {
self.marked_cells.marking_cells = false;
return;
}
// Extend queued work from its final destination, not the miner's current cell.
let start: (usize, usize) = selected_unit
.get_ref_tasks()
.back()
.and_then(|task| task.get_path().back())
.copied()
.unwrap_or((row, col));
self.focused_cell.set_focused_cell(start);
let start: (usize, usize) = self.focused_cell.get_coords();
self.get_mut_cell(row, col).set_selected(false);
self.get_mut_cell(start.0, start.1)
.set_selected(true)
.set_marked(true);
if !self.is_focused_cell_visible() {
self.vertical_offset
.set_value(start.0.saturating_sub(self.rows / 2));
self.horizontal_offset
.set_value(start.1.saturating_sub(self.cols / 2));
}
self.marked_cells.selected_unit = selected_unit;
self.marked_cells.marked_cells.push_back(start);
let old_cell: &mut CellWidget = self.get_mut_cell(old.0, old.1).set_selected(false);
if old_is_unique {
old_cell.set_marked(false);
}
pub fn end_marking_cells(&mut self, del: bool) {
self.marked_cells.marking_cells = false;
for (row, col) in self.marked_cells.marked_cells.clone() {
self.get_mut_cell(row, col).set_marked(false);
self.get_mut_cell(new.0, new.1).set_selected(true);
self.marked_cells.marked_cells.pop_back();
self.focused_cell.set_focused_cell(new);
}
pub fn start_marking_cells(&mut self) {
let (row, col) = self.focused_cell.get_coords();
let selected_unit: Option<Units> = self.get_ref_cell(row, col).get_option_unit();
if selected_unit.get_owner() != Players::Player || !selected_unit.get_can_dig() {
self.marked_cells.marking_cells = false;
return;
}
// Extend queued work from its final destination, not the miner's current cell.
let start: (usize, usize) = selected_unit
.get_ref_tasks()
.back()
.and_then(|task| task.get_path().back())
.copied()
.unwrap_or((row, col));
self.focused_cell.set_focused_cell(start);
let start: (usize, usize) = self.focused_cell.get_coords();
self.get_mut_cell(row, col).set_selected(false);
self
.get_mut_cell(start.0, start.1)
.set_selected(true)
.set_marked(true);
if !self.is_focused_cell_visible() {
self
.vertical_offset
.set_value(start.0.saturating_sub(self.rows / 2));
self
.horizontal_offset
.set_value(start.1.saturating_sub(self.cols / 2));
}
self.marked_cells.selected_unit = selected_unit;
self.marked_cells.marked_cells.push_back(start);
}
pub fn end_marking_cells(&mut self, del: bool) {
self.marked_cells.marking_cells = false;
for (row, col) in self.marked_cells.marked_cells.clone() {
self.get_mut_cell(row, col).set_marked(false);
}
let (row, col) = self.marked_cells.selected_unit.get_coords();
if self.marked_cells.marked_cells.len() > 1 && !del {
let path: Vec<(usize, usize)> = self
.marked_cells
.marked_cells
.iter()
.skip(1)
.cloned()
.collect();
let tick_increase: f32 = self.marked_cells.selected_unit.get_digging_power();
let tasks: VecDeque<Tasks> = path
.chunk_by(|a, b| self.is_walkable(*a) == self.is_walkable(*b))
.map(|segment| {
let path = segment.iter().copied().collect();
if self.is_walkable(segment[0]) {
Tasks::Moving(MovingTask::new(path))
} else {
Tasks::Digging(DiggingTask::new(path, tick_increase))
}
})
.collect();
if let Some(unit) = self.get_mut_cell(row, col).get_mut_option_unit().as_mut() {
unit.get_mut_tasks().extend(tasks);
}
}
self.marked_cells.selected_unit = None;
self.marked_cells.marked_cells.clear();
}
fn max_offset(map_size: usize, size: usize) -> usize {
if map_size > size { map_size - size } else { 0 }
}
fn is_walkable(&self, (row, col): (usize, usize)) -> bool {
self
.cells
.get(row)
.and_then(|cells| cells.get(col))
.is_some_and(|cell| {
matches!(
cell.get_structure(),
Structures::Tunnel(_) | Structures::Base(_)
)
})
}
pub fn get_focused_cell(&self) -> &FocusedCell {
&self.focused_cell
}
pub fn is_focused_cell_visible(&self) -> bool {
let vertical_offset: usize = self.vertical_offset.get_value();
let horizontal_offset: usize = self.horizontal_offset.get_value();
let (row, col) = self.focused_cell.get_coords();
if 0usize.saturating_add(vertical_offset) > row
|| row >= self.rows.saturating_add(vertical_offset)
{
return false;
}
if 0usize.saturating_add(horizontal_offset) > col
|| col >= self.cols.saturating_add(horizontal_offset)
{
return false;
}
true
}
pub fn change_resize(&mut self, area: &Rect, side_panel: bool) {
self.cells_area = cells_area_helper(area, side_panel);
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
self.horizontal_offset =
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
self.vertical_offset = Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
}
pub fn change_zoom(&mut self, new_zoom_level: ZoomLevel) {
self.zoom_level = new_zoom_level;
self.cell_width = new_zoom_level.get_cell_size(CellSizes::Width);
self.cell_height = new_zoom_level.get_cell_size(CellSizes::Height);
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
self.vertical_offset = Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
self.horizontal_offset =
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
let (focused_row, focused_coll) = self.focused_cell.get_coords();
self.focused_cell =
FocusedCell::new(focused_row, focused_coll, self.map_height, self.map_width);
for row in 0..self.map_height {
for col in 0..self.map_width {
self.get_mut_cell(row, col).set_zoom_level(new_zoom_level);
}
}
}
pub fn change_focused_cell(&mut self, direction: MoveFocusedCell) {
let old_cell: (usize, usize) = self.focused_cell.get_coords();
let new_cell: (usize, usize) = self.focused_cell.move_focused_cell(direction);
self
.get_mut_cell(old_cell.0, old_cell.1)
.set_selected(false);
self.get_mut_cell(new_cell.0, new_cell.1).set_selected(true);
if self.marked_cells.marking_cells {
self.push_marked_cell(new_cell);
}
}
pub fn advance_turn(&mut self) {
// Snapshot positions so moving to a later cell cannot grant another turn.
let active: Vec<(usize, usize)> = self
.cells
.iter()
.enumerate()
.flat_map(|(row, cells)| {
cells.iter().enumerate().filter_map(move |(col, cell)| {
cell
.get_ref_option_unit()
.as_ref()
.filter(|unit| !unit.get_ref_tasks().is_empty())
.map(|_| (row, col))
})
})
.collect();
for (row, col) in active {
// Keep the route's origin stable while the player is drawing it.
if self.marked_cells.marking_cells
&& self.marked_cells.selected_unit.get_coords() == (row, col)
{
continue;
}
let mut unit: Units = self
.get_mut_cell(row, col)
.get_mut_option_unit()
.take()
.unwrap();
let mut task: Tasks = unit.get_mut_tasks().pop_front().unwrap();
let mut destination = (row, col);
if let Some((target_row, target_col)) = task.get_path().front().copied() {
let adjacent: bool = row
.abs_diff(target_row)
.saturating_add(col.abs_diff(target_col))
== 1;
if target_row >= self.map_height || target_col >= self.map_width || !adjacent {
// Discard invalid routes rather than teleporting or indexing outside the map.
*self.get_mut_cell(row, col).get_mut_option_unit() = Some(unit);
continue;
}
let (row, col) = self.marked_cells.selected_unit.get_coords();
if self.marked_cells.marked_cells.len() > 1 && !del {
let path: Vec<(usize, usize)> = self
.marked_cells
.marked_cells
.iter()
.skip(1)
.cloned()
.collect();
let tick_increase: f32 = self.marked_cells.selected_unit.get_digging_power();
let tasks: VecDeque<Tasks> = path
.chunk_by(|a, b| self.is_walkable(*a) == self.is_walkable(*b))
.map(|segment| {
let path = segment.iter().copied().collect();
if self.is_walkable(segment[0]) {
Tasks::Moving(MovingTask::new(path))
} else {
Tasks::Digging(DiggingTask::new(path, tick_increase))
}
})
.collect();
if let Some(unit) = self.get_mut_cell(row, col).get_mut_option_unit().as_mut() {
unit.get_mut_tasks().extend(tasks);
let cell: &mut CellWidget = self.get_mut_cell(target_row, target_col);
if cell.get_ref_option_unit().is_none() {
let structure: Structures = cell.get_structure();
// Previously planned stone may already be a tunnel when this task starts.
task = match (task, structure) {
(Tasks::Digging(dig), Structures::Tunnel(_) | Structures::Base(_)) => {
Tasks::Moving(MovingTask::from_digging(dig))
}
}
self.marked_cells.selected_unit = None;
self.marked_cells.marked_cells.clear();
}
fn max_offset(map_size: usize, size: usize) -> usize {
if map_size > size { map_size - size } else { 0 }
}
fn is_walkable(&self, (row, col): (usize, usize)) -> bool {
self.cells
.get(row)
.and_then(|cells| cells.get(col))
.is_some_and(|cell| {
matches!(
cell.get_structure(),
Structures::Tunnel(_) | Structures::Base(_)
)
})
}
pub fn get_focused_cell(&self) -> &FocusedCell {
&self.focused_cell
}
pub fn is_focused_cell_visible(&self) -> bool {
let vertical_offset: usize = self.vertical_offset.get_value();
let horizontal_offset: usize = self.horizontal_offset.get_value();
let (row, col) = self.focused_cell.get_coords();
if 0usize.saturating_add(vertical_offset) > row
|| row >= self.rows.saturating_add(vertical_offset)
{
return false;
}
if 0usize.saturating_add(horizontal_offset) > col
|| col >= self.cols.saturating_add(horizontal_offset)
{
return false;
}
true
}
pub fn change_resize(&mut self, area: &Rect, side_panel: bool) {
self.cells_area = cells_area_helper(area, side_panel);
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
self.horizontal_offset =
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
self.vertical_offset =
Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
}
pub fn change_zoom(&mut self, new_zoom_level: ZoomLevel) {
self.zoom_level = new_zoom_level;
self.cell_width = new_zoom_level.get_cell_size(CellSizes::Width);
self.cell_height = new_zoom_level.get_cell_size(CellSizes::Height);
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
self.vertical_offset =
Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
self.horizontal_offset =
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
let (focused_row, focused_coll) = self.focused_cell.get_coords();
self.focused_cell =
FocusedCell::new(focused_row, focused_coll, self.map_height, self.map_width);
for row in 0..self.map_height {
for col in 0..self.map_width {
self.get_mut_cell(row, col).set_zoom_level(new_zoom_level);
(Tasks::Moving(moving), Structures::Stone(_)) => {
Tasks::Digging(DiggingTask::from_moving(moving, unit.get_digging_power()))
}
}
}
pub fn change_focused_cell(&mut self, direction: MoveFocusedCell) {
let old_cell: (usize, usize) = self.focused_cell.get_coords();
let new_cell: (usize, usize) = self.focused_cell.move_focused_cell(direction);
self.get_mut_cell(old_cell.0, old_cell.1)
.set_selected(false);
self.get_mut_cell(new_cell.0, new_cell.1).set_selected(true);
if self.marked_cells.marking_cells {
self.push_marked_cell(new_cell);
}
}
pub fn advance_turn(&mut self) {
// Snapshot positions so moving to a later cell cannot grant another turn.
let active: Vec<(usize, usize)> = self
.cells
.iter()
.enumerate()
.flat_map(|(row, cells)| {
cells.iter().enumerate().filter_map(move |(col, cell)| {
cell.get_ref_option_unit()
.as_ref()
.filter(|unit| !unit.get_ref_tasks().is_empty())
.map(|_| (row, col))
})
})
.collect();
for (row, col) in active {
// Keep the route's origin stable while the player is drawing it.
if self.marked_cells.marking_cells
&& self.marked_cells.selected_unit.get_coords() == (row, col)
{
continue;
(task, _) => task,
};
match (&mut task, structure) {
(Tasks::Digging(dig), Structures::Stone(mut stone)) => {
let max: f32 = f32::from(stone.get_max_durability());
let before: u16 = (dig.get_progress() * max / 100.0) as u16;
dig.progress();
let after: u16 = (dig.get_progress() * max / 100.0) as u16;
stone.take_damage(after.saturating_sub(before));
if stone.get_durability() == 0 {
cell.set_structure(Structures::Tunnel(Tunnel::new()));
destination = (target_row, target_col);
dig.pop_front();
} else {
cell.set_structure(Structures::Stone(stone));
}
}
let mut unit: Units = self
.get_mut_cell(row, col)
.get_mut_option_unit()
.take()
.unwrap();
let mut task: Tasks = unit.get_mut_tasks().pop_front().unwrap();
let mut destination = (row, col);
if let Some((target_row, target_col)) = task.get_path().front().copied() {
let adjacent: bool = row
.abs_diff(target_row)
.saturating_add(col.abs_diff(target_col))
== 1;
if target_row >= self.map_height || target_col >= self.map_width || !adjacent {
// Discard invalid routes rather than teleporting or indexing outside the map.
*self.get_mut_cell(row, col).get_mut_option_unit() = Some(unit);
continue;
}
let cell: &mut CellWidget = self.get_mut_cell(target_row, target_col);
if cell.get_ref_option_unit().is_none() {
let structure: Structures = cell.get_structure();
// Previously planned stone may already be a tunnel when this task starts.
task = match (task, structure) {
(Tasks::Digging(dig), Structures::Tunnel(_) | Structures::Base(_)) => {
Tasks::Moving(MovingTask::from_digging(dig))
}
(Tasks::Moving(moving), Structures::Stone(_)) => Tasks::Digging(
DiggingTask::from_moving(moving, unit.get_digging_power()),
),
(task, _) => task,
};
match (&mut task, structure) {
(Tasks::Digging(dig), Structures::Stone(mut stone)) => {
let max: f32 = f32::from(stone.get_max_durability());
let before: u16 = (dig.get_progress() * max / 100.0) as u16;
dig.progress();
let after: u16 = (dig.get_progress() * max / 100.0) as u16;
stone.take_damage(after.saturating_sub(before));
if stone.get_durability() == 0 {
cell.set_structure(Structures::Tunnel(Tunnel::new()));
destination = (target_row, target_col);
dig.pop_front();
} else {
cell.set_structure(Structures::Stone(stone));
}
}
(Tasks::Moving(moving), Structures::Tunnel(_) | Structures::Base(_)) => {
moving.progress();
if moving.is_finished() {
destination = (target_row, target_col);
moving.pop_front();
}
}
_ => {}
}
}
if !task.get_path().is_empty() {
unit.get_mut_tasks().push_front(task);
}
(Tasks::Moving(moving), Structures::Tunnel(_) | Structures::Base(_)) => {
moving.progress();
if moving.is_finished() {
destination = (target_row, target_col);
moving.pop_front();
}
}
unit.set_coords(destination);
*self
.get_mut_cell(destination.0, destination.1)
.get_mut_option_unit() = Some(unit);
_ => {}
}
}
if !task.get_path().is_empty() {
unit.get_mut_tasks().push_front(task);
}
}
unit.set_coords(destination);
*self
.get_mut_cell(destination.0, destination.1)
.get_mut_option_unit() = Some(unit);
}
}
}
+41 -41
View File
@@ -1,53 +1,53 @@
pub enum MoveFocusedCell {
Up,
Down,
Left,
Right,
Up,
Down,
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FocusedCell {
row: usize,
col: usize,
max_row: usize,
max_col: usize,
row: usize,
col: usize,
max_row: usize,
max_col: usize,
}
impl FocusedCell {
pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self {
Self {
row,
col,
max_row,
max_col,
}
pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self {
Self {
row,
col,
max_row,
max_col,
}
}
pub fn get_coords(&self) -> (usize, usize) {
(self.row, self.col)
}
pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) {
match direction {
MoveFocusedCell::Up => {
self.row = self.row.saturating_sub(1).max(0);
}
MoveFocusedCell::Down => {
self.row = self.row.saturating_add(1).min(self.max_row - 1);
}
MoveFocusedCell::Left => {
self.col = self.col.saturating_sub(1).max(0);
}
MoveFocusedCell::Right => {
self.col = self.col.saturating_add(1).min(self.max_col - 1);
}
}
pub fn get_coords(&self) -> (usize, usize) {
(self.row, self.col)
}
(self.row, self.col)
}
pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) {
match direction {
MoveFocusedCell::Up => {
self.row = self.row.saturating_sub(1).max(0);
}
MoveFocusedCell::Down => {
self.row = self.row.saturating_add(1).min(self.max_row - 1);
}
MoveFocusedCell::Left => {
self.col = self.col.saturating_sub(1).max(0);
}
MoveFocusedCell::Right => {
self.col = self.col.saturating_add(1).min(self.max_col - 1);
}
}
(self.row, self.col)
}
pub fn set_focused_cell(&mut self, cell: (usize, usize)) {
self.row = cell.0.max(0).min(self.max_row - 1);
self.col = cell.1.max(0).min(self.max_col - 1);
}
pub fn set_focused_cell(&mut self, cell: (usize, usize)) {
self.row = cell.0.max(0).min(self.max_row - 1);
self.col = cell.1.max(0).min(self.max_col - 1);
}
}
+2 -2
View File
@@ -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,
}
}
}
+23 -23
View File
@@ -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)
}
}
+23 -23
View File
@@ -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()),
}
}
}
@@ -3,57 +3,57 @@ use ratatui::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BaseBuilding {
durability: u16,
stress: u8,
owner: Players,
level: u8,
durability: u16,
stress: u8,
owner: Players,
level: u8,
}
impl BaseBuilding {
pub fn new(owner: Players) -> Self {
Self {
durability: 1500,
stress: 0,
owner,
level: 1,
}
pub fn new(owner: Players) -> Self {
Self {
durability: 1500,
stress: 0,
owner,
level: 1,
}
}
}
impl Structure for BaseBuilding {
fn get_tag(&self) -> char {
'B'
}
fn get_tag(&self) -> char {
'B'
}
fn get_color(&self) -> Color {
match self.owner {
Players::Player => Color::LightBlue,
Players::Enemy => Color::LightRed,
Players::Unclaimed => Color::LightGreen,
}
fn get_color(&self) -> Color {
match self.owner {
Players::Player => Color::LightBlue,
Players::Enemy => Color::LightRed,
Players::Unclaimed => Color::LightGreen,
}
}
fn get_level(&self) -> char {
(self.level + b'0') as char
}
fn get_level(&self) -> char {
(self.level + b'0') as char
}
fn get_durability(&self) -> u16 {
self.durability
}
fn get_durability(&self) -> u16 {
self.durability
}
fn get_max_durability(&self) -> u16 {
1500
}
fn get_max_durability(&self) -> u16 {
1500
}
fn get_stress(&self) -> u8 {
self.stress
}
fn get_stress(&self) -> u8 {
self.stress
}
fn get_name(&self) -> &'static str {
"Base"
}
fn get_name(&self) -> &'static str {
"Base"
}
fn get_owner(&self) -> Players {
self.owner
}
fn get_owner(&self) -> Players {
self.owner
}
}
@@ -1,94 +1,94 @@
use crate::app::states::skirmish_states::{Players, structures::Structure};
use ratatui::{
style::{Color, Stylize},
text::Line,
style::{Color, Stylize},
text::Line,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ore {
owner: Players,
ore_amount: u16,
durability: u16,
stress: u8,
level: u8,
owner: Players,
ore_amount: u16,
durability: u16,
stress: u8,
level: u8,
}
impl Ore {
pub fn new(owner: Players, level: u8) -> Self {
Self {
owner,
ore_amount: 5000,
durability: 1000,
stress: 0,
level,
}
pub fn new(owner: Players, level: u8) -> Self {
Self {
owner,
ore_amount: 5000,
durability: 1000,
stress: 0,
level,
}
}
pub fn get_ore_amount(&self) -> u16 {
self.ore_amount
}
pub fn get_ore_amount(&self) -> u16 {
self.ore_amount
}
pub fn get_max_ore_amount(&self) -> u16 {
5000
}
pub fn get_max_ore_amount(&self) -> u16 {
5000
}
pub fn set_owner(&mut self, new_owner: Players) -> &mut Self {
self.owner = new_owner;
self
}
pub fn set_owner(&mut self, new_owner: Players) -> &mut Self {
self.owner = new_owner;
self
}
}
impl Structure for Ore {
fn get_tag(&self) -> char {
'O'
fn get_tag(&self) -> char {
'O'
}
fn get_color(&self) -> Color {
match self.owner {
Players::Player => self.owner.get_color(),
Players::Enemy => self.owner.get_color(),
Players::Unclaimed => Color::Indexed(217),
}
}
fn get_color(&self) -> Color {
match self.owner {
Players::Player => self.owner.get_color(),
Players::Enemy => self.owner.get_color(),
Players::Unclaimed => Color::Indexed(217),
}
fn get_durability(&self) -> u16 {
self.durability
}
fn get_level(&self) -> char {
if self.level != 0 {
(self.level + b'0') as char
} else {
' '
}
}
fn get_durability(&self) -> u16 {
self.durability
}
fn get_max_durability(&self) -> u16 {
1000
}
fn get_level(&self) -> char {
if self.level != 0 {
(self.level + b'0') as char
} else {
' '
}
}
fn get_name(&self) -> &'static str {
"Ore"
}
fn get_max_durability(&self) -> u16 {
1000
}
fn get_owner(&self) -> Players {
self.owner
}
fn get_name(&self) -> &'static str {
"Ore"
}
fn get_stress(&self) -> u8 {
self.stress
}
fn get_owner(&self) -> Players {
self.owner
}
fn text(&self) -> Vec<Line<'_>> {
let mut lines: Vec<Line<'_>> = Structure::base_text(self);
fn get_stress(&self) -> u8 {
self.stress
}
lines.push(Line::from_iter([
"Ore: ".gray(),
self.get_ore_amount().to_string().cyan(),
"/".gray(),
self.get_max_ore_amount().to_string().cyan(),
]));
fn text(&self) -> Vec<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
}
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(),
]));
}
lines
if level != ' ' {
lines.push(Line::from_iter([
"Level: ".gray(),
level.to_string().cyan(),
]));
}
fn text(&self) -> Vec<Line<'_>> {
self.base_text()
}
lines
}
fn text(&self) -> Vec<Line<'_>> {
self.base_text()
}
}
@@ -1,78 +1,78 @@
use crate::app::states::skirmish_states::{
Players,
structures::{BaseBuilding, Ore, Stone, Structure, Tunnel},
Players,
structures::{BaseBuilding, Ore, Stone, Structure, Tunnel},
};
use ratatui::{
style::{Color, Stylize},
text::Line,
style::{Color, Stylize},
text::Line,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Structures {
Base(BaseBuilding),
Tunnel(Tunnel),
Stone(Stone),
Ore(Ore),
Base(BaseBuilding),
Tunnel(Tunnel),
Stone(Stone),
Ore(Ore),
}
impl Structures {
#[inline]
fn s(&self) -> &dyn Structure {
match self {
Self::Base(b) => b,
Self::Tunnel(t) => t,
Self::Stone(s) => s,
Self::Ore(o) => o,
}
#[inline]
fn s(&self) -> &dyn Structure {
match self {
Self::Base(b) => b,
Self::Tunnel(t) => t,
Self::Stone(s) => s,
Self::Ore(o) => o,
}
}
}
impl Structure for Structures {
fn get_color(&self) -> Color {
self.s().get_color()
}
fn get_tag(&self) -> char {
self.s().get_tag()
}
fn get_level(&self) -> char {
self.s().get_level()
}
fn get_name(&self) -> &'static str {
self.s().get_name()
}
fn get_durability(&self) -> u16 {
self.s().get_durability()
}
fn get_max_durability(&self) -> u16 {
self.s().get_max_durability()
}
fn get_stress(&self) -> u8 {
self.s().get_stress()
}
fn get_owner(&self) -> Players {
self.s().get_owner()
}
fn text(&self) -> Vec<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
}
}
+49 -49
View File
@@ -3,71 +3,71 @@ use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub struct DiggingTask {
path: VecDeque<(usize, usize)>,
progress: f32,
tick_increase: f32,
total_steps: usize,
path: VecDeque<(usize, usize)>,
progress: f32,
tick_increase: f32,
total_steps: usize,
}
impl DiggingTask {
pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self {
Self {
total_steps: path.len(),
path,
progress: 0.0,
tick_increase,
}
pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self {
Self {
total_steps: path.len(),
path,
progress: 0.0,
tick_increase,
}
}
pub fn pop_front(&mut self) {
self.path.pop_front();
self.progress = 0.0;
}
pub fn pop_front(&mut self) {
self.path.pop_front();
self.progress = 0.0;
}
pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self {
Self {
path: task.get_path().clone(),
progress: 0.0,
tick_increase,
total_steps: task.get_total_steps(),
}
pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self {
Self {
path: task.get_path().clone(),
progress: 0.0,
tick_increase,
total_steps: task.get_total_steps(),
}
}
pub fn current_target(&self) -> Option<(usize, usize)> {
self.path.front().copied()
}
pub fn current_target(&self) -> Option<(usize, usize)> {
self.path.front().copied()
}
}
impl Task for DiggingTask {
fn get_icon(&self) -> char {
'⛏'
}
fn get_icon(&self) -> char {
'⛏'
}
fn get_path(&self) -> &VecDeque<(usize, usize)> {
&self.path
}
fn get_path(&self) -> &VecDeque<(usize, usize)> {
&self.path
}
fn get_path_front_coords(&self) -> &(usize, usize) {
self.path.front().map_or(&(0, 0), |c| c)
}
fn get_path_front_coords(&self) -> &(usize, usize) {
self.path.front().map_or(&(0, 0), |c| c)
}
fn get_path_back_coords(&self) -> &(usize, usize) {
self.path.back().map_or(&(0, 0), |c| c)
}
fn get_path_back_coords(&self) -> &(usize, usize) {
self.path.back().map_or(&(0, 0), |c| c)
}
fn get_progress(&self) -> f32 {
self.progress
}
fn get_progress(&self) -> f32 {
self.progress
}
fn get_total_steps(&self) -> usize {
self.total_steps
}
fn get_total_steps(&self) -> usize {
self.total_steps
}
fn progress(&mut self) {
self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0);
}
fn progress(&mut self) {
self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0);
}
fn is_finished(&self) -> bool {
self.progress >= 100.0
}
fn is_finished(&self) -> bool {
self.progress >= 100.0
}
}
+48 -48
View File
@@ -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;
}
let completed: usize = total.saturating_sub(self.get_path().len());
((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0)
fn get_total_progress(&self) -> f32 {
let total: usize = self.get_total_steps();
if total == 0 {
return 100.0;
}
let completed: usize = total.saturating_sub(self.get_path().len());
((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0)
}
fn progress(&mut self);
fn progress(&mut self);
fn is_finished(&self) -> bool;
fn is_finished(&self) -> bool;
}
@@ -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();
}
}
+95 -94
View File
@@ -1,124 +1,125 @@
use ratatui::{style::Stylize, text::Line};
use crate::app::states::skirmish_states::{
Players,
tasks::{Task, Tasks},
units::Unit,
Players,
tasks::{Task, Tasks},
units::Unit,
};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub struct MinerUnit {
owner: Players,
hp: u16,
can_dig: bool,
digging_power: f32,
coords: (usize, usize),
tasks: VecDeque<Tasks>,
owner: Players,
hp: u16,
can_dig: bool,
digging_power: f32,
coords: (usize, usize),
tasks: VecDeque<Tasks>,
}
impl MinerUnit {
pub fn new(owner: Players, coords: (usize, usize)) -> Self {
Self {
owner,
hp: 100,
can_dig: true,
digging_power: 0.55,
coords,
tasks: VecDeque::new(),
}
pub fn new(owner: Players, coords: (usize, usize)) -> Self {
Self {
owner,
hp: 100,
can_dig: true,
digging_power: 0.55,
coords,
tasks: VecDeque::new(),
}
}
fn col_to_letters(&self, col: usize) -> String {
let mut col: usize = col + 1;
let mut letters: Vec<char> = Vec::new();
while col > 0 {
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
col = (col - 1) / 26;
}
fn col_to_letters(&self, col: usize) -> String {
let mut col: usize = col + 1;
let mut letters: Vec<char> = Vec::new();
letters.iter().rev().collect()
}
while col > 0 {
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
col = (col - 1) / 26;
}
letters.iter().rev().collect()
}
fn display_coords(&self, coords: &(usize, usize)) -> String {
format!("{}{}", self.col_to_letters(coords.0), coords.1)
}
fn display_coords(&self, coords: &(usize, usize)) -> String {
format!("{}{}", self.col_to_letters(coords.0), coords.1)
}
}
impl Unit for MinerUnit {
fn get_owner(&self) -> Players {
self.owner
}
fn get_owner(&self) -> Players {
self.owner
}
fn get_tag(&self) -> char {
'M'
}
fn get_tag(&self) -> char {
'M'
}
fn get_name(&self) -> &'static str {
"Miner"
}
fn get_name(&self) -> &'static str {
"Miner"
}
fn get_hp(&self) -> u16 {
self.hp
}
fn get_hp(&self) -> u16 {
self.hp
}
fn get_max_hp(&self) -> u16 {
100
}
fn get_max_hp(&self) -> u16 {
100
}
fn get_can_dig(&self) -> bool {
self.can_dig
}
fn get_can_dig(&self) -> bool {
self.can_dig
}
fn get_digging_power(&self) -> f32 {
self.digging_power
}
fn get_digging_power(&self) -> f32 {
self.digging_power
}
fn get_coords(&self) -> (usize, usize) {
self.coords
}
fn get_coords(&self) -> (usize, usize) {
self.coords
}
fn set_coords(&mut self, coords: (usize, usize)) {
self.coords = coords;
}
fn set_coords(&mut self, coords: (usize, usize)) {
self.coords = coords;
}
fn get_ref_tasks(&self) -> &VecDeque<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,82 +1,82 @@
use crate::app::states::skirmish_states::{Players, tasks::Tasks};
use ratatui::{
style::Stylize,
text::{Line, Span},
style::Stylize,
text::{Line, Span},
};
use std::collections::VecDeque;
pub trait Unit {
fn is_unit(&self) -> bool {
true
fn is_unit(&self) -> bool {
true
}
fn get_owner(&self) -> Players;
fn get_tag(&self) -> char;
fn get_name(&self) -> &'static str;
fn get_hp(&self) -> u16;
fn get_max_hp(&self) -> u16;
fn get_can_dig(&self) -> bool;
fn get_digging_power(&self) -> f32;
fn get_coords(&self) -> (usize, usize);
fn set_coords(&mut self, coords: (usize, usize));
fn get_ref_tasks(&self) -> &VecDeque<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 get_owner(&self) -> Players;
fn get_tag(&self) -> char;
fn get_name(&self) -> &'static str;
fn get_hp(&self) -> u16;
fn get_max_hp(&self) -> u16;
fn get_can_dig(&self) -> bool;
fn get_digging_power(&self) -> f32;
fn get_coords(&self) -> (usize, usize);
fn set_coords(&mut self, coords: (usize, usize));
fn get_ref_tasks(&self) -> &VecDeque<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 !have_any_tasks {
lines.push(Line::from("Tasks Queue: ".gray()));
for task in tasks {
lines.push(task);
}
}
lines
if !have_any_tasks {
lines.push(Line::from("Tasks Queue: ".gray()));
for task in tasks {
lines.push(task);
}
}
fn text(&self) -> Vec<Line<'_>> {
self.base_text()
}
lines
}
fn text(&self) -> Vec<Line<'_>> {
self.base_text()
}
}
+104 -101
View File
@@ -1,147 +1,150 @@
use crate::app::states::skirmish_states::{
Players,
tasks::Tasks,
units::{MinerUnit, Unit},
Players,
tasks::Tasks,
units::{MinerUnit, Unit},
};
use ratatui::text::Line;
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub enum Units {
Miner(MinerUnit),
Miner(MinerUnit),
}
impl Units {
#[inline]
pub fn u(&self) -> &dyn Unit {
match self {
Self::Miner(m) => m,
}
#[inline]
pub fn u(&self) -> &dyn Unit {
match self {
Self::Miner(m) => m,
}
}
#[inline]
pub fn u_mut(&mut self) -> &mut dyn Unit {
match self {
Self::Miner(m) => m,
}
#[inline]
pub fn u_mut(&mut self) -> &mut dyn Unit {
match self {
Self::Miner(m) => m,
}
}
}
impl Unit for Units {
fn get_tag(&self) -> char {
self.u().get_tag()
}
fn get_tag(&self) -> char {
self.u().get_tag()
}
fn get_name(&self) -> &'static str {
self.u().get_name()
}
fn get_name(&self) -> &'static str {
self.u().get_name()
}
fn get_owner(&self) -> Players {
self.u().get_owner()
}
fn get_owner(&self) -> Players {
self.u().get_owner()
}
fn get_hp(&self) -> u16 {
self.u().get_hp()
}
fn get_hp(&self) -> u16 {
self.u().get_hp()
}
fn get_max_hp(&self) -> u16 {
self.u().get_max_hp()
}
fn get_max_hp(&self) -> u16 {
self.u().get_max_hp()
}
fn get_can_dig(&self) -> bool {
self.u().get_can_dig()
}
fn get_can_dig(&self) -> bool {
self.u().get_can_dig()
}
fn get_digging_power(&self) -> f32 {
self.u().get_digging_power()
}
fn get_digging_power(&self) -> f32 {
self.u().get_digging_power()
}
fn get_coords(&self) -> (usize, usize) {
self.u().get_coords()
}
fn get_coords(&self) -> (usize, usize) {
self.u().get_coords()
}
fn set_coords(&mut self, coords: (usize, usize)) {
self.u_mut().set_coords(coords);
}
fn set_coords(&mut self, coords: (usize, usize)) {
self.u_mut().set_coords(coords);
}
fn get_ref_tasks(&self) -> &VecDeque<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 is_unit(&self) -> bool {
self.as_ref().map_or(false, |u| u.is_unit())
}
fn is_unit(&self) -> bool {
self.as_ref().map_or(false, |u| u.is_unit())
}
fn get_tag(&self) -> char {
self.as_ref().map_or(' ', |u| u.get_tag())
}
fn get_tag(&self) -> char {
self.as_ref().map_or(' ', |u| u.get_tag())
}
fn get_name(&self) -> &'static str {
self.as_ref().map_or("", |u| u.get_name())
}
fn get_name(&self) -> &'static str {
self.as_ref().map_or("", |u| u.get_name())
}
fn get_owner(&self) -> Players {
self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner())
}
fn get_owner(&self) -> Players {
self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner())
}
fn get_hp(&self) -> u16 {
self.as_ref().map_or(0, |u| u.get_hp())
}
fn get_hp(&self) -> u16 {
self.as_ref().map_or(0, |u| u.get_hp())
}
fn get_max_hp(&self) -> u16 {
self.as_ref().map_or(0, |u| u.get_max_hp())
}
fn get_max_hp(&self) -> u16 {
self.as_ref().map_or(0, |u| u.get_max_hp())
}
fn get_can_dig(&self) -> bool {
self.as_ref().map_or(false, |u| u.get_can_dig())
}
fn get_can_dig(&self) -> bool {
self.as_ref().map_or(false, |u| u.get_can_dig())
}
fn get_digging_power(&self) -> f32 {
self.as_ref().map_or(0.0, |u| u.get_digging_power())
}
fn get_digging_power(&self) -> f32 {
self.as_ref().map_or(0.0, |u| u.get_digging_power())
}
fn get_coords(&self) -> (usize, usize) {
self.as_ref().map_or((0, 0), |u| u.get_coords())
}
fn get_coords(&self) -> (usize, usize) {
self.as_ref().map_or((0, 0), |u| u.get_coords())
}
fn set_coords(&mut self, coords: (usize, usize)) {
if let Some(unit) = self.as_mut() {
unit.set_coords(coords);
}
fn set_coords(&mut self, coords: (usize, usize)) {
if let Some(unit) = self.as_mut() {
unit.set_coords(coords);
}
}
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
self.as_ref()
.expect("attempted to get referenced tasks from `None`")
.get_ref_tasks()
}
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_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 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))
}
fn set_task(&mut self, task: Tasks) {
self.as_mut().map_or((), |u| u.set_task(task))
}
}
+43 -43
View File
@@ -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))],
}
}
}
+125 -125
View File
@@ -1,175 +1,175 @@
use clap::ValueEnum;
use once_cell::sync::Lazy;
use rodio::{
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize,
decoder::LoopedDecoder, source::Amplify,
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize,
decoder::LoopedDecoder, source::Amplify,
};
use std::{
collections::HashMap,
fs::read,
io::{BufReader, Cursor},
process::exit,
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
collections::HashMap,
fs::read,
io::{BufReader, Cursor},
process::exit,
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
};
#[derive(Debug)]
pub enum AudioCmd {
Mute,
VolumeUp,
VolumeDown,
ChangePart(SoundrackParts),
ChangeSoundtrack(Soundtrack),
Mute,
VolumeUp,
VolumeDown,
ChangePart(SoundrackParts),
ChangeSoundtrack(Soundtrack),
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, ValueEnum)]
pub enum Soundtrack {
Default,
Default,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
pub enum SoundrackParts {
Calm,
Buildup,
Assault,
Outro,
Calm,
Buildup,
Assault,
Outro,
}
type SoundtrackCache = HashMap<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 base: String = format!("soundtracks/{}", folder);
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
let base: String = format!("soundtracks/{}", folder);
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
macro_rules! load {
($part:ident, $file:expr) => {
let path = format!("{}/{}", base, $file);
cache.insert(
SoundrackParts::$part,
read(&path).expect(&format!("cannot read {}", path)),
);
};
}
macro_rules! load {
($part:ident, $file:expr) => {
let path = format!("{}/{}", base, $file);
cache.insert(
SoundrackParts::$part,
read(&path).expect(&format!("cannot read {}", path)),
);
};
}
load!(Calm, "calm.ogg"); // calm.ogg
load!(Buildup, "test.ogg"); // buildup.ogg
load!(Assault, "test.ogg"); // assault.ogg
load!(Outro, "test.ogg"); // outro.ogg
load!(Calm, "calm.ogg"); // calm.ogg
load!(Buildup, "test.ogg"); // buildup.ogg
load!(Assault, "test.ogg"); // assault.ogg
load!(Outro, "test.ogg"); // outro.ogg
cache
cache
}
pub fn load_audio(
soundtrack: &Soundtrack,
part: &SoundrackParts,
soundtrack: &Soundtrack,
part: &SoundrackParts,
) -> Amplify<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);
}
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);
}
{
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 folder_name: &str = match soundtrack {
Soundtrack::Default => "default",
};
if let Some(soundtrack_cache) = guard.get(soundtrack) {
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
let new_cache: Arc<HashMap<SoundrackParts, Vec<u8>>> =
Arc::new(load_soundtrack_folder(folder_name));
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
.expect("decoder issue")
.amplify(0.20);
}
let data: &Vec<u8> = new_cache.get(&part).expect("part missing after load");
let folder_name: &str = match soundtrack {
Soundtrack::Default => "default",
};
guard.insert(*soundtrack, Arc::clone(&new_cache));
let new_cache: Arc<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)
}
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(&current_soundtrack, &current_part));
let mut current_soundtrack: Soundtrack = soundtrack;
let mut current_part: SoundrackParts = SoundrackParts::Calm;
player.append(load_audio(&current_soundtrack, &current_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(&current_soundtrack, &current_part));
player.skip_one();
}
AudioCmd::ChangeSoundtrack(st) => {
current_soundtrack = st;
player.append(load_audio(&current_soundtrack, &current_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(&current_soundtrack, &current_part));
player.skip_one();
}
AudioCmd::ChangeSoundtrack(st) => {
current_soundtrack = st;
player.append(load_audio(&current_soundtrack, &current_part));
player.skip_one();
}
}
}
}
+14 -14
View File
@@ -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;
}
}
Ok(Event::Resize(cols, rows)) => {
if resize_tx.send((cols, rows)).is_err() {
break;
}
}
Ok(_) => {}
Err(_) => continue,
loop {
match read() {
Ok(Event::Key(k)) => {
if input_tx.send(k).is_err() {
break;
}
}
Ok(Event::Resize(cols, rows)) => {
if resize_tx.send((cols, rows)).is_err() {
break;
}
}
Ok(_) => {}
Err(_) => continue,
}
}
}
+13 -13
View File
@@ -1,21 +1,21 @@
use std::{
sync::mpsc::Sender,
thread,
time::{Duration, Instant},
sync::mpsc::Sender,
thread,
time::{Duration, Instant},
};
pub const TICK_MS: u8 = 33;
pub fn handle_tick_event(tx: Sender<()>) {
let interval: Duration = Duration::from_millis(TICK_MS as u64);
loop {
if tx.send(()).is_err() {
break;
}
let elapsed: Duration = Instant::now().elapsed();
if interval > elapsed {
thread::sleep(interval - elapsed);
}
let interval: Duration = Duration::from_millis(TICK_MS as u64);
loop {
if tx.send(()).is_err() {
break;
}
let elapsed: Duration = Instant::now().elapsed();
if interval > elapsed {
thread::sleep(interval - elapsed);
}
}
}
+15 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
}
+130 -130
View File
@@ -1,153 +1,153 @@
use crate::app::{
App,
helpers::block_title_helper,
keybindings::{Action, count_largest_group},
states::skirmish_states::BoardState,
widgets::{BoardWidget, KeybindingsWidget, SidePanelWidget},
App,
helpers::block_title_helper,
keybindings::{Action, count_largest_group},
states::skirmish_states::BoardState,
widgets::{BoardWidget, KeybindingsWidget, SidePanelWidget},
};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::{Color, Stylize},
text::Line,
widgets::{Block, Borders, Padding, Paragraph, Widget},
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::{Color, Stylize},
text::Line,
widgets::{Block, Borders, Padding, Paragraph, Widget},
};
const ACTIONS: &[Action] = &[
Action::Up,
Action::Down,
Action::Left,
Action::Right,
Action::ScrollUp,
Action::ScrollDown,
Action::ScrollLeft,
Action::ScrollRight,
Action::ZoomIn,
Action::ZoomOut,
Action::VolumeUp,
Action::VolumeDown,
Action::Space,
Action::Backspace,
Action::Delete,
Action::Mute,
Action::Tab,
Action::Quit,
Action::Quit2,
Action::Esc,
Action::Up,
Action::Down,
Action::Left,
Action::Right,
Action::ScrollUp,
Action::ScrollDown,
Action::ScrollLeft,
Action::ScrollRight,
Action::ZoomIn,
Action::ZoomOut,
Action::VolumeUp,
Action::VolumeDown,
Action::Space,
Action::Backspace,
Action::Delete,
Action::Mute,
Action::Tab,
Action::Quit,
Action::Quit2,
Action::Esc,
];
pub fn skirmish_layout(area: Rect) -> [Rect; 3] {
Layout::vertical([
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Length(count_largest_group(&ACTIONS) + 2),
])
.areas(area)
Layout::vertical([
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Length(count_largest_group(&ACTIONS) + 2),
])
.areas(area)
}
pub fn skirmish_main_area_layout(area: Rect, side_panel: bool) -> [Rect; 2] {
let constraint: [Constraint; 2] = if side_panel {
[Constraint::Fill(1), Constraint::Length(39)]
} else {
[Constraint::Percentage(100), Constraint::Percentage(0)]
};
let constraint: [Constraint; 2] = if side_panel {
[Constraint::Fill(1), Constraint::Length(39)]
} else {
[Constraint::Percentage(100), Constraint::Percentage(0)]
};
Layout::horizontal(constraint).areas(area)
Layout::horizontal(constraint).areas(area)
}
pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
let Some(states) = app.states_ref() else {
return;
};
let Some(states) = app.states_ref() else {
return;
};
let board: &BoardState = &states.skirmish.board;
let board: &BoardState = &states.skirmish.board;
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
{
let lines: Vec<Line<'_>> = Vec::from_iter([
Line::raw("War in Tunnels").yellow(),
Line::from_iter([
format!("Wood: {} (+{}) | ", states.settings.starting_wood, 5),
format!("Iron: {} (+{}) | ", states.settings.starting_iron, 1),
format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit),
format!(
"Skills points: {} ({}/{}) | ",
1, 20, states.settings.skill_points_limit
),
format!("Perk Deck: {}/9 | ", 5),
format!("Tick: {}", states.skirmish.turn_counter),
]),
]);
{
let lines: Vec<Line<'_>> = Vec::from_iter([
Line::raw("War in Tunnels").yellow(),
Line::from_iter([
format!("Wood: {} (+{}) | ", states.settings.starting_wood, 5),
format!("Iron: {} (+{}) | ", states.settings.starting_iron, 1),
format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit),
format!(
"Skills points: {} ({}/{}) | ",
1, 20, states.settings.skill_points_limit
),
format!("Perk Deck: {}/9 | ", 5),
format!("Tick: {}", states.skirmish.turn_counter),
]),
]);
Paragraph::new(lines)
.alignment(Alignment::Left)
.block(
Block::new()
.gray()
.padding(Padding {
left: 1,
right: 1,
top: 0,
bottom: 1,
})
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
)
.render(title_area, buf);
Paragraph::new(lines)
.alignment(Alignment::Left)
.block(
Block::new()
.gray()
.padding(Padding {
left: 1,
right: 1,
top: 0,
bottom: 1,
})
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
)
.render(title_area, buf);
}
{
let main_block: Block = Block::new()
.gray()
.title(block_title_helper(
&[
("Skirmish".to_string(), Color::Magenta),
("Map".to_string(), Color::Green),
],
Some(" - "),
))
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT);
let main_inner_area: Rect = main_block.inner(main_area);
main_block.render(main_area, buf);
let [board_area, side_panel_area] =
skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel);
let cells_area: Rect = board_area.inner(Margin {
horizontal: 1,
vertical: 1,
});
// .centered(
// Constraint::Length(
// (board.cell_width * board.cols) as u16,
// ),
// Constraint::Length(
// (board.cell_height * board.rows) as u16,
// ),
// );
BoardWidget::new(&board).render(cells_area, buf);
if states.skirmish.side_panel {
let (row, col) = board.get_focused_cell().get_coords();
let mut is_unit_marked: bool = false;
if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) {
is_unit_marked = first_row == &row && first_col == &col;
}
SidePanelWidget::new(
(row, col),
&board.get_ref_cell(row, col).get_structure(),
&board.get_ref_cell(row, col).get_ref_option_unit(),
is_unit_marked,
)
.render(side_panel_area, buf);
}
}
{
let main_block: Block = Block::new()
.gray()
.title(block_title_helper(
&[
("Skirmish".to_string(), Color::Magenta),
("Map".to_string(), Color::Green),
],
Some(" - "),
))
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT);
let main_inner_area: Rect = main_block.inner(main_area);
main_block.render(main_area, buf);
let [board_area, side_panel_area] =
skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel);
let cells_area: Rect = board_area.inner(Margin {
horizontal: 1,
vertical: 1,
});
// .centered(
// Constraint::Length(
// (board.cell_width * board.cols) as u16,
// ),
// Constraint::Length(
// (board.cell_height * board.rows) as u16,
// ),
// );
BoardWidget::new(&board).render(cells_area, buf);
if states.skirmish.side_panel {
let (row, col) = board.get_focused_cell().get_coords();
let mut is_unit_marked: bool = false;
if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) {
is_unit_marked = first_row == &row && first_col == &col;
}
SidePanelWidget::new(
(row, col),
&board.get_ref_cell(row, col).get_structure(),
&board.get_ref_cell(row, col).get_ref_option_unit(),
is_unit_marked,
)
.render(side_panel_area, buf);
}
}
{
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
}
{
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
}
}
+30 -29
View File
@@ -1,45 +1,46 @@
use crate::app::states::skirmish_states::BoardState;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
widgets::Widget,
buffer::Buffer,
layout::{Constraint, Layout, Rect},
widgets::Widget,
};
use std::rc::Rc;
pub struct BoardWidget<'a> {
state: &'a BoardState,
state: &'a BoardState,
}
impl<'a> BoardWidget<'a> {
pub fn new(state: &'a BoardState) -> Self {
Self { state }
}
pub fn new(state: &'a BoardState) -> Self {
Self { state }
}
}
impl Widget for BoardWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let horizontal: Rc<[Rect]> =
Layout::horizontal(vec![
Constraint::Length(self.state.cell_width as u16);
self.state.cols
])
.split(area);
fn render(self, area: Rect, buf: &mut Buffer) {
let horizontal: Rc<[Rect]> =
Layout::horizontal(vec![
Constraint::Length(self.state.cell_width as u16);
self.state.cols
])
.split(area);
for (col_idx, col_area) in horizontal.iter().enumerate() {
let vertical: Rc<[Rect]> =
Layout::vertical(vec![
Constraint::Length(self.state.cell_height as u16);
self.state.rows
])
.split(*col_area);
for (col_idx, col_area) in horizontal.iter().enumerate() {
let vertical: Rc<[Rect]> =
Layout::vertical(vec![
Constraint::Length(self.state.cell_height as u16);
self.state.rows
])
.split(*col_area);
for (row_idx, cell_area) in vertical.iter().enumerate() {
self.state
.cells
.get(row_idx + self.state.vertical_offset.get_value())
.and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value()))
.map(|cell| cell.clone().render(*cell_area, buf));
}
}
for (row_idx, cell_area) in vertical.iter().enumerate() {
self
.state
.cells
.get(row_idx + self.state.vertical_offset.get_value())
.and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value()))
.map(|cell| cell.clone().render(*cell_area, buf));
}
}
}
}
+139 -138
View File
@@ -1,161 +1,162 @@
use crate::app::states::skirmish_states::{
ZoomLevel,
structures::{Stone, Structure, Structures},
tasks::Tasks,
units::{Unit, Units},
ZoomLevel,
structures::{Stone, Structure, Structures},
tasks::Tasks,
units::{Unit, Units},
};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Style, Stylize},
text::{Line, Span, ToSpan},
widgets::{Block, BorderType, Borders, Paragraph, Widget},
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Style, Stylize},
text::{Line, Span, ToSpan},
widgets::{Block, BorderType, Borders, Paragraph, Widget},
};
#[derive(Debug, Clone, PartialEq)]
pub struct CellWidget {
row: usize,
col: usize,
selected: bool,
zoom_level: ZoomLevel,
structure: Structures,
marked: bool,
unit: Option<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 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 set_selected(&mut self, selected: bool) -> &mut Self {
self.selected = selected;
self
letters.iter().rev().collect()
}
fn display_coords(&self) -> Span<'_> {
if self.selected || self.structure != Structures::Stone(Stone::new()) || self.marked {
format!("{}{}", self.col_to_letters(), self.row).green()
} else {
"".to_span()
}
}
pub fn set_zoom_level(&mut self, zoom_level: ZoomLevel) -> &mut Self {
self.zoom_level = zoom_level;
self
fn fg_color(&self) -> Color {
if self.marked && self.selected {
Color::Magenta
} else if self.marked && !self.selected {
Color::LightMagenta
} else if !self.marked && self.selected {
Color::LightYellow
} else {
self.structure.get_color()
}
}
pub fn get_structure(&self) -> Structures {
self.structure
fn get_text_area(&self) -> Vec<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 Some(unit) = &self.unit
&& let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front()
&& let Some(percent) = task.get_movement_progress()
{
let middle = lines.len() / 2;
lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color());
}
lines
}
pub fn get_ref_option_unit(&self) -> &Option<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 Some(unit) = &self.unit
&& let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front()
&& let Some(percent) = task.get_movement_progress()
{
let middle = lines.len() / 2;
lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color());
}
lines
}
fn get_block(&self) -> Block<'_> {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(self.fg_color()))
.title(self.display_coords())
.border_type(BorderType::Rounded)
}
fn get_block(&self) -> Block<'_> {
Block::default()
.borders(Borders::ALL)
.style(Style::default().fg(self.fg_color()))
.title(self.display_coords())
.border_type(BorderType::Rounded)
}
}
impl Widget for CellWidget {
fn render(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.get_text_area())
.alignment(Alignment::Center)
.block(self.get_block())
.render(area, buf);
}
fn render(self, area: Rect, buf: &mut Buffer) {
Paragraph::new(self.get_text_area())
.alignment(Alignment::Center)
.block(self.get_block())
.render(area, buf);
}
}
+103 -108
View File
@@ -1,14 +1,14 @@
use crate::app::{
helpers::block_single_title_helper,
keybindings::{Action, Group, KeyBinding, binding_for},
helpers::block_single_title_helper,
keybindings::{Action, Group, KeyBinding, binding_for},
};
use clap::ValueEnum;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Stylize},
text::Line,
widgets::{Block, Borders, Padding, Paragraph, Widget},
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Color, Stylize},
text::Line,
widgets::{Block, Borders, Padding, Paragraph, Widget},
};
use std::collections::HashSet;
@@ -18,123 +18,118 @@ use std::collections::HashSet;
/// logical group of keybindings (e.g. navigation, editing, etc.). The paragraphs
/// are rendered side‑by‑side inside a bordered container.
pub struct KeybindingsWidget {
/// Paragraphs, one per used `Group`, containing the formatted keybinding lines.
grouped: Vec<Paragraph<'static>>,
/// Paragraphs, one per used `Group`, containing the formatted keybinding lines.
grouped: Vec<Paragraph<'static>>,
}
impl KeybindingsWidget {
/// Create a new widget from a list of actions.
///
/// For each action, the corresponding static [`KeyBinding`] is looked up
/// using [`binding_for`]. Only groups that have at least one binding are
/// displayed. The resulting widget contains a paragraph for each group,
/// formatted with the key symbol (red & bold) followed by its description.
///
/// # Parameters
///
/// * `actions` – A slice of actions for which keybindings should be shown.
/// * `area_height` – The height of the area in which the widget will be rendered.
/// This value is used to calculate vertical padding so that the keybinding rows
/// are vertically centered when the widget's allocated height is larger than the
/// number of lines needed to display the bindings.
///
/// # Returns
///
/// A fully‑initialised `KeybindingsWidget` ready for rendering.
pub fn new(actions: &[Action], area_height: u16) -> Self {
let keybindings: Vec<Option<&'static KeyBinding>> =
actions.iter().map(|a| binding_for(*a)).collect();
/// Create a new widget from a list of actions.
///
/// For each action, the corresponding static [`KeyBinding`] is looked up
/// using [`binding_for`]. Only groups that have at least one binding are
/// displayed. The resulting widget contains a paragraph for each group,
/// formatted with the key symbol (red & bold) followed by its description.
///
/// # Parameters
///
/// * `actions` – A slice of actions for which keybindings should be shown.
/// * `area_height` – The height of the area in which the widget will be rendered.
/// This value is used to calculate vertical padding so that the keybinding rows
/// are vertically centered when the widget's allocated height is larger than the
/// number of lines needed to display the bindings.
///
/// # Returns
///
/// A fully‑initialised `KeybindingsWidget` ready for rendering.
pub fn new(actions: &[Action], area_height: u16) -> Self {
let keybindings: Vec<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 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 grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
for group in Group::value_variants().iter() {
if !used_groups.contains(group) {
continue;
}
for group in Group::value_variants().iter() {
if !used_groups.contains(group) {
continue;
}
let grouped_lines: Vec<Line<'_>> = keybindings
.iter()
.filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group))
.filter_map(|b| {
b.as_ref().map(|kb| {
Line::from_iter([
kb.symbol.red().bold(),
" - ".into(),
kb.description.into(),
])
})
})
.collect();
let grouped_lines: Vec<Line<'_>> = keybindings
.iter()
.filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group))
.filter_map(|b| {
b.as_ref().map(|kb| {
Line::from_iter([kb.symbol.red().bold(), " - ".into(), kb.description.into()])
})
})
.collect();
let height_diff: u16 =
area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16);
let height_diff: u16 =
area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16);
let padding_top: u16 = if height_diff <= 1 {
height_diff
} else {
height_diff / 2
};
let padding_top: u16 = if height_diff <= 1 {
height_diff
} else {
height_diff / 2
};
let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0));
let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0));
if !grouped_keybindings.is_empty() {
block = block.borders(Borders::LEFT);
}
if !grouped_keybindings.is_empty() {
block = block.borders(Borders::LEFT);
}
grouped_keybindings.push(
Paragraph::new(grouped_lines)
.alignment(Alignment::Center)
.block(block),
);
}
Self {
grouped: grouped_keybindings,
}
grouped_keybindings.push(
Paragraph::new(grouped_lines)
.alignment(Alignment::Center)
.block(block),
);
}
Self {
grouped: grouped_keybindings,
}
}
}
impl Widget for KeybindingsWidget {
/// Render the widget inside the given rectangular area.
///
/// The widget first draws a surrounding border with the title *Keybindings*.
/// It then divides the inner area into equal horizontal slices—one per
/// used group—and renders each group's paragraph into its slice.
///
/// If no groups are present, the function returns early without drawing
/// anything.
fn render(self, area: Rect, buf: &mut Buffer) {
let count: u16 = self.grouped.len() as u16;
/// Render the widget inside the given rectangular area.
///
/// The widget first draws a surrounding border with the title *Keybindings*.
/// It then divides the inner area into equal horizontal slices—one per
/// used group—and renders each group's paragraph into its slice.
///
/// If no groups are present, the function returns early without drawing
/// anything.
fn render(self, area: Rect, buf: &mut Buffer) {
let count: u16 = self.grouped.len() as u16;
if count == 0 {
return;
}
let block: Block<'_> =
Block::default()
.borders(Borders::ALL)
.title(block_single_title_helper(
"Keybindings".to_string(),
Color::Magenta,
));
let inner: Rect = block.inner(area);
block.render(area, buf);
let base: u16 = if count == 0 { 0 } else { 100u16 / count };
let constraints: Vec<Constraint> = vec![Constraint::Percentage(base); count as usize];
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);
}
if count == 0 {
return;
}
let block: Block<'_> = Block::default()
.borders(Borders::ALL)
.title(block_single_title_helper(
"Keybindings".to_string(),
Color::Magenta,
));
let inner: Rect = block.inner(area);
block.render(area, buf);
let base: u16 = if count == 0 { 0 } else { 100u16 / count };
let constraints: Vec<Constraint> = vec![Constraint::Percentage(base); count as usize];
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);
}
}
}
+80 -80
View File
@@ -1,108 +1,108 @@
use crate::app::{
helpers::block_single_title_helper,
states::skirmish_states::{
structures::{Structure, Structures},
units::{Unit, Units},
},
helpers::block_single_title_helper,
states::skirmish_states::{
structures::{Structure, Structures},
units::{Unit, Units},
},
};
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::Color,
widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget},
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
style::Color,
widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget},
};
#[derive(Debug, Clone, PartialEq)]
pub struct SidePanelWidget<'a> {
coords: (usize, usize),
structure: &'a Structures,
unit: &'a Option<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,
}
}
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;
}
fn col_to_letters(&self) -> String {
let mut col: usize = self.coords.1 + 1;
let mut letters: Vec<char> = Vec::new();
letters.iter().rev().collect()
}
while col > 0 {
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
col = (col - 1) / 26;
}
fn get_outer_block(&self) -> Block<'_> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Double)
.title(block_single_title_helper(
format!("{}{}", self.col_to_letters(), self.coords.0),
Color::Yellow,
))
.title_alignment(Alignment::Center)
}
letters.iter().rev().collect()
}
fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> {
Block::default()
.borders(Borders::TOP)
.title(block_single_title_helper(title, color))
.title_alignment(Alignment::Center)
.padding(Padding::symmetric(1, 1))
}
fn get_outer_block(&self) -> Block<'_> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Double)
.title(block_single_title_helper(
format!("{}{}", self.col_to_letters(), self.coords.0),
Color::Yellow,
))
.title_alignment(Alignment::Center)
}
fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> {
Block::default()
.borders(Borders::TOP)
.title(block_single_title_helper(title, color))
.title_alignment(Alignment::Center)
.padding(Padding::symmetric(1, 1))
}
fn get_area_constraints(&self) -> [Constraint; 2] {
if self.unit.is_unit() {
[Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
[Constraint::Percentage(100), Constraint::Percentage(0)]
}
fn get_area_constraints(&self) -> [Constraint; 2] {
if self.unit.is_unit() {
[Constraint::Percentage(50), Constraint::Percentage(50)]
} else {
[Constraint::Percentage(100), Constraint::Percentage(0)]
}
}
}
impl Widget for SidePanelWidget<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let block: Block<'_> = self.get_outer_block();
let inner_area: Rect = block.inner(area).inner(Margin {
horizontal: 2,
vertical: 1,
});
block.render(area, buf);
fn render(self, area: Rect, buf: &mut Buffer) {
let block: Block<'_> = self.get_outer_block();
let inner_area: Rect = block.inner(area).inner(Margin {
horizontal: 2,
vertical: 1,
});
block.render(area, buf);
let [structure_area, unit_area] =
Layout::vertical(self.get_area_constraints()).areas(inner_area);
let [structure_area, unit_area] =
Layout::vertical(self.get_area_constraints()).areas(inner_area);
Paragraph::new(self.structure.text())
.alignment(Alignment::Left)
.block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan))
.render(structure_area, buf);
Paragraph::new(self.structure.text())
.alignment(Alignment::Left)
.block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan))
.render(structure_area, buf);
if self.unit.is_unit() {
let mut unit_name: String = self.unit.get_name().to_string();
if self.unit.is_unit() {
let mut unit_name: String = self.unit.get_name().to_string();
if self.is_unit_marked {
unit_name = format!("> {} <", unit_name);
}
if self.is_unit_marked {
unit_name = format!("> {} <", unit_name);
}
Paragraph::new(self.unit.text())
.alignment(Alignment::Left)
.block(self.get_inner_block(unit_name, Color::Green))
.render(unit_area, buf);
}
Paragraph::new(self.unit.text())
.alignment(Alignment::Left)
.block(self.get_inner_block(unit_name, Color::Green))
.render(unit_area, buf);
}
}
}