Task Infrastructure — Task Enum, Queue, Events, Executor
This commit is contained in:
@@ -4,5 +4,6 @@ pub mod livestock;
|
||||
pub mod sentient;
|
||||
pub mod shared_components;
|
||||
pub mod shared_systems;
|
||||
pub mod tasks;
|
||||
|
||||
pub use shared_systems::*;
|
||||
|
||||
@@ -3,11 +3,13 @@ use crate::constants::TILE_SIZE;
|
||||
use crate::constants::*;
|
||||
use crate::entities::cargo::{CarryVisualState, HaulSlot};
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::tasks::{Task, TaskQueue, TaskState};
|
||||
use crate::game::SpawnDelay;
|
||||
use crate::world::VisibleGameEntity;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rand::prelude::*;
|
||||
use rand::RngExt;
|
||||
use smallvec::smallvec;
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct Dorf {
|
||||
@@ -17,6 +19,8 @@ pub struct Dorf {
|
||||
visibility: Visibility,
|
||||
haul_slot: HaulSlot,
|
||||
carry_visual: CarryVisualState,
|
||||
task_queue: TaskQueue,
|
||||
task_state: TaskState,
|
||||
}
|
||||
|
||||
impl Dorf {
|
||||
@@ -25,6 +29,7 @@ impl Dorf {
|
||||
let run_speed = 6.;
|
||||
let normal_sprite = asset_server.load("dorf.png");
|
||||
let carry_sprite = asset_server.load("dorf.png");
|
||||
let origin = (position / TILE_SIZE).as_ivec3();
|
||||
|
||||
Dorf {
|
||||
ambulatory: Ambulatory {
|
||||
@@ -47,6 +52,14 @@ impl Dorf {
|
||||
visibility: Visibility::Hidden,
|
||||
haul_slot: HaulSlot::default(),
|
||||
carry_visual: CarryVisualState::new(normal_sprite, carry_sprite),
|
||||
task_queue: TaskQueue {
|
||||
tasks: smallvec![Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 10,
|
||||
origin,
|
||||
}],
|
||||
},
|
||||
task_state: TaskState::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ use crate::entities::shared_systems::constants::{
|
||||
WALK_SPEED_DIVISOR,
|
||||
};
|
||||
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
||||
use crate::entities::tasks::task_executor_system;
|
||||
use crate::world::tiles::tile_changed::{PathfindingDirtyChunks, TileChangedEvent};
|
||||
use crate::world::tiles::TileMap;
|
||||
use crate::world::{
|
||||
@@ -236,8 +237,10 @@ impl Plugin for PathfindingPlugin {
|
||||
collect_pathfinding_dirty_chunks,
|
||||
invalidate_paths_on_tile_change,
|
||||
prepare_paths,
|
||||
// DEPRECATED: Wandering now handled by Task::Idle in task executor
|
||||
update_wandering_targets,
|
||||
movement,
|
||||
task_executor_system,
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
//! Task system components — the data backbone for entity behaviour.
|
||||
//!
|
||||
//! # Design
|
||||
//! - Tasks are enum variants with associated data, not trait objects — zero-cost dispatch.
|
||||
//! - TaskQueue holds a VecDeque of pending tasks; front() is current active task.
|
||||
//! - TaskState tracks execution phase: Pending → Active → Completed/Failed.
|
||||
//! - All task data is owned — no references to world state — for cache locality.
|
||||
//!
|
||||
//! # Performance
|
||||
//! - SmallVec<[Task; 4]> for queue — avoids heap alloc for typical short queues.
|
||||
//! - Task enum is #[repr(u8)] for compact storage and fast matching.
|
||||
//! - No dynamic dispatch — match on Task variant directly in executor.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A single task an entity can execute.
|
||||
///
|
||||
/// Tasks are self-contained: they carry all data needed for execution.
|
||||
/// The executor matches on the variant and drives behaviour accordingly.
|
||||
///
|
||||
/// # Variant lifecycle
|
||||
/// - Created by task-generating systems (e.g., "chop that tree")
|
||||
/// - Pushed to TaskQueue
|
||||
/// - Popped to Active by executor when ready
|
||||
/// - Completed/Failed → removed, events fired
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum Task {
|
||||
/// Idle/wandering behaviour. Refactored from existing wandering system.
|
||||
/// Target is a standable tile within wander_radius; entity moves toward it.
|
||||
/// When reached, picks new random target. Never "completes" — cycles forever
|
||||
/// until replaced by a higher-priority task.
|
||||
Idle {
|
||||
/// Current wander target tile (world position).
|
||||
target: IVec3,
|
||||
/// Max distance from origin to wander (Chebyshev).
|
||||
wander_radius: i32,
|
||||
/// Origin tile for wander bounds.
|
||||
origin: IVec3,
|
||||
},
|
||||
|
||||
/// Move to a tile within "close enough" threshold.
|
||||
/// Completes when entity is within `threshold_tiles` of target.
|
||||
/// Uses existing pathfinding infrastructure.
|
||||
GoTo {
|
||||
target: IVec3,
|
||||
/// Completion threshold in tiles (Chebyshev distance).
|
||||
threshold_tiles: i32,
|
||||
},
|
||||
|
||||
/// Placeholder for Stage 3 implementations.
|
||||
/// Executor should match and log "unimplemented" for now.
|
||||
ChopTree {
|
||||
target_fixture: IVec3,
|
||||
},
|
||||
HaulObject {
|
||||
target_cargo_tile: IVec3,
|
||||
},
|
||||
DropHauled {
|
||||
drop_tile: IVec3,
|
||||
},
|
||||
}
|
||||
|
||||
impl Task {
|
||||
/// Returns true if this task is "terminal" — i.e., should be popped from
|
||||
/// the queue when done, rather than cycled (like Idle).
|
||||
#[inline]
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
!matches!(self, Task::Idle { .. })
|
||||
}
|
||||
|
||||
/// Human-readable name for debug logging.
|
||||
#[inline]
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Task::Idle { .. } => "Idle",
|
||||
Task::GoTo { .. } => "GoTo",
|
||||
Task::ChopTree { .. } => "ChopTree",
|
||||
Task::HaulObject { .. } => "HaulObject",
|
||||
Task::DropHauled { .. } => "DropHauled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue of tasks for an entity. Front of queue is currently executing task.
|
||||
///
|
||||
/// Tasks are pushed to the back; executor pops from front when complete.
|
||||
/// Higher-priority tasks can be inserted at front via `push_front`.
|
||||
///
|
||||
/// Uses SmallVec to avoid heap allocation for typical queue lengths (<4 tasks).
|
||||
#[derive(Component, Debug, Default)]
|
||||
pub struct TaskQueue {
|
||||
pub tasks: SmallVec<[Task; 4]>,
|
||||
}
|
||||
|
||||
impl TaskQueue {
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tasks.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current(&self) -> Option<&Task> {
|
||||
self.tasks.first()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn current_mut(&mut self) -> Option<&mut Task> {
|
||||
self.tasks.first_mut()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn push(&mut self, task: Task) {
|
||||
self.tasks.push(task);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn push_front(&mut self, task: Task) {
|
||||
self.tasks.insert(0, task);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pop(&mut self) -> Option<Task> {
|
||||
if self.tasks.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.tasks.remove(0))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
self.tasks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Tracks execution state of the current task.
|
||||
///
|
||||
/// Allows systems to react to task phase changes without polling TaskQueue.
|
||||
/// Updated by executor system each tick.
|
||||
#[derive(Component, Debug, Default, PartialEq, Eq)]
|
||||
pub enum TaskState {
|
||||
/// Task is queued but not yet started.
|
||||
#[default]
|
||||
Pending,
|
||||
/// Task is actively being executed this frame.
|
||||
Active,
|
||||
/// Task completed successfully — will be popped next tick.
|
||||
Completed,
|
||||
/// Task failed or was interrupted — will be popped next tick.
|
||||
Failed,
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Task lifecycle events — for decoupled reaction to task state changes.
|
||||
//!
|
||||
//! Events are fired by the executor when a task transitions state.
|
||||
//! Systems can listen for these to trigger UI updates, logging, or
|
||||
//! downstream task generation (e.g., "ChopTree completed" → spawn Cargo).
|
||||
|
||||
use crate::entities::tasks::components::Task;
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Message, Clone)]
|
||||
pub struct TaskClaimed {
|
||||
pub entity: Entity,
|
||||
pub task: Task,
|
||||
}
|
||||
|
||||
#[derive(Message, Clone)]
|
||||
pub struct TaskCompleted {
|
||||
pub entity: Entity,
|
||||
pub task: Task,
|
||||
}
|
||||
|
||||
#[derive(Message, Clone)]
|
||||
pub struct TaskFailed {
|
||||
pub entity: Entity,
|
||||
pub task: Task,
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Message, Clone)]
|
||||
pub struct TaskDropped {
|
||||
pub entity: Entity,
|
||||
pub task: Task,
|
||||
}
|
||||
|
||||
#[derive(Message, Clone)]
|
||||
pub struct TaskBlocked {
|
||||
pub entity: Entity,
|
||||
pub task: Task,
|
||||
pub blocker: &'static str,
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
//! Task executor system — the dispatcher that drives entity behaviour.
|
||||
//!
|
||||
//! Runs in FixedUpdate. For each entity with TaskQueue:
|
||||
//! 1. If no active task, pop front of queue → set TaskState::Active
|
||||
//! 2. Match on active Task variant, execute corresponding logic
|
||||
//! 3. On completion/failure, update TaskState, fire event, pop task
|
||||
//! 4. If queue empty after pop, assign default Task::Idle
|
||||
//!
|
||||
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimize queries.
|
||||
|
||||
use crate::constants::ITILE_SIZE;
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::tasks::components::{Task, TaskQueue, TaskState};
|
||||
use crate::entities::tasks::events::{TaskClaimed, TaskCompleted, TaskFailed};
|
||||
use crate::entities::tasks::idle::execute_idle;
|
||||
use crate::world::tiles::tilemap::TileMap;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rand::prelude::*;
|
||||
|
||||
/// Main task executor. Runs in FixedUpdate.
|
||||
pub fn task_executor_system(
|
||||
mut commands: Commands,
|
||||
tilemap: Res<TileMap>,
|
||||
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
||||
mut query: Query<(
|
||||
Entity,
|
||||
&mut TaskQueue,
|
||||
&mut TaskState,
|
||||
&mut Ambulatory,
|
||||
&Transform,
|
||||
)>,
|
||||
mut claimed_writer: MessageWriter<TaskClaimed>,
|
||||
mut completed_writer: MessageWriter<TaskCompleted>,
|
||||
mut failed_writer: MessageWriter<TaskFailed>,
|
||||
) {
|
||||
let Ok(mut rng) = rng_q.single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (entity, mut queue, mut state, mut ambulatory, transform) in query.iter_mut() {
|
||||
// If queue empty, assign default Idle task
|
||||
if queue.is_empty() {
|
||||
let origin = (transform.translation / ITILE_SIZE as f32).as_ivec3();
|
||||
queue.push(Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 10,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
|
||||
// Promote Pending → Active if needed
|
||||
if *state == TaskState::Pending && !queue.is_empty() {
|
||||
*state = TaskState::Active;
|
||||
if let Some(task) = queue.current() {
|
||||
claimed_writer.write(TaskClaimed {
|
||||
entity,
|
||||
task: task.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute current task if Active
|
||||
if *state == TaskState::Active {
|
||||
if let Some(current_task) = queue.current_mut() {
|
||||
match current_task {
|
||||
Task::Idle { .. } => {
|
||||
execute_idle(
|
||||
entity,
|
||||
current_task,
|
||||
transform,
|
||||
&mut ambulatory,
|
||||
&tilemap,
|
||||
&mut rng,
|
||||
);
|
||||
// Idle never completes - it cycles forever
|
||||
}
|
||||
Task::GoTo {
|
||||
target,
|
||||
threshold_tiles,
|
||||
} => {
|
||||
let entity_pos = (transform.translation / ITILE_SIZE as f32).as_ivec3();
|
||||
// Chebyshev distance (max of absolute differences)
|
||||
let distance = (entity_pos.x - target.x)
|
||||
.abs()
|
||||
.max((entity_pos.y - target.y).abs());
|
||||
|
||||
if distance <= *threshold_tiles || !tilemap.is_standable(*target) {
|
||||
*state = TaskState::Completed;
|
||||
} else {
|
||||
// Set target - pathfinding handles the rest
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target.x as f32,
|
||||
target.y as f32,
|
||||
transform.translation.z,
|
||||
));
|
||||
ambulatory.current_path = None;
|
||||
}
|
||||
}
|
||||
// Stage 3 placeholders - log and fail for now
|
||||
Task::ChopTree { .. } | Task::HaulObject { .. } | Task::DropHauled { .. } => {
|
||||
debug!(
|
||||
"Task {} unimplemented for entity {:?}",
|
||||
current_task.name(),
|
||||
entity
|
||||
);
|
||||
*state = TaskState::Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle task completion/failure
|
||||
if *state == TaskState::Completed || *state == TaskState::Failed {
|
||||
if let Some(completed_task) = queue.pop() {
|
||||
if *state == TaskState::Completed {
|
||||
completed_writer.write(TaskCompleted {
|
||||
entity,
|
||||
task: completed_task.clone(),
|
||||
});
|
||||
} else {
|
||||
failed_writer.write(TaskFailed {
|
||||
entity,
|
||||
task: completed_task.clone(),
|
||||
reason: "unimplemented",
|
||||
});
|
||||
}
|
||||
|
||||
// If terminal task completed, ensure Idle is queued
|
||||
if completed_task.is_terminal() && queue.is_empty() {
|
||||
let origin = (transform.translation / ITILE_SIZE as f32).as_ivec3();
|
||||
queue.push(Task::Idle {
|
||||
target: origin,
|
||||
wander_radius: 10,
|
||||
origin,
|
||||
});
|
||||
}
|
||||
}
|
||||
*state = TaskState::Pending;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Idle task implementation — wandering behaviour refactored here.
|
||||
//!
|
||||
//! This module contains the logic that was previously in the wandering
|
||||
//! system. Now it's called by the task executor when Task::Idle is active.
|
||||
|
||||
use crate::constants::ITILE_SIZE;
|
||||
use crate::entities::shared_components::Ambulatory;
|
||||
use crate::entities::tasks::components::Task;
|
||||
use crate::world::tiles::tilemap::TileMap;
|
||||
use bevy::prelude::*;
|
||||
use bevy_rand::prelude::*;
|
||||
use rand::RngExt;
|
||||
|
||||
/// Execute Idle task: set target for wandering, let pathfinding handle movement.
|
||||
pub(super) fn execute_idle(
|
||||
entity: Entity,
|
||||
task: &mut Task,
|
||||
transform: &Transform,
|
||||
ambulatory: &mut Ambulatory,
|
||||
tilemap: &TileMap,
|
||||
rng: &mut WyRand,
|
||||
) {
|
||||
let Task::Idle {
|
||||
target,
|
||||
wander_radius,
|
||||
origin,
|
||||
} = task
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity_pos = (transform.translation / ITILE_SIZE as f32).as_ivec3();
|
||||
|
||||
// If reached target or target is no longer standable, pick new target
|
||||
if entity_pos == *target || !tilemap.is_standable(*target) {
|
||||
if let Some(new_target) = pick_wander_target(origin, *wander_radius, tilemap, rng) {
|
||||
*target = new_target;
|
||||
}
|
||||
}
|
||||
|
||||
// Set ambulatory target - pathfinding system will compute path
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target.x as f32,
|
||||
target.y as f32,
|
||||
transform.translation.z,
|
||||
));
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.path_index = 0;
|
||||
}
|
||||
|
||||
/// Pick a random standable tile within wander radius of origin.
|
||||
fn pick_wander_target(
|
||||
origin: &IVec3,
|
||||
radius: i32,
|
||||
tilemap: &TileMap,
|
||||
rng: &mut WyRand,
|
||||
) -> Option<IVec3> {
|
||||
let mut candidates = smallvec::SmallVec::<[IVec3; 16]>::new();
|
||||
|
||||
// Collect valid tiles within radius
|
||||
for dx in -radius..=radius {
|
||||
for dy in -radius..=radius {
|
||||
let candidate = IVec3::new(
|
||||
origin.x + dx * ITILE_SIZE,
|
||||
origin.y + dy * ITILE_SIZE,
|
||||
origin.z,
|
||||
);
|
||||
if tilemap.is_standable(candidate) {
|
||||
candidates.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if candidates.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let idx = rng.random_range(0..candidates.len());
|
||||
Some(candidates[idx])
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod components;
|
||||
pub mod events;
|
||||
pub mod executor;
|
||||
pub mod idle;
|
||||
|
||||
pub use components::{Task, TaskQueue, TaskState};
|
||||
pub use events::{TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
|
||||
pub use executor::task_executor_system;
|
||||
@@ -6,6 +6,7 @@ use crate::entities::{
|
||||
cargo::any_hauling,
|
||||
item::{initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer},
|
||||
shared_systems::digging::dig_system,
|
||||
tasks::{TaskClaimed, TaskCompleted, TaskFailed, TaskDropped, TaskBlocked},
|
||||
};
|
||||
use crate::game::SpawnDelay;
|
||||
|
||||
@@ -47,6 +48,11 @@ fn main() {
|
||||
.insert_resource(ClearColor(Color::srgb(0., 0., 0.)))
|
||||
.add_plugins(world::WorldPlugin)
|
||||
.add_plugins(entities::pathfinding::PathfindingPlugin)
|
||||
.add_message::<TaskClaimed>()
|
||||
.add_message::<TaskCompleted>()
|
||||
.add_message::<TaskFailed>()
|
||||
.add_message::<TaskDropped>()
|
||||
.add_message::<TaskBlocked>()
|
||||
.add_systems(
|
||||
Startup,
|
||||
(camera::spawn_panning_camera, cursor::setup_cursor),
|
||||
|
||||
Reference in New Issue
Block a user