Add MovingTask and progress tracking

Introduce a new MovingTask type for movement segments and extend
the Tasks enum
to include it.  Add total‑step tracking and a get_total_progress default
in the
Task trait, and implement the corresponding methods for MovingTask.
Update
units to allow coordinate changes via set_coords, add stone damage
handling,
and adjust board advancement logic to use the new task system.  Update
UI
code to display step and total percentages in the Tab panel.  Expand
documentation (README, AGENTS, CODE_MAP, PROGRESS, TESTING) to describe
digging
and moving, and add extensive tests (including tests/digging.rs) for the
new
behaviour.
This commit is contained in:
GarandPLG committed 2026-10-18 21:21:00 +02:00
1 parent fcdbbaf74c
commit 9e09aaf75e
21 files changed
+1049 -144

No files matched your search

+139 -101
View File
@@ -4,8 +4,8 @@ use crate::app::{
FocusedCell, Offset,
skirmish_states::{
CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel,
structures::{BaseBuilding, Ore, Stone, Structures, Tunnel},
tasks::{DiggingTask, Task, Tasks},
structures::{BaseBuilding, Ore, Stone, Structure, Structures, Tunnel},
tasks::{DiggingTask, MovingTask, Task, Tasks},
units::{MinerUnit, Unit, Units},
},
},
@@ -160,6 +160,9 @@ impl BoardState {
}
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() {
@@ -203,15 +206,36 @@ impl BoardState {
pub fn start_marking_cells(&mut self) {
let (row, col) = self.focused_cell.get_coords();
let cell: &mut CellWidget = self.get_mut_cell(row, col);
cell.set_marked(true);
self.marked_cells.selected_unit = cell.get_option_unit();
self.marked_cells.marked_cells.push_back((row, col));
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) {
// let todo_taks_tyle = Tasks::Digging(DiggingTask::);
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);
}
@@ -219,7 +243,7 @@ impl BoardState {
let (row, col) = self.marked_cells.selected_unit.get_coords();
if self.marked_cells.marked_cells.len() > 1 && !del {
let path: VecDeque<(usize, usize)> = self
let path: Vec<(usize, usize)> = self
.marked_cells
.marked_cells
.iter()
@@ -229,11 +253,20 @@ impl BoardState {
let tick_increase: f32 = self.marked_cells.selected_unit.get_digging_power();
let task: Tasks = Tasks::Digging(DiggingTask::new(path, tick_increase));
self.get_mut_cell(row, col)
.get_mut_option_unit()
.set_task(task);
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;
@@ -244,6 +277,18 @@ impl BoardState {
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
}
@@ -325,101 +370,94 @@ impl BoardState {
}
}
// fn get_active_tasks(&self) -> Vec<&Tasks> {
// self.cells
// .iter()
// .flat_map(|row| row.iter())
// .filter_map(|cell| {
// cell.get_ref_option_unit()
// .as_ref()
// .and_then(|unit| unit.get_ref_tasks().front())
// })
// .collect()
// // for row in self.marked_cells.marked_cells {
// // }
// }
pub fn advance_turn(&mut self) {
for row in 0..self.map_height {
for col in 0..self.map_width {
/* ---------------------------------------------------------
* 1️⃣ Take the unit out of the current cell (if any). This
* gives us an *owned* `Units`, so we no longer hold a
* mutable reference into `self`.
* ------------------------------------------------------- */
let maybe_unit: Option<Units> =
self.get_mut_cell(row, col).get_mut_option_unit().take();
// 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();
let Some(mut unit) = maybe_unit else {
continue; // no unit in this cell
};
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);
/* ---------------------------------------------------------
* 2️⃣ Look at the unit’s task queue. We need a mutable
* reference to the *front* task only while we are
* processing it. The mutable borrow is confined to a
* small inner block so that it ends before we move the
* unit itself.
* ------------------------------------------------------- */
if let Some(task) = unit.get_mut_tasks().front_mut() {
// `Tasks` currently only has the Digging variant.
// If new variants appear later, you can handle them here.
let maybe_target: Option<(usize, usize)> = {
// -------------------------------------------------
// Mutable borrow of the task lives only inside this
// block. When the block ends, `dig` is dropped.
// -------------------------------------------------
let dig: &mut DiggingTask = match task {
Tasks::Digging(d) => d,
};
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;
}
dig.progress();
// Remember the coordinates of the next tunnel cell.
let target: Option<(usize, usize)> = dig.current_target();
// If we actually moved, the current step of the
// digging task is finished – pop it from the queue.
if target.is_some() {
dig.pop_front();
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))
}
target
}; // ← `dig` (and the mutable borrow) ends here
if let Some((t_row, t_col)) = maybe_target {
/* -------------------------------------------------
* 3️⃣ We have a target – create the tunnel, move the
* unit, and clean up the source cell.
* ------------------------------------------------- */
// a) create the tunnel in the target cell
self.get_mut_cell(t_row, t_col)
.set_structure(Structures::Tunnel(Tunnel::new()));
// b) (optional) keep the old coordinates – they are
// no longer needed but kept for possible future use
let _old_coords: (usize, usize) = unit.get_coords();
// c) move the unit into the target cell
*self.get_mut_cell(t_row, t_col).get_mut_option_unit() = Some(unit);
// d) the source cell is already empty because of the
// earlier `take()`. Nothing else to do.
} else {
// No reachable target – put the unit back where it was.
self.get_mut_cell(row, col)
.get_mut_option_unit()
.replace(unit);
(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();
}
}
_ => {}
}
} else {
// The unit has no tasks – simply put it back.
self.get_mut_cell(row, col)
.get_mut_option_unit()
.replace(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);
}
}
}
@@ -8,6 +8,10 @@ pub struct Stone {
}
impl Stone {
pub fn take_damage(&mut self, damage: u16) {
self.durability = self.durability.saturating_sub(damage);
}
pub fn new() -> Self {
Self {
durability: 1000,
@@ -1,4 +1,4 @@
use crate::app::states::skirmish_states::tasks::Task;
use crate::app::states::skirmish_states::tasks::{MovingTask, Task};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
@@ -6,11 +6,13 @@ pub struct DiggingTask {
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,
@@ -22,6 +24,15 @@ impl DiggingTask {
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 current_target(&self) -> Option<(usize, usize)> {
self.path.front().copied()
}
@@ -48,6 +59,10 @@ impl Task for DiggingTask {
self.progress
}
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);
}
@@ -1,7 +1,9 @@
mod digging;
mod moving;
mod task_trait;
mod tasks_enum;
pub use digging::DiggingTask;
pub use moving::MovingTask;
pub use task_trait::Task;
pub use tasks_enum::Tasks;
@@ -0,0 +1,75 @@
use crate::app::{
states::skirmish_states::tasks::{DiggingTask, Task},
threads::TICK_MS,
};
use std::collections::VecDeque;
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,
}
impl MovingTask {
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 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)
}
}
impl Task for MovingTask {
fn get_icon(&self) -> char {
'→'
}
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_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_total_steps(&self) -> usize {
self.total_steps
}
fn progress(&mut self) {
self.ticks = (self.ticks + 1).min(MOVEMENT_TICKS);
}
fn is_finished(&self) -> bool {
self.ticks == MOVEMENT_TICKS
}
}
@@ -11,6 +11,17 @@ pub trait Task {
fn get_progress(&self) -> f32;
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 progress(&mut self);
fn is_finished(&self) -> bool;
@@ -1,9 +1,10 @@
use crate::app::states::skirmish_states::tasks::{DiggingTask, Task};
use crate::app::states::skirmish_states::tasks::{DiggingTask, MovingTask, Task};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub enum Tasks {
Digging(DiggingTask),
Moving(MovingTask),
}
impl Tasks {
@@ -11,6 +12,7 @@ impl Tasks {
fn t(&self) -> &dyn Task {
match self {
Self::Digging(d) => d,
Self::Moving(m) => m,
}
}
@@ -18,6 +20,7 @@ impl Tasks {
fn t_mut(&mut self) -> &mut dyn Task {
match self {
Self::Digging(d) => d,
Self::Moving(m) => m,
}
}
}
@@ -43,6 +46,10 @@ impl Task for Tasks {
self.t().get_progress()
}
fn get_total_steps(&self) -> usize {
self.t().get_total_steps()
}
fn is_finished(&self) -> bool {
self.t().is_finished()
}
+21 -9
View File
@@ -79,6 +79,10 @@ impl Unit for MinerUnit {
self.coords
}
fn set_coords(&mut self, coords: (usize, usize)) {
self.coords = coords;
}
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
&self.tasks
}
@@ -92,15 +96,23 @@ impl Unit for MinerUnit {
self.tasks
.iter()
.enumerate()
.map(|(i, task)| {
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(),
])
.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<'_>>>(),
)
@@ -26,6 +26,8 @@ pub trait Unit {
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>;
@@ -60,6 +60,10 @@ impl Unit for Units {
self.u().get_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()
}
@@ -114,6 +118,12 @@ impl Unit for Option<Units> {
self.as_ref().map_or((0, 0), |u| u.get_coords())
}
fn set_coords(&mut self, coords: (usize, usize)) {
if let Some(unit) = self.as_mut() {
unit.set_coords(coords);
}
}
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
self.as_ref()
.expect("attempted to get referenced tasks from `None`")
+14 -6
View File
@@ -1,5 +1,5 @@
use clap::ValueEnum;
use ratatui::text::Line;
use ratatui::text::{Line, Span};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
pub enum ZoomLevel {
@@ -25,22 +25,30 @@ impl ZoomLevel {
}
}
pub fn get_cell_text_area(
pub fn get_cell_text_area<'a>(
&self,
tag: char,
structure_level: char,
unit: char,
) -> Vec<Line<'_>> {
unit: Span<'a>,
) -> Vec<Line<'a>> {
match self {
ZoomLevel::ZoomedIn => vec![
Line::from(format!(" {} {} ", structure_level, unit)),
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(format!(" {} {} ", structure_level, unit)),
Line::from(vec![
Span::raw(format!(" {} ", structure_level)),
unit,
Span::raw(" "),
]),
Line::from(format!(" {} ", tag)),
Line::from(format!(" ")),
],