generated from GarandPLG/rust-flake-template
Add tab navigation for building and recruitment UI
- Add selection indices and option lists to SkirmishState. - Implement `cycle_popup_option` and expose Left/Right actions in keybindings. - Introduce `cancel_focused_tasks` in BoardState for Delete handling. - CellWidget now displays recruitment progress on Base structures. - Update README and documentation to describe new tabs and arrow navigation. - Adjust test counts and add extensive task‑cancellation tests.
This commit is contained in:
1 parent
6c1141a711
commit
110d4c411a
16 files changed
+546
-48
No files matched your search
@@ -14,14 +14,20 @@ use ratatui::{crossterm::event::KeyEvent, layout::Rect};
|
||||
|
||||
pub fn available_skirmish_actions(state: &SkirmishState) -> Vec<Action> {
|
||||
if state.recruitment_view {
|
||||
let mut actions: Vec<Action> = vec![Action::Recruitment, Action::Esc, Action::Quit2];
|
||||
let mut actions: Vec<Action> = vec![
|
||||
Action::Recruitment,
|
||||
Action::Left,
|
||||
Action::Right,
|
||||
Action::Quit2,
|
||||
];
|
||||
if state.can_recruit_miner(state.board.get_focused_cell().get_coords(), Players::Player) {
|
||||
actions.push(Action::Enter);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
if state.building_view {
|
||||
let mut actions: Vec<Action> = vec![Action::Building, Action::Esc, Action::Quit2];
|
||||
let mut actions: Vec<Action> =
|
||||
vec![Action::Building, Action::Left, Action::Right, Action::Quit2];
|
||||
let coords: (usize, usize) = state.board.get_focused_cell().get_coords();
|
||||
if let Structures::MineConstruction(site) =
|
||||
state.board.get_ref_cell(coords.0, coords.1).get_structure()
|
||||
@@ -72,6 +78,9 @@ pub fn available_skirmish_actions(state: &SkirmishState) -> Vec<Action> {
|
||||
if unit.get_owner() == Players::Player && unit.get_can_dig() {
|
||||
actions.push(Action::Space);
|
||||
}
|
||||
if state.side_panel && unit.get_owner() == Players::Player && !unit.get_ref_tasks().is_empty() {
|
||||
actions.extend([Action::Backspace, Action::Delete]);
|
||||
}
|
||||
actions
|
||||
}
|
||||
|
||||
@@ -83,7 +92,10 @@ pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) -> () {
|
||||
};
|
||||
if states.skirmish.recruitment_view {
|
||||
match action {
|
||||
Action::Recruitment | Action::Esc => states.skirmish.recruitment_view = false,
|
||||
Action::Recruitment => states.skirmish.recruitment_view = false,
|
||||
Action::Left => states.skirmish.cycle_popup_option(false),
|
||||
Action::Right => states.skirmish.cycle_popup_option(true),
|
||||
Action::Esc => (),
|
||||
Action::Enter => {
|
||||
let coords: (usize, usize) =
|
||||
states.skirmish.board.get_focused_cell().get_coords();
|
||||
@@ -95,7 +107,10 @@ pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) -> () {
|
||||
}
|
||||
if states.skirmish.building_view {
|
||||
match action {
|
||||
Action::Building | Action::Esc => states.skirmish.building_view = false,
|
||||
Action::Building => states.skirmish.building_view = false,
|
||||
Action::Left => states.skirmish.cycle_popup_option(false),
|
||||
Action::Right => states.skirmish.cycle_popup_option(true),
|
||||
Action::Esc => (),
|
||||
Action::Enter => {
|
||||
let coords: (usize, usize) =
|
||||
states.skirmish.board.get_focused_cell().get_coords();
|
||||
@@ -201,7 +216,13 @@ pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) -> () {
|
||||
Action::Space => board.toggle_marking(),
|
||||
Action::Building => states.skirmish.building_view = true,
|
||||
Action::Recruitment => states.skirmish.recruitment_view = true,
|
||||
Action::Backspace => board.pop_marked_cell(),
|
||||
Action::Backspace if board.marked_cells.marking_cells => board.pop_marked_cell(),
|
||||
Action::Backspace => {
|
||||
board.cancel_focused_tasks(false);
|
||||
}
|
||||
Action::Delete if !board.marked_cells.marking_cells => {
|
||||
board.cancel_focused_tasks(true);
|
||||
}
|
||||
Action::Delete => {
|
||||
board.marked_cells.marking_cells = false;
|
||||
board.end_marking_cells(true)
|
||||
|
||||
@@ -40,6 +40,8 @@ impl GameStates {
|
||||
enemy_resources: Resources::new(args.starting_wood, args.starting_metal),
|
||||
building_view: false,
|
||||
recruitment_view: false,
|
||||
selected_building: 0,
|
||||
selected_recruit: 0,
|
||||
},
|
||||
perk_decks: PerkDecksState {
|
||||
id: 2,
|
||||
|
||||
@@ -17,9 +17,29 @@ pub struct SkirmishState {
|
||||
pub enemy_resources: Resources,
|
||||
pub building_view: bool,
|
||||
pub recruitment_view: bool,
|
||||
pub selected_building: usize,
|
||||
pub selected_recruit: usize,
|
||||
}
|
||||
|
||||
impl SkirmishState {
|
||||
pub const BUILDING_OPTIONS: &'static [&'static str] = &["Mine"];
|
||||
pub const RECRUITMENT_OPTIONS: &'static [&'static str] = &["Miner"];
|
||||
|
||||
pub fn cycle_popup_option(&mut self, next: bool) -> () {
|
||||
let (selected, count): (&mut usize, usize) = if self.recruitment_view {
|
||||
(&mut self.selected_recruit, Self::RECRUITMENT_OPTIONS.len())
|
||||
} else if self.building_view {
|
||||
(&mut self.selected_building, Self::BUILDING_OPTIONS.len())
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
*selected = if next {
|
||||
(*selected + 1) % count
|
||||
} else {
|
||||
(*selected + count - 1) % count
|
||||
};
|
||||
}
|
||||
|
||||
pub fn can_recruit_miner(&self, (row, col): (usize, usize), owner: Players) -> bool {
|
||||
let resources: &Resources = match owner {
|
||||
Players::Player => &self.player_resources,
|
||||
|
||||
@@ -500,6 +500,25 @@ impl BoardState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cancel_focused_tasks(&mut self, all: bool) -> bool {
|
||||
if self.marked_cells.marking_cells {
|
||||
return false;
|
||||
}
|
||||
let (row, col): (usize, usize) = self.focused_cell.get_coords();
|
||||
let Some(unit) = self.cells[row][col].get_mut_option_unit().as_mut() else {
|
||||
return false;
|
||||
};
|
||||
if unit.get_owner() != Players::Player || unit.get_ref_tasks().is_empty() {
|
||||
return false;
|
||||
}
|
||||
if all {
|
||||
unit.get_mut_tasks().clear();
|
||||
} else {
|
||||
unit.get_mut_tasks().pop_back();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn advance_turn(&mut self) {
|
||||
// Snapshot positions so moving to a later cell cannot grant another turn.
|
||||
let active: Vec<(usize, usize)> = self
|
||||
|
||||
+53
-24
@@ -2,6 +2,7 @@ use crate::app::{
|
||||
App,
|
||||
helpers::block_title_helper,
|
||||
keybindings::{Action, available_skirmish_actions},
|
||||
states::SkirmishState,
|
||||
states::skirmish_states::{
|
||||
BoardState, Players,
|
||||
resources::{WOOD_DELIVERY_AMOUNT, WOOD_DELIVERY_INTERVAL_MS},
|
||||
@@ -13,9 +14,9 @@ use crate::app::{
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Stylize},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget},
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Tabs, Widget},
|
||||
};
|
||||
|
||||
pub fn skirmish_layout(area: Rect) -> [Rect; 3] {
|
||||
@@ -137,7 +138,31 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
|
||||
{
|
||||
let actions: Vec<Action> = available_skirmish_actions(&states.skirmish);
|
||||
let descriptions: [(Action, &'static str); 11] = [
|
||||
let descriptions: [(Action, &'static str); 14] = [
|
||||
(
|
||||
Action::Left,
|
||||
if states.skirmish.building_view || states.skirmish.recruitment_view {
|
||||
"Previous option"
|
||||
} else {
|
||||
"Left"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Right,
|
||||
if states.skirmish.building_view || states.skirmish.recruitment_view {
|
||||
"Next option"
|
||||
} else {
|
||||
"Right"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Backspace,
|
||||
if board.marked_cells.marking_cells {
|
||||
"Undo last cell"
|
||||
} else {
|
||||
"Cancel last task"
|
||||
},
|
||||
),
|
||||
(
|
||||
Action::Enter,
|
||||
if states.skirmish.recruitment_view {
|
||||
@@ -158,6 +183,8 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
Action::Delete,
|
||||
if states.skirmish.building_view {
|
||||
"Cancel construction"
|
||||
} else if !board.marked_cells.marking_cells {
|
||||
"Cancel all tasks"
|
||||
} else {
|
||||
"Cancel marking"
|
||||
},
|
||||
@@ -172,11 +199,7 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
),
|
||||
(
|
||||
Action::Esc,
|
||||
if states.skirmish.recruitment_view {
|
||||
"Close recruitment"
|
||||
} else if states.skirmish.building_view {
|
||||
"Close buildings"
|
||||
} else if board.marked_cells.marking_cells {
|
||||
if board.marked_cells.marking_cells {
|
||||
"Cancel route"
|
||||
} else {
|
||||
"Menu"
|
||||
@@ -228,7 +251,6 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
vec![Line::from("Construction unavailable: check site, builder and resources").red()]
|
||||
};
|
||||
let mut lines: Vec<Line<'_>> = vec![
|
||||
Line::from("Mine").yellow().bold(),
|
||||
Line::from(format!(
|
||||
"Cost: {} Wood + {} Metal | Build time: 10s",
|
||||
Mine::COST.wood,
|
||||
@@ -240,13 +262,17 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
];
|
||||
lines.extend(status);
|
||||
Clear.render(popup, buf);
|
||||
Paragraph::new(lines)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title(Line::from("Buildings").magenta().bold())
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(popup, buf);
|
||||
let block: Block<'_> = Block::bordered()
|
||||
.title(Line::from("Buildings").magenta().bold())
|
||||
.padding(Padding::uniform(1));
|
||||
let [tabs_area, details_area]: [Rect; 2] =
|
||||
Layout::vertical([Constraint::Length(2), Constraint::Min(0)]).areas(block.inner(popup));
|
||||
block.render(popup, buf);
|
||||
Tabs::new(SkirmishState::BUILDING_OPTIONS.iter().copied())
|
||||
.select(states.skirmish.selected_building)
|
||||
.highlight_style(Style::default().fg(Color::LightMagenta).bold())
|
||||
.render(tabs_area, buf);
|
||||
Paragraph::new(lines).render(details_area, buf);
|
||||
}
|
||||
if states.skirmish.recruitment_view {
|
||||
let popup: Rect = main_area.centered(Constraint::Length(64), Constraint::Length(14));
|
||||
@@ -263,7 +289,6 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Insufficient resources or queue full").red()
|
||||
};
|
||||
vec![
|
||||
Line::from("Miner").yellow().bold(),
|
||||
Line::from(format!(
|
||||
"Cost: {} Wood + {} Metal | Recruit time: 10s",
|
||||
MinerUnit::RECRUIT_COST.wood,
|
||||
@@ -287,12 +312,16 @@ pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
vec![Line::from("Select your Base to recruit Miners").red()]
|
||||
};
|
||||
Clear.render(popup, buf);
|
||||
Paragraph::new(lines)
|
||||
.block(
|
||||
Block::bordered()
|
||||
.title(Line::from("Recruitment").magenta().bold())
|
||||
.padding(Padding::uniform(1)),
|
||||
)
|
||||
.render(popup, buf);
|
||||
let block: Block<'_> = Block::bordered()
|
||||
.title(Line::from("Recruitment").magenta().bold())
|
||||
.padding(Padding::uniform(1));
|
||||
let [tabs_area, details_area]: [Rect; 2] =
|
||||
Layout::vertical([Constraint::Length(2), Constraint::Min(0)]).areas(block.inner(popup));
|
||||
block.render(popup, buf);
|
||||
Tabs::new(SkirmishState::RECRUITMENT_OPTIONS.iter().copied())
|
||||
.select(states.skirmish.selected_recruit)
|
||||
.highlight_style(Style::default().fg(Color::LightMagenta).bold())
|
||||
.render(tabs_area, buf);
|
||||
Paragraph::new(lines).render(details_area, buf);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::app::states::skirmish_states::BoardState;
|
||||
use crate::app::states::skirmish_states::{BoardState, Players, tasks::Task, units::Unit};
|
||||
use crate::app::widgets::CellWidget;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
widgets::Widget,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
use std::{collections::HashSet, rc::Rc};
|
||||
|
||||
pub struct BoardWidget<'a> {
|
||||
state: &'a BoardState,
|
||||
@@ -18,6 +19,18 @@ impl<'a> BoardWidget<'a> {
|
||||
|
||||
impl Widget for BoardWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut queued_path: HashSet<(usize, usize)> = HashSet::new();
|
||||
for row in &self.state.cells {
|
||||
for cell in row {
|
||||
if let Some(unit) = cell.get_ref_option_unit()
|
||||
&& unit.get_owner() == Players::Player
|
||||
{
|
||||
for task in unit.get_ref_tasks() {
|
||||
queued_path.extend(task.get_path().iter().copied());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let horizontal: Rc<[Rect]> =
|
||||
Layout::horizontal(vec![
|
||||
Constraint::Length(self.state.cell_width as u16);
|
||||
@@ -38,7 +51,17 @@ impl Widget for BoardWidget<'_> {
|
||||
.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));
|
||||
.map(|cell: &CellWidget| -> () {
|
||||
let mut cell: CellWidget = cell.clone();
|
||||
let coords: (usize, usize) = (
|
||||
row_idx + self.state.vertical_offset.get_value(),
|
||||
col_idx + self.state.horizontal_offset.get_value(),
|
||||
);
|
||||
if queued_path.contains(&coords) {
|
||||
cell.set_marked(true);
|
||||
}
|
||||
cell.render(*cell_area, buf);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,18 @@ impl CellWidget {
|
||||
let middle = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color());
|
||||
}
|
||||
if let Structures::Base(base) = self.structure
|
||||
&& base.queued_miners() > 0
|
||||
{
|
||||
let middle: usize = lines.len() / 2;
|
||||
let percent: u16 = base.recruitment_progress();
|
||||
let label: String = if self.zoom_level == ZoomLevel::ZoomedOut && percent == 100 {
|
||||
percent.to_string()
|
||||
} else {
|
||||
format!("{percent}%")
|
||||
};
|
||||
lines[middle] = Line::from(label).cyan();
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user