Pumpkin/crates/pumpkin-util/src/biome.rs
2026-08-06 12:56:40 +02:00

172 lines
5.6 KiB
Rust

use std::sync::LazyLock;
use serde::Deserialize;
use crate::{noise::simplex::OctaveSimplexNoiseSampler, random::legacy_rand::LegacyRand};
/// Global noise sampler for biome temperature variation.
pub static TEMPERATURE_NOISE: LazyLock<OctaveSimplexNoiseSampler> = LazyLock::new(|| {
let mut rand = LegacyRand::from_seed(1234);
OctaveSimplexNoiseSampler::new(&mut rand, &[0])
});
/// Global noise sampler for frozen ocean biome adjustments.
pub static FROZEN_OCEAN_NOISE: LazyLock<OctaveSimplexNoiseSampler> = LazyLock::new(|| {
let mut rand = LegacyRand::from_seed(3456);
OctaveSimplexNoiseSampler::new(&mut rand, &[-2, -1, 0])
});
/// Global noise sampler for foliage-based temperature adjustments.
pub static FOLIAGE_NOISE: LazyLock<OctaveSimplexNoiseSampler> = LazyLock::new(|| {
let mut rand = LegacyRand::from_seed(2345);
OctaveSimplexNoiseSampler::new(&mut rand, &[0])
});
/// Modifiers that adjust the base biome temperature.
#[derive(Clone, Deserialize, Copy, Hash, PartialEq, Eq, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub enum TemperatureModifier {
/// No temperature modification.
None,
/// Frozen biome adjustment, typically used for frozen oceans and snowy areas.
Frozen,
}
impl TemperatureModifier {
/// Applies the temperature modifier to a given position and base temperature.
///
/// # Parameters
/// - `x`: X coordinate in the world.
/// - `z`: Z coordinate in the world.
/// - `temperature`: The base biome temperature.
///
/// # Returns
/// - The modified temperature as an `f32`.
pub fn convert_temperature(&self, x: f64, z: f64, temperature: f32) -> f32 {
match self {
Self::None => temperature,
Self::Frozen => {
let frozen_ocean_sample =
FROZEN_OCEAN_NOISE.sample(x * 0.05, z * 0.05, false) * 7.0;
let foliage_sample = FOLIAGE_NOISE.sample(x * 0.2, z * 0.2, false);
let threshold = frozen_ocean_sample + foliage_sample;
if threshold < 0.3 {
let foliage_sample = FOLIAGE_NOISE.sample(x * 0.09, z * 0.09, false);
if foliage_sample < 0.8 {
return 0.2f32;
}
}
temperature
}
}
}
}
/// Represents weather information for a biome, including temperature and precipitation.
#[derive(Clone, Debug)]
pub struct Weather {
has_precipitation: bool,
/// Base temperature of the biome.
temperature: f32,
/// Modifier affecting the base temperature.
temperature_modifier: TemperatureModifier,
/// Rate of rainfall or snowfall in the biome.
#[expect(dead_code)]
downfall: f32,
}
impl Weather {
/// Creates a new `Weather` instance.
///
/// # Parameters
/// - `has_precipitation`: Whether the biome has precipitation.
/// - `temperature`: Base temperature of the biome.
/// - `temperature_modifier`: Modifier affecting the temperature.
/// - `downfall`: Amount of rainfall or snowfall.
#[must_use]
pub const fn new(
has_precipitation: bool,
temperature: f32,
temperature_modifier: TemperatureModifier,
downfall: f32,
) -> Self {
Self {
has_precipitation,
temperature,
temperature_modifier,
downfall,
}
}
/// Returns the raw base temperature of the biome, without any height or modifier adjustment.
#[must_use]
pub const fn base_temperature(&self) -> f32 {
self.temperature
}
/// Computes the effective temperature at a given position.
///
/// # Parameters
/// - `x`, `z`: World coordinates.
/// - `y`: Y-level of the position.
/// - `sea_level`: Sea level in the world.
///
/// # Returns
/// - The temperature at the position as `f32`.
///
/// # Notes
/// - This function is computationally expensive and should be cached.
/// - Temperature is influenced by `TemperatureModifier` and noise samplers.
pub fn compute_temperature(&self, x: f64, y: i32, z: f64, sea_level: i32) -> f32 {
let modified_temperature =
self.temperature_modifier
.convert_temperature(x, z, self.temperature);
let offset_sea_level = sea_level + 17;
if y > offset_sea_level {
let temperature_noise =
(TEMPERATURE_NOISE.sample(x / 8.0, z / 8.0, false) * 8.0) as f32;
modified_temperature
- (temperature_noise + y as f32 - offset_sea_level as f32) * 0.05f32 / 40.0f32
} else {
modified_temperature
}
}
#[must_use]
pub fn warm_enough_to_rain(&self, x: i32, y: i32, z: i32, sea_level: i32) -> bool {
self.compute_temperature(f64::from(x), y, f64::from(z), sea_level) >= 0.15
}
#[must_use]
pub fn is_rain_at(&self, x: i32, y: i32, z: i32, sea_level: i32) -> bool {
self.has_precipitation && self.warm_enough_to_rain(x, y, z, sea_level)
}
}
#[cfg(test)]
mod tests {
use super::{TemperatureModifier, Weather};
#[test]
fn precipitation_controls_rain() {
let weather = Weather::new(false, 1.0, TemperatureModifier::None, 0.0);
assert!(!weather.is_rain_at(0, 64, 0, 63));
}
#[test]
fn warm_precipitation_is_rain() {
let weather = Weather::new(true, 1.0, TemperatureModifier::None, 0.0);
assert!(weather.is_rain_at(0, 64, 0, 63));
}
#[test]
fn cold_precipitation_is_snow() {
let weather = Weather::new(true, 0.0, TemperatureModifier::None, 0.0);
assert!(!weather.is_rain_at(0, 64, 0, 63));
}
}