Files
dorf/full.patch
T
popertots 90a6196443 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)
2026-03-21 01:36:38 +00:00

1424 lines
53 KiB
Diff

diff --git a/Cargo.lock b/Cargo.lock
index fb9dc22..a8e57fe 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2432,6 +2432,7 @@ name = "dorf"
version = "0.1.0"
dependencies = [
"ahash",
+ "arrayvec",
"bevy",
"bevy_platform",
"bevy_rand",
diff --git a/Cargo.toml b/Cargo.toml
index 1f33281..19ee719 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ rustc-hash = "2.1.1"
ahash = "0.8.12"
nohash-hasher = "0.2.0"
futures-lite = "2.6.1"
+arrayvec = "0.7.6"
[build-dependencies]
image = "0.25.10"
diff --git a/assets/tileset.png b/assets/tileset.png
index 6130ddc..36274f6 100644
Binary files a/assets/tileset.png and b/assets/tileset.png differ
diff --git a/config.toml b/config.toml
index 419b4cd..da69d4f 100644
--- a/config.toml
+++ b/config.toml
@@ -1,9 +1,9 @@
-initial_chunk_radius = 15
+initial_chunk_radius = 8
[display]
vsync = "mailbox"
[spawn_counts]
-dorfs = 5
+dorfs = 50
pigs = 5
rabbits = 5
\ No newline at end of file
diff --git a/full.patch b/full.patch
new file mode 100644
index 0000000..e69de29
diff --git a/src/constants.rs b/src/constants.rs
index 5025d11..83a29b1 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -5,16 +5,11 @@ pub const ITILE_SIZE: i32 = TILE_SIZE as i32;
pub const SEED: u32 = 420;
pub const PATHFINDER_SHORT_PATH_MAX_TILES: i32 = 64;
-pub const PATHFINDER_MAX_NODES: usize = 5000;
-pub const PATHFINDER_WAYPOINT_THRESHOLD_TILES: i32 = 100;
-pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 64;
+pub const PATHFINDER_MAX_NODES: usize = 15000;
+pub const PATHFINDER_PROVISIONAL_NODE_LIMIT: usize = 256;
// Hierarchical pathfinding thresholds
// Tier 1: Same/adjacent chunk -> sync A* (fast, ~87µs)
// Tier 2: 2-4 chunks away -> Provisional + full path via queue
// Tier 3: >4 chunks away -> Hierarchical chunk-path + async segmented A*
pub const PATHFINDER_HIERARCHICAL_THRESHOLD_CHUNKS: i32 = 4;
-pub const PATHFINDER_ASYNC_NODE_BUDGET_PER_FRAME: usize = 2000;
-
-// Snapshot bounds for async pathfinding (in chunks)
-pub const PATHFINDER_SNAPSHOT_CHUNK_RADIUS: i32 = 2;
diff --git a/src/debug/entity_dump.rs b/src/debug/entity_dump.rs
new file mode 100644
index 0000000..b79403a
--- /dev/null
+++ b/src/debug/entity_dump.rs
@@ -0,0 +1,74 @@
+use bevy::prelude::*;
+use std::fs::OpenOptions;
+use std::io::Write;
+
+use crate::entities::shared_components::Ambulatory;
+
+pub fn dump_entity_positions(
+ keys: Res<ButtonInput<KeyCode>>,
+ time: Res<Time>,
+ query: Query<(Entity, &Transform, Option<&Ambulatory>), With<Ambulatory>>,
+) {
+ if !keys.just_pressed(KeyCode::Space) {
+ return;
+ }
+
+ let timestamp = time.elapsed_secs();
+ let filename = format!("entity_dump_{:.2}.csv", timestamp);
+
+ let mut file = match OpenOptions::new()
+ .create(true)
+ .write(true)
+ .truncate(true)
+ .open(&filename)
+ {
+ Ok(f) => f,
+ Err(e) => {
+ eprintln!("Failed to create dump file: {}", e);
+ return;
+ }
+ };
+
+ writeln!(
+ file,
+ "timestamp,entity_id,x,y,z,has_path,path_index,path_len,target_x,target_y,target_z"
+ )
+ .ok();
+
+ let mut count = 0;
+ for (entity, transform, ambulatory) in query.iter() {
+ let pos = transform.translation;
+ let (has_path, path_index, path_len, tx, ty, tz) = match ambulatory {
+ Some(a) => {
+ let path_len = a.current_path.as_ref().map(|p| p.len()).unwrap_or(0);
+ let has_path = path_len > 0;
+ let (tx, ty, tz) = match a.target {
+ Some(t) => (t.x, t.y, t.z),
+ None => (0.0, 0.0, 0.0),
+ };
+ (has_path, a.path_index, path_len, tx, ty, tz)
+ }
+ None => (false, 0, 0, 0.0, 0.0, 0.0),
+ };
+
+ writeln!(
+ file,
+ "{:.3},{},{:.1},{:.1},{:.1},{},{},{},{:.1},{:.1},{:.1}",
+ timestamp,
+ entity.index(),
+ pos.x,
+ pos.y,
+ pos.z,
+ has_path,
+ path_index,
+ path_len,
+ tx,
+ ty,
+ tz,
+ )
+ .ok();
+ count += 1;
+ }
+
+ println!("[DUMP] {} entities written to {}", count, filename);
+}
diff --git a/src/debug/mod.rs b/src/debug/mod.rs
new file mode 100644
index 0000000..e875351
--- /dev/null
+++ b/src/debug/mod.rs
@@ -0,0 +1 @@
+pub mod entity_dump;
diff --git a/src/entities/livestock/pig.rs b/src/entities/livestock/pig.rs
index d5304ab..714a975 100644
--- a/src/entities/livestock/pig.rs
+++ b/src/entities/livestock/pig.rs
@@ -30,6 +30,7 @@ impl Pig {
path_index: 0,
step_recovery: 0,
validation_cooldown: 0,
+ collision_delay: 0,
},
sprite: Sprite {
image: asset_server.load("pig.png"),
diff --git a/src/entities/livestock/rabbit.rs b/src/entities/livestock/rabbit.rs
index e044043..9e8326a 100644
--- a/src/entities/livestock/rabbit.rs
+++ b/src/entities/livestock/rabbit.rs
@@ -27,6 +27,7 @@ impl Rabbit {
path_index: 0,
step_recovery: 0,
validation_cooldown: 0,
+ collision_delay: 0,
},
sprite: Sprite {
image: asset_server.load("rabbit.png"),
diff --git a/src/entities/sentient/dorf.rs b/src/entities/sentient/dorf.rs
index 02210c0..96a66fe 100644
--- a/src/entities/sentient/dorf.rs
+++ b/src/entities/sentient/dorf.rs
@@ -27,6 +27,7 @@ impl Dorf {
path_index: 0,
step_recovery: 0,
validation_cooldown: 0,
+ collision_delay: 0,
},
sprite: Sprite {
image: asset_server.load("dorf.png"),
diff --git a/src/entities/shared_components/ambulatory.rs b/src/entities/shared_components/ambulatory.rs
index e2f4b8d..a969ed4 100644
--- a/src/entities/shared_components/ambulatory.rs
+++ b/src/entities/shared_components/ambulatory.rs
@@ -10,6 +10,7 @@ pub struct Ambulatory {
pub target: Option<Vec3>,
pub step_recovery: u32,
pub validation_cooldown: u8,
+ pub collision_delay: u8,
}
impl Default for Ambulatory {
@@ -22,6 +23,7 @@ impl Default for Ambulatory {
target: None,
step_recovery: 0,
validation_cooldown: 0,
+ collision_delay: 0,
}
}
}
diff --git a/src/entities/shared_systems/mod.rs b/src/entities/shared_systems/mod.rs
index 1bc27c5..ff161f5 100644
--- a/src/entities/shared_systems/mod.rs
+++ b/src/entities/shared_systems/mod.rs
@@ -1 +1,2 @@
+pub mod occupancy;
pub mod pathfinding;
diff --git a/src/entities/shared_systems/occupancy.rs b/src/entities/shared_systems/occupancy.rs
new file mode 100644
index 0000000..58e7597
--- /dev/null
+++ b/src/entities/shared_systems/occupancy.rs
@@ -0,0 +1,33 @@
+use crate::constants::ITILE_SIZE;
+use crate::entities::shared_components::Ambulatory;
+use bevy::prelude::*;
+use rustc_hash::FxHashMap;
+
+#[derive(Resource, Default)]
+pub struct TileOccupancy {
+ pub counts: FxHashMap<(i32, i32), u8>,
+}
+
+impl TileOccupancy {
+ #[inline]
+ pub fn count_at(&self, world_pos: Vec3) -> u8 {
+ let tx = (world_pos.x as i32) / ITILE_SIZE;
+ let ty = (world_pos.y as i32) / ITILE_SIZE;
+ *self.counts.get(&(tx, ty)).unwrap_or(&0)
+ }
+}
+
+pub fn rebuild_tile_occupancy(
+ mut occupancy: ResMut<TileOccupancy>,
+ query: Query<&Transform, With<Ambulatory>>,
+) {
+ occupancy.counts.clear();
+ for transform in query.iter() {
+ let tx = (transform.translation.x as i32) / ITILE_SIZE;
+ let ty = (transform.translation.y as i32) / ITILE_SIZE;
+ let count = occupancy.counts.entry((tx, ty)).or_insert(0);
+ if *count < 255 {
+ *count += 1;
+ }
+ }
+}
diff --git a/src/entities/shared_systems/pathfinding.rs b/src/entities/shared_systems/pathfinding.rs
index 92e5288..ee0a616 100644
--- a/src/entities/shared_systems/pathfinding.rs
+++ b/src/entities/shared_systems/pathfinding.rs
@@ -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;
-
- 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;
- }
+ 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());
+
+ // 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);
+ }
+ }
+
+ // 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,41 @@ 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 occupied = 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 +644,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 +681,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
+ (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 {
- (false, cy, cx, cx + CHUNK_SIZE - 1) // south edge
+ (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);
}
- };
-
- // 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)
+ 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 +870,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 +928,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 +942,6 @@ fn calculate_path_with_scratchpad(
let current_g = *scratch.g_scores.get(&current).unwrap_or(&i32::MAX);
- if let Some(&parent) = scratch.came_from.get(&current) {
- 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(&current).unwrap_or(&i32::MAX);
-
for &move_dir in &ALLOWED_MOVES {
let neighbor_pos = current + move_dir;
@@ -930,36 +957,16 @@ fn calculate_path_with_scratchpad(
continue;
}
- let via_current_g = current_g_updated + 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;
- }
- }
- }
- }
+ let new_g = current_g + movement_cost;
- 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 +1028,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);
diff --git a/src/main.rs b/src/main.rs
index d7c9c98..ecd3c70 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -11,6 +11,7 @@ mod camera;
mod config;
mod constants;
mod cursor;
+mod debug;
mod entities;
mod game;
mod system_info;
@@ -67,5 +68,6 @@ fn main() {
Update,
item_tile_management_system.after(initialize_item_rotation_state),
)
+ .add_systems(Update, debug::entity_dump::dump_entity_positions)
.run();
}
diff --git a/src/world/generation/forestry.rs b/src/world/generation/forestry.rs
index e5cb370..d69cfac 100644
--- a/src/world/generation/forestry.rs
+++ b/src/world/generation/forestry.rs
@@ -12,8 +12,7 @@ use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::{FixtureTileData, TileMap},
- ChunkForrestryEvent, ChunkMap, ChunkOwner, FixtureTilePrefab, TextureIDs, Textures,
- VisibleGameEntity,
+ ChunkForrestryEvent, ChunkMap, ChunkOwner, TextureIDs, Textures, VisibleGameEntity,
},
};
@@ -72,8 +71,12 @@ pub fn generate_chunk_forrestry(
continue;
}
- let trunk_entity =
- FixtureTilePrefab::log(trunk_pos).spawn(&mut commands);
+ let trunk_entity = commands
+ .spawn((
+ Transform::from_translation(trunk_pos),
+ Visibility::Hidden,
+ ))
+ .id();
tree_positions.push(trunk_pos);
commands
.entity(trunk_entity)
@@ -136,7 +139,6 @@ pub fn generate_chunk_forrestry(
* (1.0 + (rng.random::<f32>() * 0.35 - 0.1));
if x_f * x_f + y_f * y_f + z_f * z_f <= radius * radius {
- FixtureTilePrefab::leaves(pos).spawn(&mut commands);
if let Some(texture_id) = texture_ids.refs.get(&500005)
{
if let Some(texture) =
@@ -166,7 +168,7 @@ pub fn generate_chunk_forrestry(
(
ivec,
FixtureTileData::new(
- 5, false, true, [0; 8],
+ 5, true, false, [0; 8],
),
),
);
diff --git a/src/world/generation/terrain.rs b/src/world/generation/terrain.rs
index ed91ba2..2257c31 100644
--- a/src/world/generation/terrain.rs
+++ b/src/world/generation/terrain.rs
@@ -9,7 +9,7 @@ use crate::{
constants::{SEED, TILE_SIZE},
world::{
tiles::{ChunkData, FloorTileData, TileMap},
- ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent,
+ ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
CHUNK_SIZE, Z_ABOVE, Z_BELOW,
},
};
@@ -47,7 +47,6 @@ pub struct TerrainBlob {
pub chunk_data: ChunkData,
pub tile_updates: Vec<(IVec3, FloorTileData)>,
pub surface_positions: Vec<(Vec3, String)>,
- pub tile_spawns: Vec<(Vec3, FloorTilePrefab)>,
}
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
@@ -75,7 +74,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
let mut chunk_data = ChunkData::new(chunk_pos);
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
- let mut tile_spawns: Vec<(Vec3, FloorTilePrefab)> = Vec::new();
for local_y in 0..CHUNK_SIZE {
for local_x in 0..CHUNK_SIZE {
@@ -107,7 +105,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
]);
if cave_value < -0.75 {
let tile = registry.floor("air");
- tile_spawns.push((position, FloorTilePrefab::air(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -130,7 +127,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
);
} else if cave_value < 0.8 {
let tile = registry.floor("rock");
- tile_spawns.push((position, FloorTilePrefab::rock(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -153,7 +149,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
);
} else {
let tile = registry.floor("dirt");
- tile_spawns.push((position, FloorTilePrefab::dirt(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -178,7 +173,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
} else if noise_position.z > position.z {
if surface_height <= position.z + TILE_SIZE {
let tile = registry.floor("grass");
- tile_spawns.push((position, FloorTilePrefab::grass(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -202,7 +196,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
surface_positions.push((position, "grass".to_string()));
} else {
let tile = registry.floor("dirt");
- tile_spawns.push((position, FloorTilePrefab::dirt(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -226,7 +219,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
}
} else {
let tile = registry.floor("air");
- tile_spawns.push((position, FloorTilePrefab::air(position)));
tile_updates.push((
pos_ivec,
FloorTileData::new(
@@ -257,7 +249,6 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
chunk_data,
tile_updates,
surface_positions,
- tile_spawns,
}
}
diff --git a/src/world/mod.rs b/src/world/mod.rs
index 4be608b..0c74ab8 100644
--- a/src/world/mod.rs
+++ b/src/world/mod.rs
@@ -2,8 +2,8 @@ use crate::{
camera,
config::{GameConfig, VsyncMode},
world::generation::{
- generate_chunk_fauna, generate_chunk_foliage, generate_chunk_forrestry,
- generate_chunk_weathering_and_precipitation, apply_terrain_blobs, spawn_terrain_tasks,
+ apply_terrain_blobs, generate_chunk_fauna, generate_chunk_foliage,
+ generate_chunk_forrestry, generate_chunk_weathering_and_precipitation, spawn_terrain_tasks,
TerrainBlobStorage,
},
};
@@ -16,12 +16,9 @@ pub mod tiles;
pub use chunks::management::*;
pub use textures::management::*;
-pub use tiles::{prefabs::*, rendering::*, visibility::*};
+pub use tiles::{rendering::*, visibility::*};
-pub fn apply_vsync_setting(
- config: Res<GameConfig>,
- mut windows: Query<&mut Window>,
-) {
+pub fn apply_vsync_setting(config: Res<GameConfig>, mut windows: Query<&mut Window>) {
if config.is_changed() {
if let Ok(mut window) = windows.single_mut() {
window.present_mode = match config.display.vsync {
diff --git a/src/world/tiles/benchmark.rs b/src/world/tiles/benchmark.rs
index 8541a71..024fe09 100644
--- a/src/world/tiles/benchmark.rs
+++ b/src/world/tiles/benchmark.rs
@@ -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 {
diff --git a/src/world/tiles/mod.rs b/src/world/tiles/mod.rs
index 346d8c6..6748da9 100644
--- a/src/world/tiles/mod.rs
+++ b/src/world/tiles/mod.rs
@@ -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::*;
diff --git a/src/world/tiles/prefabs.rs b/src/world/tiles/prefabs.rs
index 28c989f..781ba26 100644
--- a/src/world/tiles/prefabs.rs
+++ b/src/world/tiles/prefabs.rs
@@ -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.
diff --git a/src/world/tiles/tilemap.rs b/src/world/tiles/tilemap.rs
index 5d31e40..95c012f 100644
--- a/src/world/tiles/tilemap.rs
+++ b/src/world/tiles/tilemap.rs
@@ -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.
diff --git a/src/world/tiles/tilemap_chunk.rs b/src/world/tiles/tilemap_chunk.rs
index 1140c26..1ffb0e0 100644
--- a/src/world/tiles/tilemap_chunk.rs
+++ b/src/world/tiles/tilemap_chunk.rs
@@ -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;