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,
/// Sprite handle for rendering when on the ground.
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.
+1
View File
@@ -93,6 +93,7 @@ pub fn spawn_log_cargo(
weight: LOG_WEIGHT_KG,
size: SIZE_LARGE,
ground_sprite: log_sprite.clone(),
delivered: false,
},
Haulable,
Sprite {
+3 -1
View File
@@ -5,6 +5,8 @@ pub mod systems;
pub use crate::world::tiles::tilemap::CargoPlaceError;
pub use components::{Cargo, CarryVisualState, HaulSlot, Haulable};
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;
+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 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::InventoryChangedEvent;
use crate::entities::shared_components::Ambulatory;
@@ -60,3 +61,16 @@ pub fn haul_encumbrance_system(
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)]
pub enum HaulStep {
/// Walking to the cargo tile.
MovingToCargo,
/// `approach` locks the target to prevent pathfinding stutter.
MovingToCargo { approach: Option<IVec3> },
/// At cargo — picking up this tick.
PickingUp,
/// 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::{
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::generation::forestry::TreePart;
use crate::world::tiles::TileMap;
@@ -84,6 +84,7 @@ pub fn demo_system(
haul_slot_query: Query<&HaulSlot>,
mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform)>,
mut task_failed: MessageReader<TaskFailed>,
mut logs_spawned: MessageReader<LogsSpawned>,
) {
// Handle task failures that should reset the demo state for retry
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 {
DemoState::Idle => {
// Find the nearest standing tree to (0,0).
@@ -345,7 +375,7 @@ pub fn demo_system(
cargo_entity: log_entity,
cargo_pos: log_pos,
dest,
step: HaulStep::MovingToCargo,
step: HaulStep::MovingToCargo { approach: None },
});
*state = TaskState::Pending;
in_progress.insert(dorf_entity);
+11
View File
@@ -6,6 +6,7 @@
use crate::entities::tasks::components::Task;
use bevy::prelude::*;
use smallvec::SmallVec;
#[derive(Message, Clone)]
pub struct TaskClaimed {
@@ -38,3 +39,13 @@ pub struct TaskBlocked {
pub task: Task,
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,
}
+107 -21
View File
@@ -14,7 +14,7 @@ use crate::entities::shared_components::Ambulatory;
use crate::entities::tasks::components::{
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::world::chunks::ChunkMap;
use crate::world::generation::forestry::{fell_tree, TreePart};
@@ -23,6 +23,7 @@ use crate::world::tiles::visibility::TileOcclusionEvent;
use crate::world::tiles::TileMap;
use bevy::prelude::*;
use bevy_rand::prelude::*;
use smallvec::SmallVec;
/// Main task executor. Runs in FixedUpdate.
pub fn task_executor_system(
@@ -47,6 +48,7 @@ pub fn task_executor_system(
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>,
) {
@@ -241,7 +243,9 @@ 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) =
crate::entities::cargo::spawn_log_cargo(
&mut commands,
&mut tilemap_mut,
@@ -249,7 +253,17 @@ pub fn task_executor_system(
fall_dir,
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;
} else {
@@ -277,7 +291,9 @@ pub fn task_executor_system(
};
match step {
HaulStep::MovingToCargo => {
HaulStep::MovingToCargo { approach } => {
use crate::constants::ITILE_SIZE;
if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) {
failed_writer.write(TaskFailed {
entity,
@@ -287,24 +303,77 @@ pub fn task_executor_system(
*state = TaskState::Failed;
continue;
}
if ambulatory.target.is_none() {
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.
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);
}
}
}
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(
cargo_pos.x as f32,
cargo_pos.y as f32,
cargo_pos.z as f32 + 1.0,
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
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;
let dist_sq = dx * dx + dy * dy;
let pickup_range_sq = (crate::constants::TILE_SIZE * 1.5)
* (crate::constants::TILE_SIZE * 1.5);
let pickup_range_sq =
(ITILE_SIZE as f32 * 1.5) * (ITILE_SIZE as f32 * 1.5);
if dist_sq <= pickup_range_sq {
ambulatory.target = None;
info!(
"[HAUL] {:?} arrived at cargo {:?}, picking up",
entity, cargo_entity
);
*step = HaulStep::PickingUp;
}
}
}
HaulStep::PickingUp => {
if haul.is_occupied() {
failed_writer.write(TaskFailed {
@@ -317,6 +386,10 @@ pub fn task_executor_system(
}
if let Some(_) = tilemap_mut.remove_cargo(cargo_pos) {
haul.pick_up(*cargo_entity);
info!(
"[HAUL] {:?} picked up {:?} → hauling to {:?}",
entity, cargo_entity, dest
);
*step = HaulStep::MovingToDest { chosen_drop: None };
} else {
failed_writer.write(TaskFailed {
@@ -337,6 +410,8 @@ pub fn task_executor_system(
}
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,
@@ -344,19 +419,28 @@ pub fn task_executor_system(
));
ambulatory.current_path = None;
}
let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO);
let dx = transform.translation.x - target_pos.x;
let dy = transform.translation.y - target_pos.y;
// 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 * 1.5)
* (crate::constants::TILE_SIZE * 1.5);
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 };
}
}
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. \
@@ -371,6 +455,7 @@ pub fn task_executor_system(
*step = HaulStep::Done;
}
HaulStep::Done => {
info!("[HAUL] {:?} haul task COMPLETE", entity);
*state = TaskState::Completed;
}
}
@@ -395,16 +480,17 @@ pub fn task_executor_system(
ambulatory.target = Some(Vec3::new(
drop_pos.x as f32,
drop_pos.y as f32,
drop_pos.z as f32 + 1.0,
drop_pos.z as f32,
));
ambulatory.current_path = None;
}
let target_pos = ambulatory.target.unwrap_or(Vec3::ZERO);
let dx = transform.translation.x - target_pos.x;
let dy = transform.translation.y - target_pos.y;
// 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 * 1.5)
* (crate::constants::TILE_SIZE * 1.5);
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 };
+2 -2
View File
@@ -5,10 +5,10 @@ pub mod executor;
pub mod idle;
pub use components::{IdleState, Task, TaskQueue, TaskState};
pub use demo::{demo_system, DemoState};
#[cfg(debug_assertions)]
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 crate::plugins::tasks::TasksPlugin;
+8 -2
View File
@@ -4,7 +4,9 @@
//! - haul_encumbrance_system (FixedUpdate, before update_encumbrance)
//! - 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::*;
pub struct CargoPlugin;
@@ -13,7 +15,11 @@ impl Plugin for CargoPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
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));
}
+3 -2
View File
@@ -6,8 +6,8 @@
//! - debug_task_queues (FixedUpdate, after demo_system, debug only)
use crate::entities::tasks::{
demo_system, task_executor_system, DemoState, TaskBlocked, TaskClaimed, TaskCompleted,
TaskDropped, TaskFailed,
demo_system, task_executor_system, DemoState, LogsSpawned, TaskBlocked, TaskClaimed,
TaskCompleted, TaskDropped, TaskFailed,
};
use bevy::prelude::*;
@@ -20,6 +20,7 @@ impl Plugin for TasksPlugin {
.add_message::<TaskFailed>()
.add_message::<TaskDropped>()
.add_message::<TaskBlocked>()
.add_message::<LogsSpawned>()
.init_resource::<DemoState>()
.add_systems(
bevy::app::FixedUpdate,