// SPDX-License-Identifier: AGPL-3.0-only //! Procedural generation logic for the voxel world. use noise::{NoiseFn, Perlin}; use serde::{Deserialize, Serialize}; use crate::world::{BlockId, CHUNK_SIZE, Chunk, ChunkPos}; /// Configuration parameters for deterministic world generation. #[derive(Serialize, Deserialize, Clone, Debug)] pub struct WorldGenConfig { /// The base height around which terrain features are generated. pub base_height: u32, /// The scale factor applied to the noise coordinates. Smaller values create wider features. pub noise_scale: f64, /// The block identifier used for the top layer of the terrain. pub surface_block: BlockId, /// The block identifier used for the layers immediately below the surface. pub subsurface_block: BlockId, /// The block identifier used for deep underground layers. pub stone_block: BlockId, } /// A deterministic terrain generator that produces voxel chunks. pub struct VoxelGenerator { /// The configuration parameters guiding the generation. pub config: WorldGenConfig, noise: Perlin, } impl VoxelGenerator { /// Initializes a new voxel generator with the specified configuration and seed. #[must_use] pub fn new(config: WorldGenConfig, seed: u32) -> Self { Self { config, noise: Perlin::new(seed), } } /// Generates a complete voxel chunk for the specified position. #[must_use] #[expect( clippy::cast_possible_wrap, clippy::cast_possible_truncation, reason = "chunk and voxel coordinates stay within the ranges these casts assume" )] pub fn generate_chunk(&self, pos: ChunkPos) -> Chunk { let mut chunk = Chunk::default(); for x in 0..CHUNK_SIZE { for z in 0..CHUNK_SIZE { let global_x = (pos.x * CHUNK_SIZE as i32) + x as i32; let global_z = (pos.z * CHUNK_SIZE as i32) + z as i32; let noise_val = self.noise.get([ f64::from(global_x) * self.config.noise_scale, f64::from(global_z) * self.config.noise_scale, ]); let amplitude = 15.0; let target_height = self.config.base_height as i32 + (noise_val * amplitude) as i32; for y in 0..CHUNK_SIZE { let global_y = (pos.y * CHUNK_SIZE as i32) + y as i32; let block = if global_y > target_height { BlockId::AIR } else if global_y == target_height { self.config.surface_block } else if global_y > target_height - 3 { self.config.subsurface_block } else { self.config.stone_block }; if block != BlockId::AIR { chunk.set(x, y, z, block); } } } } chunk } }