attempt 3
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
use image::{GenericImageView, ImageBuffer, Rgba};
|
use image::{ImageBuffer, Rgba};
|
||||||
|
|
||||||
const TILE_PX: u32 = 16;
|
const TILE_PX: u32 = 16;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,6 +4,6 @@ initial_chunk_radius = 6
|
|||||||
vsync = "mailbox"
|
vsync = "mailbox"
|
||||||
|
|
||||||
[spawn_counts]
|
[spawn_counts]
|
||||||
dorfs = 2
|
dorfs = 5
|
||||||
pigs = 0
|
pigs = 0
|
||||||
rabbits = 0
|
rabbits = 0
|
||||||
+27
-33
@@ -20,20 +20,6 @@ use crate::world::VisibleGameEntity;
|
|||||||
/// Weight of a single log in kg. Enough to encumber a dorf carrying one.
|
/// Weight of a single log in kg. Enough to encumber a dorf carrying one.
|
||||||
pub const LOG_WEIGHT_KG: u32 = 15;
|
pub const LOG_WEIGHT_KG: u32 = 15;
|
||||||
|
|
||||||
/// Find the standable surface tile at world XY. Returns the floor tile
|
|
||||||
/// where an entity can stand, or None if not found.
|
|
||||||
/// The standable position IS the floor tile itself (has can_stand_in=true),
|
|
||||||
/// not the air above it.
|
|
||||||
fn find_surface_at(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option<IVec3> {
|
|
||||||
for z in -3i32..=4i32 {
|
|
||||||
let floor_pos = IVec3::new(world_x, world_y, z * ITILE_SIZE);
|
|
||||||
if tilemap.floor_tiles.contains_key(&floor_pos) && tilemap.is_standable(floor_pos) {
|
|
||||||
return Some(floor_pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn a Cargo log entity with a directional fall bias.
|
/// Spawn a Cargo log entity with a directional fall bias.
|
||||||
///
|
///
|
||||||
/// `tile_pos` — world position of the trunk tile this log came from.
|
/// `tile_pos` — world position of the trunk tile this log came from.
|
||||||
@@ -56,7 +42,7 @@ pub fn spawn_log_cargo(
|
|||||||
fall_direction: Vec2,
|
fall_direction: Vec2,
|
||||||
log_sprite: Handle<Image>,
|
log_sprite: Handle<Image>,
|
||||||
rng: &mut WyRand,
|
rng: &mut WyRand,
|
||||||
) -> Option<Entity> {
|
) -> Option<(Entity, IVec3)> {
|
||||||
// Distance along fall direction: 0–3 tiles
|
// Distance along fall direction: 0–3 tiles
|
||||||
let fall_tiles = rng.random_range(0u32..=3) as f32;
|
let fall_tiles = rng.random_range(0u32..=3) as f32;
|
||||||
|
|
||||||
@@ -65,26 +51,34 @@ pub fn spawn_log_cargo(
|
|||||||
let jitter_tiles = rng.random_range(0u32..=2) as f32 - 1.0; // -1, 0, or +1
|
let jitter_tiles = rng.random_range(0u32..=2) as f32 - 1.0; // -1, 0, or +1
|
||||||
|
|
||||||
let offset = fall_direction * fall_tiles + perp * jitter_tiles;
|
let offset = fall_direction * fall_tiles + perp * jitter_tiles;
|
||||||
let biased_xy_x = tile_pos.x + (offset.x * ITILE_SIZE as f32).round() as i32;
|
|
||||||
let biased_xy_y = tile_pos.y + (offset.y * ITILE_SIZE as f32).round() as i32;
|
|
||||||
|
|
||||||
// Find the actual standable surface at this XY — logs land on the floor
|
// Snap to grid
|
||||||
let biased_pos = find_surface_at(biased_xy_x, biased_xy_y, tilemap).unwrap_or(IVec3::new(
|
let offset_tiles_x = offset.x.round() as i32;
|
||||||
biased_xy_x,
|
let offset_tiles_y = offset.y.round() as i32;
|
||||||
biased_xy_y,
|
let biased_xy_x = tile_pos.x + offset_tiles_x * ITILE_SIZE;
|
||||||
tile_pos.z,
|
let biased_xy_y = tile_pos.y + offset_tiles_y * ITILE_SIZE;
|
||||||
));
|
|
||||||
|
|
||||||
// Find nearest free tile from biased position (increased radius for scattered logs)
|
// Find the actual standable surface at this XY — start search from tree's Z
|
||||||
|
let mut biased_pos = IVec3::new(biased_xy_x, biased_xy_y, tile_pos.z);
|
||||||
|
|
||||||
|
// Search up and down for a standable tile (air above a solid floor)
|
||||||
|
let start_z_idx = tile_pos.z / ITILE_SIZE;
|
||||||
|
let mut found_surface = false;
|
||||||
|
|
||||||
|
// Search nearest Z first
|
||||||
|
let z_search_order = [0, -1, 1, -2, 2, -3, 3, -4, 4];
|
||||||
|
for dz in z_search_order {
|
||||||
|
let check_pos = IVec3::new(biased_xy_x, biased_xy_y, (start_z_idx + dz) * ITILE_SIZE);
|
||||||
|
if tilemap.is_standable(check_pos) {
|
||||||
|
biased_pos = check_pos;
|
||||||
|
found_surface = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find nearest free tile from biased position
|
||||||
let drop_pos = tilemap.find_nearest_free_cargo_tile(biased_pos, 8, &[])?;
|
let drop_pos = tilemap.find_nearest_free_cargo_tile(biased_pos, 8, &[])?;
|
||||||
|
|
||||||
// TODO: OuchEvent — if a living entity occupies drop_pos, they take impact damage.
|
|
||||||
// Check tilemap occupancy here when the combat/injury system exists.
|
|
||||||
// For now: debug log so we know when it would have fired.
|
|
||||||
// if occupancy.count_at_ivec3(drop_pos) > 0 {
|
|
||||||
// debug!("Log landed on occupied tile {:?} — ouch! (TODO: damage)", drop_pos);
|
|
||||||
// }
|
|
||||||
|
|
||||||
let entity = commands
|
let entity = commands
|
||||||
.spawn((
|
.spawn((
|
||||||
Cargo {
|
Cargo {
|
||||||
@@ -115,12 +109,12 @@ pub fn spawn_log_cargo(
|
|||||||
"spawn_log_cargo: tile_pos={:?} drop_pos={:?} entity_z={}",
|
"spawn_log_cargo: tile_pos={:?} drop_pos={:?} entity_z={}",
|
||||||
tile_pos,
|
tile_pos,
|
||||||
drop_pos,
|
drop_pos,
|
||||||
drop_pos.z as f32 / 16.0 - 1.0
|
drop_pos.z as f32 / 16.0
|
||||||
);
|
);
|
||||||
|
|
||||||
tilemap
|
tilemap
|
||||||
.place_cargo(drop_pos, entity)
|
.place_cargo(drop_pos, entity)
|
||||||
.expect("place_cargo failed after find_nearest_free_cargo_tile succeeded");
|
.expect("place_cargo failed after find_nearest_free_cargo_tile succeeded");
|
||||||
|
|
||||||
Some(entity)
|
Some((entity, drop_pos))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ pub fn spawn_dorfs(
|
|||||||
let mut rng = WyRand::seed_from_u64(seed);
|
let mut rng = WyRand::seed_from_u64(seed);
|
||||||
|
|
||||||
for _ in 0..config.spawn_counts.dorfs {
|
for _ in 0..config.spawn_counts.dorfs {
|
||||||
let raw_x = rng.random_range(-8.0f32..8.0f32);
|
let raw_x = rng.random_range(-512.0f32..512.0f32);
|
||||||
let raw_y = rng.random_range(-8.0f32..8.0f32);
|
let raw_y = rng.random_range(-512.0f32..512.0f32);
|
||||||
let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE;
|
let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE;
|
||||||
let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE;
|
let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE;
|
||||||
let grid_z = 35.0 * TILE_SIZE;
|
let grid_z = 35.0 * TILE_SIZE;
|
||||||
|
|||||||
@@ -314,14 +314,10 @@ pub fn prepare_paths(
|
|||||||
tilemap: Res<TileMap>,
|
tilemap: Res<TileMap>,
|
||||||
chunk_map: Res<ChunkMap>,
|
chunk_map: Res<ChunkMap>,
|
||||||
) {
|
) {
|
||||||
let mut paths_needed = 0;
|
|
||||||
let mut paths_computed = 0;
|
|
||||||
|
|
||||||
for (entity, mut ambulatory, transform) in query.iter_mut() {
|
for (entity, mut ambulatory, transform) in query.iter_mut() {
|
||||||
if ambulatory.current_path.is_some() || ambulatory.target.is_none() {
|
if ambulatory.current_path.is_some() || ambulatory.target.is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
paths_needed += 1;
|
|
||||||
|
|
||||||
let Some(target) = ambulatory.target else {
|
let Some(target) = ambulatory.target else {
|
||||||
continue;
|
continue;
|
||||||
@@ -338,7 +334,6 @@ pub fn prepare_paths(
|
|||||||
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||||
ambulatory.current_path = Some(path);
|
ambulatory.current_path = Some(path);
|
||||||
ambulatory.path_index = 0;
|
ambulatory.path_index = 0;
|
||||||
paths_computed += 1;
|
|
||||||
} else if chunk_distance > PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS {
|
} else if chunk_distance > PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS {
|
||||||
let chunk_path = calculate_chunk_path(&chunk_map, start_chunk, goal_chunk);
|
let chunk_path = calculate_chunk_path(&chunk_map, start_chunk, goal_chunk);
|
||||||
let provisional_goal = goal;
|
let provisional_goal = goal;
|
||||||
@@ -624,6 +619,10 @@ pub fn movement(
|
|||||||
query
|
query
|
||||||
.par_iter_mut()
|
.par_iter_mut()
|
||||||
.for_each(|(mut ambulatory, mut transform)| {
|
.for_each(|(mut ambulatory, mut transform)| {
|
||||||
|
info!(
|
||||||
|
"[PATH] Moving: target={:?} current={:?}",
|
||||||
|
ambulatory.target, transform.translation
|
||||||
|
);
|
||||||
let current_pos = transform.translation;
|
let current_pos = transform.translation;
|
||||||
if !is_standable_tile(&tilemap, current_pos.as_ivec3()) {
|
if !is_standable_tile(&tilemap, current_pos.as_ivec3()) {
|
||||||
// Entities can spawn above the loaded world range (z > Z_ABOVE*TILE_SIZE).
|
// Entities can spawn above the loaded world range (z > Z_ABOVE*TILE_SIZE).
|
||||||
@@ -714,45 +713,24 @@ pub fn movement(
|
|||||||
let forward_2d = our_dir;
|
let forward_2d = our_dir;
|
||||||
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
||||||
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
||||||
let cur_z = transform.translation.z;
|
|
||||||
|
|
||||||
let candidates = [
|
let candidates = [
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(
|
+ Vec3::new(
|
||||||
(left_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
(left_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
(left_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
(left_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(left_2d.x * TILE_SIZE, left_2d.y * TILE_SIZE, 0.0),
|
+ Vec3::new(left_2d.x * TILE_SIZE, left_2d.y * TILE_SIZE, 0.0),
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(
|
+ Vec3::new(
|
||||||
(right_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
(right_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
(right_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
(right_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(
|
+ Vec3::new(right_2d.x * TILE_SIZE, right_2d.y * TILE_SIZE, 0.0),
|
||||||
right_2d.x * TILE_SIZE,
|
|
||||||
right_2d.y * TILE_SIZE,
|
|
||||||
0.0,
|
|
||||||
),
|
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let sidestep = candidates.iter().copied().find(|&c| {
|
let sidestep = candidates.iter().copied().find(|&c| {
|
||||||
@@ -783,45 +761,24 @@ pub fn movement(
|
|||||||
let forward_2d = our_dir;
|
let forward_2d = our_dir;
|
||||||
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
let left_2d = Vec2::new(-forward_2d.y, forward_2d.x);
|
||||||
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
let right_2d = Vec2::new(forward_2d.y, -forward_2d.x);
|
||||||
let cur_z = transform.translation.z;
|
|
||||||
|
|
||||||
let candidates = [
|
let candidates = [
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(
|
+ Vec3::new(
|
||||||
(left_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
(left_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
(left_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
(left_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(left_2d.x * TILE_SIZE, left_2d.y * TILE_SIZE, 0.0),
|
+ Vec3::new(left_2d.x * TILE_SIZE, left_2d.y * TILE_SIZE, 0.0),
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(
|
+ Vec3::new(
|
||||||
(right_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
(right_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
(right_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
(right_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
0.0,
|
0.0,
|
||||||
),
|
),
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
snap_to_grid(
|
|
||||||
transform.translation
|
transform.translation
|
||||||
+ Vec3::new(
|
+ Vec3::new(right_2d.x * TILE_SIZE, right_2d.y * TILE_SIZE, 0.0),
|
||||||
right_2d.x * TILE_SIZE,
|
|
||||||
right_2d.y * TILE_SIZE,
|
|
||||||
0.0,
|
|
||||||
),
|
|
||||||
cur_z,
|
|
||||||
TILE_SIZE,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let sidestep = candidates.iter().copied().find(|&c| {
|
let sidestep = candidates.iter().copied().find(|&c| {
|
||||||
@@ -945,16 +902,25 @@ fn validate_next_steps(tilemap: &TileMap, path: &[Vec3], start_index: usize, ste
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
|
fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
|
||||||
tilemap.is_standable(pos)
|
let result = tilemap.is_standable(pos);
|
||||||
}
|
if !result {
|
||||||
|
let chunk_pos = crate::world::chunks::world_to_chunk(pos);
|
||||||
|
let chunk_exists = tilemap.chunks.contains_key(&chunk_pos);
|
||||||
|
let (local_x, local_y, z) = if let Some(c) = tilemap.chunks.get(&chunk_pos) {
|
||||||
|
crate::world::tiles::chunk_data::ChunkData::world_to_local(pos)
|
||||||
|
} else {
|
||||||
|
(0, 0, 0)
|
||||||
|
};
|
||||||
|
|
||||||
#[inline]
|
let floor_exists = tilemap.floor_tiles.contains_key(&pos);
|
||||||
fn snap_to_grid(raw: Vec3, preserve_z: f32, tile_size: f32) -> Vec3 {
|
let fixture_exists = tilemap.fixture_tiles.contains_key(&pos);
|
||||||
Vec3::new(
|
|
||||||
(raw.x / tile_size).round() * tile_size,
|
info!(
|
||||||
(raw.y / tile_size).round() * tile_size,
|
"[PATH] is_standable_tile=false: pos={:?} chunk={:?} chunk_loaded={} local=({},{},{}) floor={} fixture={}",
|
||||||
preserve_z,
|
pos, chunk_pos, chunk_exists, local_x, local_y, z, floor_exists, fixture_exists
|
||||||
)
|
);
|
||||||
|
}
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sample a random standable tile on an edge of `next_chunk`, picking the one
|
/// Sample a random standable tile on an edge of `next_chunk`, picking the one
|
||||||
@@ -1103,7 +1069,20 @@ fn directional_chunk_waypoint(
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn get_tile_weight(tilemap: &TileMap, pos: IVec3) -> u8 {
|
fn get_tile_weight(tilemap: &TileMap, pos: IVec3) -> u8 {
|
||||||
let floor_pos = IVec3::new(pos.x, pos.y, pos.z - ITILE_SIZE);
|
let floor_pos = IVec3::new(pos.x, pos.y, pos.z - ITILE_SIZE);
|
||||||
tilemap.get_astar_weight(floor_pos)
|
let weight = tilemap.get_astar_weight(floor_pos);
|
||||||
|
if weight == 100 {
|
||||||
|
let floor_tile = tilemap.floor_tiles.get(&floor_pos);
|
||||||
|
let fixture_tile = tilemap.fixture_tiles.get(&floor_pos);
|
||||||
|
let chunk_pos = crate::world::chunks::world_to_chunk(floor_pos);
|
||||||
|
let chunk_exists = tilemap.chunks.contains_key(&chunk_pos);
|
||||||
|
info!(
|
||||||
|
"[PATH] get_tile_weight=100 (default): pos={:?} floor_pos={:?} chunk_loaded={} floor={:?} fixture={:?}",
|
||||||
|
pos, floor_pos, chunk_exists,
|
||||||
|
floor_tile.map(|f| f.id.clone()),
|
||||||
|
fixture_tile.map(|f| f.id.clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
weight
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate movement cost including tile weight.
|
/// Calculate movement cost including tile weight.
|
||||||
@@ -1119,7 +1098,10 @@ fn calculate_movement_cost(move_dir: IVec3, tile_weight: u8) -> i32 {
|
|||||||
(1, 1, 0) => 14, // Diagonal
|
(1, 1, 0) => 14, // Diagonal
|
||||||
(1, 0, 1) | (0, 1, 1) => 42, // Vertical + cardinal
|
(1, 0, 1) | (0, 1, 1) => 42, // Vertical + cardinal
|
||||||
(1, 1, 1) => 56, // Vertical + diagonal
|
(1, 1, 1) => 56, // Vertical + diagonal
|
||||||
_ => 0,
|
_ => {
|
||||||
|
info!("[PATH] calculate_movement_cost=0: move_dir={:?}", move_dir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if base_cost == 0 {
|
if base_cost == 0 {
|
||||||
@@ -1274,13 +1256,40 @@ pub fn calculate_provisional_path(
|
|||||||
) -> Vec<Vec3> {
|
) -> Vec<Vec3> {
|
||||||
let timer = Instant::now();
|
let timer = Instant::now();
|
||||||
|
|
||||||
if !is_standable_tile(tilemap, start) {
|
let start_standable = is_standable_tile(tilemap, start);
|
||||||
|
if !start_standable {
|
||||||
LOCAL_FAILED_PATHS.with(|f| {
|
LOCAL_FAILED_PATHS.with(|f| {
|
||||||
*f.borrow_mut() += 1;
|
*f.borrow_mut() += 1;
|
||||||
});
|
});
|
||||||
|
info!(
|
||||||
|
"[PATH] FAIL: start not standable start={:?} z_level={} floor={:?} fixture={:?}",
|
||||||
|
start,
|
||||||
|
start.z,
|
||||||
|
tilemap.floor_tiles.get(&start).map(|f| f.id.clone()),
|
||||||
|
tilemap.fixture_tiles.get(&start).map(|f| f.id.clone())
|
||||||
|
);
|
||||||
return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)];
|
return vec![Vec3::new(start.x as f32, start.y as f32, start.z as f32)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let goal_standable = is_standable_tile(tilemap, goal);
|
||||||
|
if !goal_standable {
|
||||||
|
info!(
|
||||||
|
"[PATH] WARN: goal not standable goal={:?} z_level={} floor={:?} fixture={:?}",
|
||||||
|
goal,
|
||||||
|
goal.z,
|
||||||
|
tilemap.floor_tiles.get(&goal).map(|f| f.id.clone()),
|
||||||
|
tilemap.fixture_tiles.get(&goal).map(|f| f.id.clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_standable_tile(tilemap, goal) {
|
||||||
|
info!(
|
||||||
|
"[PATH] WARN: is_standable check false: goal={:?}, is_standable={}",
|
||||||
|
goal,
|
||||||
|
tilemap.is_standable(goal)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
|
let estimated_tiles = octile_distance_3d(start, goal) / ITILE_SIZE;
|
||||||
|
|
||||||
let result = SCRATCHPAD.with(|s| {
|
let result = SCRATCHPAD.with(|s| {
|
||||||
@@ -1318,6 +1327,10 @@ pub fn calculate_provisional_path(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if nodes_expanded >= node_limit {
|
if nodes_expanded >= node_limit {
|
||||||
|
info!(
|
||||||
|
"[PATH] FAIL: node_limit hit limit={} start={:?} goal={:?} expanded={}",
|
||||||
|
node_limit, start, goal, nodes_expanded
|
||||||
|
);
|
||||||
return (Vec::new(), nodes_expanded);
|
return (Vec::new(), nodes_expanded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1326,9 +1339,10 @@ pub fn calculate_provisional_path(
|
|||||||
for &move_dir in &ALLOWED_MOVES {
|
for &move_dir in &ALLOWED_MOVES {
|
||||||
let neighbor_pos = current + move_dir;
|
let neighbor_pos = current + move_dir;
|
||||||
|
|
||||||
if !is_standable_tile(tilemap, neighbor_pos)
|
if !is_standable_tile(tilemap, neighbor_pos) {
|
||||||
|| scratch.closed_set.contains(&neighbor_pos)
|
continue;
|
||||||
{
|
}
|
||||||
|
if scratch.closed_set.contains(&neighbor_pos) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1353,6 +1367,10 @@ pub fn calculate_provisional_path(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[PATH] FAIL: no_path_found start={:?} goal={:?} best_node={:?} best_h={}",
|
||||||
|
start, goal, best_node, best_h
|
||||||
|
);
|
||||||
(
|
(
|
||||||
reconstruct_path(&scratch.came_from, best_node),
|
reconstruct_path(&scratch.came_from, best_node),
|
||||||
nodes_expanded,
|
nodes_expanded,
|
||||||
|
|||||||
+41
-386
@@ -1,150 +1,58 @@
|
|||||||
//! Demo loop — one tree at a time, chop and haul to origin.
|
|
||||||
//!
|
|
||||||
//! # Behaviour
|
|
||||||
//! 1. Find the nearest standing tree trunk to (0,0) in the loaded world.
|
|
||||||
//! 2. Assign a ChopTree task to one idle dorf.
|
|
||||||
//! 3. When the tree is felled (trunk fixtures gone, Cargo logs exist):
|
|
||||||
//! - All logs enter the haul queue ordered by proximity to origin.
|
|
||||||
//! - Each tick: assign idle dorfs to the nearest unassigned log.
|
|
||||||
//! - Dorfs that finish hauling become available for the next log.
|
|
||||||
//! 4. When the haul queue is empty AND no in-progress hauls remain:
|
|
||||||
//! - Find the next tree.
|
|
||||||
//! 5. Dorfs with no task remain on Task::Idle (wander).
|
|
||||||
//!
|
|
||||||
//! # State machine
|
|
||||||
//! Tracked in DemoState resource.
|
|
||||||
//!
|
|
||||||
//! # Limitations (acceptable for demo)
|
|
||||||
//! - Only one tree targeted at a time.
|
|
||||||
//! - Does not use the JobQueue — tasks pushed directly.
|
|
||||||
//! - Does not handle dorf death mid-chop.
|
|
||||||
//! - Haul destination is a fixed search near IVec3::ZERO — not a stockpile.
|
|
||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
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, HaulSlot, Haulable};
|
use crate::entities::behaviour::EntityType;
|
||||||
use crate::entities::tasks::components::{
|
use crate::entities::tasks::components::{Task, TaskQueue, TaskState};
|
||||||
ChopStep, HaulStep, Task, TaskQueue, TaskState, CHOP_TICKS_DEFAULT,
|
use crate::entities::tasks::job_queue::{JobKind, JobQueue};
|
||||||
};
|
|
||||||
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;
|
||||||
|
|
||||||
/// Radius (in tiles, Chebyshev) to search for unclaimed logs after felling.
|
|
||||||
/// Logs scatter within ~3 tiles of trunk positions.
|
|
||||||
const LOG_SEARCH_RADIUS_TILES: i32 = 12;
|
|
||||||
|
|
||||||
/// Radius to search for a haul drop destination near origin.
|
|
||||||
const HAUL_DEST_SEARCH_RADIUS: i32 = 32;
|
|
||||||
|
|
||||||
/// Tracks what the demo loop is currently doing.
|
|
||||||
#[derive(Resource, Debug, Default)]
|
|
||||||
pub enum DemoState {
|
|
||||||
/// No active tree — searching for one.
|
|
||||||
#[default]
|
|
||||||
Idle,
|
|
||||||
/// A ChopTree task has been assigned to `chopper`.
|
|
||||||
/// `trunk_pos` is the lowest trunk tile of the target tree.
|
|
||||||
Chopping { trunk_pos: IVec3, chopper: Entity },
|
|
||||||
/// Tree has been felled. Working through the haul queue.
|
|
||||||
///
|
|
||||||
/// `unassigned` — logs not yet claimed, ordered nearest-to-origin first.
|
|
||||||
/// `in_progress` — dorf entities currently executing a HaulCargo task for this tree.
|
|
||||||
/// A dorf is removed when they return to idle (log dropped, task complete).
|
|
||||||
Hauling {
|
|
||||||
felled_trunk_pos: IVec3,
|
|
||||||
/// Queue of (log_entity, log_tile_pos) not yet assigned.
|
|
||||||
/// Front = highest priority (nearest to origin).
|
|
||||||
unassigned: VecDeque<(Entity, IVec3)>,
|
|
||||||
/// Dorf entities currently hauling a log for this tree.
|
|
||||||
in_progress: FxHashSet<Entity>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The demo loop system. Runs in FixedUpdate after task_executor_system.
|
|
||||||
///
|
|
||||||
/// State transitions:
|
|
||||||
/// Idle → Chopping: found a tree, assigned ChopTree to one idle dorf
|
|
||||||
/// Chopping → Hauling: trunk no longer in fixture_tiles (tree felled)
|
|
||||||
/// Hauling → Idle: both unassigned and in_progress are empty
|
|
||||||
///
|
|
||||||
/// Performance: scans fixture_tiles once per state transition (infrequent),
|
|
||||||
/// not per tick. During Chopping and Hauling states the system does O(1) checks.
|
|
||||||
pub fn demo_system(
|
pub fn demo_system(
|
||||||
mut demo_state: ResMut<DemoState>,
|
mut job_queue: ResMut<JobQueue>,
|
||||||
tilemap: Res<TileMap>,
|
tilemap: Res<TileMap>,
|
||||||
chunk_map: Res<ChunkMap>,
|
chunk_map: Res<ChunkMap>,
|
||||||
tree_parts: Query<(Entity, &TreePart)>,
|
tree_parts: Query<(Entity, &TreePart)>,
|
||||||
cargo_query: Query<(Entity, &Cargo), With<Haulable>>,
|
dorf_query: Query<(&TaskQueue, &TaskState), With<EntityType>>,
|
||||||
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
|
let any_chopping = dorf_query.iter().any(|(queue, state)| {
|
||||||
for event in task_failed.read() {
|
let is_active_or_completing = *state == TaskState::Active || *state == TaskState::Completed;
|
||||||
if event.reason == "no adjacent standable tile to approach tree" {
|
if is_active_or_completing {
|
||||||
if let DemoState::Chopping { trunk_pos, chopper } = *demo_state {
|
if let Some(current) = queue.current() {
|
||||||
warn!(
|
return matches!(current, Task::ChopTree { .. });
|
||||||
"[DEMO] ChopTree failed for tree at {:?} (chopper={:?}): {}, resetting to Idle",
|
|
||||||
trunk_pos, chopper, event.reason
|
|
||||||
);
|
|
||||||
*demo_state = DemoState::Idle;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
false
|
||||||
|
});
|
||||||
|
|
||||||
|
if !job_queue.has_fell_tree() && !any_chopping {
|
||||||
|
if let Some(trunk_pos) = find_tree_nearest_origin(&tilemap, &chunk_map, &tree_parts) {
|
||||||
|
job_queue.push(JobKind::FellTree { trunk_pos });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle logs spawned from felling — transition to Hauling
|
if !job_queue.is_empty() {
|
||||||
// Collect events first to avoid double-mutable borrow conflict with demo_state
|
let (fell, haul) = job_queue.debug_counts();
|
||||||
let pending_logs: Vec<_> = logs_spawned.read().collect();
|
info!("[QUEUE] FellTree: {}, HaulCargo: {}", fell, haul);
|
||||||
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 {
|
fn find_tree_nearest_origin(
|
||||||
DemoState::Idle => {
|
tilemap: &TileMap,
|
||||||
// Find the nearest standing tree to (0,0).
|
chunk_map: &ChunkMap,
|
||||||
// A "tree" is identified by a TreePart with is_trunk=true whose
|
tree_parts: &Query<(Entity, &TreePart)>,
|
||||||
// tile_pos is still in fixture_tiles (not yet felled).
|
) -> Option<IVec3> {
|
||||||
let origin = IVec3::ZERO;
|
let origin = IVec3::ZERO;
|
||||||
|
let mut best_xy: Option<(IVec2, i32)> = None;
|
||||||
let mut best: Option<(IVec3, i32)> = None; // (trunk_pos, chebyshev_dist)
|
|
||||||
|
|
||||||
for (_, part) in tree_parts.iter() {
|
for (_, part) in tree_parts.iter() {
|
||||||
if !part.is_trunk {
|
if !part.is_trunk {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !tilemap.fixture_tiles.contains_key(&part.tile_pos) {
|
if !tilemap.fixture_tiles.contains_key(&part.tile_pos) {
|
||||||
continue; // already felled
|
continue;
|
||||||
}
|
}
|
||||||
// Only consider trees in fully-loaded chunks
|
|
||||||
let chunk = crate::world::chunks::world_to_chunk(part.tile_pos);
|
let chunk = crate::world::chunks::world_to_chunk(part.tile_pos);
|
||||||
let loaded = chunk_map.loaded_chunks.contains_key(&(chunk + IVec2::X))
|
let loaded = chunk_map.loaded_chunks.contains_key(&(chunk + IVec2::X))
|
||||||
&& chunk_map.loaded_chunks.contains_key(&(chunk - IVec2::X))
|
&& chunk_map.loaded_chunks.contains_key(&(chunk - IVec2::X))
|
||||||
@@ -158,282 +66,29 @@ pub fn demo_system(
|
|||||||
let dy = (part.tile_pos.y - origin.y).abs() / ITILE_SIZE;
|
let dy = (part.tile_pos.y - origin.y).abs() / ITILE_SIZE;
|
||||||
let dist = dx.max(dy);
|
let dist = dx.max(dy);
|
||||||
|
|
||||||
if best.map_or(true, |(_, best_dist)| dist < best_dist) {
|
if best_xy.map_or(true, |(_, best_dist)| dist < best_dist) {
|
||||||
best = Some((part.tile_pos, dist));
|
best_xy = Some((part.tile_pos.xy(), dist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some((trunk_pos, _)) = best else {
|
let Some((nearest_xy, _)) = best_xy else {
|
||||||
// No trees found — nothing to do
|
return None;
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Find the lowest trunk tile (minimum z) for this tree's XY column.
|
|
||||||
let lowest_trunk = tree_parts
|
let lowest_trunk = tree_parts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, p)| {
|
.filter_map(|(_, p)| {
|
||||||
p.is_trunk
|
if p.is_trunk
|
||||||
&& p.tile_pos.x == trunk_pos.x
|
&& p.tile_pos.x == nearest_xy.x
|
||||||
&& p.tile_pos.y == trunk_pos.y
|
&& p.tile_pos.y == nearest_xy.y
|
||||||
&& tilemap.fixture_tiles.contains_key(&p.tile_pos)
|
&& tilemap.fixture_tiles.contains_key(&p.tile_pos)
|
||||||
})
|
|
||||||
.map(|(_, p)| p.tile_pos)
|
|
||||||
.min_by_key(|pos| pos.z)
|
|
||||||
.unwrap_or(trunk_pos);
|
|
||||||
|
|
||||||
// Find one idle dorf — prefer closest to the tree.
|
|
||||||
// Don't interrupt a dorf still carrying cargo.
|
|
||||||
let mut best_dorf: Option<(Entity, i32)> = None;
|
|
||||||
let mut considered = 0u32;
|
|
||||||
let mut rejected_busy = 0u32;
|
|
||||||
let mut rejected_hauling = 0u32;
|
|
||||||
|
|
||||||
for (entity, queue, state, transform) in dorf_query.iter() {
|
|
||||||
considered += 1;
|
|
||||||
if !is_idle_dorf(&queue, &state) {
|
|
||||||
rejected_busy += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip dorfs still carrying cargo
|
|
||||||
if haul_slot_query
|
|
||||||
.get(entity)
|
|
||||||
.map(|h| h.is_occupied())
|
|
||||||
.unwrap_or(false)
|
|
||||||
{
|
{
|
||||||
rejected_hauling += 1;
|
Some(p.tile_pos)
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let pos = transform.translation.as_ivec3();
|
|
||||||
let dx = (pos.x - lowest_trunk.x).abs() / ITILE_SIZE;
|
|
||||||
let dy = (pos.y - lowest_trunk.y).abs() / ITILE_SIZE;
|
|
||||||
let dist = dx.max(dy);
|
|
||||||
if best_dorf.map_or(true, |(_, d)| dist < d) {
|
|
||||||
best_dorf = Some((entity, dist));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some((chopper, _)) = best_dorf else {
|
|
||||||
info!(
|
|
||||||
"[DEMO] No idle dorf found for ChopTree (considered={} busy={} hauling={})",
|
|
||||||
considered, rejected_busy, rejected_hauling
|
|
||||||
);
|
|
||||||
return; // no idle dorfs available
|
|
||||||
};
|
|
||||||
|
|
||||||
// Assign ChopTree task
|
|
||||||
if let Ok((_, mut queue, mut state, _)) = dorf_query.get_mut(chopper) {
|
|
||||||
queue.clear();
|
|
||||||
queue.push(Task::ChopTree {
|
|
||||||
trunk_pos: lowest_trunk,
|
|
||||||
chop_ticks: CHOP_TICKS_DEFAULT,
|
|
||||||
step: ChopStep::MovingToTree { approach: None },
|
|
||||||
});
|
|
||||||
*state = TaskState::Pending;
|
|
||||||
}
|
|
||||||
|
|
||||||
*demo_state = DemoState::Chopping {
|
|
||||||
trunk_pos: lowest_trunk,
|
|
||||||
chopper,
|
|
||||||
};
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"[DEMO] → Chopping: chopper={:?} trunk={:?}",
|
|
||||||
chopper, lowest_trunk
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DemoState::Chopping { trunk_pos, chopper } => {
|
|
||||||
let trunk_pos = *trunk_pos;
|
|
||||||
let chopper = *chopper;
|
|
||||||
|
|
||||||
// Check if the tree has been felled (fixture gone from tilemap)
|
|
||||||
if tilemap.fixture_tiles.contains_key(&trunk_pos) {
|
|
||||||
// Still standing — check chopper hasn't abandoned the task
|
|
||||||
if let Ok((_, queue, state, _)) = dorf_query.get(chopper) {
|
|
||||||
let still_chopping = queue.current().map_or(
|
|
||||||
false,
|
|
||||||
|t| matches!(t, Task::ChopTree { trunk_pos: tp, .. } if *tp == trunk_pos),
|
|
||||||
);
|
|
||||||
if !still_chopping && queue.is_empty() {
|
|
||||||
*demo_state = DemoState::Idle;
|
|
||||||
warn!("[DEMO] chopper {:?} abandoned ChopTree at {:?} — queue={:?} state={:?}",
|
|
||||||
chopper, trunk_pos,
|
|
||||||
queue.current().map(|t| t.name()),
|
|
||||||
state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tree is felled — transition to Hauling
|
|
||||||
info!("[DEMO] → Hauling: tree at {:?} felled", trunk_pos);
|
|
||||||
|
|
||||||
// Find all Cargo logs near the trunk position
|
|
||||||
let search_world = LOG_SEARCH_RADIUS_TILES * ITILE_SIZE;
|
|
||||||
let mut logs: SmallVec<[(Entity, IVec3, i32); 8]> = cargo_query
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, cargo)| {
|
|
||||||
cargo.name == "log"
|
|
||||||
&& (cargo.tile_pos.x - trunk_pos.x).abs() <= search_world
|
|
||||||
&& (cargo.tile_pos.y - trunk_pos.y).abs() <= search_world
|
|
||||||
})
|
|
||||||
.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();
|
|
||||||
|
|
||||||
// Nearest to origin first — dorfs haul the closest logs first
|
|
||||||
logs.sort_by_key(|(_, _, dist)| *dist);
|
|
||||||
|
|
||||||
if logs.is_empty() {
|
|
||||||
warn!("[DEMO] no logs found after felling {:?}", trunk_pos);
|
|
||||||
*demo_state = DemoState::Idle;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to VecDeque, dropping sort key
|
|
||||||
let unassigned: VecDeque<(Entity, IVec3)> =
|
|
||||||
logs.into_iter().map(|(e, pos, _)| (e, pos)).collect();
|
|
||||||
|
|
||||||
info!("[DEMO] → Hauling: {} logs queued", unassigned.len());
|
|
||||||
|
|
||||||
*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 dorfs that have returned to idle — their haul is complete
|
|
||||||
in_progress.retain(|&dorf_entity| {
|
|
||||||
dorf_query
|
|
||||||
.get(dorf_entity)
|
|
||||||
.map(|(_, queue, state, _)| !is_idle_dorf(queue, state))
|
|
||||||
.unwrap_or(false) // entity gone = treat as done
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Track destinations reserved this tick to avoid assigning the same tile
|
|
||||||
// to multiple dorfs before any have physically dropped their cargo.
|
|
||||||
let mut reserved: SmallVec<[IVec3; 8]> = SmallVec::new();
|
|
||||||
|
|
||||||
for dorf_entity in idle_dorfs {
|
|
||||||
// Compute haul destination fresh for each assignment,
|
|
||||||
// excluding tiles already reserved this tick.
|
|
||||||
let dest = tilemap
|
|
||||||
.find_nearest_free_cargo_tile(
|
|
||||||
IVec3::ZERO,
|
|
||||||
HAUL_DEST_SEARCH_RADIUS,
|
|
||||||
&reserved,
|
|
||||||
)
|
|
||||||
.unwrap_or(IVec3::ZERO);
|
|
||||||
reserved.push(dest);
|
|
||||||
|
|
||||||
let Some((log_entity, log_pos)) = unassigned.pop_front() else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
queue.clear();
|
|
||||||
queue.push(Task::HaulCargo {
|
|
||||||
cargo_entity: log_entity,
|
|
||||||
cargo_pos: log_pos,
|
|
||||||
dest,
|
|
||||||
step: HaulStep::MovingToCargo { approach: None },
|
|
||||||
});
|
|
||||||
*state = TaskState::Pending;
|
|
||||||
in_progress.insert(dorf_entity);
|
|
||||||
info!(
|
|
||||||
"[DEMO] assigned HaulCargo log={:?} → dorf={:?} dest={:?}",
|
|
||||||
log_entity, dorf_entity, dest
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Couldn't assign — put log back at front of queue
|
None
|
||||||
unassigned.push_front((log_entity, log_pos));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.min_by_key(|pos| pos.z);
|
||||||
|
|
||||||
// Check if all work is done
|
lowest_trunk
|
||||||
if unassigned.is_empty() && in_progress.is_empty() {
|
|
||||||
info!("[DEMO] → Idle: all hauled, seeking next tree");
|
|
||||||
*demo_state = DemoState::Idle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if a dorf has no active task or is purely wandering idle.
|
|
||||||
/// Used to find dorfs available for task assignment.
|
|
||||||
#[inline]
|
|
||||||
fn is_idle_dorf(queue: &TaskQueue, state: &TaskState) -> bool {
|
|
||||||
// Dorf is available if: queue is empty, OR current task is Idle (wandering)
|
|
||||||
queue.is_empty()
|
|
||||||
|| matches!(queue.current(), Some(Task::Idle { .. }))
|
|
||||||
|| *state == TaskState::Pending
|
|
||||||
&& queue
|
|
||||||
.current()
|
|
||||||
.map_or(true, |t| matches!(t, Task::Idle { .. }))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Debug system — prints full task queue state whenever any TaskQueue changes.
|
|
||||||
/// Only compiles in debug builds.
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
pub fn debug_task_queues(query: Query<(Entity, &TaskQueue, &TaskState), Changed<TaskQueue>>) {
|
|
||||||
for (entity, queue, state) in query.iter() {
|
|
||||||
let current = queue
|
|
||||||
.current()
|
|
||||||
.map(|t| format!("{}[{:?}]", t.name(), state))
|
|
||||||
.unwrap_or_else(|| format!("EMPTY[{:?}]", state));
|
|
||||||
|
|
||||||
let pending: Vec<&str> = queue.tasks.iter().skip(1).map(|t| t.name()).collect();
|
|
||||||
|
|
||||||
if pending.is_empty() {
|
|
||||||
info!("[TASK] {:?} → {}", entity, current);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"[TASK] {:?} → {} pending:[{}]",
|
|
||||||
entity,
|
|
||||||
current,
|
|
||||||
pending.join(",")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+284
-54
@@ -8,6 +8,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimise queries.
|
//! Uses Changed<TaskQueue> + Changed<TaskState> to minimise queries.
|
||||||
|
|
||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
||||||
use crate::entities::cargo::{Cargo, HaulSlot};
|
use crate::entities::cargo::{Cargo, HaulSlot};
|
||||||
use crate::entities::shared_components::Ambulatory;
|
use crate::entities::shared_components::Ambulatory;
|
||||||
@@ -15,7 +16,8 @@ use crate::entities::tasks::components::{
|
|||||||
ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState,
|
ChopStep, DropStep, HaulStep, IdleState, Task, TaskQueue, TaskState,
|
||||||
};
|
};
|
||||||
use crate::entities::tasks::events::{LogsSpawned, TaskClaimed, TaskCompleted, TaskFailed};
|
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::chunks::ChunkMap;
|
||||||
use crate::world::generation::forestry::{fell_tree, TreePart};
|
use crate::world::generation::forestry::{fell_tree, TreePart};
|
||||||
use crate::world::tiles::tile_changed::TileChangedEvent;
|
use crate::world::tiles::tile_changed::TileChangedEvent;
|
||||||
@@ -24,6 +26,7 @@ use crate::world::tiles::TileMap;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy_rand::prelude::*;
|
use bevy_rand::prelude::*;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
/// Main task executor. Runs in FixedUpdate.
|
/// Main task executor. Runs in FixedUpdate.
|
||||||
pub fn task_executor_system(
|
pub fn task_executor_system(
|
||||||
@@ -45,6 +48,7 @@ pub fn task_executor_system(
|
|||||||
&EntityType,
|
&EntityType,
|
||||||
Option<&mut HaulSlot>,
|
Option<&mut HaulSlot>,
|
||||||
)>,
|
)>,
|
||||||
|
mut job_queue: ResMut<JobQueue>,
|
||||||
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>,
|
||||||
@@ -98,6 +102,8 @@ pub fn task_executor_system(
|
|||||||
// Execute current task if Active
|
// Execute current task if Active
|
||||||
if *state == TaskState::Active {
|
if *state == TaskState::Active {
|
||||||
if let Some(current_task) = queue.current_mut() {
|
if let Some(current_task) = queue.current_mut() {
|
||||||
|
let mut failed_reason: Option<&'static str> = None;
|
||||||
|
|
||||||
match current_task {
|
match current_task {
|
||||||
Task::Idle { .. } => {
|
Task::Idle { .. } => {
|
||||||
execute_idle(
|
execute_idle(
|
||||||
@@ -139,6 +145,10 @@ pub fn task_executor_system(
|
|||||||
} => match step {
|
} => match step {
|
||||||
ChopStep::MovingToTree { ref mut approach } => {
|
ChopStep::MovingToTree { ref mut approach } => {
|
||||||
if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) {
|
if !tilemap_mut.fixture_tiles.contains_key(trunk_pos) {
|
||||||
|
info!(
|
||||||
|
"TASK FAILED: {:?} for {:?} - {}",
|
||||||
|
current_task, entity, "tree already gone"
|
||||||
|
);
|
||||||
failed_writer.write(TaskFailed {
|
failed_writer.write(TaskFailed {
|
||||||
entity,
|
entity,
|
||||||
task: current_task.clone(),
|
task: current_task.clone(),
|
||||||
@@ -187,10 +197,15 @@ pub fn task_executor_system(
|
|||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
let reason = "no adjacent standable tile to approach tree";
|
||||||
|
info!(
|
||||||
|
"TASK FAILED: {:?} for {:?} - {}",
|
||||||
|
current_task, entity, reason
|
||||||
|
);
|
||||||
failed_writer.write(TaskFailed {
|
failed_writer.write(TaskFailed {
|
||||||
entity,
|
entity,
|
||||||
task: current_task.clone(),
|
task: current_task.clone(),
|
||||||
reason: "no adjacent standable tile to approach tree",
|
reason,
|
||||||
});
|
});
|
||||||
*state = TaskState::Failed;
|
*state = TaskState::Failed;
|
||||||
continue;
|
continue;
|
||||||
@@ -224,8 +239,9 @@ pub fn task_executor_system(
|
|||||||
// If target is Some, pathfinding is handling movement — nothing to do
|
// If target is Some, pathfinding is handling movement — nothing to do
|
||||||
}
|
}
|
||||||
ChopStep::Chopping { ticks_remaining } => {
|
ChopStep::Chopping { ticks_remaining } => {
|
||||||
|
info!("[EXECUTOR] Chop tick: {:?}", ticks_remaining);
|
||||||
if *ticks_remaining == 0 {
|
if *ticks_remaining == 0 {
|
||||||
let trunk_positions = fell_tree(
|
let (trunk_position, trunk_count) = fell_tree(
|
||||||
*trunk_pos,
|
*trunk_pos,
|
||||||
&tree_parts,
|
&tree_parts,
|
||||||
&mut commands,
|
&mut commands,
|
||||||
@@ -243,9 +259,14 @@ 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();
|
let mut log_entities: SmallVec<[(Entity, IVec3); 8]> =
|
||||||
for &pos in trunk_positions.iter() {
|
SmallVec::new();
|
||||||
if let Some(log_entity) =
|
// 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(
|
crate::entities::cargo::spawn_log_cargo(
|
||||||
&mut commands,
|
&mut commands,
|
||||||
&mut tilemap_mut,
|
&mut tilemap_mut,
|
||||||
@@ -255,14 +276,42 @@ pub fn task_executor_system(
|
|||||||
&mut rng,
|
&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() {
|
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 {
|
logs_spawned_writer.write(LogsSpawned {
|
||||||
log_entities,
|
log_entities: log_entities
|
||||||
dest: IVec3::ZERO,
|
.iter()
|
||||||
|
.map(|(e, _)| *e)
|
||||||
|
.collect(),
|
||||||
|
dest,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
*step = ChopStep::Done;
|
*step = ChopStep::Done;
|
||||||
@@ -294,7 +343,14 @@ pub fn task_executor_system(
|
|||||||
HaulStep::MovingToCargo { approach } => {
|
HaulStep::MovingToCargo { approach } => {
|
||||||
use crate::constants::ITILE_SIZE;
|
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) {
|
if !tilemap_mut.cargo_tiles.contains_key(cargo_pos) {
|
||||||
|
info!(
|
||||||
|
"TASK FAILED: {:?} for {:?} - {}",
|
||||||
|
current_task, entity, "cargo no longer exists"
|
||||||
|
);
|
||||||
failed_writer.write(TaskFailed {
|
failed_writer.write(TaskFailed {
|
||||||
entity,
|
entity,
|
||||||
task: current_task.clone(),
|
task: current_task.clone(),
|
||||||
@@ -305,40 +361,107 @@ pub fn task_executor_system(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if approach.is_none() {
|
if approach.is_none() {
|
||||||
// Search for nearest standable tile to approach cargo.
|
const NODE_CAP: usize = 1024;
|
||||||
// Radius 0 = cargo tile itself (can stand in same tile as cargo).
|
let mut frontier: VecDeque<IVec3> = VecDeque::new();
|
||||||
// Radius 1-2 = adjacent tiles if cargo tile is blocked.
|
let mut visited: std::collections::HashSet<IVec3> =
|
||||||
|
std::collections::HashSet::new();
|
||||||
|
|
||||||
let cargo_z = cargo_pos.z;
|
let cargo_z = cargo_pos.z;
|
||||||
let approach_tile = (0..=2i32).find_map(|radius: i32| {
|
|
||||||
for dx in -radius..=radius {
|
for dx in -1i32..=1 {
|
||||||
for dy in -radius..=radius {
|
for dy in -1i32..=1 {
|
||||||
if radius > 0
|
if dx == 0 && dy == 0 {
|
||||||
&& dx.abs() != radius
|
|
||||||
&& dy.abs() != radius
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let candidate = IVec3::new(
|
let neighbor = IVec3::new(
|
||||||
cargo_pos.x + dx * ITILE_SIZE,
|
cargo_pos.x + dx * ITILE_SIZE,
|
||||||
cargo_pos.y + dy * ITILE_SIZE,
|
cargo_pos.y + dy * ITILE_SIZE,
|
||||||
cargo_z,
|
cargo_z,
|
||||||
);
|
);
|
||||||
if tilemap_mut.is_standable(candidate) {
|
if visited.insert(neighbor) {
|
||||||
return Some(candidate);
|
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 {
|
match approach_tile {
|
||||||
Some(tile) => {
|
Some(tile) => {
|
||||||
*approach = Some(tile);
|
*approach = Some(tile);
|
||||||
info!(
|
info!(
|
||||||
"[HAUL] {:?} target set: cargo={:?} approach={:?}",
|
"[HAUL] {:?} setting target: cargo={:?} approach={:?} target={:?}",
|
||||||
entity, cargo_pos, tile
|
entity, cargo_pos, tile, ambulatory.target
|
||||||
);
|
);
|
||||||
// +1.0 z-offset for entity standing height (same as trees)
|
|
||||||
ambulatory.target = Some(Vec3::new(
|
ambulatory.target = Some(Vec3::new(
|
||||||
tile.x as f32,
|
tile.x as f32,
|
||||||
tile.y as f32,
|
tile.y as f32,
|
||||||
@@ -359,22 +482,26 @@ pub fn task_executor_system(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check arrival at cargo tile
|
// Check arrival at cargo tile
|
||||||
if approach.is_some() && ambulatory.target.is_none() {
|
// Use approach tile position for distance check, not target None
|
||||||
let dx = transform.translation.x - cargo_pos.x as f32;
|
if let Some(approach_tile) = *approach {
|
||||||
let dy = transform.translation.y - cargo_pos.y as f32;
|
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 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);
|
(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!(
|
info!(
|
||||||
"[HAUL] {:?} arrived at cargo {:?}, picking up",
|
"[HAUL] {:?} arrived at approach {:?}, picking up cargo at {:?}",
|
||||||
entity, cargo_entity
|
entity, approach_tile, cargo_pos
|
||||||
);
|
);
|
||||||
*step = HaulStep::PickingUp;
|
*step = HaulStep::PickingUp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HaulStep::PickingUp => {
|
HaulStep::PickingUp => {
|
||||||
|
info!("[HAUL] {:?} PickingUp: cargo_pos={:?}", entity, cargo_pos);
|
||||||
if haul.is_occupied() {
|
if haul.is_occupied() {
|
||||||
failed_writer.write(TaskFailed {
|
failed_writer.write(TaskFailed {
|
||||||
entity,
|
entity,
|
||||||
@@ -390,7 +517,14 @@ pub fn task_executor_system(
|
|||||||
"[HAUL] {:?} picked up {:?} → hauling to {:?}",
|
"[HAUL] {:?} picked up {:?} → hauling to {:?}",
|
||||||
entity, cargo_entity, dest
|
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 {
|
} else {
|
||||||
failed_writer.write(TaskFailed {
|
failed_writer.write(TaskFailed {
|
||||||
entity,
|
entity,
|
||||||
@@ -401,38 +535,43 @@ pub fn task_executor_system(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
HaulStep::MovingToDest { chosen_drop } => {
|
HaulStep::MovingToDest { chosen_drop } => {
|
||||||
if chosen_drop.is_none() {
|
let drop_pos = *chosen_drop;
|
||||||
*chosen_drop = Some(
|
if let Some(drop) = drop_pos {
|
||||||
tilemap_mut
|
|
||||||
.find_nearest_free_cargo_tile(*dest, 8, &[])
|
|
||||||
.unwrap_or(*dest),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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);
|
info!(
|
||||||
// +1.0 z-offset for entity standing height (same as approach)
|
"[HAUL] {:?} setting target to drop at {:?}",
|
||||||
|
entity, drop
|
||||||
|
);
|
||||||
ambulatory.target = Some(Vec3::new(
|
ambulatory.target = Some(Vec3::new(
|
||||||
drop_pos.x as f32,
|
drop.x as f32,
|
||||||
drop_pos.y as f32,
|
drop.y as f32,
|
||||||
drop_pos.z as f32 + 1.0,
|
drop.z as f32 + 1.0,
|
||||||
));
|
));
|
||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always check arrival distance, not gated by target status
|
let dx = transform.translation.x - drop.x as f32;
|
||||||
let dx = transform.translation.x - drop_pos.x as f32;
|
let dy = transform.translation.y - drop.y 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 as f32 * 1.5)
|
let arrive_sq = (crate::constants::TILE_SIZE as f32 * 1.5)
|
||||||
* (crate::constants::TILE_SIZE as f32 * 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;
|
||||||
|
tilemap_mut.claimed_tiles.remove(&drop);
|
||||||
info!(
|
info!(
|
||||||
"[HAUL] {:?} arrived at drop point {:?}",
|
"[HAUL] {:?} arrived at drop point {:?}",
|
||||||
entity, drop_pos
|
entity, drop
|
||||||
);
|
);
|
||||||
*step = HaulStep::Dropping { drop_pos };
|
*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 } => {
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
use crate::constants::{ITILE_SIZE, TILE_SIZE};
|
|
||||||
use crate::entities::behaviour::IdleBehaviour;
|
|
||||||
use crate::entities::shared_components::Ambulatory;
|
|
||||||
use crate::entities::tasks::components::{IdleState, Task, IDLE_MAX_RETRIES};
|
|
||||||
use crate::world::chunks::ChunkMap;
|
|
||||||
use crate::world::chunks::CHUNK_SIZE;
|
|
||||||
use crate::world::tiles::TileMap;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use bevy_rand::prelude::*;
|
|
||||||
use rand::RngExt;
|
|
||||||
|
|
||||||
pub(super) fn execute_idle(
|
|
||||||
task: &mut Task,
|
|
||||||
transform: &Transform,
|
|
||||||
ambulatory: &mut Ambulatory,
|
|
||||||
sprite: &mut Sprite,
|
|
||||||
tilemap: &TileMap,
|
|
||||||
chunk_map: &ChunkMap,
|
|
||||||
behaviour: &IdleBehaviour,
|
|
||||||
rng: &mut WyRand,
|
|
||||||
current_tick: u32,
|
|
||||||
) {
|
|
||||||
let Task::Idle {
|
|
||||||
origin,
|
|
||||||
sigma_world,
|
|
||||||
state,
|
|
||||||
} = task
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match state {
|
|
||||||
IdleState::Picking {
|
|
||||||
retry_after_tick,
|
|
||||||
retry_count,
|
|
||||||
} => {
|
|
||||||
if current_tick < *retry_after_tick {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match pick_gaussian_target(origin, *sigma_world, tilemap, chunk_map, rng) {
|
|
||||||
Some(target) => {
|
|
||||||
ambulatory.target = Some(Vec3::new(
|
|
||||||
target.x as f32,
|
|
||||||
target.y as f32,
|
|
||||||
target.z as f32 + 1.0,
|
|
||||||
));
|
|
||||||
ambulatory.current_path = None;
|
|
||||||
ambulatory.path_index = 0;
|
|
||||||
*state = IdleState::Moving { target };
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
*retry_count += 1;
|
|
||||||
if *retry_count >= IDLE_MAX_RETRIES {
|
|
||||||
panic!(
|
|
||||||
"Entity stuck: pick_gaussian_target returned None {} times \
|
|
||||||
consecutively. origin={:?} sigma_world={:.1} \
|
|
||||||
loaded_chunks={} \
|
|
||||||
— no standable tile found. Check tilemap state.",
|
|
||||||
retry_count,
|
|
||||||
origin,
|
|
||||||
sigma_world,
|
|
||||||
chunk_map.loaded_chunks.len(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
*retry_after_tick = current_tick.saturating_add(30);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IdleState::Moving { target } => {
|
|
||||||
let dx = transform.translation.x - target.x as f32;
|
|
||||||
let dy = transform.translation.y - target.y as f32;
|
|
||||||
let dist_sq = dx * dx + dy * dy;
|
|
||||||
let arrive_threshold_sq = (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5);
|
|
||||||
|
|
||||||
let arrived = dist_sq < arrive_threshold_sq;
|
|
||||||
let no_nav = ambulatory.target.is_none() && ambulatory.current_path.is_none();
|
|
||||||
|
|
||||||
if arrived || no_nav {
|
|
||||||
ambulatory.target = None;
|
|
||||||
ambulatory.current_path = None;
|
|
||||||
|
|
||||||
let loiter_roll: f32 = rng.random();
|
|
||||||
if loiter_roll < behaviour.loiter_chance {
|
|
||||||
let duration_range = behaviour.loiter_max_ticks - behaviour.loiter_min_ticks;
|
|
||||||
let duration =
|
|
||||||
behaviour.loiter_min_ticks + rng.random_range(0..=duration_range);
|
|
||||||
|
|
||||||
let flip1: f32 = rng.random();
|
|
||||||
let flip2: f32 = rng.random();
|
|
||||||
let flips_remaining = (flip1 < behaviour.flip_chance) as u8
|
|
||||||
+ (flip2 < behaviour.flip_chance) as u8;
|
|
||||||
|
|
||||||
let next_flip_at = if flips_remaining > 0 {
|
|
||||||
current_tick.saturating_add(rng.random_range(1..=duration / 2))
|
|
||||||
} else {
|
|
||||||
u32::MAX
|
|
||||||
};
|
|
||||||
|
|
||||||
*state = IdleState::Loitering {
|
|
||||||
ticks_remaining: duration,
|
|
||||||
flips_remaining,
|
|
||||||
next_flip_at,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
*state = IdleState::Picking {
|
|
||||||
retry_after_tick: 0,
|
|
||||||
retry_count: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IdleState::Loitering {
|
|
||||||
ticks_remaining,
|
|
||||||
flips_remaining,
|
|
||||||
next_flip_at,
|
|
||||||
} => {
|
|
||||||
if *flips_remaining > 0 && current_tick >= *next_flip_at {
|
|
||||||
sprite.flip_x = !sprite.flip_x;
|
|
||||||
*flips_remaining -= 1;
|
|
||||||
|
|
||||||
if *flips_remaining > 0 && *ticks_remaining > 2 {
|
|
||||||
*next_flip_at =
|
|
||||||
current_tick.saturating_add(rng.random_range(1..=*ticks_remaining / 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if *ticks_remaining == 0 {
|
|
||||||
sprite.flip_x = false;
|
|
||||||
*state = IdleState::Picking {
|
|
||||||
retry_after_tick: 0,
|
|
||||||
retry_count: 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
*ticks_remaining -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_gaussian_target(
|
|
||||||
origin: &IVec3,
|
|
||||||
sigma_world: f32,
|
|
||||||
tilemap: &TileMap,
|
|
||||||
chunk_map: &ChunkMap,
|
|
||||||
rng: &mut WyRand,
|
|
||||||
) -> Option<IVec3> {
|
|
||||||
let two_sigma_sq = 2.0 * sigma_world * sigma_world;
|
|
||||||
let mut chosen: Option<IVec3> = None;
|
|
||||||
let mut weight_sum = 0.0f32;
|
|
||||||
|
|
||||||
for &chunk_pos in chunk_map.loaded_chunks.keys() {
|
|
||||||
let has_all_neighbours = chunk_map
|
|
||||||
.loaded_chunks
|
|
||||||
.contains_key(&(chunk_pos + IVec2::X))
|
|
||||||
&& chunk_map
|
|
||||||
.loaded_chunks
|
|
||||||
.contains_key(&(chunk_pos - IVec2::X))
|
|
||||||
&& chunk_map
|
|
||||||
.loaded_chunks
|
|
||||||
.contains_key(&(chunk_pos + IVec2::Y))
|
|
||||||
&& chunk_map
|
|
||||||
.loaded_chunks
|
|
||||||
.contains_key(&(chunk_pos - IVec2::Y));
|
|
||||||
if !has_all_neighbours {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SAMPLES_PER_CHUNK: usize = 4;
|
|
||||||
for _ in 0..SAMPLES_PER_CHUNK {
|
|
||||||
let local_x = rng.random_range(0..CHUNK_SIZE);
|
|
||||||
let local_y = rng.random_range(0..CHUNK_SIZE);
|
|
||||||
let world_x = (chunk_pos.x * CHUNK_SIZE + local_x) * ITILE_SIZE;
|
|
||||||
let world_y = (chunk_pos.y * CHUNK_SIZE + local_y) * ITILE_SIZE;
|
|
||||||
|
|
||||||
let Some(candidate) = find_surface(world_x, world_y, tilemap) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let dx = (candidate.x - origin.x) as f32;
|
|
||||||
let dy = (candidate.y - origin.y) as f32;
|
|
||||||
let dist_sq = dx * dx + dy * dy;
|
|
||||||
let weight = (-dist_sq / two_sigma_sq).exp();
|
|
||||||
|
|
||||||
weight_sum += weight;
|
|
||||||
let accept: f32 = rng.random();
|
|
||||||
if accept < weight / weight_sum {
|
|
||||||
chosen = Some(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
chosen
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn find_surface(world_x: i32, world_y: i32, tilemap: &TileMap) -> Option<IVec3> {
|
|
||||||
for z in -3i32..=4i32 {
|
|
||||||
let floor_pos = IVec3::new(world_x, world_y, z * ITILE_SIZE);
|
|
||||||
if tilemap.floor_tiles.contains_key(&floor_pos) {
|
|
||||||
let above = IVec3::new(world_x, world_y, floor_pos.z + ITILE_SIZE);
|
|
||||||
if tilemap.is_standable(above) {
|
|
||||||
return Some(above);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
use crate::entities::behaviour::{EntityBehaviourRegistry, EntityType};
|
||||||
|
use crate::entities::cargo::HaulSlot;
|
||||||
|
use crate::entities::shared_components::Ambulatory;
|
||||||
|
use crate::entities::tasks::components::{Task, TaskQueue, TaskState};
|
||||||
|
use crate::entities::tasks::job_queue::{JobKind, JobQueue};
|
||||||
|
use crate::entities::tasks::jobs::{FellTreeJob, HaulCargoJob, IdleJob};
|
||||||
|
use crate::world::tiles::TileMap;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn job_assignment_system(
|
||||||
|
mut job_queue: ResMut<JobQueue>,
|
||||||
|
tilemap: Res<TileMap>,
|
||||||
|
mut dorf_query: Query<
|
||||||
|
(
|
||||||
|
&mut TaskQueue,
|
||||||
|
&mut TaskState,
|
||||||
|
&Transform,
|
||||||
|
&HaulSlot,
|
||||||
|
&mut Ambulatory,
|
||||||
|
),
|
||||||
|
With<EntityType>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
for (mut queue, mut state, transform, haul_slot, mut ambulatory) in dorf_query.iter_mut() {
|
||||||
|
let is_idle = queue.is_empty() || matches!(queue.current(), Some(Task::Idle { .. }));
|
||||||
|
|
||||||
|
if !is_idle {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if haul_slot.is_occupied() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dorf_pos_ivec = transform.translation.as_ivec3();
|
||||||
|
let dorf_pos_2d = Vec2::new(transform.translation.x, transform.translation.y);
|
||||||
|
|
||||||
|
if let Some(job) = job_queue.pop_best_pathfinding(&tilemap, dorf_pos_ivec, dorf_pos_2d) {
|
||||||
|
info!(
|
||||||
|
"[ASSIGN] Job assigned to dorf at {:?}: {:?}",
|
||||||
|
dorf_pos_ivec.xy(),
|
||||||
|
job
|
||||||
|
);
|
||||||
|
|
||||||
|
// Stop the dorf in its tracks so the new Task can take over movement.
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
ambulatory.target = None;
|
||||||
|
ambulatory.path_index = 0;
|
||||||
|
|
||||||
|
match job {
|
||||||
|
JobKind::FellTree { trunk_pos } => {
|
||||||
|
info!("[ASSIGN] Dorf assigned to FellTree at {:?}", trunk_pos);
|
||||||
|
queue.clear();
|
||||||
|
queue.push(FellTreeJob::start(trunk_pos));
|
||||||
|
*state = TaskState::Pending;
|
||||||
|
}
|
||||||
|
JobKind::HaulCargo {
|
||||||
|
cargo_entity,
|
||||||
|
cargo_pos,
|
||||||
|
dest,
|
||||||
|
} => {
|
||||||
|
info!("[ASSIGN] Dorf assigned to HaulCargo at {:?}", cargo_pos);
|
||||||
|
queue.clear();
|
||||||
|
queue.push(HaulCargoJob::start(cargo_entity, cargo_pos, dest));
|
||||||
|
*state = TaskState::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if queue.is_empty() {
|
||||||
|
let behaviour = EntityBehaviourRegistry::global_get("dorf");
|
||||||
|
let origin = transform.translation.as_ivec3();
|
||||||
|
if queue.is_empty() {
|
||||||
|
queue.push(IdleJob::start(origin, &behaviour.idle));
|
||||||
|
*state = TaskState::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use crate::constants::ITILE_SIZE;
|
||||||
|
use crate::entities::shared_systems::pathfinding::calculate_provisional_path;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum JobKind {
|
||||||
|
FellTree {
|
||||||
|
trunk_pos: IVec3,
|
||||||
|
},
|
||||||
|
HaulCargo {
|
||||||
|
cargo_entity: Entity,
|
||||||
|
cargo_pos: IVec3,
|
||||||
|
dest: IVec3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobKind {
|
||||||
|
#[inline]
|
||||||
|
pub fn priority(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
JobKind::FellTree { .. } => 2,
|
||||||
|
JobKind::HaulCargo { .. } => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn target(&self) -> IVec3 {
|
||||||
|
match self {
|
||||||
|
JobKind::FellTree { trunk_pos, .. } => *trunk_pos,
|
||||||
|
JobKind::HaulCargo { cargo_pos, .. } => *cargo_pos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_fell_tree(&self) -> bool {
|
||||||
|
matches!(self, JobKind::FellTree { .. })
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn is_haul_cargo(&self) -> bool {
|
||||||
|
matches!(self, JobKind::HaulCargo { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
kind: JobKind,
|
||||||
|
claimed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct JobQueue {
|
||||||
|
jobs: VecDeque<Entry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobQueue {
|
||||||
|
#[inline]
|
||||||
|
pub fn push(&mut self, kind: JobKind) {
|
||||||
|
self.jobs.push_back(Entry {
|
||||||
|
kind,
|
||||||
|
claimed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_best_pathfinding(
|
||||||
|
&mut self,
|
||||||
|
tilemap: &crate::world::tiles::TileMap,
|
||||||
|
dorf_pos: IVec3,
|
||||||
|
_dorf_pos_2d: Vec2,
|
||||||
|
) -> Option<JobKind> {
|
||||||
|
if self.jobs.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Gather all unclaimed jobs and score them for sorting.
|
||||||
|
// We store: (Queue Index, Priority, Rough Distance)
|
||||||
|
let mut candidates: Vec<(usize, u8, i32)> = self
|
||||||
|
.jobs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, entry)| !entry.claimed)
|
||||||
|
.map(|(i, entry)| {
|
||||||
|
let target = entry.kind.target();
|
||||||
|
// Manhattan distance is a cheap heuristic for sorting
|
||||||
|
let dist = (target.x - dorf_pos.x).abs()
|
||||||
|
+ (target.y - dorf_pos.y).abs()
|
||||||
|
+ (target.z - dorf_pos.z).abs();
|
||||||
|
(i, entry.kind.priority(), dist)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// 2. Sort: Highest Priority first. If tied, Shortest Distance first.
|
||||||
|
candidates.sort_by(|a, b| {
|
||||||
|
b.1.cmp(&a.1) // Descending priority
|
||||||
|
.then(a.2.cmp(&b.2)) // Ascending distance
|
||||||
|
});
|
||||||
|
|
||||||
|
let max_path_dist = 1024;
|
||||||
|
|
||||||
|
// 3. Evaluate the sorted jobs with actual pathfinding
|
||||||
|
for (idx, _pri, _dist) in candidates {
|
||||||
|
let entry = &self.jobs[idx];
|
||||||
|
|
||||||
|
let best_path_target = match &entry.kind {
|
||||||
|
JobKind::FellTree { trunk_pos } => {
|
||||||
|
let standable_tiles = Self::find_all_standable_adjacent(trunk_pos, tilemap);
|
||||||
|
|
||||||
|
let mut best_target = None;
|
||||||
|
let mut shortest_path_len = usize::MAX;
|
||||||
|
|
||||||
|
// Pathfind to EVERY standable adjacent tile to find the absolute closest one
|
||||||
|
for tile in standable_tiles {
|
||||||
|
let path =
|
||||||
|
calculate_provisional_path(tilemap, dorf_pos, tile, max_path_dist);
|
||||||
|
if !path.is_empty() && path.len() < shortest_path_len {
|
||||||
|
shortest_path_len = path.len();
|
||||||
|
best_target = Some(tile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best_target
|
||||||
|
}
|
||||||
|
JobKind::HaulCargo { cargo_pos, .. } => {
|
||||||
|
let path =
|
||||||
|
calculate_provisional_path(tilemap, dorf_pos, *cargo_pos, max_path_dist);
|
||||||
|
if !path.is_empty() {
|
||||||
|
Some(*cargo_pos)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. If we found a valid path to this job, claim it and return it
|
||||||
|
if let Some(target_pos) = best_path_target {
|
||||||
|
info!(
|
||||||
|
"[QUEUE] Job Claimed: kind={:?} target={:?} dorf={:?}",
|
||||||
|
entry.kind, target_pos, dorf_pos
|
||||||
|
);
|
||||||
|
|
||||||
|
self.jobs[idx].claimed = true;
|
||||||
|
// Note: JobKind needs `#[derive(Clone)]` if it doesn't have it already
|
||||||
|
return Some(self.jobs[idx].kind.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("[QUEUE] No reachable jobs found for dorf at {:?}", dorf_pos);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ALL standable tiles immediately adjacent to the tree on the same Z level.
|
||||||
|
// job_queue.rs
|
||||||
|
|
||||||
|
fn find_all_standable_adjacent(
|
||||||
|
trunk_pos: &IVec3,
|
||||||
|
tilemap: &crate::world::tiles::TileMap,
|
||||||
|
) -> SmallVec<[IVec3; 8]> {
|
||||||
|
let mut tiles = SmallVec::new();
|
||||||
|
|
||||||
|
// The standing position is the SAME Z as the trunk.
|
||||||
|
// If trunk is at 16, dorf stands at 16 (in the air).
|
||||||
|
let standing_z = trunk_pos.z;
|
||||||
|
|
||||||
|
for dx in -1i32..=1 {
|
||||||
|
for dy in -1i32..=1 {
|
||||||
|
if dx == 0 && dy == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let candidate = IVec3::new(
|
||||||
|
trunk_pos.x + dx * ITILE_SIZE,
|
||||||
|
trunk_pos.y + dy * ITILE_SIZE,
|
||||||
|
standing_z,
|
||||||
|
);
|
||||||
|
|
||||||
|
if tilemap.is_standable(candidate) {
|
||||||
|
tiles.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tiles
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.jobs.is_empty()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.jobs.len()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
pub fn has_fell_tree(&self) -> bool {
|
||||||
|
self.jobs.iter().any(|e| e.kind.is_fell_tree())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_counts(&self) -> (usize, usize) {
|
||||||
|
let fell = self.jobs.iter().filter(|e| e.kind.is_fell_tree()).count();
|
||||||
|
let haul = self.jobs.iter().filter(|e| e.kind.is_haul_cargo()).count();
|
||||||
|
(fell, haul)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = &JobKind> {
|
||||||
|
self.jobs.iter().map(|e| &e.kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use crate::entities::behaviour::IdleBehaviour;
|
||||||
|
use crate::entities::tasks::components::{ChopStep, HaulStep, IdleState, Task, CHOP_TICKS_DEFAULT};
|
||||||
|
use bevy::prelude::{Entity, IVec3};
|
||||||
|
|
||||||
|
pub struct FellTreeJob;
|
||||||
|
|
||||||
|
impl FellTreeJob {
|
||||||
|
pub fn start(trunk_pos: IVec3) -> Task {
|
||||||
|
Task::ChopTree {
|
||||||
|
trunk_pos,
|
||||||
|
chop_ticks: CHOP_TICKS_DEFAULT,
|
||||||
|
step: ChopStep::MovingToTree { approach: None },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HaulCargoJob;
|
||||||
|
|
||||||
|
impl HaulCargoJob {
|
||||||
|
pub fn start(cargo_entity: Entity, cargo_pos: IVec3, dest: IVec3) -> Task {
|
||||||
|
Task::HaulCargo {
|
||||||
|
cargo_entity,
|
||||||
|
cargo_pos,
|
||||||
|
dest,
|
||||||
|
step: HaulStep::MovingToCargo { approach: None },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IdleJob;
|
||||||
|
|
||||||
|
impl IdleJob {
|
||||||
|
pub fn start(origin: IVec3, behaviour: &IdleBehaviour) -> Task {
|
||||||
|
Task::Idle {
|
||||||
|
origin,
|
||||||
|
sigma_world: behaviour.sigma_world,
|
||||||
|
state: IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,13 +2,18 @@ pub mod components;
|
|||||||
pub mod demo;
|
pub mod demo;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod executor;
|
pub mod executor;
|
||||||
pub mod idle;
|
pub mod job_assignment;
|
||||||
|
pub mod job_queue;
|
||||||
|
pub mod jobs;
|
||||||
|
pub mod queue_debug;
|
||||||
|
pub mod tasks;
|
||||||
|
|
||||||
pub use components::{IdleState, Task, TaskQueue, TaskState};
|
pub use components::{IdleState, Task, TaskQueue, TaskState};
|
||||||
#[cfg(debug_assertions)]
|
pub use demo::demo_system;
|
||||||
pub use demo::debug_task_queues;
|
|
||||||
pub use demo::{demo_system, DemoState};
|
|
||||||
pub use events::{LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
|
pub use events::{LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped, TaskFailed};
|
||||||
pub use executor::task_executor_system;
|
pub use executor::task_executor_system;
|
||||||
|
pub use job_assignment::job_assignment_system;
|
||||||
|
pub use job_queue::{JobKind, JobQueue};
|
||||||
|
pub use queue_debug::queue_debug_system;
|
||||||
|
|
||||||
pub use crate::plugins::tasks::TasksPlugin;
|
pub use crate::plugins::tasks::TasksPlugin;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::entities::behaviour::EntityType;
|
||||||
|
use crate::entities::tasks::components::{TaskQueue, TaskState};
|
||||||
|
use crate::entities::tasks::job_queue::{JobKind, JobQueue};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub fn queue_debug_system(
|
||||||
|
job_queue: Res<JobQueue>,
|
||||||
|
dorf_query: Query<(&TaskQueue, &TaskState, &Transform), With<EntityType>>,
|
||||||
|
) {
|
||||||
|
let (fell_count, haul_count) = job_queue.debug_counts();
|
||||||
|
let total = job_queue.len();
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"=== QUEUE === fell:{}, haul:{}, total:{}",
|
||||||
|
fell_count, haul_count, total
|
||||||
|
);
|
||||||
|
|
||||||
|
for (i, kind) in job_queue.iter().enumerate() {
|
||||||
|
match kind {
|
||||||
|
JobKind::FellTree { trunk_pos } => {
|
||||||
|
info!(" [{}] FellTree at {:?}", i, trunk_pos);
|
||||||
|
}
|
||||||
|
JobKind::HaulCargo {
|
||||||
|
cargo_entity,
|
||||||
|
cargo_pos,
|
||||||
|
dest,
|
||||||
|
} => {
|
||||||
|
info!(
|
||||||
|
" [{}] HaulCargo entity:{:?} from {:?} to {:?}",
|
||||||
|
i, cargo_entity, cargo_pos, dest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("=== DORFS ===");
|
||||||
|
for (queue, state, transform) in dorf_query.iter() {
|
||||||
|
let pos = transform.translation.truncate();
|
||||||
|
let current_task = queue.current().map(|t| t.name()).unwrap_or("EMPTY");
|
||||||
|
let state_str = match *state {
|
||||||
|
TaskState::Pending => "PENDING",
|
||||||
|
TaskState::Active => "ACTIVE",
|
||||||
|
TaskState::Completed => "COMPLETED",
|
||||||
|
TaskState::Failed => "FAILED",
|
||||||
|
};
|
||||||
|
info!(
|
||||||
|
" Dorf at {:?}: task={} state={} queue_len={}",
|
||||||
|
pos,
|
||||||
|
current_task,
|
||||||
|
state_str,
|
||||||
|
queue.tasks.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
use super::TaskResult;
|
||||||
|
use crate::constants::{ITILE_SIZE, TILE_SIZE};
|
||||||
|
use crate::entities::behaviour::IdleBehaviour;
|
||||||
|
use crate::entities::shared_components::Ambulatory;
|
||||||
|
use crate::entities::tasks::components::{IdleState, Task, IDLE_MAX_RETRIES};
|
||||||
|
use crate::world::chunks::ChunkMap;
|
||||||
|
use crate::world::chunks::CHUNK_SIZE;
|
||||||
|
use crate::world::tiles::TileMap;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_rand::prelude::*;
|
||||||
|
use rand::RngExt;
|
||||||
|
|
||||||
|
pub fn execute_idle(
|
||||||
|
task: &mut Task,
|
||||||
|
transform: &Transform,
|
||||||
|
ambulatory: &mut Ambulatory,
|
||||||
|
sprite: &mut Sprite,
|
||||||
|
tilemap: &TileMap,
|
||||||
|
chunk_map: &ChunkMap,
|
||||||
|
behaviour: &IdleBehaviour,
|
||||||
|
rng: &mut WyRand,
|
||||||
|
current_tick: u32,
|
||||||
|
) -> TaskResult {
|
||||||
|
let Task::Idle {
|
||||||
|
origin,
|
||||||
|
sigma_world,
|
||||||
|
state,
|
||||||
|
} = task
|
||||||
|
else {
|
||||||
|
return TaskResult::Failed("not an idle task");
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.clone() {
|
||||||
|
IdleState::Picking {
|
||||||
|
retry_after_tick,
|
||||||
|
retry_count,
|
||||||
|
} => {
|
||||||
|
if current_tick < retry_after_tick {
|
||||||
|
return TaskResult::Continue(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
match pick_gaussian_target(origin, *sigma_world, tilemap, chunk_map, rng) {
|
||||||
|
Some(floor_pos) => {
|
||||||
|
// 1. The PATHFINDER needs to target the air ABOVE the floor
|
||||||
|
let standable_target = floor_pos + IVec3::new(0, 0, ITILE_SIZE);
|
||||||
|
|
||||||
|
// 2. The AMBULATORY system (floats) needs the target + 1.0 offset
|
||||||
|
ambulatory.target = Some(Vec3::new(
|
||||||
|
standable_target.x as f32,
|
||||||
|
standable_target.y as f32,
|
||||||
|
standable_target.z as f32 + 1.0,
|
||||||
|
));
|
||||||
|
|
||||||
|
// 3. Reset movement state
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
ambulatory.path_index = 0;
|
||||||
|
ambulatory.move_direction = Vec2::ZERO;
|
||||||
|
|
||||||
|
// 4. Update the Task State with the STANDABLE target
|
||||||
|
if let Task::Idle {
|
||||||
|
state: ref mut s, ..
|
||||||
|
} = task
|
||||||
|
{
|
||||||
|
*s = IdleState::Moving {
|
||||||
|
target: standable_target,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the required TaskResult
|
||||||
|
TaskResult::Continue(0)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// If no target found, retry later
|
||||||
|
if let Task::Idle {
|
||||||
|
state: ref mut s, ..
|
||||||
|
} = task
|
||||||
|
{
|
||||||
|
*s = IdleState::Picking {
|
||||||
|
retry_after_tick: current_tick + 30,
|
||||||
|
retry_count: retry_count + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
TaskResult::Continue(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IdleState::Moving { target } => {
|
||||||
|
info!(
|
||||||
|
"[IDLE] Moving: target={:?} current={:?}",
|
||||||
|
target, transform.translation
|
||||||
|
);
|
||||||
|
let dx = transform.translation.x - target.x as f32;
|
||||||
|
let dy = transform.translation.y - target.y as f32;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
let arrive_threshold_sq = (TILE_SIZE * 1.5) * (TILE_SIZE * 1.5);
|
||||||
|
|
||||||
|
let arrived = dist_sq < arrive_threshold_sq;
|
||||||
|
let no_nav = ambulatory.target.is_none() && ambulatory.current_path.is_none();
|
||||||
|
|
||||||
|
info!("[IDLE] Moving: arrived={} no_nav={}", arrived, no_nav);
|
||||||
|
if arrived || no_nav {
|
||||||
|
ambulatory.target = None;
|
||||||
|
ambulatory.current_path = None;
|
||||||
|
|
||||||
|
let loiter_roll: f32 = rng.random();
|
||||||
|
if loiter_roll < behaviour.loiter_chance {
|
||||||
|
let duration_range = behaviour.loiter_max_ticks - behaviour.loiter_min_ticks;
|
||||||
|
let duration =
|
||||||
|
behaviour.loiter_min_ticks + rng.random_range(0..=duration_range);
|
||||||
|
|
||||||
|
let flip1: f32 = rng.random();
|
||||||
|
let flip2: f32 = rng.random();
|
||||||
|
let flips_remaining = (flip1 < behaviour.flip_chance) as u8
|
||||||
|
+ (flip2 < behaviour.flip_chance) as u8;
|
||||||
|
|
||||||
|
let next_flip_at = if flips_remaining > 0 {
|
||||||
|
current_tick.saturating_add(rng.random_range(1..=duration / 2))
|
||||||
|
} else {
|
||||||
|
u32::MAX
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Task::Idle {
|
||||||
|
state: ref mut s, ..
|
||||||
|
} = task
|
||||||
|
{
|
||||||
|
*s = IdleState::Loitering {
|
||||||
|
ticks_remaining: duration,
|
||||||
|
flips_remaining,
|
||||||
|
next_flip_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Task::Idle {
|
||||||
|
state: ref mut s, ..
|
||||||
|
} = task
|
||||||
|
{
|
||||||
|
*s = IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TaskResult::Continue(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
IdleState::Loitering {
|
||||||
|
ticks_remaining,
|
||||||
|
flips_remaining,
|
||||||
|
next_flip_at,
|
||||||
|
} => {
|
||||||
|
if flips_remaining > 0 && current_tick >= next_flip_at {
|
||||||
|
sprite.flip_x = !sprite.flip_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ticks_remaining == 0 {
|
||||||
|
sprite.flip_x = false;
|
||||||
|
if let Task::Idle {
|
||||||
|
state: ref mut s, ..
|
||||||
|
} = task
|
||||||
|
{
|
||||||
|
*s = IdleState::Picking {
|
||||||
|
retry_after_tick: 0,
|
||||||
|
retry_count: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Task::Idle {
|
||||||
|
state:
|
||||||
|
IdleState::Loitering {
|
||||||
|
ref mut ticks_remaining,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
..
|
||||||
|
} = task
|
||||||
|
{
|
||||||
|
*ticks_remaining -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TaskResult::Continue(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_gaussian_target(
|
||||||
|
origin: &IVec3,
|
||||||
|
sigma_world: f32,
|
||||||
|
tilemap: &TileMap,
|
||||||
|
_chunk_map: &ChunkMap, // No longer needed for O(1) lookups
|
||||||
|
rng: &mut WyRand,
|
||||||
|
) -> Option<IVec3> {
|
||||||
|
if tilemap.surface_positions.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let two_sigma_sq = 2.0 * sigma_world * sigma_world;
|
||||||
|
let mut chosen: Option<IVec3> = None;
|
||||||
|
let mut weight_sum = 0.0f32;
|
||||||
|
|
||||||
|
// We sample a fixed number of times globally from known surface tiles.
|
||||||
|
// This is O(SAMPLES) instead of O(CHUNKS * SAMPLES * TOTAL_TILES).
|
||||||
|
const TOTAL_SAMPLES: usize = 32;
|
||||||
|
|
||||||
|
for _ in 0..TOTAL_SAMPLES {
|
||||||
|
// 1. Pick a random surface tile directly from the pre-populated list
|
||||||
|
let idx = rng.random_range(0..tilemap.surface_positions.len());
|
||||||
|
let candidate = tilemap.surface_positions[idx];
|
||||||
|
|
||||||
|
// 2. Calculate Gaussian weight based on 2D distance
|
||||||
|
// (Allows dorfs to choose targets on different Z-levels if they are surface tiles)
|
||||||
|
let dx = (candidate.x - origin.x) as f32;
|
||||||
|
let dy = (candidate.y - origin.y) as f32;
|
||||||
|
let dist_sq = dx * dx + dy * dy;
|
||||||
|
|
||||||
|
// Use a small epsilon to prevent exp(0) issues
|
||||||
|
let weight = (-(dist_sq / two_sigma_sq)).exp().max(0.0001);
|
||||||
|
|
||||||
|
// 3. Reservoir Sampling: Update the chosen target based on relative weight
|
||||||
|
weight_sum += weight;
|
||||||
|
if rng.random_range(0.0..1.0) < (weight / weight_sum) {
|
||||||
|
chosen = Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pos) = chosen {
|
||||||
|
info!(
|
||||||
|
"[IDLE] Target picked at {:?} (Weight Sum: {:.4})",
|
||||||
|
pos, weight_sum
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod idle;
|
||||||
|
|
||||||
|
pub enum TaskResult {
|
||||||
|
Complete,
|
||||||
|
Continue(u32),
|
||||||
|
Failed(&'static str),
|
||||||
|
}
|
||||||
+11
-19
@@ -1,13 +1,7 @@
|
|||||||
//! TasksPlugin — registers task infrastructure: executor, events, idle logic, demo loop.
|
|
||||||
//!
|
|
||||||
//! Systems:
|
|
||||||
//! - task_executor_system (FixedUpdate)
|
|
||||||
//! - demo_system (FixedUpdate, after task_executor_system)
|
|
||||||
//! - debug_task_queues (FixedUpdate, after demo_system, debug only)
|
|
||||||
|
|
||||||
use crate::entities::tasks::{
|
use crate::entities::tasks::{
|
||||||
demo_system, task_executor_system, DemoState, LogsSpawned, TaskBlocked, TaskClaimed,
|
demo_system, job_assignment_system, job_queue::JobQueue, queue_debug_system,
|
||||||
TaskCompleted, TaskDropped, TaskFailed,
|
task_executor_system, LogsSpawned, TaskBlocked, TaskClaimed, TaskCompleted, TaskDropped,
|
||||||
|
TaskFailed,
|
||||||
};
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
@@ -21,17 +15,15 @@ impl Plugin for TasksPlugin {
|
|||||||
.add_message::<TaskDropped>()
|
.add_message::<TaskDropped>()
|
||||||
.add_message::<TaskBlocked>()
|
.add_message::<TaskBlocked>()
|
||||||
.add_message::<LogsSpawned>()
|
.add_message::<LogsSpawned>()
|
||||||
.init_resource::<DemoState>()
|
.init_resource::<JobQueue>()
|
||||||
.add_systems(
|
.add_systems(
|
||||||
bevy::app::FixedUpdate,
|
FixedUpdate,
|
||||||
demo_system.after(task_executor_system),
|
(
|
||||||
|
demo_system,
|
||||||
|
job_assignment_system,
|
||||||
|
task_executor_system,
|
||||||
|
queue_debug_system,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Debug only — compiles away in release
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
use crate::entities::tasks::debug_task_queues;
|
|
||||||
app.add_systems(bevy::app::FixedUpdate, debug_task_queues.after(demo_system));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,7 +292,8 @@ pub fn fell_tree(
|
|||||||
tilemap: &mut TileMap,
|
tilemap: &mut TileMap,
|
||||||
tile_changed: &mut MessageWriter<TileChangedEvent>,
|
tile_changed: &mut MessageWriter<TileChangedEvent>,
|
||||||
occlusion: &mut MessageWriter<TileOcclusionEvent>,
|
occlusion: &mut MessageWriter<TileOcclusionEvent>,
|
||||||
) -> SmallVec<[IVec3; 8]> {
|
) -> (IVec3, usize) {
|
||||||
|
// Changed return type
|
||||||
let target_chunk = world_to_chunk(trunk_pos);
|
let target_chunk = world_to_chunk(trunk_pos);
|
||||||
let trunk_x = trunk_pos.x;
|
let trunk_x = trunk_pos.x;
|
||||||
let trunk_y = trunk_pos.y;
|
let trunk_y = trunk_pos.y;
|
||||||
@@ -333,5 +334,19 @@ pub fn fell_tree(
|
|||||||
for pos in dirty_columns {
|
for pos in dirty_columns {
|
||||||
occlusion.write(TileOcclusionEvent { tile_position: pos });
|
occlusion.write(TileOcclusionEvent { tile_position: pos });
|
||||||
}
|
}
|
||||||
trunk_positions
|
|
||||||
|
let trunk_count = trunk_positions.len();
|
||||||
|
// Default to the input trunk_pos if for some reason no trunk parts were found
|
||||||
|
let lowest_trunk = trunk_positions
|
||||||
|
.iter()
|
||||||
|
.min_by_key(|p| p.z)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(trunk_pos);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"[FORESTRY] fell_tree: trunk_pos={:?} lowest={:?} count={}",
|
||||||
|
trunk_pos, lowest_trunk, trunk_count
|
||||||
|
);
|
||||||
|
|
||||||
|
(lowest_trunk, trunk_count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -329,6 +329,10 @@ pub fn apply_terrain_blobs(
|
|||||||
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
|
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for surface in blob.surface_positions.iter() {
|
||||||
|
tilemap.surface_positions.push(surface.0.as_ivec3());
|
||||||
|
}
|
||||||
|
|
||||||
forrestry_event_writer.write(ChunkForrestryEvent {
|
forrestry_event_writer.write(ChunkForrestryEvent {
|
||||||
chunk_position: blob.chunk_pos,
|
chunk_position: blob.chunk_pos,
|
||||||
floor_tiles: blob.surface_positions,
|
floor_tiles: blob.surface_positions,
|
||||||
|
|||||||
@@ -96,14 +96,11 @@ impl ChunkData {
|
|||||||
/// - (can_stand_in_floor OR can_stand_in_fixture) at (x, y, z)
|
/// - (can_stand_in_floor OR can_stand_in_fixture) at (x, y, z)
|
||||||
/// - AND (can_stand_on_floor OR can_stand_on_fixture) at (x, y, z-1)
|
/// - AND (can_stand_on_floor OR can_stand_on_fixture) at (x, y, z-1)
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_standable(&self, local_x: i32, local_y: i32, z: i32) -> bool {
|
// chunkdata.rs
|
||||||
// Bounds check: z must be within -Z_BELOW..Z_ABOVE
|
|
||||||
if z < -(Z_BELOW as i32) || z > (Z_ABOVE as i32) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds check: local coords must be within chunk
|
pub fn is_standable(&self, local_x: i32, local_y: i32, z: i32) -> bool {
|
||||||
if local_x < 0 || local_x >= CHUNK_SIZE || local_y < 0 || local_y >= CHUNK_SIZE {
|
// 1. Bounds check (z is an index here, e.g., 0 to 64)
|
||||||
|
if z < 0 || z >= (Z_BELOW + Z_ABOVE) as i32 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,15 +109,15 @@ impl ChunkData {
|
|||||||
let bit = idx % 32;
|
let bit = idx % 32;
|
||||||
let mask = 1u32 << bit;
|
let mask = 1u32 << bit;
|
||||||
|
|
||||||
|
// Am I in Air?
|
||||||
let in_floor = (self.stand_in_floor[word] & mask) != 0;
|
let in_floor = (self.stand_in_floor[word] & mask) != 0;
|
||||||
let in_fixture = (self.stand_in_fixture[word] & mask) != 0;
|
let in_fixture = (self.stand_in_fixture[word] & mask) != 0;
|
||||||
|
|
||||||
// Can't stand at the very bottom of the world
|
// 2. Check the tile immediately below (z - 1)
|
||||||
if z <= -(Z_BELOW as i32) {
|
if z <= 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
} // Bottom of the world
|
||||||
|
|
||||||
// Check tile below for "stand on"
|
|
||||||
let below_idx = Self::pos_to_index(local_x, local_y, z - 1);
|
let below_idx = Self::pos_to_index(local_x, local_y, z - 1);
|
||||||
let below_word = below_idx / 32;
|
let below_word = below_idx / 32;
|
||||||
let below_bit = below_idx % 32;
|
let below_bit = below_idx % 32;
|
||||||
@@ -129,6 +126,7 @@ impl ChunkData {
|
|||||||
let on_floor = (self.stand_on_floor[below_word] & below_mask) != 0;
|
let on_floor = (self.stand_on_floor[below_word] & below_mask) != 0;
|
||||||
let on_fixture = (self.stand_on_fixture[below_word] & below_mask) != 0;
|
let on_fixture = (self.stand_on_fixture[below_word] & below_mask) != 0;
|
||||||
|
|
||||||
|
// Logic: Current tile is passable AND tile below is solid
|
||||||
(in_floor || in_fixture) && (on_floor || on_fixture)
|
(in_floor || in_fixture) && (on_floor || on_fixture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,12 @@ pub struct TileMap {
|
|||||||
/// One Cargo entity per tile position. Enforces single-occupancy.
|
/// One Cargo entity per tile position. Enforces single-occupancy.
|
||||||
/// Cargo does not affect standability — purely for lookup and placement validation.
|
/// Cargo does not affect standability — purely for lookup and placement validation.
|
||||||
pub cargo_tiles: FxHashMap<IVec3, Entity>,
|
pub cargo_tiles: FxHashMap<IVec3, Entity>,
|
||||||
|
/// Tiles claimed by dorfs for drop destinations. Prevents multiple dorfs
|
||||||
|
/// from targeting the same drop tile. Maps tile position -> entity doing the claim.
|
||||||
|
pub claimed_tiles: FxHashMap<IVec3, Entity>,
|
||||||
|
/// Surface tile positions for idle pathfinding and valid spawn targets.
|
||||||
|
/// Populated from TerrainBlob during terrain processing.
|
||||||
|
pub surface_positions: Vec<IVec3>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileMap {
|
impl TileMap {
|
||||||
|
|||||||
Reference in New Issue
Block a user