784 lines
39 KiB
Rust
784 lines
39 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 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;
|
|
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::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;
|
|
use crate::world::tiles::visibility::TileOcclusionEvent;
|
|
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(
|
|
mut commands: Commands,
|
|
mut tilemap_mut: ResMut<TileMap>,
|
|
chunk_map: Res<ChunkMap>,
|
|
asset_server: Res<AssetServer>,
|
|
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
|
mut tick: Local<u32>,
|
|
tree_parts: Query<(Entity, &TreePart)>,
|
|
mut cargo_query: Query<&mut Cargo>,
|
|
mut query: Query<(
|
|
Entity,
|
|
&mut TaskQueue,
|
|
&mut TaskState,
|
|
&mut Ambulatory,
|
|
&Transform,
|
|
&mut Sprite,
|
|
&EntityType,
|
|
Option<&mut HaulSlot>,
|
|
)>,
|
|
mut job_queue: ResMut<JobQueue>,
|
|
mut claimed_writer: MessageWriter<TaskClaimed>,
|
|
mut completed_writer: MessageWriter<TaskCompleted>,
|
|
mut failed_writer: MessageWriter<TaskFailed>,
|
|
mut logs_spawned_writer: MessageWriter<LogsSpawned>,
|
|
mut tile_changed: MessageWriter<TileChangedEvent>,
|
|
mut occlusion: MessageWriter<TileOcclusionEvent>,
|
|
) {
|
|
let Ok(mut rng) = rng_q.single_mut() else {
|
|
return;
|
|
};
|
|
|
|
*tick = tick.wrapping_add(1);
|
|
let current_tick = *tick;
|
|
|
|
for (
|
|
entity,
|
|
mut queue,
|
|
mut state,
|
|
mut ambulatory,
|
|
transform,
|
|
mut sprite,
|
|
entity_type,
|
|
mut haul_slot,
|
|
) in query.iter_mut()
|
|
{
|
|
let origin = transform.translation.as_ivec3();
|
|
let behaviour = EntityBehaviourRegistry::global_get(entity_type.0);
|
|
|
|
if queue.is_empty() {
|
|
queue.push(Task::Idle {
|
|
origin,
|
|
sigma_world: behaviour.idle.sigma_world,
|
|
state: IdleState::Picking {
|
|
retry_after_tick: 0,
|
|
retry_count: 0,
|
|
},
|
|
});
|
|
}
|
|
|
|
// 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() {
|
|
let mut failed_reason: Option<&'static str> = None;
|
|
|
|
match current_task {
|
|
Task::Idle { .. } => {
|
|
execute_idle(
|
|
current_task,
|
|
transform,
|
|
&mut ambulatory,
|
|
&mut sprite,
|
|
&*tilemap_mut,
|
|
&chunk_map,
|
|
&behaviour.idle,
|
|
&mut rng,
|
|
current_tick,
|
|
);
|
|
}
|
|
Task::GoTo {
|
|
target,
|
|
threshold_tiles,
|
|
} => {
|
|
let entity_pos = (transform.translation / 16.0f32).as_ivec3();
|
|
let distance = (entity_pos.x - target.x)
|
|
.abs()
|
|
.max((entity_pos.y - target.y).abs());
|
|
|
|
if distance <= *threshold_tiles || !tilemap_mut.is_standable(*target) {
|
|
*state = TaskState::Completed;
|
|
} else {
|
|
ambulatory.target = Some(Vec3::new(
|
|
target.x as f32,
|
|
target.y as f32,
|
|
target.z as f32 + 1.0,
|
|
));
|
|
ambulatory.current_path = None;
|
|
}
|
|
}
|
|
Task::ChopTree {
|
|
trunk_pos,
|
|
chop_ticks,
|
|
step,
|
|
} => 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(),
|
|
reason: "tree already gone",
|
|
});
|
|
*state = TaskState::Failed;
|
|
continue;
|
|
}
|
|
|
|
use crate::constants::ITILE_SIZE;
|
|
|
|
if approach.is_none() {
|
|
// First entry: compute approach tile and set target.
|
|
// Neighbours at the same z as the trunk base — this IS the floor level.
|
|
let trunk_z = trunk_pos.z;
|
|
|
|
// Search expanding outward from the trunk for a standable tile
|
|
let approach_target = (1..=4).find_map(|radius: i32| {
|
|
for dx in -radius..=radius {
|
|
for dy in -radius..=radius {
|
|
// Only check the outer ring of this radius
|
|
if dx.abs() != radius && dy.abs() != radius {
|
|
continue;
|
|
}
|
|
let candidate = IVec3::new(
|
|
trunk_pos.x + dx * ITILE_SIZE,
|
|
trunk_pos.y + dy * ITILE_SIZE,
|
|
trunk_z,
|
|
);
|
|
if tilemap_mut.is_standable(candidate) {
|
|
*approach = Some(candidate);
|
|
return Some(Vec3::new(
|
|
candidate.x as f32,
|
|
candidate.y as f32,
|
|
candidate.z as f32 + 1.0,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
});
|
|
|
|
match approach_target {
|
|
Some(target) => {
|
|
ambulatory.target = Some(target);
|
|
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,
|
|
});
|
|
*state = TaskState::Failed;
|
|
continue;
|
|
}
|
|
}
|
|
} else if ambulatory.target.is_none() {
|
|
// Arrived at approach tile (or path failed and target cleared).
|
|
// Check distance to trunk; if close enough, start chopping.
|
|
let dx = transform.translation.x - trunk_pos.x as f32;
|
|
let dy = transform.translation.y - trunk_pos.y as f32;
|
|
let dist_sq = dx * dx + dy * dy;
|
|
let chop_range_sq =
|
|
(ITILE_SIZE as f32 * 2.5) * (ITILE_SIZE as f32 * 2.5);
|
|
|
|
if dist_sq <= chop_range_sq {
|
|
// Close enough — transition to chopping
|
|
ambulatory.current_path = None;
|
|
*step = ChopStep::Chopping {
|
|
ticks_remaining: *chop_ticks,
|
|
};
|
|
} else if let Some(approach_tile) = *approach {
|
|
// Not in range — re-set target to approach tile
|
|
ambulatory.target = Some(Vec3::new(
|
|
approach_tile.x as f32,
|
|
approach_tile.y as f32,
|
|
approach_tile.z as f32 + 1.0,
|
|
));
|
|
ambulatory.current_path = None;
|
|
}
|
|
}
|
|
// 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_position, trunk_count) = fell_tree(
|
|
*trunk_pos,
|
|
&tree_parts,
|
|
&mut commands,
|
|
&mut tilemap_mut,
|
|
&mut tile_changed,
|
|
&mut occlusion,
|
|
);
|
|
// Compute fall direction: from chopper (entity's position) toward trunk
|
|
let chopper_pos = transform.translation.truncate(); // XY only
|
|
let trunk_xy = Vec2::new(trunk_pos.x as f32, trunk_pos.y as f32);
|
|
let mut fall_dir = (trunk_xy - chopper_pos).normalize_or_zero();
|
|
// If dorf is standing on the trunk (distance ~0), normalize_or_zero returns ZERO.
|
|
// Safe to use == here because normalize_or_zero produces exact zero components.
|
|
if fall_dir == Vec2::ZERO {
|
|
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, 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,
|
|
pos,
|
|
fall_dir,
|
|
log_sprite.clone(),
|
|
&mut rng,
|
|
)
|
|
{
|
|
log_entities.push((log_entity, drop_pos));
|
|
}
|
|
}
|
|
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: log_entities
|
|
.iter()
|
|
.map(|(e, _)| *e)
|
|
.collect(),
|
|
dest,
|
|
});
|
|
}
|
|
*step = ChopStep::Done;
|
|
} else {
|
|
*ticks_remaining -= 1;
|
|
}
|
|
}
|
|
ChopStep::Done => {
|
|
*state = TaskState::Completed;
|
|
}
|
|
},
|
|
Task::HaulCargo {
|
|
cargo_entity,
|
|
cargo_pos,
|
|
dest,
|
|
step,
|
|
} => {
|
|
let Some(ref mut haul) = haul_slot else {
|
|
failed_writer.write(TaskFailed {
|
|
entity,
|
|
task: current_task.clone(),
|
|
reason: "entity has no HaulSlot",
|
|
});
|
|
*state = TaskState::Failed;
|
|
continue;
|
|
};
|
|
|
|
match step {
|
|
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(),
|
|
reason: "cargo no longer exists",
|
|
});
|
|
*state = TaskState::Failed;
|
|
continue;
|
|
}
|
|
|
|
if approach.is_none() {
|
|
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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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] {:?} setting target: cargo={:?} approach={:?} target={:?}",
|
|
entity, cargo_pos, tile, ambulatory.target
|
|
);
|
|
ambulatory.target = Some(Vec3::new(
|
|
tile.x as f32,
|
|
tile.y as f32,
|
|
tile.z as f32 + 1.0,
|
|
));
|
|
ambulatory.current_path = None;
|
|
}
|
|
None => {
|
|
failed_writer.write(TaskFailed {
|
|
entity,
|
|
task: current_task.clone(),
|
|
reason: "no standable tile near cargo",
|
|
});
|
|
*state = TaskState::Failed;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check arrival at cargo tile
|
|
// 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 arrive_sq =
|
|
(ITILE_SIZE as f32 * 1.5) * (ITILE_SIZE as f32 * 1.5);
|
|
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 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,
|
|
task: current_task.clone(),
|
|
reason: "HaulSlot already occupied",
|
|
});
|
|
*state = TaskState::Failed;
|
|
continue;
|
|
}
|
|
if let Some(_) = tilemap_mut.remove_cargo(cargo_pos) {
|
|
haul.pick_up(*cargo_entity);
|
|
info!(
|
|
"[HAUL] {:?} picked up {:?} → hauling to {:?}",
|
|
entity, cargo_entity, dest
|
|
);
|
|
|
|
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,
|
|
task: current_task.clone(),
|
|
reason: "cargo vanished before pickup",
|
|
});
|
|
*state = TaskState::Failed;
|
|
}
|
|
}
|
|
HaulStep::MovingToDest { chosen_drop } => {
|
|
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;
|
|
}
|
|
|
|
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 } => {
|
|
if let Some(cargo_entity) = haul.release() {
|
|
info!(
|
|
"[HAUL] {:?} dropped {:?} at {:?}",
|
|
entity, cargo_entity, drop_pos
|
|
);
|
|
if tilemap_mut.place_cargo(*drop_pos, cargo_entity).is_err() {
|
|
warn!(
|
|
"place_cargo failed at {:?} — tile occupied. \
|
|
Cargo entity {:?} orphaned.",
|
|
drop_pos, cargo_entity
|
|
);
|
|
} else if let Ok(mut cargo) = cargo_query.get_mut(cargo_entity)
|
|
{
|
|
cargo.tile_pos = *drop_pos;
|
|
}
|
|
}
|
|
*step = HaulStep::Done;
|
|
}
|
|
HaulStep::Done => {
|
|
info!("[HAUL] {:?} haul task COMPLETE", entity);
|
|
*state = TaskState::Completed;
|
|
}
|
|
}
|
|
}
|
|
Task::DropHauled { pos, step } => {
|
|
let Some(ref mut haul) = haul_slot else {
|
|
*state = TaskState::Completed;
|
|
continue;
|
|
};
|
|
|
|
match step {
|
|
DropStep::Moving { chosen_drop } => {
|
|
if chosen_drop.is_none() {
|
|
*chosen_drop = Some(
|
|
tilemap_mut
|
|
.find_nearest_free_cargo_tile(*pos, 8, &[])
|
|
.unwrap_or(*pos),
|
|
);
|
|
}
|
|
let drop_pos = chosen_drop.unwrap();
|
|
if ambulatory.target.is_none() {
|
|
ambulatory.target = Some(Vec3::new(
|
|
drop_pos.x as f32,
|
|
drop_pos.y as f32,
|
|
drop_pos.z as f32,
|
|
));
|
|
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;
|
|
*step = DropStep::Dropping { drop_pos };
|
|
}
|
|
}
|
|
DropStep::Dropping { drop_pos } => {
|
|
if let Some(cargo_entity) = haul.release() {
|
|
if tilemap_mut.place_cargo(*drop_pos, cargo_entity).is_err() {
|
|
warn!(
|
|
"place_cargo failed at {:?} — tile occupied. \
|
|
Cargo entity {:?} orphaned.",
|
|
drop_pos, cargo_entity
|
|
);
|
|
} else if let Ok(mut cargo) = cargo_query.get_mut(cargo_entity)
|
|
{
|
|
cargo.tile_pos = *drop_pos;
|
|
}
|
|
}
|
|
*step = DropStep::Done;
|
|
}
|
|
DropStep::Done => {
|
|
*state = TaskState::Completed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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: completed_task.name(),
|
|
});
|
|
}
|
|
|
|
if completed_task.is_terminal() && queue.is_empty() {
|
|
queue.push(Task::Idle {
|
|
origin,
|
|
sigma_world: behaviour.idle.sigma_world,
|
|
state: IdleState::Picking {
|
|
retry_after_tick: 0,
|
|
retry_count: 0,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
*state = TaskState::Pending;
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|