feat: soft entity collision, dead code removal, leaf standability fix
- Add TileOccupancy resource and rebuild_tile_occupancy system to track per-tile entity counts for soft collision avoidance - Add collision_delay to Ambulatory; entities try relative-left step on occupied tiles, wait 1 tick, then push through - Fix leaf canopy standability: leaves are now can_stand_in=true, can_stand_on=false (walkable, not standable-on) - Delete FloorTilePrefab / FixtureTilePrefab / FloorTile / FixtureTile / TileState (all fully dead — TileMap + ChunkData are sole truth) - Delete tile_spawns from TerrainBlob (populated but never consumed) - Delete leaf ghost entity spawn in forestry (orphaned invisible ECS entity) - Replace log prefab spawn with inline commands.spawn(Transform, Visibility) - Add TileMap::remove_fixture for future digging/explosion use - Skip collision avoidance when current tile has >2 entities (handles spawn cluster deadlock)
This commit is contained in:
@@ -61,8 +61,9 @@ use std::{cell::RefCell, collections::BinaryHeap, collections::VecDeque, time::I
|
||||
|
||||
use crate::constants::{
|
||||
ITILE_SIZE, PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS, PATHFINDER_MAX_NODES,
|
||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, TILE_SIZE,
|
||||
PATHFINDER_PROVISIONAL_NODE_LIMIT, PIXEL_RATIO, TILE_SIZE,
|
||||
};
|
||||
use crate::entities::shared_systems::occupancy::{rebuild_tile_occupancy, TileOccupancy};
|
||||
use crate::world::tiles::TileMap;
|
||||
use crate::world::{
|
||||
chunks::CHUNK_SIZE,
|
||||
@@ -215,9 +216,16 @@ impl Plugin for PathfindingPlugin {
|
||||
.insert_resource(crate::entities::shared_components::CompletedPaths::default())
|
||||
.insert_resource(crate::entities::shared_components::PathRequestCounter::default())
|
||||
.insert_resource(PathRequestQueue::default())
|
||||
.init_resource::<TileOccupancy>()
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(prepare_paths, update_wandering_targets, movement).chain(),
|
||||
(
|
||||
rebuild_tile_occupancy,
|
||||
prepare_paths,
|
||||
update_wandering_targets,
|
||||
movement,
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
@@ -266,10 +274,11 @@ pub fn prepare_paths(
|
||||
ambulatory.path_index = 0;
|
||||
} else if chunk_distance > PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS {
|
||||
let chunk_path = calculate_chunk_path(&chunk_map, start_chunk, goal_chunk);
|
||||
let provisional_goal = goal;
|
||||
let provisional = calculate_provisional_path(
|
||||
&tilemap,
|
||||
start,
|
||||
goal,
|
||||
provisional_goal,
|
||||
PATHFINDER_PROVISIONAL_NODE_LIMIT,
|
||||
);
|
||||
if !provisional.is_empty() {
|
||||
@@ -296,8 +305,14 @@ pub fn prepare_paths(
|
||||
});
|
||||
} else {
|
||||
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||
ambulatory.current_path = Some(path);
|
||||
ambulatory.path_index = 0;
|
||||
if path.len() <= 1 {
|
||||
// Path failed — clear target so entity picks a new reachable one
|
||||
ambulatory.target = None;
|
||||
ambulatory.current_path = None;
|
||||
} else {
|
||||
ambulatory.current_path = Some(path);
|
||||
ambulatory.path_index = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let provisional = calculate_provisional_path(
|
||||
@@ -326,8 +341,14 @@ pub fn prepare_paths(
|
||||
});
|
||||
} else {
|
||||
let path = calculate_path_benchmarked(&tilemap, start, goal);
|
||||
ambulatory.current_path = Some(path);
|
||||
ambulatory.path_index = 0;
|
||||
if path.len() <= 1 {
|
||||
// Path failed — clear target so entity picks a new reachable one
|
||||
ambulatory.target = None;
|
||||
ambulatory.current_path = None;
|
||||
} else {
|
||||
ambulatory.current_path = Some(path);
|
||||
ambulatory.path_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,17 +358,12 @@ pub fn process_path_queue(
|
||||
mut commands: Commands,
|
||||
mut queue: ResMut<PathRequestQueue>,
|
||||
tilemap: Res<TileMap>,
|
||||
_chunk_map: Res<ChunkMap>,
|
||||
mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
|
||||
chunk_map: Res<ChunkMap>,
|
||||
mut query: Query<
|
||||
(Entity, &mut Ambulatory, &Transform),
|
||||
With<crate::entities::shared_components::PendingPath>,
|
||||
>,
|
||||
) {
|
||||
let Ok(mut rng) = rng_q.single_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut processed = 0;
|
||||
while processed < MAX_PATHS_PER_FRAME {
|
||||
if let Some(request) = queue.pending.pop_front() {
|
||||
@@ -360,18 +376,27 @@ pub fn process_path_queue(
|
||||
let current_chunk = world_to_chunk(actual_start);
|
||||
if let Some(next_chunk) = chunk_waypoints.iter().find(|&&c| c != current_chunk)
|
||||
{
|
||||
let waypoint = directional_chunk_waypoint(
|
||||
match directional_chunk_waypoint(
|
||||
actual_start,
|
||||
*next_chunk,
|
||||
request.goal,
|
||||
&tilemap,
|
||||
&mut rng,
|
||||
);
|
||||
let segment_path =
|
||||
calculate_path_benchmarked(&tilemap, actual_start, waypoint);
|
||||
if !segment_path.is_empty() {
|
||||
ambulatory.current_path = Some(segment_path);
|
||||
ambulatory.path_index = 0;
|
||||
&chunk_map,
|
||||
) {
|
||||
None => {
|
||||
// Chunk unloaded or no standable tiles — abandon this path,
|
||||
// entity will retarget via prepare_paths next frame
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.target = None;
|
||||
}
|
||||
Some(waypoint) => {
|
||||
let segment_path =
|
||||
calculate_path_benchmarked(&tilemap, actual_start, waypoint);
|
||||
if !segment_path.is_empty() {
|
||||
ambulatory.current_path = Some(segment_path);
|
||||
ambulatory.path_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -447,37 +472,75 @@ pub fn update_wandering_targets(
|
||||
return;
|
||||
};
|
||||
|
||||
for (mut ambulatory, _) in query.iter_mut() {
|
||||
if ambulatory.target.is_none()
|
||||
|| (ambulatory.current_path.is_some()
|
||||
&& ambulatory.path_index >= ambulatory.current_path.as_ref().unwrap().len())
|
||||
{
|
||||
let loaded_chunks: Vec<&IVec2> = chunk_map.loaded_chunks.keys().collect();
|
||||
if !loaded_chunks.is_empty() {
|
||||
let random_index = rng.random_range(0..loaded_chunks.len());
|
||||
if let Some(&chunk_pos) = loaded_chunks.get(random_index) {
|
||||
let chunk_x = chunk_pos.x * CHUNK_SIZE;
|
||||
let chunk_y = chunk_pos.y * CHUNK_SIZE;
|
||||
for (mut ambulatory, transform) in query.iter_mut() {
|
||||
if ambulatory.target.is_none() {
|
||||
// Only target interior chunks — exclude boundary chunks that
|
||||
// have unloaded neighbours, which strand entities at world edges.
|
||||
let current_chunk = world_to_chunk(transform.translation.as_ivec3());
|
||||
|
||||
let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
|
||||
let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
|
||||
// Reservoir sampling - pick one interior chunk with zero allocation
|
||||
let mut chosen: Option<IVec2> = None;
|
||||
let mut count = 0usize;
|
||||
for &chunk in chunk_map.loaded_chunks.keys() {
|
||||
let dx = (chunk.x - current_chunk.x).abs();
|
||||
let dy = (chunk.y - current_chunk.y).abs();
|
||||
if dx <= 2 && dy <= 2 {
|
||||
continue;
|
||||
}
|
||||
if !chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&IVec2::new(chunk.x + 1, chunk.y))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&IVec2::new(chunk.x - 1, chunk.y))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&IVec2::new(chunk.x, chunk.y + 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !chunk_map
|
||||
.loaded_chunks
|
||||
.contains_key(&IVec2::new(chunk.x, chunk.y - 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
count += 1;
|
||||
if rng.random_range(0..count) == 0 {
|
||||
chosen = Some(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
for z in -3..=4 {
|
||||
let mut target_pos = IVec3::new(target_x, target_y, z) * ITILE_SIZE;
|
||||
if tilemap.floor_tiles.get(&target_pos).is_some() {
|
||||
target_pos.z += ITILE_SIZE;
|
||||
if tilemap.floor_tiles.get(&target_pos).is_some()
|
||||
&& is_standable_tile(&tilemap, target_pos)
|
||||
{
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target_pos.x as f32,
|
||||
target_pos.y as f32,
|
||||
target_pos.z as f32 + 1.0,
|
||||
));
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.path_index = 0;
|
||||
break;
|
||||
}
|
||||
// Fallback to any chunk if no interior chunks found (small map / startup)
|
||||
let target_chunk = chosen.or_else(|| chunk_map.loaded_chunks.keys().next().copied());
|
||||
|
||||
if let Some(chunk_pos) = target_chunk {
|
||||
let chunk_x = chunk_pos.x * CHUNK_SIZE;
|
||||
let chunk_y = chunk_pos.y * CHUNK_SIZE;
|
||||
let target_x = chunk_x + rng.random_range(0..CHUNK_SIZE);
|
||||
let target_y = chunk_y + rng.random_range(0..CHUNK_SIZE);
|
||||
|
||||
for z in -3..=4 {
|
||||
let mut target_pos = IVec3::new(target_x, target_y, z) * ITILE_SIZE;
|
||||
if tilemap.floor_tiles.get(&target_pos).is_some() {
|
||||
target_pos.z += ITILE_SIZE;
|
||||
if tilemap.floor_tiles.get(&target_pos).is_some()
|
||||
&& is_standable_tile(&tilemap, target_pos)
|
||||
{
|
||||
ambulatory.target = Some(Vec3::new(
|
||||
target_pos.x as f32,
|
||||
target_pos.y as f32,
|
||||
target_pos.z as f32 + 1.0,
|
||||
));
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.path_index = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -486,7 +549,11 @@ pub fn update_wandering_targets(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Res<TileMap>) {
|
||||
pub fn movement(
|
||||
mut query: Query<(&mut Ambulatory, &mut Transform)>,
|
||||
tilemap: Res<TileMap>,
|
||||
occupancy: Res<TileOccupancy>,
|
||||
) {
|
||||
query
|
||||
.par_iter_mut()
|
||||
.for_each(|(mut ambulatory, mut transform)| {
|
||||
@@ -528,6 +595,42 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
|
||||
if let Some(path) = &ambulatory.current_path {
|
||||
if ambulatory.path_index < path.len() {
|
||||
let next_point = path[ambulatory.path_index];
|
||||
|
||||
if ambulatory.collision_delay > 0 {
|
||||
ambulatory.collision_delay -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let current_crowded = occupancy.count_at(transform.translation) > 2;
|
||||
let occupied = !current_crowded && occupancy.count_at(next_point) > 0;
|
||||
if occupied {
|
||||
let move_dir = next_point - transform.translation;
|
||||
if move_dir.length_squared() > 0.0 {
|
||||
let left_dir =
|
||||
Vec3::new(-move_dir.y, move_dir.x, 0.0).normalize() * TILE_SIZE;
|
||||
let left_raw = transform.translation + left_dir;
|
||||
let left_point = Vec3::new(
|
||||
(left_raw.x / TILE_SIZE).round() * TILE_SIZE,
|
||||
(left_raw.y / TILE_SIZE).round() * TILE_SIZE,
|
||||
transform.translation.z,
|
||||
);
|
||||
let left_free = tilemap.is_standable(left_point.as_ivec3())
|
||||
&& occupancy.count_at(left_point) == 0;
|
||||
|
||||
if left_free {
|
||||
if left_dir.x > 0.0 {
|
||||
transform.scale.x = PIXEL_RATIO;
|
||||
} else if left_dir.x < 0.0 {
|
||||
transform.scale.x = -PIXEL_RATIO;
|
||||
}
|
||||
transform.translation = left_point;
|
||||
return;
|
||||
}
|
||||
}
|
||||
ambulatory.collision_delay = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let direction = (next_point - transform.translation).normalize();
|
||||
transform.translation = next_point;
|
||||
|
||||
@@ -542,7 +645,11 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
|
||||
}
|
||||
} else {
|
||||
ambulatory.current_path = None;
|
||||
ambulatory.target = None;
|
||||
if let Some(target) = ambulatory.target {
|
||||
if transform.translation.distance(target) < TILE_SIZE * 2.0 {
|
||||
ambulatory.target = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -575,93 +682,130 @@ fn directional_chunk_waypoint(
|
||||
next_chunk: IVec2,
|
||||
goal: IVec3,
|
||||
tilemap: &TileMap,
|
||||
_rng: &mut WyRand,
|
||||
) -> IVec3 {
|
||||
chunk_map: &ChunkMap,
|
||||
) -> Option<IVec3> {
|
||||
if !chunk_map.loaded_chunks.contains_key(&next_chunk) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cx = next_chunk.x * CHUNK_SIZE;
|
||||
let cy = next_chunk.y * CHUNK_SIZE;
|
||||
|
||||
// Determine entry edge of the destination chunk.
|
||||
// We work in ITILE units so 1 step = one tile of movement.
|
||||
let cur_tile = current_pos / ITILE_SIZE;
|
||||
let goal_tile = goal / ITILE_SIZE;
|
||||
let dir = goal_tile - cur_tile;
|
||||
let straight = goal - current_pos;
|
||||
|
||||
// Edge: X-dominant → top/bottom; Y-dominant → left/right; tie → use Y
|
||||
let (fixed_axis, fixed_tile, var_min, var_max): (bool, i32, i32, i32) =
|
||||
if dir.x.abs() >= dir.y.abs() {
|
||||
// Moving east (+) or west (-)
|
||||
if dir.x >= 0 {
|
||||
(true, cx + CHUNK_SIZE - 1, cy, cy + CHUNK_SIZE - 1) // east edge
|
||||
} else {
|
||||
(true, cx, cy, cy + CHUNK_SIZE - 1) // west edge
|
||||
}
|
||||
let mut candidates = arrayvec::ArrayVec::<IVec3, 16>::new();
|
||||
|
||||
let mut scan_edge = |fixed_axis: bool, fixed_tile: i32, var_min: i32, var_max: i32| {
|
||||
let var_range = var_max - var_min;
|
||||
let step = if var_range <= 0 {
|
||||
1
|
||||
} else {
|
||||
// Moving north (+) or south (-)
|
||||
if dir.y >= 0 {
|
||||
(false, cy + CHUNK_SIZE - 1, cx, cx + CHUNK_SIZE - 1) // north edge
|
||||
} else {
|
||||
(false, cy, cx, cx + CHUNK_SIZE - 1) // south edge
|
||||
}
|
||||
(var_range / 7).max(1)
|
||||
};
|
||||
|
||||
// Collect up to 8 standable edge tiles.
|
||||
let mut candidates: Vec<IVec3> = Vec::with_capacity(8);
|
||||
let var_range = var_max - var_min;
|
||||
let step = if var_range <= 0 {
|
||||
1
|
||||
} else {
|
||||
(var_range / 7).max(1)
|
||||
let mut var = var_min;
|
||||
while var <= var_max {
|
||||
let (tile_x, tile_y) = if fixed_axis {
|
||||
(fixed_tile, var)
|
||||
} else {
|
||||
(var, fixed_tile)
|
||||
};
|
||||
let world_pos = IVec3::new(
|
||||
tile_x * ITILE_SIZE,
|
||||
tile_y * ITILE_SIZE,
|
||||
cur_tile.z * ITILE_SIZE,
|
||||
);
|
||||
if tilemap.is_standable(world_pos) {
|
||||
candidates.push(world_pos);
|
||||
}
|
||||
var += step;
|
||||
}
|
||||
};
|
||||
|
||||
let mut var = var_min;
|
||||
while var <= var_max {
|
||||
let (tile_x, tile_y) = if fixed_axis {
|
||||
(fixed_tile, var)
|
||||
} else {
|
||||
(var, fixed_tile)
|
||||
};
|
||||
let near_diagonal = (dir.x.abs() - dir.y.abs()).abs() < CHUNK_SIZE / 2;
|
||||
|
||||
// Match the z-level of the current tile so we don't jump z-levels here.
|
||||
let world_pos = IVec3::new(
|
||||
tile_x * ITILE_SIZE,
|
||||
tile_y * ITILE_SIZE,
|
||||
cur_tile.z * ITILE_SIZE,
|
||||
if dir.x.abs() >= dir.y.abs() {
|
||||
scan_edge(
|
||||
true,
|
||||
if dir.x >= 0 { cx } else { cx + CHUNK_SIZE - 1 },
|
||||
cy,
|
||||
cy + CHUNK_SIZE - 1,
|
||||
);
|
||||
if tilemap.is_standable(world_pos) {
|
||||
candidates.push(world_pos);
|
||||
if candidates.len() >= 8 {
|
||||
break;
|
||||
}
|
||||
if near_diagonal {
|
||||
scan_edge(
|
||||
false,
|
||||
if dir.y >= 0 { cy } else { cy + CHUNK_SIZE - 1 },
|
||||
cx,
|
||||
cx + CHUNK_SIZE - 1,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
scan_edge(
|
||||
false,
|
||||
if dir.y >= 0 { cy } else { cy + CHUNK_SIZE - 1 },
|
||||
cx,
|
||||
cx + CHUNK_SIZE - 1,
|
||||
);
|
||||
if near_diagonal {
|
||||
scan_edge(
|
||||
true,
|
||||
if dir.x >= 0 { cx } else { cx + CHUNK_SIZE - 1 },
|
||||
cy,
|
||||
cy + CHUNK_SIZE - 1,
|
||||
);
|
||||
}
|
||||
|
||||
var += step;
|
||||
}
|
||||
|
||||
if candidates.is_empty() {
|
||||
// Fallback: chunk centre (the old behaviour)
|
||||
IVec3::new(
|
||||
let centre = IVec3::new(
|
||||
(cx + CHUNK_SIZE / 2) * ITILE_SIZE,
|
||||
(cy + CHUNK_SIZE / 2) * ITILE_SIZE,
|
||||
cur_tile.z * ITILE_SIZE,
|
||||
)
|
||||
);
|
||||
if tilemap.is_standable(centre) {
|
||||
return Some(centre);
|
||||
}
|
||||
for lx in 0..CHUNK_SIZE {
|
||||
for ly in 0..CHUNK_SIZE {
|
||||
let p = IVec3::new(
|
||||
(cx + lx) * ITILE_SIZE,
|
||||
(cy + ly) * ITILE_SIZE,
|
||||
cur_tile.z * ITILE_SIZE,
|
||||
);
|
||||
if tilemap.is_standable(p) {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
} else {
|
||||
// Pick the candidate closest to the straight-line projection.
|
||||
let straight = goal - current_pos;
|
||||
let _straight_len_sq =
|
||||
straight.x * straight.x + straight.y * straight.y + straight.z * straight.z;
|
||||
|
||||
candidates
|
||||
// Find the candidate most aligned with the straight-line direction
|
||||
let best = candidates
|
||||
.iter()
|
||||
.min_by(|&&a, &&b| {
|
||||
.enumerate()
|
||||
.max_by(|(_, &a), (_, &b)| {
|
||||
let da = a - current_pos;
|
||||
let db = b - current_pos;
|
||||
// Dot product with straight direction: higher = more aligned
|
||||
let proj_a = (da.x * straight.x + da.y * straight.y + da.z * straight.z) as i64;
|
||||
let proj_b = (db.x * straight.x + db.y * straight.y + db.z * straight.z) as i64;
|
||||
proj_b.cmp(&proj_a)
|
||||
(da.x * straight.x + da.y * straight.y + da.z * straight.z)
|
||||
.cmp(&(db.x * straight.x + db.y * straight.y + db.z * straight.z))
|
||||
})
|
||||
.copied()
|
||||
.unwrap()
|
||||
.map(|(idx, _)| idx)
|
||||
.unwrap_or(0);
|
||||
|
||||
// Spread entities using both their start position AND goal position as entropy.
|
||||
// current_pos varies per entity (each is at a different world position).
|
||||
// goal varies per entity (each has a different random wander target).
|
||||
// Together they produce unique spread values for entities even when
|
||||
// heading through the same chunk, without needing to pass entity ID.
|
||||
let entropy = (cur_tile.x.unsigned_abs() as usize)
|
||||
.wrapping_mul(1619)
|
||||
.wrapping_add((cur_tile.y.unsigned_abs() as usize).wrapping_mul(31337))
|
||||
.wrapping_add((goal_tile.x.unsigned_abs() as usize).wrapping_mul(6271))
|
||||
.wrapping_add((goal_tile.y.unsigned_abs() as usize).wrapping_mul(2053));
|
||||
let spread = entropy % candidates.len();
|
||||
let idx = (best + spread) % candidates.len();
|
||||
Some(candidates[idx])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,103 +871,6 @@ fn reconstruct_path(came_from: &FxHashMap<IVec3, IVec3>, mut current: IVec3) ->
|
||||
path
|
||||
}
|
||||
|
||||
fn line_of_sight(tilemap: &TileMap, from: IVec3, to: IVec3) -> bool {
|
||||
let dx = (to.x - from.x).abs();
|
||||
let dy = (to.y - from.y).abs();
|
||||
let dz = (to.z - from.z).abs();
|
||||
let sx = if from.x < to.x {
|
||||
1
|
||||
} else if from.x > to.x {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let sy = if from.y < to.y {
|
||||
1
|
||||
} else if from.y > to.y {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let sz = if from.z < to.z {
|
||||
1
|
||||
} else if from.z > to.z {
|
||||
-1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut x = from.x;
|
||||
let mut y = from.y;
|
||||
let mut z = from.z;
|
||||
let mut err_x = dx / 2;
|
||||
let mut err_y = dy / 2;
|
||||
let mut err_z = dz / 2;
|
||||
let mut err = err_x + err_y + err_z;
|
||||
let steps = (dx + dy + dz) as i32;
|
||||
for _ in 0..steps {
|
||||
if !is_standable_tile(tilemap, IVec3::new(x, y, z)) {
|
||||
return false;
|
||||
}
|
||||
if err_x < 0 {
|
||||
x += sx;
|
||||
err_x += dy + dz;
|
||||
} else if err_x >= dx {
|
||||
x -= sx;
|
||||
err_x -= dy + dz;
|
||||
}
|
||||
if err_y < 0 {
|
||||
y += sy;
|
||||
err_y += dx + dz;
|
||||
} else if err_y >= dy {
|
||||
y -= sy;
|
||||
err_y -= dx + dz;
|
||||
}
|
||||
if err_z < 0 {
|
||||
z += sz;
|
||||
err_z += dx + dy;
|
||||
} else if err_z >= dz {
|
||||
z -= sz;
|
||||
err_z -= dx + dy;
|
||||
}
|
||||
if x == to.x && y == to.y && z == to.z {
|
||||
return true;
|
||||
}
|
||||
err = err_x + err_y + err_z;
|
||||
}
|
||||
is_standable_tile(tilemap, to)
|
||||
}
|
||||
|
||||
fn theta_line_cost(from: IVec3, to: IVec3) -> i32 {
|
||||
let dx = (to.x - from.x).abs() / ITILE_SIZE;
|
||||
let dy = (to.y - from.y).abs() / ITILE_SIZE;
|
||||
let dz = (to.z - from.z).abs() / ITILE_SIZE;
|
||||
let dmax = dx.max(dy).max(dz);
|
||||
let dmid = dx.min(dy).min(dz);
|
||||
let dmin = dx + dy + dz - dmax - dmid;
|
||||
(10 * dmax + 4 * dmid + dmin).max(10)
|
||||
}
|
||||
|
||||
fn smooth_path(path: &[Vec3], tilemap: &TileMap) -> Vec<Vec3> {
|
||||
if path.len() < 3 {
|
||||
return path.to_vec();
|
||||
}
|
||||
let mut result = Vec::with_capacity(path.len());
|
||||
result.push(path[0]);
|
||||
let mut i = 0;
|
||||
while i < path.len() - 1 {
|
||||
let mut j = path.len() - 1;
|
||||
while j > i + 1 {
|
||||
if line_of_sight(tilemap, path[i].as_ivec3(), path[j].as_ivec3()) {
|
||||
break;
|
||||
}
|
||||
j -= 1;
|
||||
}
|
||||
result.push(path[j]);
|
||||
i = j;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn calculate_path_benchmarked(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
|
||||
let timer = Instant::now();
|
||||
|
||||
@@ -882,10 +929,7 @@ fn calculate_path_with_scratchpad(
|
||||
nodes_expanded += 1;
|
||||
|
||||
if nodes_expanded > PATHFINDER_MAX_NODES {
|
||||
return (
|
||||
reconstruct_path(&scratch.came_from, current),
|
||||
nodes_expanded,
|
||||
);
|
||||
return (Vec::new(), nodes_expanded); // force retarget
|
||||
}
|
||||
|
||||
if current == goal {
|
||||
@@ -899,22 +943,6 @@ fn calculate_path_with_scratchpad(
|
||||
|
||||
let current_g = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX);
|
||||
|
||||
if let Some(&parent) = scratch.came_from.get(¤t) {
|
||||
if scratch.came_from.contains_key(&parent) {
|
||||
let gp = *scratch.came_from.get(&parent).unwrap();
|
||||
if line_of_sight(tilemap, gp, current) {
|
||||
let gp_g = *scratch.g_scores.get(&gp).unwrap_or(&i32::MAX);
|
||||
let via_gp = gp_g + theta_line_cost(gp, current);
|
||||
if via_gp < current_g {
|
||||
scratch.came_from.insert(current, gp);
|
||||
scratch.g_scores.insert(current, via_gp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_g_updated = *scratch.g_scores.get(¤t).unwrap_or(&i32::MAX);
|
||||
|
||||
for &move_dir in &ALLOWED_MOVES {
|
||||
let neighbor_pos = current + move_dir;
|
||||
|
||||
@@ -930,36 +958,16 @@ fn calculate_path_with_scratchpad(
|
||||
continue;
|
||||
}
|
||||
|
||||
let via_current_g = current_g_updated + movement_cost;
|
||||
let new_g = current_g + movement_cost;
|
||||
|
||||
if let Some(&neighbor_parent) = scratch.came_from.get(&neighbor_pos) {
|
||||
if scratch.came_from.contains_key(&neighbor_parent) {
|
||||
let np_g = *scratch.g_scores.get(&neighbor_parent).unwrap_or(&i32::MAX);
|
||||
if line_of_sight(tilemap, neighbor_parent, neighbor_pos) {
|
||||
let via_np_gp = np_g + theta_line_cost(neighbor_parent, neighbor_pos);
|
||||
if via_np_gp < via_current_g {
|
||||
scratch.came_from.insert(neighbor_pos, neighbor_parent);
|
||||
scratch.g_scores.insert(neighbor_pos, via_np_gp);
|
||||
let f = via_np_gp + octile_distance_3d(neighbor_pos, goal);
|
||||
scratch.open_set.push(PathNode {
|
||||
position: neighbor_pos,
|
||||
f_score: f,
|
||||
g_score: via_np_gp,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if via_current_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||||
if new_g < *scratch.g_scores.get(&neighbor_pos).unwrap_or(&i32::MAX) {
|
||||
scratch.came_from.insert(neighbor_pos, current);
|
||||
scratch.g_scores.insert(neighbor_pos, via_current_g);
|
||||
let f = via_current_g + octile_distance_3d(neighbor_pos, goal);
|
||||
scratch.g_scores.insert(neighbor_pos, new_g);
|
||||
let f = new_g + octile_distance_3d(neighbor_pos, goal);
|
||||
scratch.open_set.push(PathNode {
|
||||
position: neighbor_pos,
|
||||
f_score: f,
|
||||
g_score: via_current_g,
|
||||
g_score: new_g,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1021,10 +1029,7 @@ pub fn calculate_provisional_path(
|
||||
}
|
||||
|
||||
if nodes_expanded >= node_limit {
|
||||
return (
|
||||
reconstruct_path(&scratch.came_from, best_node),
|
||||
nodes_expanded,
|
||||
);
|
||||
return (Vec::new(), nodes_expanded);
|
||||
}
|
||||
|
||||
scratch.closed_set.insert(current);
|
||||
|
||||
Reference in New Issue
Block a user