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, TerrainSpriteState, CurrentWorldSpriteState}, ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE, Z_ABOVE, Z_BELOW, ChunkMap, ChunkOwner, }, }; /// Thread-safe storage for completed terrain blobs. /// Uses type erasure to avoid Debug bounds on TerrainBlob. type BlobStorage = Arc>>; /// Typed wrapper for terrain blob storage. #[derive(Resource)] pub struct TerrainBlobStorage { pub blobs: Arc>>, } 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 tile_spawns: Vec<(Vec3, FloorTilePrefab)>, } 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(); let mut tile_spawns: Vec<(Vec3, FloorTilePrefab)> = 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_spawns.push((position, FloorTilePrefab::air(position))); 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_spawns.push((position, FloorTilePrefab::rock(position))); 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_spawns.push((position, FloorTilePrefab::dirt(position))); 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_spawns.push((position, FloorTilePrefab::grass(position))); 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_spawns.push((position, FloorTilePrefab::dirt(position))); 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_spawns.push((position, FloorTilePrefab::air(position))); 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, tile_spawns, } } /// Spawns async terrain generation tasks on AsyncComputeTaskPool. /// Fast - just reads events and spawns tasks. pub fn spawn_terrain_tasks( mut events: MessageReader, mut cwss: ResMut, blob_storage: Res, ) { 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(); } cwss.state = TerrainSpriteState::WaitingForRender; 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, mut tilemap: ResMut, mut chunk_map: ResMut, mut forrestry_event_writer: MessageWriter, mut occlusion_event_writer: MessageWriter, ) { let start = Instant::now(); let mut applied_count = 0; let completed: Vec = blob_storage.blobs.lock().unwrap().drain(..).collect(); for blob in completed { let new_positions: Vec = blob .tile_updates .into_iter() .map(|(pos, data)| { tilemap.insert_floor(pos, data); pos }) .collect(); tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data); for (_position, prefab) in blob.tile_spawns { let entity = prefab.spawn(&mut commands); commands.entity(entity).insert(ChunkOwner(blob.chunk_pos)); chunk_map .chunk_entity_index .entry(blob.chunk_pos) .or_default() .push(entity); } 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 }