feat: data-oriented chunks optimization with tile registry

- Phase 1: Bit-packed standability (ChunkData with bitsets)
- Phase 2: Reactive connectivity (dirty chunks)
- Phase 3: Async terrain baking (AsyncComputeTaskPool)
- Pathfinding weight system (rock=50 preferred, bedrock=150 avoided)
- Movement speed affected by tile weight
- External tiles.toml for hot-reloadable tile definitions
- TileRegistry singleton for async-safe tile lookups
- Fixed world_to_chunk to use CHUNK_SIZE_TILE (128) not CHUNK_SIZE (8)
- Fixed infinite spawner with Local<bool> state guards
- Fixed spawn coordinate grid alignment

Note: Zigzag pathfinding bug introduced - needs investigation
This commit is contained in:
2026-03-19 19:15:36 +00:00
parent 50788af3c7
commit 9535d65bd7
14 changed files with 423 additions and 84 deletions
+34 -9
View File
@@ -507,7 +507,11 @@ pub fn movement(mut query: Query<(&mut Ambulatory, &mut Transform)>, tilemap: Re
}
if ambulatory.walk_speed > 0. {
if ambulatory.step_recovery <= ambulatory.walk_speed as u32 {
let tile_weight = get_tile_weight(&tilemap, current_pos.as_ivec3());
let speed_multiplier = (tile_weight as f32) / 50.0;
let threshold = (ambulatory.walk_speed * speed_multiplier) as u32;
if ambulatory.step_recovery <= threshold {
ambulatory.step_recovery += 1;
return;
} else {
@@ -553,18 +557,37 @@ fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
tilemap.is_standable(pos)
}
fn calculate_movement_cost(move_dir: IVec3) -> i32 {
match (
/// Get the A* weight for a tile position. Lower is better (faster to traverse).
/// Returns 100 (default) if tile not found.
#[inline]
fn get_tile_weight(tilemap: &TileMap, pos: IVec3) -> u8 {
tilemap.get_astar_weight(pos)
}
/// Calculate movement cost including tile weight.
/// Base costs: cardinal=10, diagonal=14, vertical~50.
/// Tile weight adds: (weight - 50) / 5 to make heavier tiles more costly.
fn calculate_movement_cost(move_dir: IVec3, tile_weight: u8) -> i32 {
let base_cost = match (
move_dir.x.abs() / ITILE_SIZE,
move_dir.y.abs() / ITILE_SIZE,
move_dir.z.abs() / ITILE_SIZE,
) {
(1, 0, 0) | (0, 1, 0) => 10,
(1, 1, 0) => 14,
(1, 0, 1) | (0, 1, 1) => 42,
(1, 1, 1) => 56,
(1, 0, 0) | (0, 1, 0) => 10, // Cardinal
(1, 1, 0) => 14, // Diagonal
(1, 0, 1) | (0, 1, 1) => 42, // Vertical + cardinal
(1, 1, 1) => 56, // Vertical + diagonal
_ => 0,
};
if base_cost == 0 {
return 0;
}
// Weight cost: normalize so rock(50) adds 0, grass(100) adds 10, bedrock(150) adds 20
let weight_cost = (tile_weight as i32).saturating_sub(50) / 5;
base_cost + weight_cost
}
fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
@@ -677,7 +700,8 @@ fn calculate_path_with_scratchpad(
continue;
}
let movement_cost = calculate_movement_cost(move_dir);
let tile_weight = get_tile_weight(tilemap, neighbor_pos);
let movement_cost = calculate_movement_cost(move_dir, tile_weight);
if movement_cost == 0 {
continue;
}
@@ -770,7 +794,8 @@ pub fn calculate_provisional_path(
continue;
}
let movement_cost = calculate_movement_cost(move_dir);
let tile_weight = get_tile_weight(tilemap, neighbor_pos);
let movement_cost = calculate_movement_cost(move_dir, tile_weight);
if movement_cost == 0 {
continue;
}