chunkgen 0.2.0

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

use serde::Deserialize;

use crate::util::math::floor_f64;
use crate::util::pos::SectionPos;
use crate::util::rng::RandomSource;
use crate::util::rng::legacy_random::LegacyRandomSource;
use crate::util::shuffle;
use crate::world::worldgen::feature::features::blockpos_iter::between_closed;
use crate::world::worldgen::region::WorldGenRegion;
use shulkr::util::BlockPosition;
use shulkr::world::block::Properties;

#[derive(Debug, Deserialize)]
pub struct EndSpikeConfig {
    #[serde(default)]
    pub crystal_invulnerable: bool,
    #[serde(default)]
    pub spikes: Vec<EndSpike>,
    #[serde(default)]
    pub crystal_beam_target: Option<[i32; 3]>,
}

#[derive(Debug, Deserialize)]
pub struct EndSpike {
    #[serde(default, rename = "centerX")]
    pub center_x: i32,
    #[serde(default, rename = "centerZ")]
    pub center_z: i32,
    #[serde(default)]
    pub radius: i32,
    #[serde(default)]
    pub height: i32,
    #[serde(default)]
    pub guarded: bool,
}

fn spikes_for_level(region: &WorldGenRegion<'_>) -> Vec<EndSpike> {
    let mut random = RandomSource::Legacy(LegacyRandomSource::from_seed(region.seed() as u64));
    let key = random.next_i64() & 65535;

    let mut sizes: Vec<i32> = (0..10).collect();
    let mut size_random = RandomSource::Legacy(LegacyRandomSource::from_seed(key as u64));
    shuffle(&mut sizes, &mut size_random);

    (0..10)
        .map(|i| {
            let angle = 2.0 * (-PI + (PI / 10.0) * f64::from(i));
            let x = floor_f64(42.0 * angle.cos());
            let z = floor_f64(42.0 * angle.sin());
            let size = sizes[i as usize];
            EndSpike {
                center_x: x,
                center_z: z,
                radius: 2 + size / 3,
                height: 76 + size * 3,
                guarded: size == 1 || size == 2,
            }
        })
        .collect()
}

const fn is_center_within_chunk(spike: &EndSpike, chunk_origin: BlockPosition) -> bool {
    SectionPos::block_to_section_coord(chunk_origin.x())
        == SectionPos::block_to_section_coord(spike.center_x)
        && SectionPos::block_to_section_coord(chunk_origin.z())
            == SectionPos::block_to_section_coord(spike.center_z)
}

pub fn place(
    region: &WorldGenRegion<'_>,
    random: &mut RandomSource,
    origin: BlockPosition,
    config: &EndSpikeConfig,
) -> bool {
    let generated;
    let spikes = if config.spikes.is_empty() {
        generated = spikes_for_level(region);
        &generated
    } else {
        &config.spikes
    };

    for spike in spikes {
        if is_center_within_chunk(spike, origin) {
            place_spike(region, random, spike);
        }
    }

    true
}

fn place_spike(region: &WorldGenRegion<'_>, random: &mut RandomSource, spike: &EndSpike) {
    let radius = spike.radius;
    let obsidian = Block::OBSIDIAN.default_state();
    let air = Block::AIR.default_state();

    for pos in between_closed(
        BlockPosition::new(
            spike.center_x - radius,
            region.min_y(),
            spike.center_z - radius,
        ),
        BlockPosition::new(
            spike.center_x + radius,
            spike.height + 10,
            spike.center_z + radius,
        ),
    ) {
        let dx = f64::from(pos.x() - spike.center_x);
        let dz = f64::from(pos.z() - spike.center_z);
        if dx * dx + dz * dz <= f64::from(radius * radius + 1) && pos.y() < spike.height {
            region.set_block(pos, obsidian);
        } else if pos.y() > 65 {
            region.set_block(pos, air);
        }
    }

    if spike.guarded {
        for dx in -2i32..=2 {
            for dz in -2i32..=2 {
                for dy in 0..=3 {
                    let is_x_side = dx.abs() == 2;
                    let is_z_side = dz.abs() == 2;
                    let top = dy == 3;
                    if is_x_side || is_z_side || top {
                        let x_edge = dx == -2 || dx == 2 || top;
                        let z_edge = dz == -2 || dz == 2 || top;
                        let state = Block::IRON_BARS
                            .default_state()
                            .with(Properties::NORTH, x_edge && dz != -2)
                            .with(Properties::SOUTH, x_edge && dz != 2)
                            .with(Properties::WEST, z_edge && dx != -2)
                            .with(Properties::EAST, z_edge && dx != 2);
                        region.set_block(
                            BlockPosition::new(
                                spike.center_x + dx,
                                spike.height + dy,
                                spike.center_z + dz,
                            ),
                            state,
                        );
                    }
                }
            }
        }
    }

    random.next_f32();
    let crystal_pos = BlockPosition::new(spike.center_x, spike.height + 1, spike.center_z);
    region.set_block(crystal_pos.below(), Block::BEDROCK.default_state());
    region.set_block(crystal_pos, Block::FIRE.default_state());
}