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:
2026-03-21 01:36:38 +00:00
parent 1fe4781bab
commit 90a6196443
25 changed files with 1909 additions and 498 deletions
+2 -6
View File
@@ -151,14 +151,10 @@ impl TilemapBenchmark {
}
/// Runs every frame — samples entity counts and frame timing.
pub fn track_benchmark(
mut bench: ResMut<TilemapBenchmark>,
time: Res<Time>,
floor_tiles: Query<(), With<super::FloorTile>>,
) {
pub fn track_benchmark(mut bench: ResMut<TilemapBenchmark>, time: Res<Time>) {
bench.frame_count += 1;
bench.frame_times.push(time.delta());
bench.floor_tile_count = floor_tiles.iter().count() as u32;
bench.floor_tile_count = 0;
// Trim rolling window to last 300 frames for p99 calculation
if bench.frame_times.len() > 300 {
-4
View File
@@ -1,7 +1,5 @@
pub mod benchmark;
pub mod chunk_data;
pub mod components;
pub mod prefabs;
pub mod rendering;
pub mod tilemap;
pub mod tilemap_chunk;
@@ -9,8 +7,6 @@ pub mod visibility;
pub use benchmark::*;
pub use chunk_data::*;
pub use components::*;
pub use prefabs::*;
pub use tilemap::*;
pub use tilemap_chunk::*;
pub use visibility::*;
+4 -195
View File
@@ -1,195 +1,4 @@
use bevy::prelude::*;
use crate::{
config::TileRegistry,
world::{
tiles::{FixtureTile, FloorTile, TileState},
FIXTURE_ID_OFFSET,
},
};
#[derive(Bundle)]
pub struct FloorTilePrefab {
transform: Transform,
tile: FloorTile,
tile_state: TileState,
visibility: Visibility,
}
impl FloorTilePrefab {
pub fn grass(position: Vec3) -> Self {
let def = TileRegistry::global().floor("grass");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn dirt(position: Vec3) -> Self {
let def = TileRegistry::global().floor("dirt");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn rock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("rock");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn air(position: Vec3) -> Self {
let def = TileRegistry::global().floor("air");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn bedrock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("bedrock");
FloorTilePrefab {
transform: Transform::from_translation(position),
tile: FloorTile {
id: def.id as u32,
opaque: !def.transparent,
walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
},
tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
},
visibility: Visibility::Hidden,
}
}
pub fn spawn(self, commands: &mut Commands) -> Entity {
commands
.spawn((self.tile, self.transform, self.tile_state, self.visibility))
.id()
}
}
#[derive(Bundle)]
pub struct FixtureTilePrefab {
transform: Transform,
tile: FixtureTile,
visibility: Visibility,
}
impl FixtureTilePrefab {
pub fn dirt_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("dirt_wall");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn rock_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("rock_wall");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn log(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("log");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn leaves(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("leaves");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn bedrock_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("bedrock_wall");
FixtureTilePrefab {
transform: Transform::from_translation(position),
tile: FixtureTile {
id: FIXTURE_ID_OFFSET + def.id,
solid: def.solid,
visible_range: [0; 8],
},
visibility: Visibility::Hidden,
}
}
pub fn spawn(self, commands: &mut Commands) -> Entity {
commands
.spawn((self.tile, self.transform, self.visibility))
.id()
}
}
// Prefab types (FloorTilePrefab, FixtureTilePrefab) were deleted — they were
// entirely dead code. FloorTile and FixtureTile ECS components (in the former
// components.rs) were also never read. The TileMap + ChunkData bitsets are the
// sole source of truth for standability.
+18
View File
@@ -237,6 +237,24 @@ impl TileMap {
.unwrap_or(100)
}
/// Remove a fixture tile, clearing both the HashMap entry and ChunkData bitsets.
/// BOTH must be cleared — leaving ChunkData stale causes is_standable bugs.
pub fn remove_fixture(&mut self, pos: &IVec3) -> Option<FixtureTileData> {
use super::chunk_data::ChunkData;
let removed = self.fixture_tiles.remove(pos);
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);
let idx = ChunkData::pos_to_index(lx, ly, z);
let word = idx / 32;
let clear_mask = !(1u32 << (idx % 32));
chunk.stand_in_fixture[word] &= clear_mask;
chunk.stand_on_fixture[word] &= clear_mask;
}
removed
}
/// Remove all tile data for a specific chunk from the TileMap.
/// Iterates all positions in the chunk volume and removes from HashMaps.
/// Used during chunk unloading to clean up tile data.
+1 -1
View File
@@ -280,7 +280,7 @@ pub fn spawn_tilemap_chunks(
let half_chunk = (CHUNK_SIZE_TILE as f32) / 2.0 - TILE_SIZE / 2.0;
let world_x = (key.chunk_pos.x as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
let world_y = (key.chunk_pos.y as f32) * (CHUNK_SIZE_TILE as f32) + half_chunk;
let z_depth = -(Z_BELOW - key.z_index as f32) * TILE_SIZE;
let z_depth = (key.z_index as f32 - Z_BELOW) * TILE_SIZE;
let visible = z_diff >= 0 && z_diff <= 8;