chunkgen 0.2.0

Minecraft world generation library
Documentation
use shulkr::world::block::Block;
use std::f32::consts::PI;

use glam::DVec3;
use serde::Deserialize;
use shulkr::util::Key;

use crate::world::chunk::heightmap::HeightmapType;

use crate::util::math;
use crate::util::rng::RandomSource;
use crate::util::value_providers::FloatProvider;
use crate::util::value_providers::int::IntProvider;
use crate::world::worldgen::block_predicate::HolderSet;
use crate::world::worldgen::column::Column;
use crate::world::worldgen::feature::features::speleothem::{
    get_speleothem_height, is_base_or_lava, is_circle_mostly_embedded_in_stone, is_empty_or_water,
    is_empty_or_water_or_lava,
};
use crate::world::worldgen::region::WorldGenRegion;
use shulkr::util::BlockPosition;

const BASE_STONE_OVERWORLD: Key = Key::const_vanilla("base_stone_overworld");

#[derive(Debug, Deserialize)]
pub struct LargeDripstoneConfig {
    #[serde(default = "default_floor_to_ceiling_search_range")]
    pub floor_to_ceiling_search_range: i32,
    pub column_radius: IntProvider,
    pub height_scale: FloatProvider,
    pub max_column_radius_to_cave_height_ratio: f32,
    pub stalactite_bluntness: FloatProvider,
    pub stalagmite_bluntness: FloatProvider,
    pub wind_speed: FloatProvider,
    pub min_radius_for_wind: i32,
    pub min_bluntness_for_wind: f32,
    pub replaceable_blocks: HolderSet,
}

const fn default_floor_to_ceiling_search_range() -> i32 {
    30
}

struct LargeDripstone {
    root: BlockPosition,
    pointing_up: bool,
    radius: i32,
    bluntness: f64,
    scale: f64,
}

struct WindOffsetter {
    origin_y: i32,
    wind_speed: Option<DVec3>,
    max_offset: i32,
}

impl WindOffsetter {
    fn new(
        origin_y: i32,
        random: &mut RandomSource,
        wind_speed_range: &FloatProvider,
        max_offset: i32,
    ) -> Self {
        let speed = wind_speed_range.sample(random);
        let direction = random.next_f32() * PI;
        Self {
            origin_y,
            wind_speed: Some(DVec3::new(
                f64::from(math::cos(f64::from(direction)) * speed),
                0.0,
                f64::from(math::sin(f64::from(direction)) * speed),
            )),
            max_offset,
        }
    }

    const fn no_wind() -> Self {
        Self {
            origin_y: 0,
            wind_speed: None,
            max_offset: 0,
        }
    }

    fn offset(&self, pos: BlockPosition) -> BlockPosition {
        let Some(wind_speed) = self.wind_speed else {
            return pos;
        };

        let dy = self.origin_y - pos.y();
        let total_wind_adjust = wind_speed * f64::from(dy);
        let dx = math::floor_f64(total_wind_adjust.x).clamp(-self.max_offset, self.max_offset);
        let dz = math::floor_f64(total_wind_adjust.z).clamp(-self.max_offset, self.max_offset);
        pos.offset(dx, 0, dz)
    }
}

impl LargeDripstone {
    fn get_height(&self) -> i32 {
        self.get_height_at_radius(0.0)
    }

    fn get_height_at_radius(&self, check_radius: f32) -> i32 {
        get_speleothem_height(
            check_radius as f64,
            self.radius as f64,
            self.scale,
            self.bluntness,
        ) as i32
    }

    fn move_back_until_base_is_inside_stone_and_shrink_radius_if_necessary(
        &mut self,
        region: &WorldGenRegion<'_>,
        wind: &WindOffsetter,
    ) -> bool {
        while self.radius > 1 {
            let mut new_root = self.root;
            let max_tries = 10.min(self.get_height());

            for _ in 0..max_tries {
                if region.get_block(new_root).is(Block::LAVA) {
                    return false;
                }

                if is_circle_mostly_embedded_in_stone(region, wind.offset(new_root), self.radius) {
                    self.root = new_root;
                    return true;
                }

                new_root = if self.pointing_up {
                    new_root.below()
                } else {
                    new_root.above()
                };
            }

            self.radius /= 2;
        }

        false
    }

    fn place_blocks(
        &self,
        region: &WorldGenRegion<'_>,
        random: &mut RandomSource,
        wind: &WindOffsetter,
    ) {
        let dripstone = Block::DRIPSTONE_BLOCK.default_state();

        for dx in -self.radius..=self.radius {
            for dz in -self.radius..=self.radius {
                let current_radius = ((dx * dx + dz * dz) as f32).sqrt();
                if current_radius > self.radius as f32 {
                    continue;
                }

                let mut height = self.get_height_at_radius(current_radius);
                if height <= 0 {
                    continue;
                }

                if random.next_f32() < 0.2 {
                    height = (height as f32 * random_between(random, 0.8, 1.0)) as i32;
                }

                let mut pos = self.root.offset(dx, 0, dz);
                let mut has_been_out_of_stone = false;
                let max_y = if self.pointing_up {
                    region.get_height(HeightmapType::WorldSurfaceWg, pos.x(), pos.z())
                } else {
                    i32::MAX
                };

                let mut i = 0;
                while i < height && pos.y() < max_y {
                    let wind_adjusted_pos = wind.offset(pos);
                    if is_empty_or_water_or_lava(region.get_block(wind_adjusted_pos)) {
                        has_been_out_of_stone = true;
                        region.set_block(wind_adjusted_pos, dripstone);
                    } else if has_been_out_of_stone
                        && region
                            .get_block(wind_adjusted_pos)
                            .as_block()
                            .has_tag(&BASE_STONE_OVERWORLD)
                    {
                        break;
                    }

                    pos = if self.pointing_up {
                        pos.above()
                    } else {
                        pos.below()
                    };
                    i += 1;
                }
            }
        }
    }

    fn is_suitable_for_wind(&self, config: &LargeDripstoneConfig) -> bool {
        self.radius >= config.min_radius_for_wind
            && self.bluntness >= f64::from(config.min_bluntness_for_wind)
    }
}

fn random_between(random: &mut RandomSource, min: f32, max: f32) -> f32 {
    random.next_f32() * (max - min) + min
}

fn make_dripstone(
    root: BlockPosition,
    pointing_up: bool,
    random: &mut RandomSource,
    radius: i32,
    bluntness: &FloatProvider,
    height_scale: &FloatProvider,
) -> LargeDripstone {
    LargeDripstone {
        root,
        pointing_up,
        radius,
        bluntness: f64::from(bluntness.sample(random)),
        scale: f64::from(height_scale.sample(random)),
    }
}

pub fn place(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    origin: BlockPosition,
    config: &LargeDripstoneConfig,
) -> bool {
    if !is_empty_or_water(region.get_block(origin)) {
        return false;
    }

    let Some(column) = Column::scan(
        region,
        origin,
        config.floor_to_ceiling_search_range,
        is_empty_or_water,
        |state| is_base_or_lava(state, Block::DRIPSTONE_BLOCK, &config.replaceable_blocks),
    ) else {
        return false;
    };

    let (Some(floor), Some(ceiling)) = (column.floor(), column.ceiling()) else {
        return false;
    };

    let Some(column_height) = column.height() else {
        return false;
    };
    if column_height < 4 {
        return false;
    }

    let max_column_radius_based_on_column_height =
        (column_height as f32 * config.max_column_radius_to_cave_height_ratio) as i32;
    let max_column_radius = max_column_radius_based_on_column_height
        .clamp(config.column_radius.min(), config.column_radius.max());
    let radius = random.next_i32_between(config.column_radius.min(), max_column_radius);

    let mut stalactite = make_dripstone(
        origin.at_y(ceiling - 1),
        false,
        random,
        radius,
        &config.stalactite_bluntness,
        &config.height_scale,
    );
    let mut stalagmite = make_dripstone(
        origin.at_y(floor + 1),
        true,
        random,
        radius,
        &config.stalagmite_bluntness,
        &config.height_scale,
    );

    let wind = if stalactite.is_suitable_for_wind(config) && stalagmite.is_suitable_for_wind(config)
    {
        WindOffsetter::new(origin.y(), random, &config.wind_speed, 16 - radius)
    } else {
        WindOffsetter::no_wind()
    };

    let stalactite_base_embedded_in_stone = stalactite
        .move_back_until_base_is_inside_stone_and_shrink_radius_if_necessary(region, &wind);
    let stalagmite_base_embedded_in_stone = stalagmite
        .move_back_until_base_is_inside_stone_and_shrink_radius_if_necessary(region, &wind);

    if stalactite_base_embedded_in_stone {
        stalactite.place_blocks(region, random, &wind);
    }

    if stalagmite_base_embedded_in_stone {
        stalagmite.place_blocks(region, random, &wind);
    }

    true
}