fix
This commit is contained in:
+103
-66
@@ -22,6 +22,8 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::constants::ITILE_SIZE;
|
use crate::constants::ITILE_SIZE;
|
||||||
use crate::entities::cargo::{Cargo, Haulable};
|
use crate::entities::cargo::{Cargo, Haulable};
|
||||||
@@ -48,11 +50,21 @@ pub enum DemoState {
|
|||||||
/// A ChopTree task has been assigned to `chopper`.
|
/// A ChopTree task has been assigned to `chopper`.
|
||||||
/// `trunk_pos` is the lowest trunk tile of the target tree.
|
/// `trunk_pos` is the lowest trunk tile of the target tree.
|
||||||
Chopping { trunk_pos: IVec3, chopper: Entity },
|
Chopping { trunk_pos: IVec3, chopper: Entity },
|
||||||
/// Tree has been felled. Waiting for all visible logs to be claimed/hauled.
|
/// Tree has been felled. Working through the haul queue.
|
||||||
/// Tracks which log entities have been assigned haul tasks.
|
///
|
||||||
|
/// `unassigned` — logs not yet claimed, ordered nearest-to-origin first.
|
||||||
|
/// `in_progress` — log entities currently assigned to a dorf.
|
||||||
|
///
|
||||||
|
/// Each tick: assign idle dorfs from unassigned. When a log entity
|
||||||
|
/// disappears from cargo_tiles it has been dropped at destination —
|
||||||
|
/// remove it from in_progress. When both are empty, go Idle.
|
||||||
Hauling {
|
Hauling {
|
||||||
felled_trunk_pos: IVec3,
|
felled_trunk_pos: IVec3,
|
||||||
assigned_logs: FxHashSet<Entity>,
|
/// Queue of (log_entity, log_tile_pos) not yet assigned.
|
||||||
|
/// Front = highest priority (nearest to origin).
|
||||||
|
unassigned: VecDeque<(Entity, IVec3)>,
|
||||||
|
/// Log entities currently being hauled by a dorf.
|
||||||
|
in_progress: FxHashSet<Entity>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +73,7 @@ pub enum DemoState {
|
|||||||
/// State transitions:
|
/// State transitions:
|
||||||
/// Idle → Chopping: found a tree, assigned ChopTree to one idle dorf
|
/// Idle → Chopping: found a tree, assigned ChopTree to one idle dorf
|
||||||
/// Chopping → Hauling: trunk no longer in fixture_tiles (tree felled)
|
/// Chopping → Hauling: trunk no longer in fixture_tiles (tree felled)
|
||||||
/// Hauling → Idle: all assigned logs cleared from cargo_tiles (hauled)
|
/// Hauling → Idle: both unassigned and in_progress are empty
|
||||||
///
|
///
|
||||||
/// Performance: scans fixture_tiles once per state transition (infrequent),
|
/// Performance: scans fixture_tiles once per state transition (infrequent),
|
||||||
/// not per tick. During Chopping and Hauling states the system does O(1) checks.
|
/// not per tick. During Chopping and Hauling states the system does O(1) checks.
|
||||||
@@ -73,7 +85,7 @@ pub fn demo_system(
|
|||||||
cargo_query: Query<(Entity, &Cargo), With<Haulable>>,
|
cargo_query: Query<(Entity, &Cargo), With<Haulable>>,
|
||||||
mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform)>,
|
mut dorf_query: Query<(Entity, &mut TaskQueue, &mut TaskState, &Transform)>,
|
||||||
) {
|
) {
|
||||||
match &*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).
|
||||||
// A "tree" is identified by a TreePart with is_trunk=true whose
|
// A "tree" is identified by a TreePart with is_trunk=true whose
|
||||||
@@ -114,7 +126,6 @@ pub fn demo_system(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Find the lowest trunk tile (minimum z) for this tree's XY column.
|
// Find the lowest trunk tile (minimum z) for this tree's XY column.
|
||||||
// fell_tree is called with the lowest trunk pos.
|
|
||||||
let lowest_trunk = tree_parts
|
let lowest_trunk = tree_parts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, p)| {
|
.filter(|(_, p)| {
|
||||||
@@ -148,7 +159,6 @@ pub fn demo_system(
|
|||||||
|
|
||||||
// Assign ChopTree task
|
// Assign ChopTree task
|
||||||
if let Ok((_, mut queue, mut state, _)) = dorf_query.get_mut(chopper) {
|
if let Ok((_, mut queue, mut state, _)) = dorf_query.get_mut(chopper) {
|
||||||
// Clear current idle task and push ChopTree
|
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.push(Task::ChopTree {
|
queue.push(Task::ChopTree {
|
||||||
trunk_pos: lowest_trunk,
|
trunk_pos: lowest_trunk,
|
||||||
@@ -182,7 +192,6 @@ pub fn demo_system(
|
|||||||
|t| matches!(t, Task::ChopTree { trunk_pos: tp, .. } if *tp == trunk_pos),
|
|t| matches!(t, Task::ChopTree { trunk_pos: tp, .. } if *tp == trunk_pos),
|
||||||
);
|
);
|
||||||
if !still_chopping && queue.is_empty() {
|
if !still_chopping && queue.is_empty() {
|
||||||
// Chopper abandoned — reassign
|
|
||||||
*demo_state = DemoState::Idle;
|
*demo_state = DemoState::Idle;
|
||||||
warn!("Demo: chopper {:?} abandoned ChopTree — resetting", chopper);
|
warn!("Demo: chopper {:?} abandoned ChopTree — resetting", chopper);
|
||||||
}
|
}
|
||||||
@@ -195,44 +204,103 @@ pub fn demo_system(
|
|||||||
|
|
||||||
// Find all Cargo logs near the trunk position
|
// Find all Cargo logs near the trunk position
|
||||||
let search_world = LOG_SEARCH_RADIUS_TILES * ITILE_SIZE;
|
let search_world = LOG_SEARCH_RADIUS_TILES * ITILE_SIZE;
|
||||||
let logs: Vec<(Entity, IVec3)> = cargo_query
|
let mut logs: SmallVec<[(Entity, IVec3, i32); 8]> = cargo_query
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, cargo)| {
|
.filter(|(_, cargo)| {
|
||||||
cargo.name == "log"
|
cargo.name == "log"
|
||||||
&& (cargo.tile_pos.x - trunk_pos.x).abs() <= search_world
|
&& (cargo.tile_pos.x - trunk_pos.x).abs() <= search_world
|
||||||
&& (cargo.tile_pos.y - trunk_pos.y).abs() <= search_world
|
&& (cargo.tile_pos.y - trunk_pos.y).abs() <= search_world
|
||||||
})
|
})
|
||||||
.map(|(e, cargo)| (e, cargo.tile_pos))
|
.map(|(e, cargo)| {
|
||||||
|
// Sort key: Chebyshev distance from origin
|
||||||
|
let dx = cargo.tile_pos.x.abs() / ITILE_SIZE;
|
||||||
|
let dy = cargo.tile_pos.y.abs() / ITILE_SIZE;
|
||||||
|
(e, cargo.tile_pos, dx.max(dy))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Nearest to origin first — dorfs haul the closest logs first
|
||||||
|
logs.sort_by_key(|(_, _, dist)| *dist);
|
||||||
|
|
||||||
if logs.is_empty() {
|
if logs.is_empty() {
|
||||||
// No logs found — tree may have had no trunks or logs all orphaned
|
|
||||||
warn!("Demo: no logs found after felling {:?}", trunk_pos);
|
warn!("Demo: no logs found after felling {:?}", trunk_pos);
|
||||||
*demo_state = DemoState::Idle;
|
*demo_state = DemoState::Idle;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find haul destination — nearest standable tile to (0,0)
|
// Convert to VecDeque, dropping sort key
|
||||||
|
let unassigned: VecDeque<(Entity, IVec3)> =
|
||||||
|
logs.into_iter().map(|(e, pos, _)| (e, pos)).collect();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Demo: {} logs to haul from {:?}",
|
||||||
|
unassigned.len(),
|
||||||
|
trunk_pos
|
||||||
|
);
|
||||||
|
|
||||||
|
*demo_state = DemoState::Hauling {
|
||||||
|
felled_trunk_pos: trunk_pos,
|
||||||
|
unassigned,
|
||||||
|
in_progress: FxHashSet::default(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DemoState::Hauling {
|
||||||
|
felled_trunk_pos,
|
||||||
|
unassigned,
|
||||||
|
in_progress,
|
||||||
|
} => {
|
||||||
|
let felled_trunk_pos = *felled_trunk_pos;
|
||||||
|
|
||||||
|
// Remove completed hauls from in_progress —
|
||||||
|
// a log is done when it's no longer in cargo_tiles (picked up by dorf).
|
||||||
|
// This is correct: once picked up, the haul is committed from demo's perspective.
|
||||||
|
in_progress.retain(|&log_entity| {
|
||||||
|
cargo_query
|
||||||
|
.get(log_entity)
|
||||||
|
.map(|(_, cargo)| tilemap.cargo_tiles.contains_key(&cargo.tile_pos))
|
||||||
|
.unwrap_or(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign idle dorfs to unassigned logs
|
||||||
|
if !unassigned.is_empty() {
|
||||||
|
// Collect idle dorfs sorted by proximity to front of log queue
|
||||||
|
let next_log_pos = unassigned.front().map(|(_, p)| *p).unwrap_or(IVec3::ZERO);
|
||||||
|
|
||||||
|
let mut idle_dorfs: SmallVec<[(Entity, i32); 8]> = dorf_query
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, queue, state, _)| is_idle_dorf(queue, state))
|
||||||
|
.map(|(e, _, _, transform)| {
|
||||||
|
let pos = transform.translation.as_ivec3();
|
||||||
|
let dx = (pos.x - next_log_pos.x).abs() / ITILE_SIZE;
|
||||||
|
let dy = (pos.y - next_log_pos.y).abs() / ITILE_SIZE;
|
||||||
|
(e, dx.max(dy))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// Sort by distance — nearest dorf gets nearest log
|
||||||
|
idle_dorfs.sort_by_key(|(_, d)| *d);
|
||||||
|
// Drop distance, keep entity
|
||||||
|
let idle_dorfs: SmallVec<[Entity; 8]> =
|
||||||
|
idle_dorfs.into_iter().map(|(e, _)| e).collect();
|
||||||
|
|
||||||
|
for dorf_entity in idle_dorfs {
|
||||||
|
// Compute haul destination fresh for each assignment —
|
||||||
|
// as logs accumulate near origin, each new assignment correctly
|
||||||
|
// finds the next free tile.
|
||||||
let dest = tilemap
|
let dest = tilemap
|
||||||
.find_nearest_free_cargo_tile(IVec3::ZERO, HAUL_DEST_SEARCH_RADIUS)
|
.find_nearest_free_cargo_tile(IVec3::ZERO, HAUL_DEST_SEARCH_RADIUS)
|
||||||
.unwrap_or(IVec3::ZERO);
|
.unwrap_or(IVec3::ZERO);
|
||||||
|
|
||||||
// Assign one HaulCargo task per log to idle dorfs
|
let Some((log_entity, log_pos)) = unassigned.pop_front() else {
|
||||||
let mut assigned_logs: FxHashSet<Entity> = FxHashSet::default();
|
break;
|
||||||
let mut logs_iter = logs.iter();
|
|
||||||
|
|
||||||
// Re-collect idle dorfs (mutable query needed)
|
|
||||||
// Must collect entities first to avoid double-borrow
|
|
||||||
let idle_dorfs: Vec<Entity> = dorf_query
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, queue, state, _)| is_idle_dorf(queue, state))
|
|
||||||
.map(|(e, _, _, _)| e)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for dorf_entity in idle_dorfs {
|
|
||||||
let Some(&(log_entity, log_pos)) = logs_iter.next() else {
|
|
||||||
break; // more dorfs than logs — remaining dorfs stay idle
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Verify log still exists and is in cargo_tiles before assigning
|
||||||
|
if !tilemap.cargo_tiles.contains_key(&log_pos) {
|
||||||
|
// Log already picked up by someone else — skip it
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok((_, mut queue, mut state, _)) = dorf_query.get_mut(dorf_entity) {
|
if let Ok((_, mut queue, mut state, _)) = dorf_query.get_mut(dorf_entity) {
|
||||||
queue.clear();
|
queue.clear();
|
||||||
queue.push(Task::HaulCargo {
|
queue.push(Task::HaulCargo {
|
||||||
@@ -242,54 +310,23 @@ pub fn demo_system(
|
|||||||
step: HaulStep::MovingToCargo,
|
step: HaulStep::MovingToCargo,
|
||||||
});
|
});
|
||||||
*state = TaskState::Pending;
|
*state = TaskState::Pending;
|
||||||
assigned_logs.insert(log_entity);
|
in_progress.insert(log_entity);
|
||||||
|
} else {
|
||||||
|
// Couldn't assign — put log back at front of queue
|
||||||
|
unassigned.push_front((log_entity, log_pos));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any remaining unassigned logs stay on the floor — future dorfs
|
// Check if all work is done
|
||||||
// will be assigned when they become idle if the demo loops.
|
if unassigned.is_empty() && in_progress.is_empty() {
|
||||||
// For simplicity: log unassigned count but don't retry this tick.
|
|
||||||
let unassigned = logs.len().saturating_sub(assigned_logs.len());
|
|
||||||
if unassigned > 0 {
|
|
||||||
info!(
|
|
||||||
"Demo: {} logs unassigned (not enough idle dorfs)",
|
|
||||||
unassigned
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
*demo_state = DemoState::Hauling {
|
|
||||||
felled_trunk_pos: trunk_pos,
|
|
||||||
assigned_logs,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
DemoState::Hauling {
|
|
||||||
felled_trunk_pos,
|
|
||||||
assigned_logs,
|
|
||||||
} => {
|
|
||||||
let felled_trunk_pos = *felled_trunk_pos;
|
|
||||||
|
|
||||||
// Check if all assigned logs have been hauled (removed from cargo_tiles)
|
|
||||||
// Check via cargo_query: if the Cargo entity still exists
|
|
||||||
// and is still in cargo_tiles, it hasn't been hauled yet.
|
|
||||||
let remaining = assigned_logs
|
|
||||||
.iter()
|
|
||||||
.filter(|&&log_entity| {
|
|
||||||
cargo_query
|
|
||||||
.get(log_entity)
|
|
||||||
.map(|(_, cargo)| tilemap.cargo_tiles.contains_key(&cargo.tile_pos))
|
|
||||||
.unwrap_or(false)
|
|
||||||
})
|
|
||||||
.count();
|
|
||||||
|
|
||||||
if remaining == 0 {
|
|
||||||
info!(
|
info!(
|
||||||
"Demo: all logs hauled from {:?}, finding next tree",
|
"Demo: all logs hauled from {:?}, finding next tree",
|
||||||
felled_trunk_pos
|
felled_trunk_pos
|
||||||
);
|
);
|
||||||
*demo_state = DemoState::Idle;
|
*demo_state = DemoState::Idle;
|
||||||
}
|
}
|
||||||
// else: still hauling, check again next tick (cheap)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ pub fn task_executor_system(
|
|||||||
let chopper_pos = transform.translation.truncate(); // XY only
|
let chopper_pos = transform.translation.truncate(); // XY only
|
||||||
let trunk_xy = Vec2::new(trunk_pos.x as f32, trunk_pos.y as f32);
|
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();
|
let mut fall_dir = (trunk_xy - chopper_pos).normalize_or_zero();
|
||||||
// If dorf is standing on the trunk (distance ~0), fall direction is arbitrary
|
// 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 {
|
if fall_dir == Vec2::ZERO {
|
||||||
fall_dir = Vec2::new(1.0, 0.0); // default: fall east
|
fall_dir = Vec2::new(1.0, 0.0); // default: fall east
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user