149 lines
5.8 KiB
Rust
149 lines
5.8 KiB
Rust
//! 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::*;
|
|
use rand::{RngExt, SeedableRng};
|
|
|
|
/// Main task executor. Runs in FixedUpdate.
|
|
pub fn task_executor_system(
|
|
mut commands: Commands,
|
|
tilemap: Res<TileMap>,
|
|
time: Res<Time>,
|
|
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>,
|
|
) {
|
|
// Create a deterministic RNG from global SEED + time
|
|
// This mirrors the forestry.rs pattern - no dependency on bevy_rand plugin
|
|
use std::collections::hash_map::DefaultHasher;
|
|
use std::hash::{Hash, Hasher};
|
|
let mut hasher = DefaultHasher::new();
|
|
crate::constants::SEED.hash(&mut hasher);
|
|
time.elapsed_secs().to_bits().hash(&mut hasher);
|
|
let seed = hasher.finish();
|
|
let mut rng = WyRand::seed_from_u64(seed);
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|