attempt 3

This commit is contained in:
2026-03-27 10:29:33 +00:00
parent 1f5dfe5182
commit bd1b45e4d7
20 changed files with 1192 additions and 865 deletions
+304 -74
View File
@@ -8,6 +8,7 @@
//!
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimise queries.
use crate::constants::ITILE_SIZE;
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
use crate::entities::cargo::{Cargo, HaulSlot};
use crate::entities::shared_components::Ambulatory;
@@ -15,7 +16,8 @@ use crate::entities::tasks::components::{
ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState,
};
use crate::entities::tasks::events::{LogsSpawned, TaskClaimed, TaskCompleted, TaskFailed};
use crate::entities::tasks::idle::execute_idle;
use crate::entities::tasks::job_queue::{JobKind, JobQueue};
use crate::entities::tasks::tasks::idle::execute_idle;
use crate::world::chunks::ChunkMap;
use crate::world::generation::forestry::{fell_tree, TreePart};
use crate::world::tiles::tile_changed::TileChangedEvent;
@@ -24,6 +26,7 @@ use crate::world::tiles::TileMap;
use bevy::prelude::*;
use bevy_rand::prelude::*;
use smallvec::SmallVec;
use std::collections::VecDeque;
/// Main task executor. Runs in FixedUpdate.
pub fn task_executor_system(
@@ -45,6 +48,7 @@ pub fn task_executor_system(
&EntityType,
Option<&mut HaulSlot>,
)>,
mut job_queue: ResMut<JobQueue>,
mut claimed_writer: MessageWriter<TaskClaimed>,
mut completed_writer: MessageWriter<TaskCompleted>,
mut failed_writer: MessageWriter<TaskFailed>,
@@ -98,6 +102,8 @@ pub fn task_executor_system(
// Execute current task if Active
if *state == TaskState::Active {
if let Some(current_task) = queue.current_mut() {
let mut failed_reason: Option<&'static str> = None;
match current_task {
Task::Idle { .. } => {
execute_idle(
@@ -139,6 +145,10 @@ pub fn task_executor_system(
} => match step {
ChopStep::MovingToTree { ref mut approach } => {
if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) {
info!(
"TASK FAILED: {:?} for {:?} - {}",
current_task, entity, "tree already gone"
);
failed_writer.write(TaskFailed {
entity,
task: current_task.clone(),
@@ -187,10 +197,15 @@ pub fn task_executor_system(
ambulatory.current_path = None;
}
None => {
let reason = "no adjacent standable tile to approach tree";
info!(
"TASK FAILED: {:?} for {:?} - {}",
current_task, entity, reason
);
failed_writer.write(TaskFailed {
entity,
task: current_task.clone(),
reason: "no adjacent standable tile to approach tree",
reason,
});
*state = TaskState::Failed;
continue;
@@ -224,8 +239,9 @@ pub fn task_executor_system(
// If target is Some, pathfinding is handling movement — nothing to do
}
ChopStep::Chopping { ticks_remaining } => {
info!("[EXECUTOR] Chop tick: {:?}", ticks_remaining);
if *ticks_remaining == 0 {
let trunk_positions = fell_tree(
let (trunk_position, trunk_count) = fell_tree(
*trunk_pos,
&tree_parts,
&mut commands,
@@ -243,9 +259,14 @@ pub fn task_executor_system(
fall_dir = Vec2::new(1.0, 0.0); // default: fall east
}
let log_sprite: Handle<Image> = asset_server.load("log_cargo.png");
let mut log_entities: SmallVec<[Entity; 8]> = SmallVec::new();
for &pos in trunk_positions.iter() {
if let Some(log_entity) =
let mut log_entities: SmallVec<[(Entity, IVec3); 8]> =
SmallVec::new();
// Loop from 0 up to the number of trunk segments found
for i in 0..trunk_count {
// Calculate the position for this specific log by offseting Z
let pos = trunk_position + IVec3::new(0, 0, i as i32);
if let Some((log_entity, drop_pos)) =
crate::entities::cargo::spawn_log_cargo(
&mut commands,
&mut tilemap_mut,
@@ -255,14 +276,42 @@ pub fn task_executor_system(
&mut rng,
)
{
log_entities.push(log_entity);
log_entities.push((log_entity, drop_pos));
}
}
// Emit event so demo can queue HaulCargo tasks for these logs
if !log_entities.is_empty() {
// Find surface Z at (0,0) - search for floor tile at different Z levels
use crate::constants::ITILE_SIZE;
let dest_z = (0..=4)
.find_map(|z_idx| {
let check_pos = IVec3::new(0, 0, z_idx * ITILE_SIZE);
if tilemap_mut.floor_tiles.contains_key(&check_pos) {
Some((z_idx + 1) * ITILE_SIZE)
} else {
None
}
})
.unwrap_or(16); // Default to z=16 if no floor found
let dest = IVec3::new(0, 0, dest_z);
for (cargo_entity, actual_cargo_pos) in log_entities.iter() {
job_queue.push(JobKind::HaulCargo {
cargo_entity: *cargo_entity,
cargo_pos: *actual_cargo_pos,
dest,
});
info!(
"[EXECUTOR] Added HaulCargo for cargo at {:?} -> {:?}",
actual_cargo_pos, dest
);
}
logs_spawned_writer.write(LogsSpawned {
log_entities,
dest: IVec3::ZERO,
log_entities: log_entities
.iter()
.map(|(e, _)| *e)
.collect(),
dest,
});
}
*step = ChopStep::Done;
@@ -294,7 +343,14 @@ pub fn task_executor_system(
HaulStep::MovingToCargo { approach } => {
use crate::constants::ITILE_SIZE;
info!("[HAUL] {:?} MovingToCargo: cargo_pos={:?}, approach={:?}, target={:?}",
entity, cargo_pos, approach, ambulatory.target);
if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) {
info!(
"TASK FAILED: {:?} for {:?} - {}",
current_task, entity, "cargo no longer exists"
);
failed_writer.write(TaskFailed {
entity,
task: current_task.clone(),
@@ -305,40 +361,107 @@ pub fn task_executor_system(
}
if approach.is_none() {
// Search for nearest standable tile to approach cargo.
// Radius 0 = cargo tile itself (can stand in same tile as cargo).
// Radius 1-2 = adjacent tiles if cargo tile is blocked.
const NODE_CAP: usize = 1024;
let mut frontier: VecDeque<IVec3> = VecDeque::new();
let mut visited: std::collections::HashSet<IVec3> =
std::collections::HashSet::new();
let cargo_z = cargo_pos.z;
let approach_tile = (0..=2i32).find_map(|radius: i32| {
for dx in -radius..=radius {
for dy in -radius..=radius {
if radius > 0
&& dx.abs() != radius
&& dy.abs() != radius
{
continue;
}
let candidate = IVec3::new(
cargo_pos.x + dx * ITILE_SIZE,
cargo_pos.y + dy * ITILE_SIZE,
cargo_z,
);
if tilemap_mut.is_standable(candidate) {
return Some(candidate);
}
for dx in -1i32..=1 {
for dy in -1i32..=1 {
if dx == 0 && dy == 0 {
continue;
}
let neighbor = IVec3::new(
cargo_pos.x + dx * ITILE_SIZE,
cargo_pos.y + dy * ITILE_SIZE,
cargo_z,
);
if visited.insert(neighbor) {
frontier.push_back(neighbor);
}
}
None
});
}
for dz in -1i32..=1 {
if dz == 0 {
continue;
}
let above = IVec3::new(
cargo_pos.x,
cargo_pos.y,
cargo_z + dz * ITILE_SIZE,
);
if visited.insert(above) {
frontier.push_back(above);
}
}
let mut approach_tile: Option<IVec3> = None;
while let Some(tile) = frontier.pop_front() {
if visited.len() > NODE_CAP {
break;
}
if !tilemap_mut.is_standable(tile) {
continue;
}
if tilemap_mut.cargo_tiles.contains_key(&tile) {
for dz in -1i32..=1 {
for dx in -1i32..=1 {
for dy in -1i32..=1 {
if dx == 0 && dy == 0 && dz == 0 {
continue;
}
let neighbor = IVec3::new(
tile.x + dx * ITILE_SIZE,
tile.y + dy * ITILE_SIZE,
tile.z + dz * ITILE_SIZE,
);
if visited.insert(neighbor) {
frontier.push_back(neighbor);
}
}
}
}
continue;
}
if tilemap_mut.claimed_tiles.contains_key(&tile) {
for dz in -1i32..=1 {
for dx in -1i32..=1 {
for dy in -1i32..=1 {
if dx == 0 && dy == 0 && dz == 0 {
continue;
}
let neighbor = IVec3::new(
tile.x + dx * ITILE_SIZE,
tile.y + dy * ITILE_SIZE,
tile.z + dz * ITILE_SIZE,
);
if visited.insert(neighbor) {
frontier.push_back(neighbor);
}
}
}
}
continue;
}
approach_tile = Some(tile);
break;
}
match approach_tile {
Some(tile) => {
*approach = Some(tile);
info!(
"[HAUL] {:?} target set: cargo={:?} approach={:?}",
entity, cargo_pos, tile
"[HAUL] {:?} setting target: cargo={:?} approach={:?} target={:?}",
entity, cargo_pos, tile, ambulatory.target
);
// +1.0 z-offset for entity standing height (same as trees)
ambulatory.target = Some(Vec3::new(
tile.x as f32,
tile.y as f32,
@@ -359,22 +482,26 @@ pub fn task_executor_system(
}
// Check arrival at cargo tile
if approach.is_some() && ambulatory.target.is_none() {
let dx = transform.translation.x - cargo_pos.x as f32;
let dy = transform.translation.y - cargo_pos.y as f32;
// Use approach tile position for distance check, not target None
if let Some(approach_tile) = *approach {
let dx = transform.translation.x - approach_tile.x as f32;
let dy = transform.translation.y - approach_tile.y as f32;
let dist_sq = dx * dx + dy * dy;
let pickup_range_sq =
let arrive_sq =
(ITILE_SIZE as f32 * 1.5) * (ITILE_SIZE as f32 * 1.5);
if dist_sq <= pickup_range_sq {
info!("[HAUL] {:?} arrival check: approach={:?} dist_sq={:.1} arrive_sq={:.1} transform={:?}",
entity, approach_tile, dist_sq, arrive_sq, transform.translation.truncate());
if dist_sq <= arrive_sq {
info!(
"[HAUL] {:?} arrived at cargo {:?}, picking up",
entity, cargo_entity
"[HAUL] {:?} arrived at approach {:?}, picking up cargo at {:?}",
entity, approach_tile, cargo_pos
);
*step = HaulStep::PickingUp;
}
}
}
HaulStep::PickingUp => {
info!("[HAUL] {:?} PickingUp: cargo_pos={:?}", entity, cargo_pos);
if haul.is_occupied() {
failed_writer.write(TaskFailed {
entity,
@@ -390,7 +517,14 @@ pub fn task_executor_system(
"[HAUL] {:?} picked up {:?} → hauling to {:?}",
entity, cargo_entity, dest
);
*step = HaulStep::MovingToDest { chosen_drop: None };
let drop_target =
find_drop_tile(&mut *tilemap_mut, *dest, entity);
tilemap_mut.claimed_tiles.insert(drop_target, entity);
*step = HaulStep::MovingToDest {
chosen_drop: Some(drop_target),
};
} else {
failed_writer.write(TaskFailed {
entity,
@@ -401,38 +535,43 @@ pub fn task_executor_system(
}
}
HaulStep::MovingToDest { chosen_drop } => {
if chosen_drop.is_none() {
*chosen_drop = Some(
tilemap_mut
.find_nearest_free_cargo_tile(*dest, 8, &[])
.unwrap_or(*dest),
);
}
let drop_pos = chosen_drop.unwrap();
if ambulatory.target.is_none() {
info!("[HAUL] {:?} target set: drop at {:?}", entity, drop_pos);
// +1.0 z-offset for entity standing height (same as approach)
ambulatory.target = Some(Vec3::new(
drop_pos.x as f32,
drop_pos.y as f32,
drop_pos.z as f32 + 1.0,
));
ambulatory.current_path = None;
}
let drop_pos = *chosen_drop;
if let Some(drop) = drop_pos {
if ambulatory.target.is_none() {
info!(
"[HAUL] {:?} setting target to drop at {:?}",
entity, drop
);
ambulatory.target = Some(Vec3::new(
drop.x as f32,
drop.y as f32,
drop.z as f32 + 1.0,
));
ambulatory.current_path = None;
}
// Always check arrival distance, not gated by target status
let dx = transform.translation.x - drop_pos.x as f32;
let dy = transform.translation.y - drop_pos.y as f32;
let dist_sq = dx * dx + dy * dy;
let arrive_sq = (crate::constants::TILE_SIZE as f32 * 1.5)
* (crate::constants::TILE_SIZE as f32 * 1.5);
if dist_sq <= arrive_sq {
ambulatory.target = None;
info!(
"[HAUL] {:?} arrived at drop point {:?}",
entity, drop_pos
);
*step = HaulStep::Dropping { drop_pos };
let dx = transform.translation.x - drop.x as f32;
let dy = transform.translation.y - drop.y as f32;
let dist_sq = dx * dx + dy * dy;
let arrive_sq = (crate::constants::TILE_SIZE as f32 * 1.5)
* (crate::constants::TILE_SIZE as f32 * 1.5);
if dist_sq <= arrive_sq {
ambulatory.target = None;
tilemap_mut.claimed_tiles.remove(&drop);
info!(
"[HAUL] {:?} arrived at drop point {:?}",
entity, drop
);
*step = HaulStep::Dropping { drop_pos: drop };
}
} else {
failed_writer.write(TaskFailed {
entity,
task: current_task.clone(),
reason: "drop target not set",
});
*state = TaskState::Failed;
continue;
}
}
HaulStep::Dropping { drop_pos } => {
@@ -551,3 +690,94 @@ pub fn task_executor_system(
}
}
}
fn find_drop_tile(tilemap: &mut TileMap, dest: IVec3, exclude_entity: Entity) -> IVec3 {
const SEARCH_RADIUS: i32 = 8;
const NODE_CAP: usize = 1024;
let mut frontier: VecDeque<IVec3> = VecDeque::new();
let mut visited: std::collections::HashSet<IVec3> = std::collections::HashSet::new();
let dest_z = dest.z;
for dx in -1i32..=1 {
for dy in -1i32..=1 {
if dx == 0 && dy == 0 {
continue;
}
let neighbor = IVec3::new(dest.x + dx * ITILE_SIZE, dest.y + dy * ITILE_SIZE, dest_z);
if visited.insert(neighbor) {
frontier.push_back(neighbor);
}
}
}
for dz in -1i32..=1 {
if dz == 0 {
continue;
}
let above = IVec3::new(dest.x, dest.y, dest_z + dz * ITILE_SIZE);
if visited.insert(above) {
frontier.push_back(above);
}
}
while let Some(tile) = frontier.pop_front() {
if visited.len() > NODE_CAP {
break;
}
if !tilemap.is_standable(tile) {
continue;
}
if tilemap.cargo_tiles.contains_key(&tile) {
for dz in -1i32..=1 {
for dx in -1i32..=1 {
for dy in -1i32..=1 {
if dx == 0 && dy == 0 && dz == 0 {
continue;
}
let neighbor = IVec3::new(
tile.x + dx * ITILE_SIZE,
tile.y + dy * ITILE_SIZE,
tile.z + dz * ITILE_SIZE,
);
if visited.insert(neighbor) {
frontier.push_back(neighbor);
}
}
}
}
continue;
}
if tilemap.claimed_tiles.contains_key(&tile) {
let claimant = tilemap.claimed_tiles.get(&tile).copied();
if claimant != Some(exclude_entity) {
for dz in -1i32..=1 {
for dx in -1i32..=1 {
for dy in -1i32..=1 {
if dx == 0 && dy == 0 && dz == 0 {
continue;
}
let neighbor = IVec3::new(
tile.x + dx * ITILE_SIZE,
tile.y + dy * ITILE_SIZE,
tile.z + dz * ITILE_SIZE,
);
if visited.insert(neighbor) {
frontier.push_back(neighbor);
}
}
}
}
continue;
}
}
return tile;
}
dest
}