Files
dorf/src/world/generation/terrain.rs
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

342 lines
12 KiB
Rust

use bevy::prelude::*;
use bevy::tasks::AsyncComputeTaskPool;
use bevy_platform::time::Instant;
use noise::{NoiseFn, Perlin};
use std::sync::{Arc, Mutex};
use crate::{
config::TileRegistry,
constants::{SEED, TILE_SIZE},
world::{
tiles::{ChunkData, FloorTileData, TileMap},
ChunkForrestryEvent, ChunkMap, ChunkTerrainEvent, TileOcclusionEvent,
CHUNK_SIZE, Z_ABOVE, Z_BELOW,
},
};
/// Thread-safe storage for completed terrain blobs.
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
/// Typed wrapper for terrain blob storage.
#[derive(Resource)]
pub struct TerrainBlobStorage {
pub blobs: Arc<Mutex<Vec<TerrainBlob>>>,
}
impl Default for TerrainBlobStorage {
fn default() -> Self {
Self {
blobs: Arc::new(Mutex::new(Vec::new())),
}
}
}
impl Clone for TerrainBlobStorage {
fn clone(&self) -> Self {
Self {
blobs: self.blobs.clone(),
}
}
}
/// Result of async terrain generation for a single chunk.
/// Contains all data needed to spawn entities and update TileMap on main thread.
pub struct TerrainBlob {
pub chunk_pos: IVec2,
pub chunk_data: ChunkData,
pub tile_updates: Vec<(IVec3, FloorTileData)>,
pub surface_positions: Vec<(Vec3, String)>,
}
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
let noise = Perlin::new(SEED);
let mut noise_value = 0.0;
let mut amplitude = 1.0;
let mut frequency = 0.008;
for _ in 0..6 {
noise_value += noise.get([x as f64 * frequency, y as f64 * frequency]) * amplitude;
amplitude *= 0.6;
frequency *= 1.8;
}
(noise_value * 2.5) as f32
}
/// Async terrain generation - runs on AsyncComputeTaskPool.
/// Computes all terrain data without ECS access, returns blob for main thread.
fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
let cave_noise = Perlin::new(SEED);
let start_x = chunk_pos.x * CHUNK_SIZE;
let start_y = chunk_pos.y * CHUNK_SIZE;
let registry = TileRegistry::global();
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();
for local_y in 0..CHUNK_SIZE {
for local_x in 0..CHUNK_SIZE {
let world_x = start_x + local_x;
let world_y = start_y + local_y;
let noise_position = Vec3::new(
(world_x as f32 * TILE_SIZE).round(),
(world_y as f32 * TILE_SIZE).round(),
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(),
);
for z in -Z_BELOW as isize..=Z_ABOVE as isize {
let position = Vec3::new(
(world_x as f32 * TILE_SIZE).round(),
(world_y as f32 * TILE_SIZE).round(),
(z as f32 * TILE_SIZE).round(),
);
let pos_ivec = position.as_ivec3();
let local_z = z as i32;
let surface_height =
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round();
if z < -5 {
let cave_value = cave_noise.get([
world_x as f64 * 0.05,
world_y as f64 * 0.05,
z as f64 * 0.05,
]);
if cave_value < -0.75 {
let tile = registry.floor("air");
tile_updates.push((
pos_ivec,
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,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} else if cave_value < 0.8 {
let tile = registry.floor("rock");
tile_updates.push((
pos_ivec,
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,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
} else {
let tile = registry.floor("dirt");
tile_updates.push((
pos_ivec,
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,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
}
} else if noise_position.z > position.z {
if surface_height <= position.z + TILE_SIZE {
let tile = registry.floor("grass");
tile_updates.push((
pos_ivec,
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,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
surface_positions.push((position, "grass".to_string()));
} else {
let tile = registry.floor("dirt");
tile_updates.push((
pos_ivec,
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,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
}
} else {
let tile = registry.floor("air");
tile_updates.push((
pos_ivec,
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,
tile.id,
tile.can_stand_in,
tile.can_stand_on,
tile.astar_weight,
);
}
}
}
}
TerrainBlob {
chunk_pos,
chunk_data,
tile_updates,
surface_positions,
}
}
/// Spawns async terrain generation tasks on AsyncComputeTaskPool.
/// Fast - just reads events and spawns tasks.
pub fn spawn_terrain_tasks(
mut events: MessageReader<ChunkTerrainEvent>,
blob_storage: Res<TerrainBlobStorage>,
) {
let start = Instant::now();
let count = events.len();
if count == 0 {
return;
}
let task_pool = AsyncComputeTaskPool::get();
let blobs = blob_storage.blobs.clone();
for event in events.read() {
let chunk_pos = event.chunk_position;
let blobs_clone = blobs.clone();
task_pool
.spawn(async move {
let blob = generate_terrain_blob(chunk_pos);
blobs_clone.lock().unwrap().push(blob);
})
.detach();
}
println!("{} terrain tasks spawned in {:.2?}", count, start.elapsed());
}
/// Applies completed terrain blobs on main thread.
/// Spawns entities, updates TileMap, sends occlusion events.
pub fn apply_terrain_blobs(
mut commands: Commands,
blob_storage: Res<TerrainBlobStorage>,
mut tilemap: ResMut<TileMap>,
mut chunk_map: ResMut<ChunkMap>,
mut forrestry_event_writer: MessageWriter<ChunkForrestryEvent>,
mut occlusion_event_writer: MessageWriter<TileOcclusionEvent>,
mut spawner: ResMut<crate::world::tiles::TilemapChunkSpawner>,
) {
let start = Instant::now();
let mut applied_count = 0;
let completed: Vec<TerrainBlob> = blob_storage.blobs.lock().unwrap().drain(..).collect();
for blob in completed {
let new_positions: Vec<IVec3> = blob
.tile_updates
.into_iter()
.map(|(pos, data)| {
tilemap.insert_floor(pos, data);
pos
})
.collect();
tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data);
spawner.queue_chunk(blob.chunk_pos);
for pos in new_positions {
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
}
forrestry_event_writer.write(ChunkForrestryEvent {
chunk_position: blob.chunk_pos,
floor_tiles: blob.surface_positions,
});
applied_count += 1;
}
if applied_count > 0 {
println!(
"{} terrain blobs applied in {:.2?}",
applied_count,
start.elapsed()
);
}
}
pub fn generate_chunk_weathering_and_precipitation() {
// TODO: Generate weathering and precipitation
// Temperature and humidity
// Erosion
// Hadley lines/cells etc
// Biomes
}