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
+2 -2
View File
@@ -1,6 +1,6 @@
initial_chunk_radius = 7 initial_chunk_radius = 7
[spawn_counts] [spawn_counts]
dorfs = 500 dorfs = 5
pigs = 5 pigs = 5
rabbits = 50 rabbits = 5
+50
View File
@@ -1,6 +1,8 @@
use bevy::prelude::Resource; use bevy::prelude::Resource;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use std::fs; use std::fs;
use std::sync::OnceLock;
#[derive(Debug, Deserialize, Clone, Resource)] #[derive(Debug, Deserialize, Clone, Resource)]
pub struct GameConfig { pub struct GameConfig {
@@ -21,3 +23,51 @@ impl GameConfig {
toml::from_str(&config_str).expect("Failed to parse config.toml") toml::from_str(&config_str).expect("Failed to parse config.toml")
} }
} }
static TILE_REGISTRY: OnceLock<TileRegistry> = OnceLock::new();
#[derive(Debug, Deserialize, Clone)]
pub struct TileRegistry {
pub floor_tiles: HashMap<String, FloorTileDef>,
pub fixture_tiles: HashMap<String, FixtureTileDef>,
}
#[derive(Debug, Deserialize, Clone, Copy)]
pub struct FloorTileDef {
pub id: u8,
pub can_stand_in: bool,
pub can_stand_on: bool,
pub transparent: bool,
pub astar_weight: u8,
}
#[derive(Debug, Deserialize, Clone, Copy)]
pub struct FixtureTileDef {
pub id: u32,
pub solid: bool,
}
impl TileRegistry {
pub fn load() -> Self {
let config_str = fs::read_to_string("tiles.toml").expect("Failed to find tiles.toml");
toml::from_str(&config_str).expect("Failed to parse tiles.toml")
}
pub fn global() -> &'static Self {
TILE_REGISTRY.get_or_init(|| Self::load())
}
pub fn floor(&self, name: &str) -> FloorTileDef {
*self
.floor_tiles
.get(name)
.unwrap_or_else(|| panic!("Unknown floor tile: {}", name))
}
pub fn fixture(&self, name: &str) -> FixtureTileDef {
*self
.fixture_tiles
.get(name)
.unwrap_or_else(|| panic!("Unknown fixture tile: {}", name))
}
}
+22 -9
View File
@@ -3,6 +3,7 @@ use crate::constants::TILE_SIZE;
use crate::constants::*; use crate::constants::*;
use crate::entities::item::{spawn_prefab, MiscPrefab}; use crate::entities::item::{spawn_prefab, MiscPrefab};
use crate::entities::shared_components::Ambulatory; use crate::entities::shared_components::Ambulatory;
use crate::game::SpawnDelay;
use crate::world::tiles::tilemap; use crate::world::tiles::tilemap;
use crate::world::VisibleGameEntity; use crate::world::VisibleGameEntity;
use bevy::prelude::*; use bevy::prelude::*;
@@ -36,7 +37,7 @@ impl Pig {
}, },
transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)),
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
drop_timer: PigDropTimer(Timer::from_seconds(5.0, TimerMode::Repeating)), // NEW drop_timer: PigDropTimer(Timer::from_seconds(5.0, TimerMode::Repeating)),
} }
} }
} }
@@ -49,18 +50,30 @@ pub fn spawn_pigs(
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut rng_q: Query<&mut WyRand, With<GlobalRng>>, mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
config: Res<GameConfig>, config: Res<GameConfig>,
mut delay: ResMut<SpawnDelay>,
mut has_spawned: Local<bool>,
) { ) {
if *has_spawned {
return;
}
if delay.0 < 60 {
delay.0 += 1;
return;
}
*has_spawned = true;
if let Ok(mut rng) = rng_q.single_mut() { if let Ok(mut rng) = rng_q.single_mut() {
for _ in 0..config.spawn_counts.pigs { for _ in 0..config.spawn_counts.pigs {
let raw_x = rng.random_range(-32.0f32..32.0f32);
let raw_y = rng.random_range(-32.0f32..32.0f32);
let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE;
let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE;
let grid_z = 35.0 * TILE_SIZE;
let pig = commands let pig = commands
.spawn(Pig::new( .spawn(Pig::new(&asset_server, Vec3::new(grid_x, grid_y, grid_z)))
&asset_server,
Vec3::new(
rng.random_range(-32.0f32..32.0f32).round(),
rng.random_range(-32.0f32..32.0f32).round(),
35.0,
) * TILE_SIZE,
))
.id(); .id();
commands.entity(pig).insert(VisibleGameEntity); commands.entity(pig).insert(VisibleGameEntity);
} }
+21 -5
View File
@@ -2,6 +2,7 @@ use crate::config::GameConfig;
use crate::constants::TILE_SIZE; use crate::constants::TILE_SIZE;
use crate::constants::*; use crate::constants::*;
use crate::entities::shared_components::Ambulatory; use crate::entities::shared_components::Ambulatory;
use crate::game::SpawnDelay;
use crate::world::VisibleGameEntity; use crate::world::VisibleGameEntity;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rand::prelude::*; use bevy_rand::prelude::*;
@@ -42,17 +43,32 @@ pub fn spawn_rabbits(
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut rng_q: Query<&mut WyRand, With<GlobalRng>>, mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
config: Res<GameConfig>, config: Res<GameConfig>,
mut delay: ResMut<SpawnDelay>,
mut has_spawned: Local<bool>,
) { ) {
if *has_spawned {
return;
}
if delay.0 < 60 {
delay.0 += 1;
return;
}
*has_spawned = true;
if let Ok(mut rng) = rng_q.single_mut() { if let Ok(mut rng) = rng_q.single_mut() {
for _ in 0..config.spawn_counts.rabbits { for _ in 0..config.spawn_counts.rabbits {
let raw_x = rng.random_range(-32.0f32..32.0f32);
let raw_y = rng.random_range(-32.0f32..32.0f32);
let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE;
let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE;
let grid_z = 35.0 * TILE_SIZE;
let rab = commands let rab = commands
.spawn(Rabbit::new( .spawn(Rabbit::new(
&asset_server, &asset_server,
Vec3::new( Vec3::new(grid_x, grid_y, grid_z),
rng.random_range(-32.0f32..32.0f32).round(),
rng.random_range(-32.0f32..32.0f32).round(),
35.0,
) * TILE_SIZE,
)) ))
.id(); .id();
commands.entity(rab).insert(VisibleGameEntity); commands.entity(rab).insert(VisibleGameEntity);
+21 -8
View File
@@ -2,6 +2,7 @@ use crate::config::GameConfig;
use crate::constants::TILE_SIZE; use crate::constants::TILE_SIZE;
use crate::constants::*; use crate::constants::*;
use crate::entities::shared_components::Ambulatory; use crate::entities::shared_components::Ambulatory;
use crate::game::SpawnDelay;
use crate::world::VisibleGameEntity; use crate::world::VisibleGameEntity;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_rand::prelude::*; use bevy_rand::prelude::*;
@@ -42,18 +43,30 @@ pub fn spawn_dorfs(
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut rng_q: Query<&mut WyRand, With<GlobalRng>>, mut rng_q: Query<&mut WyRand, With<GlobalRng>>,
config: Res<GameConfig>, config: Res<GameConfig>,
mut delay: ResMut<SpawnDelay>,
mut has_spawned: Local<bool>,
) { ) {
if *has_spawned {
return;
}
if delay.0 < 60 {
delay.0 += 1;
return;
}
*has_spawned = true;
if let Ok(mut rng) = rng_q.single_mut() { if let Ok(mut rng) = rng_q.single_mut() {
for _ in 0..config.spawn_counts.dorfs { for _ in 0..config.spawn_counts.dorfs {
let raw_x = rng.random_range(-8.0f32..8.0f32);
let raw_y = rng.random_range(-8.0f32..8.0f32);
let grid_x = (raw_x / TILE_SIZE).round() * TILE_SIZE;
let grid_y = (raw_y / TILE_SIZE).round() * TILE_SIZE;
let grid_z = 35.0 * TILE_SIZE;
let cit = commands let cit = commands
.spawn(Dorf::new( .spawn(Dorf::new(&asset_server, Vec3::new(grid_x, grid_y, grid_z)))
&asset_server,
Vec3::new(
rng.random_range(-8.0f32..8.0f32).round(),
rng.random_range(-8.0f32..8.0f32).round(),
35.0,
) * TILE_SIZE,
))
.id(); .id();
commands.entity(cit).insert(VisibleGameEntity); commands.entity(cit).insert(VisibleGameEntity);
} }
+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.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; ambulatory.step_recovery += 1;
return; return;
} else { } else {
@@ -553,18 +557,37 @@ fn is_standable_tile(tilemap: &TileMap, pos: IVec3) -> bool {
tilemap.is_standable(pos) tilemap.is_standable(pos)
} }
fn calculate_movement_cost(move_dir: IVec3) -> i32 { /// Get the A* weight for a tile position. Lower is better (faster to traverse).
match ( /// 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.x.abs() / ITILE_SIZE,
move_dir.y.abs() / ITILE_SIZE, move_dir.y.abs() / ITILE_SIZE,
move_dir.z.abs() / ITILE_SIZE, move_dir.z.abs() / ITILE_SIZE,
) { ) {
(1, 0, 0) | (0, 1, 0) => 10, (1, 0, 0) | (0, 1, 0) => 10, // Cardinal
(1, 1, 0) => 14, (1, 1, 0) => 14, // Diagonal
(1, 0, 1) | (0, 1, 1) => 42, (1, 0, 1) | (0, 1, 1) => 42, // Vertical + cardinal
(1, 1, 1) => 56, (1, 1, 1) => 56, // Vertical + diagonal
_ => 0, _ => 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 { fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
@@ -677,7 +700,8 @@ fn calculate_path_with_scratchpad(
continue; 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 { if movement_cost == 0 {
continue; continue;
} }
@@ -770,7 +794,8 @@ pub fn calculate_provisional_path(
continue; 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 { if movement_cost == 0 {
continue; continue;
} }
+3
View File
@@ -2,3 +2,6 @@ use bevy::prelude::*;
#[derive(Resource)] #[derive(Resource)]
pub struct ZIndex(pub f32); pub struct ZIndex(pub f32);
#[derive(Resource, Default)]
pub struct SpawnDelay(pub u32);
+6 -7
View File
@@ -5,6 +5,7 @@ use crate::entities::{
item::{initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer}, item::{initialize_item_rotation_state, item_tile_management_system, ItemRotationTimer},
livestock::pig::pig_drop_system, livestock::pig::pig_drop_system,
}; };
use crate::game::SpawnDelay;
mod camera; mod camera;
mod config; mod config;
@@ -25,6 +26,7 @@ fn main() {
}) })
.insert_resource(camera::CameraMoved(false)) .insert_resource(camera::CameraMoved(false))
.insert_resource(world::tiles::QuiltCache::default()) .insert_resource(world::tiles::QuiltCache::default())
.init_resource::<SpawnDelay>()
.add_systems(PreStartup, world::textures::initialize_textures) .add_systems(PreStartup, world::textures::initialize_textures)
.add_plugins( .add_plugins(
DefaultPlugins DefaultPlugins
@@ -45,13 +47,7 @@ fn main() {
.add_plugins(entities::pathfinding::PathfindingPlugin) .add_plugins(entities::pathfinding::PathfindingPlugin)
.add_systems( .add_systems(
Startup, Startup,
( (camera::spawn_panning_camera, cursor::setup_cursor),
camera::spawn_panning_camera,
cursor::setup_cursor,
entities::sentient::dorf::spawn_dorfs,
entities::livestock::pig::spawn_pigs,
entities::livestock::rabbit::spawn_rabbits,
),
) )
.add_systems( .add_systems(
Update, Update,
@@ -60,6 +56,9 @@ fn main() {
camera::camera_z_movement, camera::camera_z_movement,
camera::camera_movement, camera::camera_movement,
cursor::move_cursor, cursor::move_cursor,
entities::sentient::dorf::spawn_dorfs,
entities::livestock::pig::spawn_pigs,
entities::livestock::rabbit::spawn_rabbits,
), ),
) )
.add_systems(Update, pig_drop_system) .add_systems(Update, pig_drop_system)
+2 -2
View File
@@ -13,8 +13,8 @@ pub const CHUNK_SIZE_TILE: i32 = CHUNK_SIZE * crate::constants::ITILE_SIZE;
#[inline] #[inline]
pub fn world_to_chunk(world_pos: IVec3) -> IVec2 { pub fn world_to_chunk(world_pos: IVec3) -> IVec2 {
IVec2::new( IVec2::new(
world_pos.x.div_euclid(CHUNK_SIZE), world_pos.x.div_euclid(CHUNK_SIZE_TILE),
world_pos.y.div_euclid(CHUNK_SIZE), world_pos.y.div_euclid(CHUNK_SIZE_TILE),
) )
} }
+110 -13
View File
@@ -1,11 +1,11 @@
use bevy::prelude::*; use bevy::prelude::*;
use bevy::tasks::AsyncComputeTaskPool; use bevy::tasks::AsyncComputeTaskPool;
use bevy_platform::collections::HashMap;
use bevy_platform::time::Instant; use bevy_platform::time::Instant;
use noise::{NoiseFn, Perlin}; use noise::{NoiseFn, Perlin};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::{ use crate::{
config::TileRegistry,
constants::{SEED, TILE_SIZE}, constants::{SEED, TILE_SIZE},
world::{ world::{
tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState}, tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState},
@@ -70,6 +70,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
let cave_noise = Perlin::new(SEED); let cave_noise = Perlin::new(SEED);
let start_x = chunk_pos.x * CHUNK_SIZE; let start_x = chunk_pos.x * CHUNK_SIZE;
let start_y = chunk_pos.y * CHUNK_SIZE; let start_y = chunk_pos.y * CHUNK_SIZE;
let registry = TileRegistry::global();
let mut chunk_data = ChunkData::new(chunk_pos); let mut chunk_data = ChunkData::new(chunk_pos);
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new(); let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
@@ -105,51 +106,147 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
z as f64 * 0.05, z as f64 * 0.05,
]); ]);
if cave_value < -0.75 { if cave_value < -0.75 {
let tile = registry.floor("air");
tile_spawns.push((position, FloorTilePrefab::air(position))); tile_spawns.push((position, FloorTilePrefab::air(position)));
tile_updates.push(( tile_updates.push((
pos_ivec, pos_ivec,
FloorTileData::new(0, true, false, true, 0, [0; 8]), FloorTileData::new(
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.transparent,
tile.astar_weight,
[0; 8],
),
)); ));
chunk_data.set_floor_tile(local_x, local_y, local_z, 0, true, false); chunk_data.set_floor_tile(
local_x,
local_y,
local_z,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} else if cave_value < 0.8 { } else if cave_value < 0.8 {
let tile = registry.floor("rock");
tile_spawns.push((position, FloorTilePrefab::rock(position))); tile_spawns.push((position, FloorTilePrefab::rock(position)));
tile_updates.push(( tile_updates.push((
pos_ivec, pos_ivec,
FloorTileData::new(2, false, true, false, 50, [0; 8]), FloorTileData::new(
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.transparent,
tile.astar_weight,
[0; 8],
),
)); ));
chunk_data.set_floor_tile(local_x, local_y, local_z, 2, false, true); chunk_data.set_floor_tile(
local_x,
local_y,
local_z,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} else { } else {
let tile = registry.floor("dirt");
tile_spawns.push((position, FloorTilePrefab::dirt(position))); tile_spawns.push((position, FloorTilePrefab::dirt(position)));
tile_updates.push(( tile_updates.push((
pos_ivec, pos_ivec,
FloorTileData::new(1, false, true, false, 85, [0; 8]), FloorTileData::new(
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.transparent,
tile.astar_weight,
[0; 8],
),
)); ));
chunk_data.set_floor_tile(local_x, local_y, local_z, 1, false, true); chunk_data.set_floor_tile(
local_x,
local_y,
local_z,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} }
} else if noise_position.z > position.z { } else if noise_position.z > position.z {
if surface_height <= position.z + TILE_SIZE { if surface_height <= position.z + TILE_SIZE {
let tile = registry.floor("grass");
tile_spawns.push((position, FloorTilePrefab::grass(position))); tile_spawns.push((position, FloorTilePrefab::grass(position)));
tile_updates.push(( tile_updates.push((
pos_ivec, pos_ivec,
FloorTileData::new(1, false, true, false, 100, [0; 8]), FloorTileData::new(
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.transparent,
tile.astar_weight,
[0; 8],
),
)); ));
chunk_data.set_floor_tile(local_x, local_y, local_z, 1, false, true); chunk_data.set_floor_tile(
local_x,
local_y,
local_z,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
surface_positions.push((position, "grass".to_string())); surface_positions.push((position, "grass".to_string()));
} else { } else {
let tile = registry.floor("dirt");
tile_spawns.push((position, FloorTilePrefab::dirt(position))); tile_spawns.push((position, FloorTilePrefab::dirt(position)));
tile_updates.push(( tile_updates.push((
pos_ivec, pos_ivec,
FloorTileData::new(1, false, true, false, 85, [0; 8]), FloorTileData::new(
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.transparent,
tile.astar_weight,
[0; 8],
),
)); ));
chunk_data.set_floor_tile(local_x, local_y, local_z, 1, false, true); chunk_data.set_floor_tile(
local_x,
local_y,
local_z,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} }
} else { } else {
let tile = registry.floor("air");
tile_spawns.push((position, FloorTilePrefab::air(position))); tile_spawns.push((position, FloorTilePrefab::air(position)));
tile_updates.push(( tile_updates.push((
pos_ivec, pos_ivec,
FloorTileData::new(0, true, false, true, 0, [0; 8]), FloorTileData::new(
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.transparent,
tile.astar_weight,
[0; 8],
),
)); ));
chunk_data.set_floor_tile(local_x, local_y, local_z, 0, true, false); chunk_data.set_floor_tile(
local_x,
local_y,
local_z,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} }
} }
} }
+21
View File
@@ -46,6 +46,10 @@ pub struct ChunkData {
/// Tile IDs for rendering. /// Tile IDs for rendering.
pub tile_ids: Vec<u8>, pub tile_ids: Vec<u8>,
/// A* pathfinding weights per tile. Lower = better path.
/// Weight 0 = impassable, weight 50 = fast (rock), weight 100 = normal (grass), weight 150 = slow (bedrock).
pub astar_weights: Vec<u8>,
} }
impl ChunkData { impl ChunkData {
@@ -57,6 +61,7 @@ impl ChunkData {
stand_in_fixture: [0u32; BITSET_WORDS], stand_in_fixture: [0u32; BITSET_WORDS],
stand_on_fixture: [0u32; BITSET_WORDS], stand_on_fixture: [0u32; BITSET_WORDS],
tile_ids: vec![0u8; TOTAL_TILES], tile_ids: vec![0u8; TOTAL_TILES],
astar_weights: vec![100u8; TOTAL_TILES],
} }
} }
@@ -184,6 +189,7 @@ impl ChunkData {
tile_id: u8, tile_id: u8,
can_stand_in: bool, can_stand_in: bool,
can_stand_on: bool, can_stand_on: bool,
astar_weight: u8,
) { ) {
let idx = Self::pos_to_index(local_x, local_y, z); let idx = Self::pos_to_index(local_x, local_y, z);
let word = idx / 32; let word = idx / 32;
@@ -203,6 +209,20 @@ impl ChunkData {
} }
self.tile_ids[idx] = tile_id; self.tile_ids[idx] = tile_id;
self.astar_weights[idx] = astar_weight;
}
/// Get A* weight at position. Returns 100 (default) if out of bounds.
#[inline]
pub fn get_astar_weight(&self, local_x: i32, local_y: i32, z: i32) -> u8 {
if local_x < 0 || local_x >= CHUNK_SIZE || local_y < 0 || local_y >= CHUNK_SIZE {
return 100;
}
if z < -(Z_BELOW as i32) || z > (Z_ABOVE as i32) {
return 100;
}
let idx = Self::pos_to_index(local_x, local_y, z);
self.astar_weights[idx]
} }
/// Set only fixture standability bits (for forestry generation). /// Set only fixture standability bits (for forestry generation).
@@ -253,6 +273,7 @@ impl ChunkData {
self.stand_in_fixture.fill(0); self.stand_in_fixture.fill(0);
self.stand_on_fixture.fill(0); self.stand_on_fixture.fill(0);
self.tile_ids.fill(0); self.tile_ids.fill(0);
self.astar_weights.fill(100);
} }
} }
+56 -29
View File
@@ -1,8 +1,11 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::world::{ use crate::{
tiles::{FixtureTile, FloorTile, TileState}, config::TileRegistry,
FIXTURE_ID_OFFSET, world::{
tiles::{FixtureTile, FloorTile, TileState},
FIXTURE_ID_OFFSET,
},
}; };
#[derive(Bundle)] #[derive(Bundle)]
@@ -15,12 +18,15 @@ pub struct FloorTilePrefab {
impl FloorTilePrefab { impl FloorTilePrefab {
pub fn grass(position: Vec3) -> Self { pub fn grass(position: Vec3) -> Self {
let def = TileRegistry::global().floor("grass");
FloorTilePrefab { FloorTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FloorTile { tile: FloorTile {
id: 1, id: def.id as u32,
astar_weight: 100, opaque: !def.transparent,
..Default::default() walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
}, },
tile_state: TileState { tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating), timer: Timer::from_seconds(1.0, TimerMode::Repeating),
@@ -30,12 +36,15 @@ impl FloorTilePrefab {
} }
pub fn dirt(position: Vec3) -> Self { pub fn dirt(position: Vec3) -> Self {
let def = TileRegistry::global().floor("dirt");
FloorTilePrefab { FloorTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FloorTile { tile: FloorTile {
id: 2, id: def.id as u32,
astar_weight: 85, opaque: !def.transparent,
..Default::default() walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
}, },
tile_state: TileState { tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating), timer: Timer::from_seconds(1.0, TimerMode::Repeating),
@@ -45,12 +54,15 @@ impl FloorTilePrefab {
} }
pub fn rock(position: Vec3) -> Self { pub fn rock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("rock");
FloorTilePrefab { FloorTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FloorTile { tile: FloorTile {
id: 3, id: def.id as u32,
astar_weight: 50, opaque: !def.transparent,
..Default::default() walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
}, },
tile_state: TileState { tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating), timer: Timer::from_seconds(1.0, TimerMode::Repeating),
@@ -60,13 +72,15 @@ impl FloorTilePrefab {
} }
pub fn air(position: Vec3) -> Self { pub fn air(position: Vec3) -> Self {
let def = TileRegistry::global().floor("air");
FloorTilePrefab { FloorTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FloorTile { tile: FloorTile {
id: 0, id: def.id as u32,
opaque: false, opaque: !def.transparent,
walkable: false, walkable: def.can_stand_on,
..Default::default() astar_weight: def.astar_weight,
visible_range: [0; 8],
}, },
tile_state: TileState { tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating), timer: Timer::from_seconds(1.0, TimerMode::Repeating),
@@ -76,12 +90,15 @@ impl FloorTilePrefab {
} }
pub fn bedrock(position: Vec3) -> Self { pub fn bedrock(position: Vec3) -> Self {
let def = TileRegistry::global().floor("bedrock");
FloorTilePrefab { FloorTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FloorTile { tile: FloorTile {
id: 4, id: def.id as u32,
astar_weight: 150, opaque: !def.transparent,
..Default::default() walkable: def.can_stand_on,
astar_weight: def.astar_weight,
visible_range: [0; 8],
}, },
tile_state: TileState { tile_state: TileState {
timer: Timer::from_seconds(1.0, TimerMode::Repeating), timer: Timer::from_seconds(1.0, TimerMode::Repeating),
@@ -104,55 +121,65 @@ pub struct FixtureTilePrefab {
impl FixtureTilePrefab { impl FixtureTilePrefab {
pub fn dirt_wall(position: Vec3) -> Self { pub fn dirt_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("dirt_wall");
FixtureTilePrefab { FixtureTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FixtureTile { tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 1, id: FIXTURE_ID_OFFSET + def.id,
..Default::default() solid: def.solid,
visible_range: [0; 8],
}, },
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
} }
} }
pub fn rock_wall(position: Vec3) -> Self { pub fn rock_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("rock_wall");
FixtureTilePrefab { FixtureTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FixtureTile { tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 2, id: FIXTURE_ID_OFFSET + def.id,
..Default::default() solid: def.solid,
visible_range: [0; 8],
}, },
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
} }
} }
pub fn log(position: Vec3) -> Self { pub fn log(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("log");
FixtureTilePrefab { FixtureTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FixtureTile { tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 4, id: FIXTURE_ID_OFFSET + def.id,
..Default::default() solid: def.solid,
visible_range: [0; 8],
}, },
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
} }
} }
pub fn leaves(position: Vec3) -> Self { pub fn leaves(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("leaves");
FixtureTilePrefab { FixtureTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FixtureTile { tile: FixtureTile {
id: FIXTURE_ID_OFFSET + 5, id: FIXTURE_ID_OFFSET + def.id,
..Default::default() solid: def.solid,
visible_range: [0; 8],
}, },
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
} }
} }
pub fn bedrock_wall(position: Vec3) -> Self { pub fn bedrock_wall(position: Vec3) -> Self {
let def = TileRegistry::global().fixture("bedrock_wall");
FixtureTilePrefab { FixtureTilePrefab {
transform: Transform::from_translation(position), transform: Transform::from_translation(position),
tile: FixtureTile { tile: FixtureTile {
id: 0, id: FIXTURE_ID_OFFSET + def.id,
..Default::default() solid: def.solid,
visible_range: [0; 8],
}, },
visibility: Visibility::Hidden, visibility: Visibility::Hidden,
} }
+14
View File
@@ -269,4 +269,18 @@ impl TileMap {
(can_stand_in_floor || can_stand_in_fixture) && (can_stand_on_floor || can_stand_on_fixture) (can_stand_in_floor || can_stand_in_fixture) && (can_stand_on_floor || can_stand_on_fixture)
} }
/// Get A* pathfinding weight for a tile position.
/// Returns 100 (default) if tile not found. Lower is better.
pub fn get_astar_weight(&self, world_pos: IVec3) -> u8 {
let chunk_pos = world_to_chunk(world_pos);
if let Some(chunk) = self.chunks.get(&chunk_pos) {
let (local_x, local_y, z) = ChunkData::world_to_local(world_pos);
return chunk.get_astar_weight(local_x, local_y, z);
}
self.floor_tiles
.get(&world_pos)
.map(|t| t.astar_weight)
.unwrap_or(100)
}
} }
+61
View File
@@ -0,0 +1,61 @@
# Floor tiles: IDs are used for texture lookup.
# Weight: lower = faster to traverse (rock=50 is best, bedrock=150 is worst).
# Stand flags: can_stand_in (air pocket), can_stand_on (solid surface), transparent (see-through).
[floor_tiles.air]
id = 0
can_stand_in = true
can_stand_on = false
transparent = true
astar_weight = 0
[floor_tiles.grass]
id = 1
can_stand_in = false
can_stand_on = true
transparent = false
astar_weight = 50
[floor_tiles.dirt]
id = 2
can_stand_in = false
can_stand_on = true
transparent = false
astar_weight = 50
[floor_tiles.rock]
id = 3
can_stand_in = false
can_stand_on = true
transparent = false
astar_weight = 50
[floor_tiles.bedrock]
id = 4
can_stand_in = false
can_stand_on = true
transparent = false
astar_weight = 50
# Fixture tiles: walls and objects above floors.
# IDs are relative to FIXTURE_ID_OFFSET (500000).
[fixture_tiles.dirt_wall]
id = 1
solid = true
[fixture_tiles.rock_wall]
id = 2
solid = true
[fixture_tiles.bedrock_wall]
id = 3
solid = true
[fixture_tiles.log]
id = 4
solid = false
[fixture_tiles.leaves]
id = 5
solid = false