198 lines
7.7 KiB
Rust
198 lines
7.7 KiB
Rust
use pumpkin_data::{Block, BlockDirection};
|
|
use pumpkin_nbt::compound::NbtCompound;
|
|
use pumpkin_util::{
|
|
math::{position::BlockPos, vector3::Vector3},
|
|
random::{RandomGenerator, RandomImpl},
|
|
};
|
|
|
|
use crate::generation::proto_chunk::GenerationCache;
|
|
|
|
/// The three mob types that can appear in a dungeon spawner.
|
|
///
|
|
/// Skeleton 25%, Zombie 50%, Spider 25%.
|
|
const DUNGEON_MOBS: [&str; 4] = [
|
|
"minecraft:skeleton",
|
|
"minecraft:zombie",
|
|
"minecraft:zombie",
|
|
"minecraft:spider",
|
|
];
|
|
|
|
pub struct DungeonFeature {}
|
|
|
|
impl DungeonFeature {
|
|
/// Generate a monster room (dungeon) at `pos`.
|
|
pub fn generate<T: GenerationCache>(
|
|
&self,
|
|
chunk: &mut T,
|
|
_min_y: i8,
|
|
_height: u16,
|
|
_feature: pumpkin_data::placed_feature::PlacedFeature,
|
|
random: &mut RandomGenerator,
|
|
pos: BlockPos,
|
|
) -> bool {
|
|
let xr = random.next_bounded_i32(2) + 2;
|
|
let zr = random.next_bounded_i32(2) + 2;
|
|
|
|
let min_x = -xr - 1;
|
|
let max_x = xr + 1;
|
|
let min_z = -zr - 1;
|
|
let max_z = zr + 1;
|
|
|
|
let mut hole_count = 0i32;
|
|
for dx in min_x..=max_x {
|
|
for dy in -1..=4i32 {
|
|
for dz in min_z..=max_z {
|
|
let check_pos = pos.0.add(&Vector3::new(dx, dy, dz));
|
|
let solid = !chunk.is_air(&check_pos);
|
|
|
|
if dy == -1 && !solid {
|
|
return false; // Floor has a gap
|
|
}
|
|
if dy == 4 && !solid {
|
|
return false; // Ceiling has a gap
|
|
}
|
|
|
|
// Wall openings
|
|
let on_wall = dx == min_x || dx == max_x || dz == min_z || dz == max_z;
|
|
if on_wall
|
|
&& dy == 0
|
|
&& chunk.is_air(&check_pos)
|
|
&& chunk.is_air(&check_pos.add(&Vector3::new(0, 1, 0)))
|
|
{
|
|
hole_count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !(1..=5).contains(&hole_count) {
|
|
return false;
|
|
}
|
|
|
|
for dx in min_x..=max_x {
|
|
for dy in (-1..=3i32).rev() {
|
|
for dz in min_z..=max_z {
|
|
let wall_pos = pos.0.add(&Vector3::new(dx, dy, dz));
|
|
|
|
let on_boundary = dx == min_x
|
|
|| dy == -1
|
|
|| dz == min_z
|
|
|| dx == max_x
|
|
|| dy == 4
|
|
|| dz == max_z;
|
|
|
|
if on_boundary {
|
|
let below_pos = wall_pos.add(&Vector3::new(0, -1, 0));
|
|
let below_solid = !chunk.is_air(&below_pos);
|
|
let cur_solid = !chunk.is_air(&wall_pos);
|
|
let is_chest = GenerationCache::get_block_state(chunk, &wall_pos)
|
|
.to_block()
|
|
== &Block::CHEST;
|
|
|
|
let world_min_y = chunk.bottom_y() as i32;
|
|
if wall_pos.y >= world_min_y && !below_solid {
|
|
chunk.set_block_state(&wall_pos, Block::CAVE_AIR.default_state);
|
|
} else if cur_solid && !is_chest {
|
|
if dy == -1 && random.next_bounded_i32(4) != 0 {
|
|
// 75% mossy cobblestone
|
|
chunk.set_block_state(
|
|
&wall_pos,
|
|
Block::MOSSY_COBBLESTONE.default_state,
|
|
);
|
|
} else {
|
|
// 25% cobblestone (or 100% if it's not the floor)
|
|
chunk.set_block_state(&wall_pos, Block::COBBLESTONE.default_state);
|
|
}
|
|
}
|
|
} else {
|
|
// Clear interior, but preserve chests and spawners
|
|
let state = GenerationCache::get_block_state(chunk, &wall_pos);
|
|
let is_chest = state.to_block() == &Block::CHEST;
|
|
let is_spawner = state.to_block() == &Block::SPAWNER;
|
|
if !is_chest && !is_spawner {
|
|
chunk.set_block_state(&wall_pos, Block::CAVE_AIR.default_state);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Chest placement attempts (rooms have 1-2 chests)
|
|
for _ in 0..2 {
|
|
for _ in 0..3 {
|
|
let cx = pos.0.x + random.next_bounded_i32(xr * 2 + 1) - xr;
|
|
let cy = pos.0.y;
|
|
let cz = pos.0.z + random.next_bounded_i32(zr * 2 + 1) - zr;
|
|
let chest_pos = Vector3::new(cx, cy, cz);
|
|
|
|
if !chunk.is_air(&chest_pos) {
|
|
continue;
|
|
}
|
|
|
|
// Count solid horizontal neighbours
|
|
let wall_count = BlockDirection::horizontal()
|
|
.iter()
|
|
.filter(|d| !chunk.is_air(&chest_pos.add(&d.to_offset())))
|
|
.count();
|
|
|
|
if wall_count == 1 {
|
|
// Orient the chest toward its single solid neighbor.
|
|
let facing_dir = BlockDirection::horizontal()
|
|
.iter()
|
|
.find(|d| !chunk.is_air(&chest_pos.add(&d.to_offset())))
|
|
.copied();
|
|
|
|
// Build the chest state: face away from the wall.
|
|
let chest_state = if let Some(dir) = facing_dir {
|
|
use pumpkin_data::block_properties::{
|
|
BlockProperties, ChestLikeProperties,
|
|
};
|
|
let mut props = ChestLikeProperties::default(&Block::CHEST);
|
|
props.facing = dir.opposite().to_cardinal_direction();
|
|
let state_id = props.to_state_id(&Block::CHEST);
|
|
pumpkin_data::BlockState::from_id(state_id)
|
|
} else {
|
|
Block::CHEST.default_state
|
|
};
|
|
|
|
chunk.set_block_state(&chest_pos, chest_state);
|
|
|
|
// Write the block-entity NBT with a deferred loot table.
|
|
let loot_seed = random.next_i64();
|
|
let mut chest_nbt = NbtCompound::new();
|
|
chest_nbt.put_string("id", "minecraft:chest".to_string());
|
|
chest_nbt.put_int("x", chest_pos.x);
|
|
chest_nbt.put_int("y", chest_pos.y);
|
|
chest_nbt.put_int("z", chest_pos.z);
|
|
chest_nbt
|
|
.put_string("LootTable", "minecraft:chests/simple_dungeon".to_string());
|
|
chest_nbt.put_long("LootTableSeed", loot_seed);
|
|
chunk.add_block_entity(&chest_pos, chest_nbt);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: set spawner entity type
|
|
let mob = DUNGEON_MOBS[random.next_bounded_i32(DUNGEON_MOBS.len() as i32) as usize];
|
|
chunk.set_block_state(&pos.0, Block::SPAWNER.default_state);
|
|
|
|
let mut entity_nbt = NbtCompound::new();
|
|
entity_nbt.put_string("id", "minecraft:mob_spawner".to_string());
|
|
entity_nbt.put_int("x", pos.0.x);
|
|
entity_nbt.put_int("y", pos.0.y);
|
|
entity_nbt.put_int("z", pos.0.z);
|
|
|
|
let mut spawn_entry = NbtCompound::new();
|
|
let mut entity_nbt_inner = NbtCompound::new();
|
|
entity_nbt_inner.put_string("id", mob.to_string());
|
|
spawn_entry.put_compound("entity", entity_nbt_inner);
|
|
entity_nbt.put_compound("SpawnData", spawn_entry);
|
|
|
|
chunk.add_block_entity(&pos.0, entity_nbt);
|
|
|
|
true
|
|
}
|
|
}
|