chunkgen 0.2.0

Minecraft world generation library
Documentation
use crate::util::rng::RandomSource;
use glam::IVec3;
use shulkr::util::BlockPosition;
use shulkr::util::BoundingBox;
use shulkr::util::Direction;
use shulkr::world::block::Rotation;

pub trait RotationExt {
    fn get_random(rng: &mut RandomSource) -> Self;

    fn get_shuffled(rng: &mut RandomSource) -> [Rotation; 4];

    fn rotate(self, dir: Direction) -> Direction;

    fn rotate_pos(self, pos: BlockPosition) -> BlockPosition;

    fn rotate_index(self, rotation: i32, steps: i32) -> i32;

    fn transform_pos(self, pos: IVec3, pivot: IVec3) -> IVec3;

    fn rotate_size(self, size: IVec3) -> IVec3;

    fn transform_pos_mirrored(self, pos: IVec3, pivot: IVec3, mirror_front_back: bool) -> IVec3;

    fn get_bounding_box_full(
        self,
        pos: IVec3,
        size: IVec3,
        pivot: IVec3,
        mirror_front_back: bool,
    ) -> BoundingBox;

    fn get_bounding_box_with_pivot(self, pos: IVec3, size: IVec3, pivot: IVec3) -> BoundingBox;

    fn get_bounding_box(self, pos: IVec3, size: IVec3) -> BoundingBox;
}

impl RotationExt for Rotation {
    fn get_random(rng: &mut RandomSource) -> Self {
        Rotation::ALL[rng.next_i32_bounded(4) as usize]
    }

    fn get_shuffled(rng: &mut RandomSource) -> [Rotation; 4] {
        let mut rotations = Rotation::ALL;
        for i in (1..4).rev() {
            let j = rng.next_i32_bounded((i + 1) as i32) as usize;
            rotations.swap(i, j);
        }
        rotations
    }

    fn rotate(self, dir: Direction) -> Direction {
        match self {
            Self::None => dir,
            Self::Clockwise90 => dir.rotate_y_clockwise(),
            Self::Clockwise180 => dir.rotate_y_clockwise().rotate_y_clockwise(),
            Self::CounterClockwise90 => dir.rotate_y_counter_clockwise(),
        }
    }

    fn rotate_pos(self, pos: BlockPosition) -> BlockPosition {
        match self {
            Self::None => pos,
            Self::Clockwise90 => BlockPosition::new(-pos.z(), pos.y(), pos.x()),
            Self::Clockwise180 => BlockPosition::new(-pos.x(), pos.y(), -pos.z()),
            Self::CounterClockwise90 => BlockPosition::new(pos.z(), pos.y(), -pos.x()),
        }
    }

    fn rotate_index(self, rotation: i32, steps: i32) -> i32 {
        match self {
            Self::None => rotation,
            Self::Clockwise90 => (rotation + steps / 4) % steps,
            Self::Clockwise180 => (rotation + steps / 2) % steps,
            Self::CounterClockwise90 => (rotation + steps * 3 / 4) % steps,
        }
    }

    fn transform_pos(self, pos: IVec3, pivot: IVec3) -> IVec3 {
        let (x, y, z) = (pos.x, pos.y, pos.z);
        let (px, pz) = (pivot.x, pivot.z);
        match self {
            Self::None => IVec3::new(x, y, z),
            Self::Clockwise90 => IVec3::new(px + pz - z, y, pz - px + x),
            Self::Clockwise180 => IVec3::new(px + px - x, y, pz + pz - z),
            Self::CounterClockwise90 => IVec3::new(px - pz + z, y, px + pz - x),
        }
    }

    fn rotate_size(self, size: IVec3) -> IVec3 {
        match self {
            Self::Clockwise90 | Self::CounterClockwise90 => IVec3::new(size.z, size.y, size.x),
            Self::None | Self::Clockwise180 => size,
        }
    }

    fn transform_pos_mirrored(self, pos: IVec3, pivot: IVec3, mirror_front_back: bool) -> IVec3 {
        let mx = if mirror_front_back { -pos.x } else { pos.x };
        let mirrored_pos = IVec3::new(mx, pos.y, pos.z);
        self.transform_pos(mirrored_pos, pivot)
    }

    fn get_bounding_box_full(
        self,
        pos: IVec3,
        size: IVec3,
        pivot: IVec3,
        mirror_front_back: bool,
    ) -> BoundingBox {
        let c1 = self.transform_pos_mirrored(IVec3::ZERO, pivot, mirror_front_back);
        let c2 = self.transform_pos_mirrored(
            IVec3::new(size.x - 1, size.y - 1, size.z - 1),
            pivot,
            mirror_front_back,
        );
        let (min, max) = (c1.min(c2) + pos, c1.max(c2) + pos);
        BoundingBox::new(min.x, min.y, min.z, max.x, max.y, max.z)
    }

    fn get_bounding_box_with_pivot(self, pos: IVec3, size: IVec3, pivot: IVec3) -> BoundingBox {
        self.get_bounding_box_full(pos, size, pivot, false)
    }

    fn get_bounding_box(self, pos: IVec3, size: IVec3) -> BoundingBox {
        self.get_bounding_box_full(pos, size, IVec3::ZERO, false)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rotate_direction_matches_vanilla() {
        assert_eq!(Rotation::None.rotate(Direction::North), Direction::North);
        assert_eq!(
            Rotation::Clockwise90.rotate(Direction::North),
            Direction::East
        );
        assert_eq!(
            Rotation::Clockwise180.rotate(Direction::North),
            Direction::South
        );
        assert_eq!(
            Rotation::CounterClockwise90.rotate(Direction::North),
            Direction::West
        );
    }

    #[test]
    fn composing_opposite_rotations_is_identity() {
        assert_eq!(
            Rotation::Clockwise90.then(Rotation::CounterClockwise90),
            Rotation::None
        );
        assert_eq!(
            Rotation::Clockwise180.then(Rotation::Clockwise180),
            Rotation::None
        );
    }

    #[test]
    fn vertical_directions_are_unaffected_by_rotation() {
        assert_eq!(Rotation::Clockwise90.rotate(Direction::Up), Direction::Up);
        assert_eq!(
            Rotation::Clockwise180.rotate(Direction::Down),
            Direction::Down
        );
    }

    #[test]
    fn transform_pos_around_zero_pivot() {
        assert_eq!(
            Rotation::Clockwise90.transform_pos(IVec3::new(3, 5, 7), IVec3::ZERO),
            IVec3::new(-7, 5, 3)
        );
        assert_eq!(
            Rotation::Clockwise180.transform_pos(IVec3::new(3, 5, 7), IVec3::ZERO),
            IVec3::new(-3, 5, -7)
        );
    }

    #[test]
    fn rotate_size_swaps_x_and_z_only_on_quarter_turns() {
        assert_eq!(
            Rotation::None.rotate_size(IVec3::new(6, 10, 8)),
            IVec3::new(6, 10, 8)
        );
        assert_eq!(
            Rotation::Clockwise90.rotate_size(IVec3::new(6, 10, 8)),
            IVec3::new(8, 10, 6)
        );
        assert_eq!(
            Rotation::Clockwise180.rotate_size(IVec3::new(6, 10, 8)),
            IVec3::new(6, 10, 8)
        );
    }

    #[test]
    fn bounding_box_none_covers_full_size_from_origin() {
        let bb = Rotation::None.get_bounding_box(IVec3::new(0, 0, 0), IVec3::new(6, 10, 6));
        assert_eq!((bb.min_x(), bb.min_y(), bb.min_z()), (0, 0, 0));
        assert_eq!((bb.max_x(), bb.max_y(), bb.max_z()), (5, 9, 5));
    }
}