Task fix attempt

This commit is contained in:
2026-03-23 19:31:30 +00:00
parent 3632836016
commit 1f5dfe5182
11 changed files with 203 additions and 48 deletions
+3
View File
@@ -49,6 +49,9 @@ pub struct Cargo {
pub size: u8, pub size: u8,
/// Sprite handle for rendering when on the ground. /// Sprite handle for rendering when on the ground.
pub ground_sprite: Handle<Image>, pub ground_sprite: Handle<Image>,
/// True once this cargo has been hauled to its destination.
/// Used to prevent re-hauling logs that have already been delivered.
pub delivered: bool,
} }
/// Marks a Cargo entity as haulable by entities with a HaulSlot. /// Marks a Cargo entity as haulable by entities with a HaulSlot.
+1
View File
@@ -93,6 +93,7 @@ pub fn spawn_log_cargo(
weight: LOG_WEIGHT_KG, weight: LOG_WEIGHT_KG,
size: SIZE_LARGE, size: SIZE_LARGE,
ground_sprite: log_sprite.clone(), ground_sprite: log_sprite.clone(),
delivered: false,
}, },
Haulable, Haulable,
Sprite { Sprite {
+3 -1
View File
@@ -5,6 +5,8 @@ pub mod systems;
pub use crate::world::tiles::tilemap::CargoPlaceError; pub use crate::world::tiles::tilemap::CargoPlaceError;
pub use components::{Cargo, CarryVisualState, HaulSlot, Haulable}; pub use components::{Cargo, CarryVisualState, HaulSlot, Haulable};
pub use log::spawn_log_cargo; pub use log::spawn_log_cargo;
pub use systems::{any_hauling, carry_visual_system, haul_encumbrance_system}; pub use systems::{
any_hauling, cargo_position_sync_system, carry_visual_system, haul_encumbrance_system,
};
pub use crate::plugins::cargo::CargoPlugin; pub use crate::plugins::cargo::CargoPlugin;
+16 -2
View File
@@ -1,8 +1,9 @@
//! Cargo systems: carry visual cycling and haul encumbrance. //! Cargo systems: carry visual cycling, position sync, and haul encumbrance.
use bevy::prelude::*; use bevy::prelude::*;
use crate::entities::cargo::components::{CarryVisualState, HaulSlot}; use crate::entities::cargo::components::{Cargo, CarryVisualState, HaulSlot};
use crate::entities::item::constants::ITEM_Z_FIGHTING_OFFSET;
use crate::entities::item::inventory::constants::ENCUMBERED_SPEED_MULTIPLIER; use crate::entities::item::inventory::constants::ENCUMBERED_SPEED_MULTIPLIER;
use crate::entities::item::inventory::InventoryChangedEvent; use crate::entities::item::inventory::InventoryChangedEvent;
use crate::entities::shared_components::Ambulatory; use crate::entities::shared_components::Ambulatory;
@@ -60,3 +61,16 @@ pub fn haul_encumbrance_system(
events.write(InventoryChangedEvent { carrier: entity }); events.write(InventoryChangedEvent { carrier: entity });
} }
} }
/// Sync cargo visual position when tile_pos changes.
/// When cargo is picked up or dropped, tile_pos updates but Transform doesn't.
/// This system keeps Transform in sync with the logical position.
pub fn cargo_position_sync_system(mut query: Query<(&Cargo, &mut Transform), Changed<Cargo>>) {
for (cargo, mut transform) in query.iter_mut() {
transform.translation = Vec3::new(
cargo.tile_pos.x as f32,
cargo.tile_pos.y as f32,
cargo.tile_pos.z as f32 - ITEM_Z_FIGHTING_OFFSET,
);
}
}
+2 -1
View File
@@ -55,7 +55,8 @@ pub enum ChopStep {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum HaulStep { pub enum HaulStep {
/// Walking to the cargo tile. /// Walking to the cargo tile.
MovingToCargo, /// `approach` locks the target to prevent pathfinding stutter.
MovingToCargo { approach: Option<IVec3> },
/// At cargo — picking up this tick. /// At cargo — picking up this tick.
PickingUp, PickingUp,
/// Cargo in HaulSlot — walking to destination. /// Cargo in HaulSlot — walking to destination.
+32 -2
View File
@@ -30,7 +30,7 @@ use crate::entities::cargo::{Cargo, HaulSlot, Haulable};
use crate::entities::tasks::components::{ use crate::entities::tasks::components::{
ChopStep, HaulStep, Task, TaskQueue, TaskState, CHOP_TICKS_DEFAULT, ChopStep, HaulStep, Task, TaskQueue, TaskState, CHOP_TICKS_DEFAULT,
}; };
use crate::entities::tasks::events::TaskFailed; use crate::entities::tasks::events::{LogsSpawned, TaskFailed};
use crate::world::chunks::ChunkMap; use crate::world::chunks::ChunkMap;
use crate::world::generation::forestry::TreePart; use crate::world::generation::forestry::TreePart;
use crate::world::tiles::TileMap; use crate::world::tiles::TileMap;
@@ -84,6 +84,7 @@ pub fn demo_system(
haul_slot_query: Query<&HaulSlot>, haul_slot_query: Query<&HaulSlot>,
mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform)>, mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform)>,
mut task_failed: MessageReader<TaskFailed>, mut task_failed: MessageReader<TaskFailed>,
mut logs_spawned: MessageReader<LogsSpawned>,
) { ) {
// Handle task failures that should reset the demo state for retry // Handle task failures that should reset the demo state for retry
for event in task_failed.read() { for event in task_failed.read() {
@@ -98,6 +99,35 @@ pub fn demo_system(
} }
} }
// Handle logs spawned from felling — transition to Hauling
// Collect events first to avoid double-mutable borrow conflict with demo_state
let pending_logs: Vec<_> = logs_spawned.read().collect();
for event in pending_logs {
if let DemoState::Chopping {
trunk_pos,
chopper: _,
} = *demo_state
{
// Look up cargo positions from the newly spawned entities
let mut unassigned: VecDeque<(Entity, IVec3)> = VecDeque::new();
for &log_entity in event.log_entities.iter() {
if let Ok((_, cargo)) = cargo_query.get(log_entity) {
unassigned.push_back((log_entity, cargo.tile_pos));
}
}
info!(
"[DEMO] → Hauling: {} logs spawned from tree at {:?}",
unassigned.len(),
trunk_pos
);
*demo_state = DemoState::Hauling {
felled_trunk_pos: trunk_pos,
unassigned,
in_progress: Default::default(),
};
}
}
match &mut *demo_state { match &mut *demo_state {
DemoState::Idle => { DemoState::Idle => {
// Find the nearest standing tree to (0,0). // Find the nearest standing tree to (0,0).
@@ -345,7 +375,7 @@ pub fn demo_system(
cargo_entity: log_entity, cargo_entity: log_entity,
cargo_pos: log_pos, cargo_pos: log_pos,
dest, dest,
step: HaulStep::MovingToCargo, step: HaulStep::MovingToCargo { approach: None },
}); });
*state = TaskState::Pending; *state = TaskState::Pending;
in_progress.insert(dorf_entity); in_progress.insert(dorf_entity);
+11
View File
@@ -6,6 +6,7 @@
use crate::entities::tasks::components::Task; use crate::entities::tasks::components::Task;
use bevy::prelude::*; use bevy::prelude::*;
use smallvec::SmallVec;
#[derive(Message, Clone)] #[derive(Message, Clone)]
pub struct TaskClaimed { pub struct TaskClaimed {
@@ -38,3 +39,13 @@ pub struct TaskBlocked {
pub task: Task, pub task: Task,
pub blocker: &'static str, pub blocker: &'static str,
} }
/// Fired when logs are spawned from felling a tree.
/// The demo listens for this to queue HaulCargo tasks.
#[derive(Message, Clone)]
pub struct LogsSpawned {
/// Entities of the spawned logs.
pub log_entities: SmallVec<[Entity; 8]>,
/// Destination for hauling (currently origin, later stockpile).
pub dest: IVec3,
}
+122 -36
View File
@@ -14,7 +14,7 @@ use crate::entities::shared_components::Ambulatory;
use crate::entities::tasks::components::{ use crate::entities::tasks::components::{
ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState, ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState,
}; };
use crate::entities::tasks::events::{TaskClaimed, TaskCompleted, TaskFailed}; use crate::entities::tasks::events::{LogsSpawned, TaskClaimed, TaskCompleted, TaskFailed};
use crate::entities::tasks::idle::execute_idle; use crate::entities::tasks::idle::execute_idle;
use crate::world::chunks::ChunkMap; use crate::world::chunks::ChunkMap;
use crate::world::generation::forestry::{fell_tree, TreePart}; use crate::world::generation::forestry::{fell_tree, TreePart};
@@ -23,6 +23,7 @@ use crate::world::tiles::visibility::TileOcclusionEvent;
use crate::world::tiles::TileMap; use crate::world::tiles::TileMap;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rand::prelude::*; use bevy_rand::prelude::*;
use smallvec::SmallVec;
/// Main task executor. Runs in FixedUpdate. /// Main task executor. Runs in FixedUpdate.
pub fn task_executor_system( pub fn task_executor_system(
@@ -47,6 +48,7 @@ pub fn task_executor_system(
mut claimed_writer: MessageWriter<TaskClaimed>, mut claimed_writer: MessageWriter<TaskClaimed>,
mut completed_writer: MessageWriter<TaskCompleted>, mut completed_writer: MessageWriter<TaskCompleted>,
mut failed_writer: MessageWriter<TaskFailed>, mut failed_writer: MessageWriter<TaskFailed>,
mut logs_spawned_writer: MessageWriter<LogsSpawned>,
mut tile_changed: MessageWriter<TileChangedEvent>, mut tile_changed: MessageWriter<TileChangedEvent>,
mut occlusion: MessageWriter<TileOcclusionEvent>, mut occlusion: MessageWriter<TileOcclusionEvent>,
) { ) {
@@ -241,15 +243,27 @@ pub fn task_executor_system(
fall_dir = Vec2::new(1.0, 0.0); // default: fall east fall_dir = Vec2::new(1.0, 0.0); // default: fall east
} }
let log_sprite: Handle<Image> = asset_server.load("log_cargo.png"); 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() { for &pos in trunk_positions.iter() {
crate::entities::cargo::spawn_log_cargo( if let Some(log_entity) =
&mut commands, crate::entities::cargo::spawn_log_cargo(
&mut tilemap_mut, &mut commands,
pos, &mut tilemap_mut,
fall_dir, pos,
log_sprite.clone(), fall_dir,
&mut rng, log_sprite.clone(),
); &mut rng,
)
{
log_entities.push(log_entity);
}
}
// Emit event so demo can queue HaulCargo tasks for these logs
if !log_entities.is_empty() {
logs_spawned_writer.write(LogsSpawned {
log_entities,
dest: IVec3::ZERO,
});
} }
*step = ChopStep::Done; *step = ChopStep::Done;
} else { } else {
@@ -277,7 +291,9 @@ pub fn task_executor_system(
}; };
match step { match step {
HaulStep::MovingToCargo => { HaulStep::MovingToCargo { approach } => {
use crate::constants::ITILE_SIZE;
if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) { if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) {
failed_writer.write(TaskFailed { failed_writer.write(TaskFailed {
entity, entity,
@@ -287,22 +303,75 @@ pub fn task_executor_system(
*state = TaskState::Failed; *state = TaskState::Failed;
continue; continue;
} }
if ambulatory.target.is_none() {
ambulatory.target = Some(Vec3::new( if approach.is_none() {
cargo_pos.x as f32, // Search for nearest standable tile to approach cargo.
cargo_pos.y as f32, // Radius 0 = cargo tile itself (can stand in same tile as cargo).
cargo_pos.z as f32 + 1.0, // Radius 1-2 = adjacent tiles if cargo tile is blocked.
)); let cargo_z = cargo_pos.z;
ambulatory.current_path = None; 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);
}
}
}
None
});
match approach_tile {
Some(tile) => {
*approach = Some(tile);
info!(
"[HAUL] {:?} target set: cargo={:?} approach={:?}",
entity, cargo_pos, tile
);
// +1.0 z-offset for entity standing height (same as trees)
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;
}
}
} }
let dx = transform.translation.x - cargo_pos.x as f32;
let dy = transform.translation.y - cargo_pos.y as f32; // Check arrival at cargo tile
let dist_sq = dx * dx + dy * dy; if approach.is_some() && ambulatory.target.is_none() {
let pickup_range_sq = (crate::constants::TILE_SIZE * 1.5) let dx = transform.translation.x - cargo_pos.x as f32;
* (crate::constants::TILE_SIZE * 1.5); let dy = transform.translation.y - cargo_pos.y as f32;
if dist_sq <= pickup_range_sq { let dist_sq = dx * dx + dy * dy;
ambulatory.target = None; let pickup_range_sq =
*step = HaulStep::PickingUp; (ITILE_SIZE as f32 * 1.5) * (ITILE_SIZE as f32 * 1.5);
if dist_sq <= pickup_range_sq {
info!(
"[HAUL] {:?} arrived at cargo {:?}, picking up",
entity, cargo_entity
);
*step = HaulStep::PickingUp;
}
} }
} }
HaulStep::PickingUp => { HaulStep::PickingUp => {
@@ -317,6 +386,10 @@ pub fn task_executor_system(
} }
if let Some(_) = tilemap_mut.remove_cargo(cargo_pos) { if let Some(_) = tilemap_mut.remove_cargo(cargo_pos) {
haul.pick_up(*cargo_entity); haul.pick_up(*cargo_entity);
info!(
"[HAUL] {:?} picked up {:?} → hauling to {:?}",
entity, cargo_entity, dest
);
*step = HaulStep::MovingToDest { chosen_drop: None }; *step = HaulStep::MovingToDest { chosen_drop: None };
} else { } else {
failed_writer.write(TaskFailed { failed_writer.write(TaskFailed {
@@ -337,6 +410,8 @@ pub fn task_executor_system(
} }
let drop_pos = chosen_drop.unwrap(); let drop_pos = chosen_drop.unwrap();
if ambulatory.target.is_none() { 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( ambulatory.target = Some(Vec3::new(
drop_pos.x as f32, drop_pos.x as f32,
drop_pos.y as f32, drop_pos.y as f32,
@@ -344,19 +419,28 @@ pub fn task_executor_system(
)); ));
ambulatory.current_path = None; ambulatory.current_path = None;
} }
let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO);
let dx = transform.translation.x - target_pos.x; // Always check arrival distance, not gated by target status
let dy = transform.translation.y - target_pos.y; 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 dist_sq = dx * dx + dy * dy;
let arrive_sq = (crate::constants::TILE_SIZE * 1.5) let arrive_sq = (crate::constants::TILE_SIZE as f32 * 1.5)
* (crate::constants::TILE_SIZE * 1.5); * (crate::constants::TILE_SIZE as f32 * 1.5);
if dist_sq <= arrive_sq { if dist_sq <= arrive_sq {
ambulatory.target = None; ambulatory.target = None;
info!(
"[HAUL] {:?} arrived at drop point {:?}",
entity, drop_pos
);
*step = HaulStep::Dropping { drop_pos }; *step = HaulStep::Dropping { drop_pos };
} }
} }
HaulStep::Dropping { drop_pos } => { HaulStep::Dropping { drop_pos } => {
if let Some(cargo_entity) = haul.release() { 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() { if tilemap_mut.place_cargo(*drop_pos, cargo_entity).is_err() {
warn!( warn!(
"place_cargo failed at {:?} — tile occupied. \ "place_cargo failed at {:?} — tile occupied. \
@@ -371,6 +455,7 @@ pub fn task_executor_system(
*step = HaulStep::Done; *step = HaulStep::Done;
} }
HaulStep::Done => { HaulStep::Done => {
info!("[HAUL] {:?} haul task COMPLETE", entity);
*state = TaskState::Completed; *state = TaskState::Completed;
} }
} }
@@ -395,16 +480,17 @@ pub fn task_executor_system(
ambulatory.target = Some(Vec3::new( ambulatory.target = Some(Vec3::new(
drop_pos.x as f32, drop_pos.x as f32,
drop_pos.y as f32, drop_pos.y as f32,
drop_pos.z as f32 + 1.0, drop_pos.z as f32,
)); ));
ambulatory.current_path = None; ambulatory.current_path = None;
} }
let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO);
let dx = transform.translation.x - target_pos.x; // Always check arrival distance, not gated by target status
let dy = transform.translation.y - target_pos.y; 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 dist_sq = dx * dx + dy * dy;
let arrive_sq = (crate::constants::TILE_SIZE * 1.5) let arrive_sq = (crate::constants::TILE_SIZE as f32 * 1.5)
* (crate::constants::TILE_SIZE * 1.5); * (crate::constants::TILE_SIZE as f32 * 1.5);
if dist_sq <= arrive_sq { if dist_sq <= arrive_sq {
ambulatory.target = None; ambulatory.target = None;
*step = DropStep::Dropping { drop_pos }; *step = DropStep::Dropping { drop_pos };
+2 -2
View File
@@ -5,10 +5,10 @@ pub mod executor;
pub mod idle; pub mod idle;
pub use components::{IdleState, Task, TaskQueue, TaskState}; pub use components::{IdleState, Task, TaskQueue, TaskState};
pub use demo::{demo_system, DemoState};
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub use demo::debug_task_queues; pub use demo::debug_task_queues;
pub use events::{TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed}; pub use demo::{demo_system, DemoState};
pub use events::{LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
pub use executor::task_executor_system; pub use executor::task_executor_system;
pub use crate::plugins::tasks::TasksPlugin; pub use crate::plugins::tasks::TasksPlugin;
+8 -2
View File
@@ -4,7 +4,9 @@
//! - haul_encumbrance_system (FixedUpdate, before update_encumbrance) //! - haul_encumbrance_system (FixedUpdate, before update_encumbrance)
//! - carry_visual_system (Update, run_if any_hauling) //! - carry_visual_system (Update, run_if any_hauling)
use crate::entities::cargo::{any_hauling, carry_visual_system, haul_encumbrance_system}; use crate::entities::cargo::{
any_hauling, cargo_position_sync_system, carry_visual_system, haul_encumbrance_system,
};
use bevy::prelude::*; use bevy::prelude::*;
pub struct CargoPlugin; pub struct CargoPlugin;
@@ -13,7 +15,11 @@ impl Plugin for CargoPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems( app.add_systems(
FixedUpdate, FixedUpdate,
haul_encumbrance_system.before(crate::entities::item::inventory::update_encumbrance), (
haul_encumbrance_system
.before(crate::entities::item::inventory::update_encumbrance),
cargo_position_sync_system,
),
) )
.add_systems(Update, carry_visual_system.run_if(any_hauling)); .add_systems(Update, carry_visual_system.run_if(any_hauling));
} }
+3 -2
View File
@@ -6,8 +6,8 @@
//! - debug_task_queues (FixedUpdate, after demo_system, debug only) //! - debug_task_queues (FixedUpdate, after demo_system, debug only)
use crate::entities::tasks::{ use crate::entities::tasks::{
demo_system, task_executor_system, DemoState, TaskBlocked, TaskClaimed, TaskCompleted, demo_system, task_executor_system, DemoState, LogsSpawned, TaskBlocked, TaskClaimed,
TaskDropped, TaskFailed, TaskCompleted, TaskDropped, TaskFailed,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -20,6 +20,7 @@ impl Plugin for TasksPlugin {
.add_message::<TaskFailed>() .add_message::<TaskFailed>()
.add_message::<TaskDropped>() .add_message::<TaskDropped>()
.add_message::<TaskBlocked>() .add_message::<TaskBlocked>()
.add_message::<LogsSpawned>()
.init_resource::<DemoState>() .init_resource::<DemoState>()
.add_systems( .add_systems(
bevy::app::FixedUpdate, bevy::app::FixedUpdate,