feat: fix log fixture passthrough, improve collision, add move_direction
Fix 1 — insert_fixture now updates ChunkData bitsets: Log trunks (can_stand_in=false) now block movement in is_standable. Leaf canopy (can_stand_in=true) remains walkable. Fix 2 — Diagonal-aware collision avoidance: Sidestep priority: left+forward, left, right+forward, right. If all blocked: push through (excuse me), advance path, apply one-step delay (speed-modulated recovery). Does not stack delays. Fix 3 — Movement direction tracking: Ambulatory now has move_direction (Vec2) and step_history ([i16; 4]). TileOccupancy tracks per-tile direction hints for Stage 2 collision (convoy skip, E/S yield) via direction_at(). move_direction is smoothed from: 2 historical steps, current confirmed step, and 2-step path lookahead. Ignore: add *.patch and *.diff to .gitignore
This commit is contained in:
@@ -19,3 +19,5 @@
|
|||||||
*/tileset.png
|
*/tileset.png
|
||||||
|
|
||||||
*.csv
|
*.csv
|
||||||
|
*.patch
|
||||||
|
*.diff
|
||||||
@@ -30,6 +30,8 @@ impl Pig {
|
|||||||
path_index: 0,
|
path_index: 0,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
move_direction: Vec2::ZERO,
|
||||||
|
step_history: [0i16; 4],
|
||||||
},
|
},
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("pig.png"),
|
image: asset_server.load("pig.png"),
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ impl Rabbit {
|
|||||||
path_index: 0,
|
path_index: 0,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
move_direction: Vec2::ZERO,
|
||||||
|
step_history: [0i16; 4],
|
||||||
},
|
},
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("rabbit.png"),
|
image: asset_server.load("rabbit.png"),
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ impl Dorf {
|
|||||||
path_index: 0,
|
path_index: 0,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
move_direction: Vec2::ZERO,
|
||||||
|
step_history: [0i16; 4],
|
||||||
},
|
},
|
||||||
sprite: Sprite {
|
sprite: Sprite {
|
||||||
image: asset_server.load("dorf.png"),
|
image: asset_server.load("dorf.png"),
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ pub struct Ambulatory {
|
|||||||
pub target: Option<Vec3>,
|
pub target: Option<Vec3>,
|
||||||
pub step_recovery: u32,
|
pub step_recovery: u32,
|
||||||
pub validation_cooldown: u8,
|
pub validation_cooldown: u8,
|
||||||
|
pub move_direction: Vec2,
|
||||||
|
pub step_history: [i16; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Ambulatory {
|
impl Default for Ambulatory {
|
||||||
@@ -22,6 +24,8 @@ impl Default for Ambulatory {
|
|||||||
target: None,
|
target: None,
|
||||||
step_recovery: 0,
|
step_recovery: 0,
|
||||||
validation_cooldown: 0,
|
validation_cooldown: 0,
|
||||||
|
move_direction: Vec2::ZERO,
|
||||||
|
step_history: [0i16; 4],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use rustc_hash::FxHashMap;
|
|||||||
#[derive(Resource, Default)]
|
#[derive(Resource, Default)]
|
||||||
pub struct TileOccupancy {
|
pub struct TileOccupancy {
|
||||||
pub counts: FxHashMap<(i32, i32), u8>,
|
pub counts: FxHashMap<(i32, i32), u8>,
|
||||||
|
pub directions: FxHashMap<(i32, i32), Vec2>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileOccupancy {
|
impl TileOccupancy {
|
||||||
@@ -15,19 +16,32 @@ impl TileOccupancy {
|
|||||||
let ty = (world_pos.y as i32) / ITILE_SIZE;
|
let ty = (world_pos.y as i32) / ITILE_SIZE;
|
||||||
*self.counts.get(&(tx, ty)).unwrap_or(&0)
|
*self.counts.get(&(tx, ty)).unwrap_or(&0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn direction_at(&self, world_pos: Vec3) -> Vec2 {
|
||||||
|
let tx = (world_pos.x as i32) / ITILE_SIZE;
|
||||||
|
let ty = (world_pos.y as i32) / ITILE_SIZE;
|
||||||
|
*self.directions.get(&(tx, ty)).unwrap_or(&Vec2::ZERO)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebuild_tile_occupancy(
|
pub fn rebuild_tile_occupancy(
|
||||||
mut occupancy: ResMut<TileOccupancy>,
|
mut occupancy: ResMut<TileOccupancy>,
|
||||||
query: Query<&Transform, With<Ambulatory>>,
|
query: Query<(&Transform, &Ambulatory)>,
|
||||||
) {
|
) {
|
||||||
occupancy.counts.clear();
|
occupancy.counts.clear();
|
||||||
for transform in query.iter() {
|
occupancy.directions.clear();
|
||||||
|
for (transform, ambulatory) in query.iter() {
|
||||||
let tx = (transform.translation.x as i32) / ITILE_SIZE;
|
let tx = (transform.translation.x as i32) / ITILE_SIZE;
|
||||||
let ty = (transform.translation.y as i32) / ITILE_SIZE;
|
let ty = (transform.translation.y as i32) / ITILE_SIZE;
|
||||||
let count = occupancy.counts.entry((tx, ty)).or_insert(0);
|
let count = occupancy.counts.entry((tx, ty)).or_insert(0);
|
||||||
if *count < 255 {
|
if *count < 255 {
|
||||||
*count += 1;
|
*count += 1;
|
||||||
}
|
}
|
||||||
|
if ambulatory.move_direction != Vec2::ZERO {
|
||||||
|
occupancy
|
||||||
|
.directions
|
||||||
|
.insert((tx, ty), ambulatory.move_direction);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -594,49 +594,146 @@ pub fn movement(
|
|||||||
|
|
||||||
if let Some(path) = &ambulatory.current_path {
|
if let Some(path) = &ambulatory.current_path {
|
||||||
if ambulatory.path_index < path.len() {
|
if ambulatory.path_index < path.len() {
|
||||||
|
let old_translation = transform.translation;
|
||||||
let next_point = path[ambulatory.path_index];
|
let next_point = path[ambulatory.path_index];
|
||||||
|
|
||||||
let current_count = occupancy.count_at(transform.translation);
|
let current_count = occupancy.count_at(transform.translation);
|
||||||
let next_count = occupancy.count_at(next_point);
|
let next_count = occupancy.count_at(next_point);
|
||||||
let current_crowded = current_count > 3;
|
let current_crowded = current_count > 3;
|
||||||
let occupied = !current_crowded && next_count > current_count;
|
let occupied = !current_crowded && next_count > current_count;
|
||||||
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 {
|
let actual_move: Vec3;
|
||||||
if left_dir.x > 0.0 {
|
let advance_path: bool;
|
||||||
transform.scale.x = PIXEL_RATIO;
|
|
||||||
} else if left_dir.x < 0.0 {
|
let move_dir = next_point - transform.translation;
|
||||||
transform.scale.x = -PIXEL_RATIO;
|
if occupied && move_dir.length_squared() > 0.0 {
|
||||||
}
|
let forward_2d = Vec2::new(move_dir.x, move_dir.y).normalize();
|
||||||
transform.translation = left_point;
|
let left_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 = [
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(
|
||||||
|
(left_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
|
(left_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(left_2d.x * TILE_SIZE, left_2d.y * TILE_SIZE, 0.0),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(
|
||||||
|
(right_2d.x + forward_2d.x).signum() * TILE_SIZE,
|
||||||
|
(right_2d.y + forward_2d.y).signum() * TILE_SIZE,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
snap_to_grid(
|
||||||
|
transform.translation
|
||||||
|
+ Vec3::new(
|
||||||
|
right_2d.x * TILE_SIZE,
|
||||||
|
right_2d.y * TILE_SIZE,
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
cur_z,
|
||||||
|
TILE_SIZE,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let sidestep = candidates.iter().copied().find(|&c| {
|
||||||
|
tilemap.is_standable(c.as_ivec3())
|
||||||
|
&& occupancy.count_at(c) <= current_count
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(step) = sidestep {
|
||||||
|
actual_move = step;
|
||||||
|
advance_path = false;
|
||||||
|
} else {
|
||||||
|
actual_move = next_point;
|
||||||
|
advance_path = true;
|
||||||
|
let tile_weight =
|
||||||
|
get_tile_weight(&tilemap, transform.translation.as_ivec3());
|
||||||
|
let threshold = if ambulatory.walk_speed > 0. {
|
||||||
|
(ambulatory.walk_speed as i32 * tile_weight as i32 / 50) as u32
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if ambulatory.step_recovery == 0 {
|
||||||
|
ambulatory.step_recovery = threshold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let direction = (next_point - transform.translation).normalize();
|
actual_move = next_point;
|
||||||
transform.translation = next_point;
|
advance_path = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let direction = (actual_move - transform.translation).normalize_or_zero();
|
||||||
|
transform.translation = actual_move;
|
||||||
if direction.x > 0.0 {
|
if direction.x > 0.0 {
|
||||||
transform.scale.x = PIXEL_RATIO;
|
transform.scale.x = PIXEL_RATIO;
|
||||||
} else if direction.x < 0.0 {
|
} else if direction.x < 0.0 {
|
||||||
transform.scale.x = -PIXEL_RATIO;
|
transform.scale.x = -PIXEL_RATIO;
|
||||||
}
|
}
|
||||||
|
|
||||||
if transform.translation.distance(next_point) < TILE_SIZE {
|
if advance_path && transform.translation.distance(next_point) < TILE_SIZE {
|
||||||
ambulatory.path_index += 1;
|
let ITILE: i16 = ITILE_SIZE as i16;
|
||||||
|
|
||||||
|
ambulatory.step_history[0] = ambulatory.step_history[2];
|
||||||
|
ambulatory.step_history[1] = ambulatory.step_history[3];
|
||||||
|
ambulatory.step_history[2] = (old_translation.x as i16) / ITILE;
|
||||||
|
ambulatory.step_history[3] = (old_translation.y as i16) / ITILE;
|
||||||
|
|
||||||
|
let mut dir_sum = Vec2::ZERO;
|
||||||
|
let mut n = 0u32;
|
||||||
|
|
||||||
|
let h = &ambulatory.step_history;
|
||||||
|
if h[0] != 0 || h[1] != 0 {
|
||||||
|
let d = Vec2::new((h[2] - h[0]) as f32, (h[3] - h[1]) as f32);
|
||||||
|
if d.length_squared() > 0.0 {
|
||||||
|
dir_sum += d.normalize();
|
||||||
|
n += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cur_d = Vec2::new(
|
||||||
|
actual_move.x - old_translation.x,
|
||||||
|
actual_move.y - old_translation.y,
|
||||||
|
);
|
||||||
|
if cur_d.length_squared() > 0.0 {
|
||||||
|
dir_sum += cur_d.normalize();
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref path) = ambulatory.current_path {
|
||||||
|
let start = ambulatory.path_index;
|
||||||
|
for i in start..((start + 2).min(path.len().saturating_sub(1))) {
|
||||||
|
let a = path[i];
|
||||||
|
let b = path[i + 1];
|
||||||
|
let d = Vec2::new(b.x - a.x, b.y - a.y);
|
||||||
|
if d.length_squared() > 0.0 {
|
||||||
|
dir_sum += d.normalize();
|
||||||
|
n += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
ambulatory.move_direction = (dir_sum / n as f32).normalize_or_zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
ambulatory.path_index += 1;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ambulatory.current_path = None;
|
ambulatory.current_path = None;
|
||||||
if let Some(target) = ambulatory.target {
|
if let Some(target) = ambulatory.target {
|
||||||
@@ -664,6 +761,15 @@ fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
|
|||||||
tilemap.is_standable(pos)
|
tilemap.is_standable(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn snap_to_grid(raw: Vec3, preserve_z: f32, tile_size: f32) -> Vec3 {
|
||||||
|
Vec3::new(
|
||||||
|
(raw.x / tile_size).round() * tile_size,
|
||||||
|
(raw.y / tile_size).round() * tile_size,
|
||||||
|
preserve_z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
/// whose world position is closest to the straight-line projection from
|
/// whose world position is closest to the straight-line projection from
|
||||||
/// `current_pos` toward `goal`. Falls back to chunk centre if no standable
|
/// `current_pos` toward `goal`. Falls back to chunk centre if no standable
|
||||||
|
|||||||
@@ -194,6 +194,11 @@ impl TileMap {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) {
|
pub fn insert_fixture(&mut self, pos: IVec3, tile: FixtureTileData) {
|
||||||
|
let chunk_pos = world_to_chunk(pos);
|
||||||
|
if let Some(chunk) = self.chunks.get_mut(&chunk_pos) {
|
||||||
|
let (lx, ly, z) = ChunkData::world_to_local(pos);
|
||||||
|
chunk.set_fixture_tile(lx, ly, z, tile.can_stand_in(), tile.can_stand_on());
|
||||||
|
}
|
||||||
self.fixture_tiles.insert(pos, tile);
|
self.fixture_tiles.insert(pos, tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user