diff --git a/crates/pumpkin-world/src/chunk_system/generation.rs b/crates/pumpkin-world/src/chunk_system/generation.rs index bf2706d15..c449b6a12 100644 --- a/crates/pumpkin-world/src/chunk_system/generation.rs +++ b/crates/pumpkin-world/src/chunk_system/generation.rs @@ -360,17 +360,21 @@ mod tests { let Chunk::Proto(chunk) = chunk else { panic!("features stage should return a proto chunk"); }; + let mut hash = 0xcbf29ce484222325u64; let mut non_air = 0; for y in 123..=146 { for x in -4896..=-4881 { - for z in -4405..=-4393 { + for z in -4400..=-4393 { let state = chunk.get_block_state(&pumpkin_util::math::vector3::Vector3::new(x, y, z)); + hash ^= u64::from(state.as_u16()); + hash = hash.wrapping_mul(0x100000001b3); non_air += usize::from(!state.to_state().is_air()); } } } assert_eq!(non_air, 59); + assert_eq!(hash, 0x5af3_06b3_536d_8053); assert!(chunk.pending_block_entities.iter().any(|nbt| { nbt.get_string("id") == Some("minecraft:skull") && nbt.get_int("x") == Some(-4888) diff --git a/crates/pumpkin-world/src/generation/proto_chunk.rs b/crates/pumpkin-world/src/generation/proto_chunk.rs index 4876cb21a..ac11b722b 100644 --- a/crates/pumpkin-world/src/generation/proto_chunk.rs +++ b/crates/pumpkin-world/src/generation/proto_chunk.rs @@ -180,6 +180,11 @@ impl TerrainCache { } impl ProtoChunk { + #[cfg(test)] + pub(crate) fn has_structure(&self, key: StructureKeys) -> bool { + self.structure_starts.contains_key(&key) + } + #[must_use] pub fn new(x: i32, z: i32, generator: &super::generator::WorldGenerator) -> Self { let dimension = generator.dimension(); @@ -348,7 +353,21 @@ impl ProtoChunk { } } - proto_chunk.stage = StagedChunkEnum::from(chunk_data.status); + let saved_stage = StagedChunkEnum::from(chunk_data.status); + proto_chunk.stage = saved_stage; + if let super::generator::WorldGenerator::Noise(generator) = generator + && (StagedChunkEnum::StructureStart..StagedChunkEnum::Features).contains(&saved_stage) + { + // Structure starts and references are currently transient proto-chunk data. + // Rebuild them when resuming a partially generated chunk so structures that + // cross chunk boundaries are not truncated at the unload boundary. + proto_chunk.stage = StagedChunkEnum::Biomes; + proto_chunk.set_structure_starts(generator); + if saved_stage >= StagedChunkEnum::StructureReferences { + proto_chunk.set_structure_references(generator); + } + proto_chunk.stage = saved_stage; + } proto_chunk } @@ -1300,7 +1319,7 @@ impl ProtoChunk { global_cache, settings.sea_level, entry, - random_config, + generator, &mut height_sampler, ); } @@ -1332,7 +1351,7 @@ impl ProtoChunk { global_cache, settings.sea_level, selected_entry, - random_config, + generator, &mut height_sampler, ) { break; @@ -1350,9 +1369,28 @@ impl ProtoChunk { global_cache: &GlobalStructureCache, sea_level: i32, entry: &WeightedEntry, - random_config: &GlobalRandomConfig, + generator: &super::generator::VanillaGenerator, height_sampler: &mut dyn crate::generation::structure::structures::HeightSampler, ) -> bool { + if entry.structure == StructureKeys::Monument { + let config = MultiNoiseSamplerBuilderOptions::new(0, 0, 0); + let mut sampler = + MultiNoiseSampler::generate(&generator.base_router.multi_noise, &config); + let center_x = chunk_pos::get_center_x(self.x); + let center_z = chunk_pos::get_center_z(self.z); + let start_y = height_sampler.estimate_ocean_floor_height(center_x, center_z); + if !crate::generation::structure::structures::ocean_monument::has_valid_biomes( + &MultiNoiseBiomeSupplier::OVERWORLD, + &mut sampler, + self.x, + self.z, + sea_level, + start_y, + ) { + return false; + } + } + let chunk_x = self.x; let chunk_z = self.z; let position = @@ -1361,7 +1399,7 @@ impl ProtoChunk { try_generate_structure( &entry.structure, structure, - random_config.seed as i64, + generator.random_config.seed as i64, self, sea_level, Some(height_sampler), diff --git a/crates/pumpkin-world/src/generation/proto_chunk_test.rs b/crates/pumpkin-world/src/generation/proto_chunk_test.rs index bd00e751c..0ba3d6553 100644 --- a/crates/pumpkin-world/src/generation/proto_chunk_test.rs +++ b/crates/pumpkin-world/src/generation/proto_chunk_test.rs @@ -6,6 +6,36 @@ mod test { use pumpkin_data::dimension::Dimension; use pumpkin_util::world_seed::Seed; + #[test] + fn structure_references_are_rebuilt_when_resuming_generation() { + use crate::chunk_system::chunk_state::Chunk; + use pumpkin_config::lighting::LightingEngineConfig; + use pumpkin_data::structures::StructureKeys; + + let seed = Seed(1_782_124_772_053_846_960); + let world_gen = get_world_gen(seed, Dimension::OVERWORLD, false, Vec::new(), String::new()); + let WorldGenerator::Noise(generator) = &*world_gen else { + unreachable!() + }; + + // This chunk contains the far edge of a monument starting at (-553, 173). + let mut proto = ProtoChunk::new(-553, 174, &world_gen); + proto.step_to_biomes(generator); + proto.set_structure_starts(generator); + proto.set_structure_references(generator); + assert!(proto.has_structure(StructureKeys::Monument)); + + let mut staged = Chunk::Proto(Box::new(proto)); + staged.upgrade_to_level_chunk(&Dimension::OVERWORLD, &LightingEngineConfig::Default); + let Chunk::Level(chunk_data) = staged else { + unreachable!() + }; + + let resumed = ProtoChunk::from_chunk_data(&chunk_data, &world_gen); + assert_eq!(resumed.stage, StagedChunkEnum::StructureReferences); + assert!(resumed.has_structure(StructureKeys::Monument)); + } + // Regression test for transposed heightmaps during Noise-stage chunk resume. // Flat terrain cannot expose this bug, so use a sloped chunk. #[test] diff --git a/crates/pumpkin-world/src/generation/structure/height_sampler.rs b/crates/pumpkin-world/src/generation/structure/height_sampler.rs index 1938cd48f..6a55e2c25 100644 --- a/crates/pumpkin-world/src/generation/structure/height_sampler.rs +++ b/crates/pumpkin-world/src/generation/structure/height_sampler.rs @@ -1,4 +1,4 @@ -use pumpkin_data::Block; +use pumpkin_data::{Block, BlockId, block_properties::blocks_movement}; use pumpkin_util::math::floor_div; use rustc_hash::FxHashMap; @@ -21,6 +21,7 @@ pub struct NoiseHeightSampler<'a> { generator: &'a VanillaGenerator, preliminary: SurfaceHeightEstimateSampler<'a>, heights: FxHashMap<(i32, i32), i32>, + ocean_floor_heights: FxHashMap<(i32, i32), i32>, } impl<'a> NoiseHeightSampler<'a> { @@ -42,10 +43,11 @@ impl<'a> NoiseHeightSampler<'a> { generator, preliminary, heights: FxHashMap::default(), + ocean_floor_heights: FxHashMap::default(), } } - fn sample_column(&mut self, x: i32, z: i32) -> i32 { + fn sample_column(&mut self, x: i32, z: i32, ocean_floor: bool) -> i32 { let settings = self.generator.settings; let shape = &settings.shape; let horizontal = i32::from(shape.horizontal_cell_block_count()); @@ -100,7 +102,11 @@ impl<'a> NoiseHeightSampler<'a> { &mut self.preliminary, ) .unwrap_or(self.generator.default_block); - if !state.is_air() { + if if ocean_floor { + blocks_movement(state, BlockId::from_state_id(state.id)) + } else { + !state.is_air() + } { return y + 1; } } @@ -116,8 +122,20 @@ impl HeightSampler for NoiseHeightSampler<'_> { if let Some(height) = self.heights.get(&key) { return *height; } - let height = self.sample_column(block_x, block_z); + let height = self.sample_column(block_x, block_z, false); self.heights.insert(key, height); height } + + fn estimate_ocean_floor_height(&mut self, block_x: i32, block_z: i32) -> i32 { + let key = (block_x, block_z); + if let Some(height) = self.ocean_floor_heights.get(&key) { + return *height; + } + // Vanilla's structure helper asks for the highest occupied block, while + // heightmaps store the first free block above it. + let height = self.sample_column(block_x, block_z, true) - 1; + self.ocean_floor_heights.insert(key, height); + height + } } diff --git a/crates/pumpkin-world/src/generation/structure/mod.rs b/crates/pumpkin-world/src/generation/structure/mod.rs index 2af04468c..17b59407b 100644 --- a/crates/pumpkin-world/src/generation/structure/mod.rs +++ b/crates/pumpkin-world/src/generation/structure/mod.rs @@ -11,14 +11,23 @@ use crate::{ noise::router::multi_noise_sampler::MultiNoiseSampler, structure::structures::{ StructureGenerator, StructureGeneratorContext, StructurePosition, - buried_treasure::BuriedTreasureGenerator, create_chunk_random, - desert_pyramid::DesertPyramidGenerator, end_city::EndCityGenerator, - igloo::IglooGenerator, jigsaw::JigsawGenerator, jungle_temple::JungleTempleGenerator, - mansion::MansionGenerator, mineshaft::MineshaftGenerator, - nether_fortress::NetherFortressGenerator, nether_fossil::NetherFossilGenerator, - ocean_monument::OceanMonumentGenerator, ocean_ruin::OceanRuinGenerator, - ruined_portal::RuinedPortalGenerator, shipwreck::ShipwreckGenerator, - stronghold::StrongholdGenerator, swamp_hut::SwampHutGenerator, + buried_treasure::BuriedTreasureGenerator, + create_chunk_random, + desert_pyramid::DesertPyramidGenerator, + end_city::EndCityGenerator, + igloo::IglooGenerator, + jigsaw::JigsawGenerator, + jungle_temple::JungleTempleGenerator, + mansion::MansionGenerator, + mineshaft::MineshaftGenerator, + nether_fortress::NetherFortressGenerator, + nether_fossil::NetherFossilGenerator, + ocean_monument::{OceanMonumentGenerator, has_valid_biomes}, + ocean_ruin::OceanRuinGenerator, + ruined_portal::RuinedPortalGenerator, + shipwreck::ShipwreckGenerator, + stronghold::StrongholdGenerator, + swamp_hut::SwampHutGenerator, }, }, }; @@ -180,10 +189,31 @@ pub fn try_generate_structure( pub fn lazily_generate_structure( key: &StructureKeys, structure: &Structure, - context: StructureGeneratorContext, // Replaces 5 separate arguments! + mut context: StructureGeneratorContext, // Replaces 5 separate arguments! biome_supplier: &dyn BiomeSupplier, multi_noise_sampler: &mut MultiNoiseSampler, ) -> Option { + if *key == StructureKeys::Monument { + let center_x = crate::generation::positions::chunk_pos::get_center_x(context.chunk_x); + let center_z = crate::generation::positions::chunk_pos::get_center_z(context.chunk_z); + let start_y = context + .height_sampler + .as_deref_mut() + .map_or(context.sea_level, |sampler| { + sampler.estimate_ocean_floor_height(center_x, center_z) + }); + if !has_valid_biomes( + biome_supplier, + multi_noise_sampler, + context.chunk_x, + context.chunk_z, + context.sea_level, + start_y, + ) { + return None; + } + } + let structure_pos = generate_structure_position(key, structure, context); if let Some(pos) = structure_pos { diff --git a/crates/pumpkin-world/src/generation/structure/structures/mod.rs b/crates/pumpkin-world/src/generation/structure/structures/mod.rs index 0661b5305..a2660e89b 100644 --- a/crates/pumpkin-world/src/generation/structure/structures/mod.rs +++ b/crates/pumpkin-world/src/generation/structure/structures/mod.rs @@ -149,7 +149,7 @@ impl StructurePiece { } } - const fn offset_pos(&self, x: i32, y: i32, z: i32) -> Vector3 { + pub(crate) const fn offset_pos(&self, x: i32, y: i32, z: i32) -> Vector3 { Vector3::new( self.apply_x_transform(x, z), self.apply_y_transform(y), @@ -672,6 +672,10 @@ pub trait StructureGenerator { pub trait HeightSampler { fn estimate_height(&mut self, block_x: i32, block_z: i32) -> i32; + + fn estimate_ocean_floor_height(&mut self, block_x: i32, block_z: i32) -> i32 { + self.estimate_height(block_x, block_z) + } } impl HeightSampler diff --git a/crates/pumpkin-world/src/generation/structure/structures/ocean_monument.rs b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument.rs deleted file mode 100644 index 1e1012ffb..000000000 --- a/crates/pumpkin-world/src/generation/structure/structures/ocean_monument.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::sync::Arc; - -use pumpkin_data::Block; -use pumpkin_util::{ - math::{block_box::BlockBox, position::BlockPos}, - random::RandomGenerator, -}; - -use crate::{ - ProtoChunk, - generation::{ - positions::chunk_pos::{get_center_x, get_center_z}, - structure::{ - piece::StructurePieceType, - structures::{ - StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase, - StructurePiecesCollector, StructurePosition, WorldPortalExt, - }, - }, - }, -}; - -pub struct OceanMonumentGenerator; - -impl StructureGenerator for OceanMonumentGenerator { - fn get_structure_position( - &self, - context: StructureGeneratorContext<'_>, - ) -> Option { - let chunk_center_x = get_center_x(context.chunk_x); - let chunk_center_z = get_center_z(context.chunk_z); - - let bounding_box = BlockBox::new( - chunk_center_x - 29, - context.min_y, - chunk_center_z - 29, - chunk_center_x + 29, - 256, - chunk_center_z + 29, - ); - - let mut collector = StructurePiecesCollector::default(); - collector.add_piece(Box::new(OceanMonumentPiece { - piece: StructurePiece::new(StructurePieceType::OceanMonumentBase, bounding_box, 0), - })); - - Some(StructurePosition { - start_pos: BlockPos::new(chunk_center_x, 64, chunk_center_z), - collector: Arc::new(collector.into()), - }) - } -} - -pub struct OceanMonumentPiece { - piece: StructurePiece, -} - -impl StructurePieceBase for OceanMonumentPiece { - fn as_any(&self) -> &dyn std::any::Any { - self - } - fn get_structure_piece(&self) -> &StructurePiece { - &self.piece - } - fn get_structure_piece_mut(&mut self) -> &mut StructurePiece { - &mut self.piece - } - fn place( - &mut self, - chunk: &mut ProtoChunk, - _block_registry: &dyn WorldPortalExt, - _random: &mut RandomGenerator, - _seed: i64, - chunk_box: &BlockBox, - ) { - let origin = self.piece.bounding_box.min; - let sample_y = chunk.get_top_y(&pumpkin_util::HeightMap::OceanFloorWg, origin.x, origin.z); - let start_y = if sample_y <= 0 { 40 } else { sample_y - 4 }; - - // Draw a giant 58x58 pyramid/stepped prismarine temple! - for dy in 0..15 { - let half_sz = 29 - dy; - if half_sz < 0 { - break; - } - let min_x = origin.x - half_sz; - let max_x = origin.x + half_sz; - let min_z = origin.z - half_sz; - let max_z = origin.z + half_sz; - let y = start_y + dy; - - for x in min_x..=max_x { - for z in min_z..=max_z { - if chunk_box.contains(x, y, z) { - let is_border = x == min_x || x == max_x || z == min_z || z == max_z; - let block = if is_border { - Block::DARK_PRISMARINE - } else if dy % 3 == 0 { - Block::PRISMARINE_BRICKS - } else { - Block::PRISMARINE - }; - chunk.set_block_state(x, y, z, block.default_state); - } - } - } - } - } -} diff --git a/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/building.rs b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/building.rs new file mode 100644 index 000000000..c714673b0 --- /dev/null +++ b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/building.rs @@ -0,0 +1,586 @@ +use pumpkin_util::{math::block_box::BlockBox, random::RandomGenerator}; + +use crate::ProtoChunk; + +use super::{BASE_BLACK, BASE_GRAY, BASE_LIGHT, LAMP, MonumentBuilding}; + +impl MonumentBuilding { + pub(super) fn place_building( + &self, + chunk: &mut ProtoChunk, + random: &mut RandomGenerator, + chunk_box: &BlockBox, + ) { + let sea_level = self.sea_level; + let water_height = sea_level.max(64) - self.base.piece.bounding_box.min.y; + self.base + .water(chunk, chunk_box, sea_level, 0, 0, 0, 58, water_height, 58); + self.generate_wing(chunk, chunk_box, false, 0); + self.generate_wing(chunk, chunk_box, true, 33); + self.generate_entrance_arches(chunk, chunk_box); + self.generate_entrance_wall(chunk, chunk_box); + self.generate_roof(chunk, chunk_box); + self.generate_lower_wall(chunk, chunk_box); + self.generate_middle_wall(chunk, chunk_box); + self.generate_upper_wall(chunk, chunk_box); + + for pillar_x in 0..7 { + let mut pillar_z = 0; + while pillar_z < 7 { + if pillar_z == 0 && pillar_x == 3 { + pillar_z = 6; + } + let x = pillar_x * 9; + let z = pillar_z * 9; + for dx in 0..4 { + for dz in 0..4 { + self.base + .block(chunk, chunk_box, x + dx, 0, z + dz, BASE_LIGHT); + self.base.piece.fill_downwards( + chunk, + BASE_LIGHT, + x + dx, + -1, + z + dz, + chunk_box, + ); + } + } + pillar_z += if pillar_x == 0 || pillar_x == 6 { 1 } else { 6 }; + } + } + + for offset in 0..5 { + self.base.water( + chunk, + chunk_box, + sea_level, + -1 - offset, + offset * 2, + -1 - offset, + -1 - offset, + 23, + 58 + offset, + ); + self.base.water( + chunk, + chunk_box, + sea_level, + 58 + offset, + offset * 2, + -1 - offset, + 58 + offset, + 23, + 58 + offset, + ); + self.base.water( + chunk, + chunk_box, + sea_level, + -offset, + offset * 2, + -1 - offset, + 57 + offset, + 23, + -1 - offset, + ); + self.base.water( + chunk, + chunk_box, + sea_level, + -offset, + offset * 2, + 58 + offset, + 57 + offset, + 23, + 58 + offset, + ); + } + + for child in &self.children { + if child.piece.piece.bounding_box.intersects(chunk_box) { + child.place(chunk, random, chunk_box, sea_level, &self.graph); + } + } + } + + fn generate_wing(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox, flipped: bool, x: i32) { + let p = &self.base; + if !p.chunk_intersects(chunk_box, x, 0, x + 23, 20) { + return; + } + p.fill(chunk, chunk_box, x, 0, 0, x + 24, 0, 20, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, x, 1, 0, x + 24, 10, 20); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + x + i, + i + 1, + i, + x + i, + i + 1, + 20, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + x + i + 7, + i + 5, + i + 7, + x + i + 7, + i + 5, + 20, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + x + 17 - i, + i + 5, + i + 7, + x + 17 - i, + i + 5, + 20, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + x + 24 - i, + i + 1, + i, + x + 24 - i, + i + 1, + 20, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + x + i + 1, + i + 1, + i, + x + 23 - i, + i + 1, + i, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + x + i + 8, + i + 5, + i + 7, + x + 16 - i, + i + 5, + i + 7, + BASE_LIGHT, + ); + } + p.fill(chunk, chunk_box, x + 4, 4, 4, x + 6, 4, 20, BASE_GRAY); + p.fill(chunk, chunk_box, x + 7, 4, 4, x + 17, 4, 6, BASE_GRAY); + p.fill(chunk, chunk_box, x + 18, 4, 4, x + 20, 4, 20, BASE_GRAY); + p.fill(chunk, chunk_box, x + 11, 8, 11, x + 13, 8, 20, BASE_GRAY); + for z in [12, 15, 18] { + p.block(chunk, chunk_box, x + 12, 9, z, BASE_LIGHT); + } + let left = x + if flipped { 19 } else { 5 }; + let right = x + if flipped { 5 } else { 19 }; + for z in (5..=20).rev().step_by(3) { + p.block(chunk, chunk_box, left, 5, z, BASE_LIGHT); + } + for z in (7..=19).rev().step_by(3) { + p.block(chunk, chunk_box, right, 5, z, BASE_LIGHT); + } + for i in 0..4 { + let pos = if flipped { + x + 24 - (17 - i * 3) + } else { + x + 17 - i * 3 + }; + p.block(chunk, chunk_box, pos, 5, 5, BASE_LIGHT); + } + p.block(chunk, chunk_box, right, 5, 5, BASE_LIGHT); + p.fill(chunk, chunk_box, x + 11, 1, 12, x + 13, 7, 12, BASE_GRAY); + p.fill(chunk, chunk_box, x + 12, 1, 11, x + 12, 7, 13, BASE_GRAY); + } + + fn generate_entrance_arches(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.base; + if !p.chunk_intersects(chunk_box, 22, 5, 35, 17) { + return; + } + p.water(chunk, chunk_box, self.sea_level, 25, 0, 0, 32, 8, 20); + for i in 0..4 { + let z = 5 + i * 4; + p.fill(chunk, chunk_box, 24, 2, z, 24, 4, z, BASE_LIGHT); + p.fill(chunk, chunk_box, 22, 4, z, 23, 4, z, BASE_LIGHT); + p.block(chunk, chunk_box, 25, 5, z, BASE_LIGHT); + p.block(chunk, chunk_box, 26, 6, z, BASE_LIGHT); + p.block(chunk, chunk_box, 26, 5, z, LAMP); + p.fill(chunk, chunk_box, 33, 2, z, 33, 4, z, BASE_LIGHT); + p.fill(chunk, chunk_box, 34, 4, z, 35, 4, z, BASE_LIGHT); + p.block(chunk, chunk_box, 32, 5, z, BASE_LIGHT); + p.block(chunk, chunk_box, 31, 6, z, BASE_LIGHT); + p.block(chunk, chunk_box, 31, 5, z, LAMP); + p.fill(chunk, chunk_box, 27, 6, z, 30, 6, z, BASE_GRAY); + } + } + + fn generate_entrance_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.base; + if !p.chunk_intersects(chunk_box, 15, 20, 42, 21) { + return; + } + p.fill(chunk, chunk_box, 15, 0, 21, 42, 0, 21, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 26, 1, 21, 31, 3, 21); + for &(x0, y, x1) in &[ + (21, 12, 36), + (17, 11, 40), + (16, 10, 41), + (15, 9, 42), + (15, 8, 42), + (15, 7, 42), + (16, 6, 41), + (17, 5, 40), + (21, 4, 36), + (22, 3, 26), + (31, 3, 35), + (23, 2, 25), + (32, 2, 34), + ] { + p.fill(chunk, chunk_box, x0, y, 21, x1, y, 21, BASE_GRAY); + } + p.fill(chunk, chunk_box, 28, 4, 20, 29, 4, 21, BASE_LIGHT); + for &(x, y) in &[(27, 3), (30, 3), (26, 2), (31, 2), (25, 1), (32, 1)] { + p.block(chunk, chunk_box, x, y, 21, BASE_LIGHT); + } + for i in 0..7 { + p.block(chunk, chunk_box, 28 - i, 6 + i, 21, BASE_BLACK); + p.block(chunk, chunk_box, 29 + i, 6 + i, 21, BASE_BLACK); + } + for i in 0..4 { + p.block(chunk, chunk_box, 28 - i, 9 + i, 21, BASE_BLACK); + p.block(chunk, chunk_box, 29 + i, 9 + i, 21, BASE_BLACK); + } + p.block(chunk, chunk_box, 28, 12, 21, BASE_BLACK); + p.block(chunk, chunk_box, 29, 12, 21, BASE_BLACK); + for i in 0..3 { + for y in 8..=9 { + p.block(chunk, chunk_box, 22 - i * 2, y, 21, BASE_BLACK); + p.block(chunk, chunk_box, 35 + i * 2, y, 21, BASE_BLACK); + } + } + for &(x0, y0, x1, y1) in &[ + (15, 13, 42, 15), + (15, 1, 15, 6), + (16, 1, 16, 5), + (17, 1, 20, 4), + (21, 1, 21, 3), + (22, 1, 22, 2), + (23, 1, 24, 1), + (42, 1, 42, 6), + (41, 1, 41, 5), + (37, 1, 40, 4), + (36, 1, 36, 3), + (33, 1, 34, 1), + (35, 1, 35, 2), + ] { + p.water(chunk, chunk_box, self.sea_level, x0, y0, 21, x1, y1, 21); + } + } + + fn generate_roof(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.base; + if !p.chunk_intersects(chunk_box, 21, 21, 36, 36) { + return; + } + p.fill(chunk, chunk_box, 21, 0, 22, 36, 0, 36, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 21, 1, 22, 36, 23, 36); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + 21 + i, + 13 + i, + 21 + i, + 36 - i, + 13 + i, + 21 + i, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 21 + i, + 13 + i, + 36 - i, + 36 - i, + 13 + i, + 36 - i, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 21 + i, + 13 + i, + 22 + i, + 21 + i, + 13 + i, + 35 - i, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 36 - i, + 13 + i, + 22 + i, + 36 - i, + 13 + i, + 35 - i, + BASE_LIGHT, + ); + } + p.fill(chunk, chunk_box, 25, 16, 25, 32, 16, 32, BASE_GRAY); + for &(x, z) in &[(25, 25), (32, 25), (25, 32), (32, 32)] { + p.fill(chunk, chunk_box, x, 17, z, x, 19, z, BASE_LIGHT); + } + for &(x, y, z, block) in &[ + (26, 20, 26, BASE_LIGHT), + (27, 21, 27, BASE_LIGHT), + (27, 20, 27, LAMP), + (26, 20, 31, BASE_LIGHT), + (27, 21, 30, BASE_LIGHT), + (27, 20, 30, LAMP), + (31, 20, 31, BASE_LIGHT), + (30, 21, 30, BASE_LIGHT), + (30, 20, 30, LAMP), + (31, 20, 26, BASE_LIGHT), + (30, 21, 27, BASE_LIGHT), + (30, 20, 27, LAMP), + ] { + p.block(chunk, chunk_box, x, y, z, block); + } + p.fill(chunk, chunk_box, 28, 21, 27, 29, 21, 27, BASE_GRAY); + p.fill(chunk, chunk_box, 27, 21, 28, 27, 21, 29, BASE_GRAY); + p.fill(chunk, chunk_box, 28, 21, 30, 29, 21, 30, BASE_GRAY); + p.fill(chunk, chunk_box, 30, 21, 28, 30, 21, 29, BASE_GRAY); + } + + fn generate_lower_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.base; + if p.chunk_intersects(chunk_box, 0, 21, 6, 58) { + p.fill(chunk, chunk_box, 0, 0, 21, 6, 0, 57, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 0, 1, 21, 6, 7, 57); + p.fill(chunk, chunk_box, 4, 4, 21, 6, 4, 53, BASE_GRAY); + for i in 0..4 { + p.fill(chunk, chunk_box, i, i + 1, 21, i, i + 1, 57 - i, BASE_LIGHT); + } + for z in (23..53).step_by(3) { + p.block(chunk, chunk_box, 5, 5, z, BASE_LIGHT); + } + p.block(chunk, chunk_box, 5, 5, 52, BASE_LIGHT); + p.fill(chunk, chunk_box, 4, 1, 52, 6, 3, 52, BASE_GRAY); + p.fill(chunk, chunk_box, 5, 1, 51, 5, 3, 53, BASE_GRAY); + } + if p.chunk_intersects(chunk_box, 51, 21, 58, 58) { + p.fill(chunk, chunk_box, 51, 0, 21, 57, 0, 57, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 51, 1, 21, 57, 7, 57); + p.fill(chunk, chunk_box, 51, 4, 21, 53, 4, 53, BASE_GRAY); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + 57 - i, + i + 1, + 21, + 57 - i, + i + 1, + 57 - i, + BASE_LIGHT, + ); + } + for z in (23..53).step_by(3) { + p.block(chunk, chunk_box, 52, 5, z, BASE_LIGHT); + } + p.block(chunk, chunk_box, 52, 5, 52, BASE_LIGHT); + p.fill(chunk, chunk_box, 51, 1, 52, 53, 3, 52, BASE_GRAY); + p.fill(chunk, chunk_box, 52, 1, 51, 52, 3, 53, BASE_GRAY); + } + if p.chunk_intersects(chunk_box, 0, 51, 57, 57) { + p.fill(chunk, chunk_box, 7, 0, 51, 50, 0, 57, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 7, 1, 51, 50, 10, 57); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + i + 1, + i + 1, + 57 - i, + 56 - i, + i + 1, + 57 - i, + BASE_LIGHT, + ); + } + } + } + + fn generate_middle_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.base; + if p.chunk_intersects(chunk_box, 7, 21, 13, 50) { + p.fill(chunk, chunk_box, 7, 0, 21, 13, 0, 50, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 7, 1, 21, 13, 10, 50); + p.fill(chunk, chunk_box, 11, 8, 21, 13, 8, 53, BASE_GRAY); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + i + 7, + i + 5, + 21, + i + 7, + i + 5, + 54, + BASE_LIGHT, + ); + } + for z in (21..=45).step_by(3) { + p.block(chunk, chunk_box, 12, 9, z, BASE_LIGHT); + } + } + if p.chunk_intersects(chunk_box, 44, 21, 50, 54) { + p.fill(chunk, chunk_box, 44, 0, 21, 50, 0, 50, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 44, 1, 21, 50, 10, 50); + p.fill(chunk, chunk_box, 44, 8, 21, 46, 8, 53, BASE_GRAY); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + 50 - i, + i + 5, + 21, + 50 - i, + i + 5, + 54, + BASE_LIGHT, + ); + } + for z in (21..=45).step_by(3) { + p.block(chunk, chunk_box, 45, 9, z, BASE_LIGHT); + } + } + if p.chunk_intersects(chunk_box, 8, 44, 49, 54) { + p.fill(chunk, chunk_box, 14, 0, 44, 43, 0, 50, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 14, 1, 44, 43, 10, 50); + for x in (12..=45).step_by(3) { + p.block(chunk, chunk_box, x, 9, 45, BASE_LIGHT); + p.block(chunk, chunk_box, x, 9, 52, BASE_LIGHT); + if [12, 18, 24, 33, 39, 45].contains(&x) { + for &(y, z) in &[ + (9, 47), + (9, 50), + (10, 45), + (10, 46), + (10, 51), + (10, 52), + (11, 47), + (11, 50), + (12, 48), + (12, 49), + ] { + p.block(chunk, chunk_box, x, y, z, BASE_LIGHT); + } + } + } + for i in 0..3 { + p.fill( + chunk, + chunk_box, + 8 + i, + 5 + i, + 54, + 49 - i, + 5 + i, + 54, + BASE_GRAY, + ); + } + p.fill(chunk, chunk_box, 11, 8, 54, 46, 8, 54, BASE_LIGHT); + p.fill(chunk, chunk_box, 14, 8, 44, 43, 8, 53, BASE_GRAY); + } + } + + fn generate_upper_wall(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.base; + if p.chunk_intersects(chunk_box, 14, 21, 20, 43) { + p.fill(chunk, chunk_box, 14, 0, 21, 20, 0, 43, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 14, 1, 22, 20, 14, 43); + p.fill(chunk, chunk_box, 18, 12, 22, 20, 12, 39, BASE_GRAY); + p.fill(chunk, chunk_box, 18, 12, 21, 20, 12, 21, BASE_LIGHT); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + i + 14, + i + 9, + 21, + i + 14, + i + 9, + 43 - i, + BASE_LIGHT, + ); + } + for z in (23..=39).step_by(3) { + p.block(chunk, chunk_box, 19, 13, z, BASE_LIGHT); + } + } + if p.chunk_intersects(chunk_box, 37, 21, 43, 43) { + p.fill(chunk, chunk_box, 37, 0, 21, 43, 0, 43, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 37, 1, 22, 43, 14, 43); + p.fill(chunk, chunk_box, 37, 12, 22, 39, 12, 39, BASE_GRAY); + p.fill(chunk, chunk_box, 37, 12, 21, 39, 12, 21, BASE_LIGHT); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + 43 - i, + i + 9, + 21, + 43 - i, + i + 9, + 43 - i, + BASE_LIGHT, + ); + } + for z in (23..=39).step_by(3) { + p.block(chunk, chunk_box, 38, 13, z, BASE_LIGHT); + } + } + if p.chunk_intersects(chunk_box, 15, 37, 42, 43) { + p.fill(chunk, chunk_box, 21, 0, 37, 36, 0, 43, BASE_GRAY); + p.water(chunk, chunk_box, self.sea_level, 21, 1, 37, 36, 14, 43); + p.fill(chunk, chunk_box, 21, 12, 37, 36, 12, 39, BASE_GRAY); + for i in 0..4 { + p.fill( + chunk, + chunk_box, + 15 + i, + i + 9, + 43 - i, + 42 - i, + i + 9, + 43 - i, + BASE_LIGHT, + ); + } + for x in (21..=36).step_by(3) { + p.block(chunk, chunk_box, x, 13, 38, BASE_LIGHT); + } + } + } +} diff --git a/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/graph.rs b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/graph.rs new file mode 100644 index 000000000..0eb1abfad --- /dev/null +++ b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/graph.rs @@ -0,0 +1,276 @@ +use pumpkin_util::{ + BlockDirection, + random::{RandomGenerator, RandomImpl}, +}; + +const GRID_SIZE: usize = 75; +const SOURCE_INDEX: i32 = room_index(2, 0, 0); +const TOP_CONNECT_INDEX: i32 = room_index(2, 2, 0); +const LEFT_CONNECT_INDEX: i32 = room_index(0, 1, 0); +const RIGHT_CONNECT_INDEX: i32 = room_index(4, 1, 0); + +#[derive(Clone)] +pub(super) struct RoomDefinition { + pub index: i32, + pub connections: [Option; 6], + pub openings: [bool; 6], + pub claimed: bool, + source: bool, +} + +impl RoomDefinition { + const fn new(index: i32) -> Self { + Self { + index, + connections: [None; 6], + openings: [false; 6], + claimed: false, + source: false, + } + } + + pub const fn is_special(&self) -> bool { + self.index >= 75 + } + + pub fn count_openings(&self) -> usize { + self.openings.iter().filter(|opening| **opening).count() + } +} + +pub(super) struct RoomGraph { + pub rooms: Vec, + pub source: usize, + pub core: usize, +} + +impl RoomGraph { + #[expect(clippy::too_many_lines)] + pub fn generate(random: &mut RandomGenerator) -> (Self, Vec) { + let mut graph = Self { + rooms: Vec::with_capacity(49), + source: 0, + core: 0, + }; + let mut grid = [None; GRID_SIZE]; + for y in 0..=1 { + for z in 0..4 { + for x in 0..5 { + let index = room_index(x, y, z); + grid[index as usize] = Some(graph.push(index)); + } + } + } + for z in 0..2 { + for x in 1..4 { + let index = room_index(x, 2, z); + grid[index as usize] = Some(graph.push(index)); + } + } + graph.source = grid[SOURCE_INDEX as usize].unwrap(); + + for x in 0..5 { + for z in 0..5 { + for y in 0..3 { + let index = room_index(x, y, z); + let Some(room) = grid[index as usize] else { + continue; + }; + for direction in DIRECTIONS { + let step = direction.to_vector(); + let nx = x + step.x; + let ny = y + step.y; + let nz = z + step.z; + if !(0..5).contains(&nx) || !(0..3).contains(&ny) || !(0..5).contains(&nz) { + continue; + } + let Some(neighbor) = grid[room_index(nx, ny, nz) as usize] else { + continue; + }; + let connection_direction = if nz == z { + direction + } else { + direction.opposite() + }; + graph.connect(room, connection_direction, neighbor); + } + } + } + } + + let roof = graph.push(1003); + let left_wing = graph.push(1001); + let right_wing = graph.push(1002); + graph.connect( + grid[TOP_CONNECT_INDEX as usize].unwrap(), + BlockDirection::Up, + roof, + ); + graph.connect( + grid[LEFT_CONNECT_INDEX as usize].unwrap(), + BlockDirection::South, + left_wing, + ); + graph.connect( + grid[RIGHT_CONNECT_INDEX as usize].unwrap(), + BlockDirection::South, + right_wing, + ); + graph.rooms[roof].claimed = true; + graph.rooms[left_wing].claimed = true; + graph.rooms[right_wing].claimed = true; + graph.rooms[graph.source].source = true; + + graph.core = grid[room_index(random.next_bounded_i32(4), 0, 2) as usize].unwrap(); + graph.claim_core(); + for room in &mut graph.rooms { + // Vanilla updates every grid room and the roof room here. The two wing + // sentinels deliberately keep their side closed so that edge pruning + // cannot disconnect their grid attachment. + if !room.is_special() || room.index == 1003 { + for direction in DIRECTIONS { + room.openings[direction as usize] = + room.connections[direction as usize].is_some(); + } + } + } + + let mut order = grid.into_iter().flatten().collect::>(); + shuffle(&mut order, random); + for &room in &order { + let mut closed = 0; + for _ in 0..5 { + if closed == 2 { + break; + } + let direction = random.next_bounded_i32(6) as usize; + if !graph.rooms[room].openings[direction] { + continue; + } + let neighbor = graph.rooms[room].connections[direction].unwrap(); + let opposite = opposite_index(direction); + graph.rooms[room].openings[direction] = false; + graph.rooms[neighbor].openings[opposite] = false; + if graph.find_source(room) && graph.find_source(neighbor) { + closed += 1; + } else { + graph.rooms[room].openings[direction] = true; + graph.rooms[neighbor].openings[opposite] = true; + } + } + } + order.extend([roof, left_wing, right_wing]); + (graph, order) + } + + fn push(&mut self, index: i32) -> usize { + let room = self.rooms.len(); + self.rooms.push(RoomDefinition::new(index)); + room + } + + fn connect(&mut self, room: usize, direction: BlockDirection, neighbor: usize) { + self.rooms[room].connections[direction as usize] = Some(neighbor); + self.rooms[neighbor].connections[direction.opposite() as usize] = Some(room); + } + + fn claim_core(&mut self) { + let east = self.connection(self.core, BlockDirection::East); + let north = self.connection(self.core, BlockDirection::North); + let north_east = self.connection(east, BlockDirection::North); + let up = self.connection(self.core, BlockDirection::Up); + let east_up = self.connection(east, BlockDirection::Up); + let north_up = self.connection(north, BlockDirection::Up); + let north_east_up = self.connection(north_east, BlockDirection::Up); + for room in [ + self.core, + east, + north, + north_east, + up, + east_up, + north_up, + north_east_up, + ] { + self.rooms[room].claimed = true; + } + } + + fn find_source(&self, start: usize) -> bool { + let mut visited = vec![false; self.rooms.len()]; + let mut pending = vec![start]; + while let Some(room) = pending.pop() { + if std::mem::replace(&mut visited[room], true) { + continue; + } + if self.rooms[room].source { + return true; + } + for direction in 0..6 { + if self.rooms[room].openings[direction] + && let Some(neighbor) = self.rooms[room].connections[direction] + { + pending.push(neighbor); + } + } + } + false + } + + pub fn connection(&self, room: usize, direction: BlockDirection) -> usize { + self.rooms[room].connections[direction as usize].unwrap() + } +} + +const DIRECTIONS: [BlockDirection; 6] = [ + BlockDirection::Down, + BlockDirection::Up, + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, +]; + +const fn room_index(x: i32, y: i32, z: i32) -> i32 { + y * 25 + z * 5 + x +} + +const fn opposite_index(direction: usize) -> usize { + [1, 0, 3, 2, 5, 4][direction] +} + +fn shuffle(values: &mut [usize], random: &mut RandomGenerator) { + for index in (1..values.len()).rev() { + values.swap(index, random.next_bounded_i32(index as i32 + 1) as usize); + } +} + +#[cfg(test)] +mod tests { + use pumpkin_util::random::{RandomGenerator, legacy_rand::LegacyRand}; + + use super::RoomGraph; + + #[test] + fn generated_graph_keeps_every_room_connected() { + for seed in 0..128 { + let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(seed)); + let (graph, order) = RoomGraph::generate(&mut random); + + assert_eq!(graph.rooms.len(), 49); + assert_eq!(order.len(), 49); + assert_eq!( + graph.rooms.iter().filter(|room| room.is_special()).count(), + 3 + ); + assert!( + graph + .rooms + .iter() + .enumerate() + .filter(|(_, definition)| !definition.is_special()) + .all(|(room, _)| graph.find_source(room)) + ); + } + } +} diff --git a/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/mod.rs b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/mod.rs new file mode 100644 index 000000000..f2b7e6d00 --- /dev/null +++ b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/mod.rs @@ -0,0 +1,652 @@ +mod building; +mod graph; +mod rooms; + +use std::sync::{Arc, Mutex}; + +use pumpkin_data::{Block, BlockState}; +use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use pumpkin_util::{ + BlockDirection, + math::{block_box::BlockBox, position::BlockPos, vector3::Vector3}, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::{ + ProtoChunk, + biome::BiomeSupplier, + generation::{ + biome_coords, + noise::router::multi_noise_sampler::MultiNoiseSampler, + positions::chunk_pos::{get_center_x, get_center_z, start_block_x, start_block_z}, + structure::{ + piece::StructurePieceType, + structures::{ + StructureGenerator, StructureGeneratorContext, StructurePiece, StructurePieceBase, + StructurePiecesCollector, StructurePosition, WorldPortalExt, + }, + }, + }, +}; + +use self::{ + graph::RoomGraph, + rooms::{RoomKind, RoomPiece}, +}; + +const BASE_GRAY: &BlockState = Block::PRISMARINE.default_state; +const BASE_LIGHT: &BlockState = Block::PRISMARINE_BRICKS.default_state; +const BASE_BLACK: &BlockState = Block::DARK_PRISMARINE.default_state; +const LAMP: &BlockState = Block::SEA_LANTERN.default_state; +const WATER: &BlockState = Block::WATER.default_state; + +pub struct OceanMonumentGenerator; + +pub(crate) fn has_valid_biomes( + biome_supplier: &dyn BiomeSupplier, + sampler: &mut MultiNoiseSampler, + chunk_x: i32, + chunk_z: i32, + sea_level: i32, + start_y: i32, +) -> bool { + let start_x = get_center_x(chunk_x); + let start_z = get_center_z(chunk_z); + let start_biomes = pumpkin_data::tag::WorldgenBiome::MINECRAFT_HAS_STRUCTURE_OCEAN_MONUMENT.1; + if !start_biomes.contains( + &(biome_supplier + .biome( + biome_coords::from_block(start_x), + biome_coords::from_block(start_y), + biome_coords::from_block(start_z), + sampler, + ) + .id as u16), + ) { + return false; + } + + let center_x = start_block_x(chunk_x) + 9; + let center_z = start_block_z(chunk_z) + 9; + let min_x = biome_coords::from_block(center_x - 29); + let max_x = biome_coords::from_block(center_x + 29); + let min_y = biome_coords::from_block(sea_level - 29); + let max_y = biome_coords::from_block(sea_level + 29); + let min_z = biome_coords::from_block(center_z - 29); + let max_z = biome_coords::from_block(center_z + 29); + let allowed = pumpkin_data::tag::WorldgenBiome::MINECRAFT_REQUIRED_OCEAN_MONUMENT_SURROUNDING.1; + + (min_x..=max_x).all(|x| { + (min_z..=max_z).all(|z| { + (min_y..=max_y) + .all(|y| allowed.contains(&(biome_supplier.biome(x, y, z, sampler).id as u16))) + }) + }) +} + +impl StructureGenerator for OceanMonumentGenerator { + fn get_structure_position( + &self, + mut context: StructureGeneratorContext<'_>, + ) -> Option { + let center_x = get_center_x(context.chunk_x); + let center_z = get_center_z(context.chunk_z); + let start_y = context + .height_sampler + .as_deref_mut() + .map_or(context.sea_level, |sampler| { + sampler.estimate_ocean_floor_height(center_x, center_z) + }); + let direction = BlockDirection::get_random_horizontal_direction(&mut context.random); + let building = MonumentBuilding::new( + &mut context.random, + start_block_x(context.chunk_x) - 29, + start_block_z(context.chunk_z) - 29, + direction, + context.sea_level, + ); + let mut collector = StructurePiecesCollector::default(); + collector.add_piece(Box::new(building)); + + Some(StructurePosition { + start_pos: BlockPos::new(center_x, start_y, center_z), + collector: Arc::new(Mutex::new(collector)), + }) + } +} + +struct MonumentBuilding { + base: MonumentPiece, + graph: RoomGraph, + children: Vec, + sea_level: i32, +} + +impl MonumentBuilding { + fn new( + random: &mut RandomGenerator, + west: i32, + north: i32, + direction: BlockDirection, + sea_level: i32, + ) -> Self { + let mut piece = StructurePiece::new( + StructurePieceType::OceanMonumentBase, + BlockBox::new(west, 39, north, west + 57, 61, north + 57), + 0, + ); + piece.set_facing(Some(direction)); + let base = MonumentPiece { piece }; + let (mut graph, room_order) = RoomGraph::generate(random); + graph.rooms[graph.source].claimed = true; + + let mut children = vec![ + RoomPiece::for_room( + RoomKind::Entry, + direction, + graph.source, + graph.rooms[graph.source].index, + ), + RoomPiece::for_room( + RoomKind::Core, + direction, + graph.core, + graph.rooms[graph.core].index, + ), + ]; + for room in room_order { + if graph.rooms[room].claimed || graph.rooms[room].is_special() { + continue; + } + children.push(RoomPiece::fit(random, direction, room, &mut graph)); + } + + let room_offset = base.piece.offset_pos(9, 0, 22); + for child in &mut children { + child + .piece + .piece + .bounding_box + .move_pos(room_offset.x, room_offset.y, room_offset.z); + } + + let left_wing = box_from_corners( + base.piece.offset_pos(1, 1, 1), + base.piece.offset_pos(23, 8, 21), + ); + let right_wing = box_from_corners( + base.piece.offset_pos(34, 1, 1), + base.piece.offset_pos(56, 8, 21), + ); + let penthouse = box_from_corners( + base.piece.offset_pos(22, 13, 22), + base.piece.offset_pos(35, 17, 35), + ); + let wing_design = random.next_i32(); + children.push(RoomPiece::special( + RoomKind::Wing(wing_design), + direction, + left_wing, + )); + children.push(RoomPiece::special( + RoomKind::Wing(wing_design.wrapping_add(1)), + direction, + right_wing, + )); + children.push(RoomPiece::special( + RoomKind::Penthouse, + direction, + penthouse, + )); + + Self { + base, + graph, + children, + sea_level, + } + } +} + +impl StructurePieceBase for MonumentBuilding { + fn get_structure_piece(&self) -> &StructurePiece { + &self.base.piece + } + + fn get_structure_piece_mut(&mut self) -> &mut StructurePiece { + &mut self.base.piece + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn place( + &mut self, + chunk: &mut ProtoChunk, + _block_registry: &dyn WorldPortalExt, + random: &mut RandomGenerator, + _seed: i64, + chunk_box: &BlockBox, + ) { + self.place_building(chunk, random, chunk_box); + } +} + +struct MonumentPiece { + piece: StructurePiece, +} + +impl MonumentPiece { + const fn for_room( + kind: RoomKind, + direction: BlockDirection, + room_index: i32, + width: i32, + height: i32, + depth: i32, + ) -> Self { + let room_x = room_index % 5; + let room_z = room_index / 5 % 5; + let room_y = room_index / 25; + let mut bounding_box = match direction { + BlockDirection::North | BlockDirection::South => { + BlockBox::new(0, 0, 0, width * 8 - 1, height * 4 - 1, depth * 8 - 1) + } + _ => BlockBox::new(0, 0, 0, depth * 8 - 1, height * 4 - 1, width * 8 - 1), + }; + match direction { + BlockDirection::North => { + bounding_box.move_pos(room_x * 8, room_y * 4, -(room_z + depth) * 8 + 1); + } + BlockDirection::South => { + bounding_box.move_pos(room_x * 8, room_y * 4, room_z * 8); + } + BlockDirection::West => { + bounding_box.move_pos(-(room_z + depth) * 8 + 1, room_y * 4, room_x * 8); + } + _ => { + bounding_box.move_pos(room_z * 8, room_y * 4, room_x * 8); + } + } + let mut piece = StructurePiece::new(kind.piece_type(), bounding_box, 1); + piece.set_facing(Some(direction)); + Self { piece } + } + + const fn special(kind: RoomKind, direction: BlockDirection, bounding_box: BlockBox) -> Self { + let mut piece = StructurePiece::new(kind.piece_type(), bounding_box, 1); + piece.set_facing(Some(direction)); + Self { piece } + } + + #[expect(clippy::too_many_arguments)] + fn fill( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + x0: i32, + y0: i32, + z0: i32, + x1: i32, + y1: i32, + z1: i32, + block: &BlockState, + ) { + self.piece + .fill(chunk, chunk_box, x0, y0, z0, x1, y1, z1, block); + } + + fn block( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + x: i32, + y: i32, + z: i32, + block: &BlockState, + ) { + self.piece.add_block(chunk, block, x, y, z, chunk_box); + } + + #[expect(clippy::too_many_arguments)] + fn water( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + sea_level: i32, + x0: i32, + y0: i32, + z0: i32, + x1: i32, + y1: i32, + z1: i32, + ) { + for y in y0..=y1 { + for x in x0..=x1 { + for z in z0..=z1 { + let state = self.piece.get_block_at(chunk, x, y, z, chunk_box); + let block_id = state.id.to_block_id(); + if [ + Block::ICE.id, + Block::PACKED_ICE.id, + Block::BLUE_ICE.id, + Block::WATER.id, + ] + .contains(&block_id) + { + continue; + } + let state = if self.piece.offset_pos(x, y, z).y >= sea_level { + Block::AIR.default_state + } else { + WATER + }; + self.block(chunk, chunk_box, x, y, z, state); + } + } + } + } + + fn default_floor( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + x: i32, + z: i32, + down_opening: bool, + ) { + if !down_opening { + self.fill(chunk, chunk_box, x, 0, z, x + 7, 0, z + 7, BASE_GRAY); + return; + } + self.fill(chunk, chunk_box, x, 0, z, x + 2, 0, z + 7, BASE_GRAY); + self.fill(chunk, chunk_box, x + 5, 0, z, x + 7, 0, z + 7, BASE_GRAY); + self.fill(chunk, chunk_box, x + 3, 0, z, x + 4, 0, z + 2, BASE_GRAY); + self.fill( + chunk, + chunk_box, + x + 3, + 0, + z + 5, + x + 4, + 0, + z + 7, + BASE_GRAY, + ); + self.fill( + chunk, + chunk_box, + x + 3, + 0, + z + 2, + x + 4, + 0, + z + 2, + BASE_LIGHT, + ); + self.fill( + chunk, + chunk_box, + x + 3, + 0, + z + 5, + x + 4, + 0, + z + 5, + BASE_LIGHT, + ); + self.fill( + chunk, + chunk_box, + x + 2, + 0, + z + 3, + x + 2, + 0, + z + 4, + BASE_LIGHT, + ); + self.fill( + chunk, + chunk_box, + x + 5, + 0, + z + 3, + x + 5, + 0, + z + 4, + BASE_LIGHT, + ); + } + + #[expect(clippy::too_many_arguments)] + fn fill_on_water( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + x0: i32, + y0: i32, + z0: i32, + x1: i32, + y1: i32, + z1: i32, + block: &BlockState, + ) { + for y in y0..=y1 { + for x in x0..=x1 { + for z in z0..=z1 { + if self + .piece + .get_block_at(chunk, x, y, z, chunk_box) + .id + .to_block_id() + == Block::WATER.id + { + self.block(chunk, chunk_box, x, y, z, block); + } + } + } + } + } + + fn chunk_intersects(&self, chunk_box: &BlockBox, x0: i32, z0: i32, x1: i32, z1: i32) -> bool { + let first = self.piece.offset_pos(x0, 0, z0); + let second = self.piece.offset_pos(x1, 0, z1); + chunk_box.intersects_raw_xz( + first.x.min(second.x), + first.z.min(second.z), + first.x.max(second.x), + first.z.max(second.z), + ) + } + + fn spawn_elder(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox, x: i32, y: i32, z: i32) { + let pos = self.piece.offset_pos(x, y, z); + if !chunk_box.contains_pos(&pos) { + return; + } + chunk.add_structure_entity(elder_guardian_nbt(pos)); + } +} + +fn elder_guardian_nbt(pos: Vector3) -> NbtCompound { + let mut nbt = NbtCompound::new(); + nbt.put_string("id", "minecraft:elder_guardian".to_string()); + nbt.put( + "Pos", + NbtTag::List(vec![ + (f64::from(pos.x) + 0.5).into(), + f64::from(pos.y).into(), + (f64::from(pos.z) + 0.5).into(), + ]), + ); + nbt.put( + "Motion", + NbtTag::List(vec![0.0f64.into(), 0.0f64.into(), 0.0f64.into()]), + ); + nbt.put("Rotation", NbtTag::List(vec![0.0f32.into(), 0.0f32.into()])); + nbt.put_float("Health", 80.0); + nbt.put_bool("PersistenceRequired", true); + nbt +} + +fn box_from_corners(first: Vector3, second: Vector3) -> BlockBox { + BlockBox::new( + first.x.min(second.x), + first.y.min(second.y), + first.z.min(second.z), + first.x.max(second.x), + first.y.max(second.y), + first.z.max(second.z), + ) +} + +#[cfg(test)] +mod tests { + use pumpkin_data::{Block, dimension::Dimension}; + use pumpkin_util::{ + BlockDirection, + math::block_box::BlockBox, + random::{RandomGenerator, legacy_rand::LegacyRand}, + world_seed::Seed, + }; + + use crate::{ + ProtoChunk, + generation::{ + get_world_gen, + positions::chunk_pos::{start_block_x, start_block_z}, + structure::structures::{HeightSampler, StructureGenerator, StructureGeneratorContext}, + }, + }; + + use super::{MonumentBuilding, OceanMonumentGenerator, elder_guardian_nbt}; + + struct OceanFloorSampler; + + impl HeightSampler for OceanFloorSampler { + fn estimate_height(&mut self, _block_x: i32, _block_z: i32) -> i32 { + panic!("monuments must not use the world-surface heightmap"); + } + + fn estimate_ocean_floor_height(&mut self, _block_x: i32, _block_z: i32) -> i32 { + 37 + } + } + + #[test] + fn structure_start_uses_the_ocean_floor_heightmap() { + let mut height_sampler = OceanFloorSampler; + let context = StructureGeneratorContext { + seed: 0, + chunk_x: 2, + chunk_z: -3, + random: RandomGenerator::Legacy(LegacyRand::from_seed(0)), + sea_level: 63, + min_y: -64, + height_sampler: Some(&mut height_sampler), + structure_key: None, + }; + + let position = OceanMonumentGenerator + .get_structure_position(context) + .unwrap(); + + assert_eq!(position.start_pos.0.y, 37); + } + + #[test] + fn orientation_does_not_move_the_monument_origin() { + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(0)); + let building = MonumentBuilding::new(&mut random, 100, 200, direction, 63); + let bounds = building.base.piece.bounding_box; + + assert_eq!((bounds.min.x, bounds.min.y, bounds.min.z), (100, 39, 200)); + assert_eq!((bounds.max.x, bounds.max.y, bounds.max.z), (157, 61, 257)); + assert!(building.children.iter().all(|child| { + let child = child.piece.piece.bounding_box; + child.min.x >= bounds.min.x + && child.min.y >= bounds.min.y + && child.min.z >= bounds.min.z + && child.max.x <= bounds.max.x + && child.max.y <= bounds.max.y + && child.max.z <= bounds.max.z + })); + } + } + + #[test] + fn elder_guardian_nbt_is_accepted_by_the_entity_loader() { + let nbt = elder_guardian_nbt(pumpkin_util::math::vector3::Vector3::new(1, 2, 3)); + + assert_eq!(nbt.get_string("id"), Some("minecraft:elder_guardian")); + assert_eq!(nbt.get_list("Pos").unwrap().len(), 3); + assert_eq!(nbt.get_list("Motion").unwrap().len(), 3); + assert_eq!(nbt.get_list("Rotation").unwrap().len(), 2); + assert_eq!(nbt.get_float("Health"), Some(80.0)); + } + + #[test] + fn every_orientation_places_the_complete_treasure_core() { + let world_gen = get_world_gen( + Seed(0), + Dimension::OVERWORLD, + false, + Vec::new(), + String::new(), + ); + + for direction in [ + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let mut random = RandomGenerator::Legacy(LegacyRand::from_seed(0)); + let building = MonumentBuilding::new(&mut random, 0, 0, direction, 63); + let bounds = building.base.piece.bounding_box; + let mut gold_blocks = 0; + + for chunk_x in bounds.min.x.div_euclid(16)..=bounds.max.x.div_euclid(16) { + for chunk_z in bounds.min.z.div_euclid(16)..=bounds.max.z.div_euclid(16) { + let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen); + let chunk_box = BlockBox::new( + start_block_x(chunk_x), + chunk.bottom_y() as i32, + start_block_z(chunk_z), + start_block_x(chunk_x) + 15, + chunk.bottom_y() as i32 + chunk.height() as i32 - 1, + start_block_z(chunk_z) + 15, + ); + let mut placement_random = RandomGenerator::Legacy(LegacyRand::from_seed(1)); + building.place_building(&mut chunk, &mut placement_random, &chunk_box); + + for x in 0..16 { + for y in 0..chunk.height() as i32 { + for z in 0..16 { + if chunk + .get_block_state_raw(x, y, z) + .to_state() + .id + .to_block_id() + == Block::GOLD_BLOCK.id + { + gold_blocks += 1; + } + } + } + } + } + } + + assert_eq!(gold_blocks, 8); + } + } +} diff --git a/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/rooms.rs b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/rooms.rs new file mode 100644 index 000000000..8f9bcaf7d --- /dev/null +++ b/crates/pumpkin-world/src/generation/structure/structures/ocean_monument/rooms.rs @@ -0,0 +1,1230 @@ +use pumpkin_data::Block; +use pumpkin_util::{ + BlockDirection, + math::block_box::BlockBox, + random::{RandomGenerator, RandomImpl}, +}; + +use crate::{ProtoChunk, generation::structure::piece::StructurePieceType}; + +use super::{ + BASE_BLACK, BASE_GRAY, BASE_LIGHT, LAMP, MonumentPiece, + graph::{RoomDefinition, RoomGraph}, +}; + +#[derive(Clone, Copy)] +pub(super) enum RoomKind { + Entry, + Core, + DoubleX, + DoubleXY, + DoubleY, + DoubleYZ, + DoubleZ, + Simple(i32), + SimpleTop, + Wing(i32), + Penthouse, +} + +impl RoomKind { + pub const fn piece_type(self) -> StructurePieceType { + match self { + Self::Entry => StructurePieceType::OceanMonumentEntryRoom, + Self::Core => StructurePieceType::OceanMonumentCoreRoom, + Self::DoubleX => StructurePieceType::OceanMonumentDoubleXRoom, + Self::DoubleXY => StructurePieceType::OceanMonumentDoubleXYRoom, + Self::DoubleY => StructurePieceType::OceanMonumentDoubleYRoom, + Self::DoubleYZ => StructurePieceType::OceanMonumentDoubleYZRoom, + Self::DoubleZ => StructurePieceType::OceanMonumentDoubleZRoom, + Self::Simple(_) => StructurePieceType::OceanMonumentSimpleRoom, + Self::SimpleTop => StructurePieceType::OceanMonumentSimpleTopRoom, + Self::Wing(_) => StructurePieceType::OceanMonumentWingRoom, + Self::Penthouse => StructurePieceType::OceanMonumentPenthouse, + } + } + + const fn dimensions(self) -> (i32, i32, i32) { + match self { + Self::Core => (2, 2, 2), + Self::DoubleX => (2, 1, 1), + Self::DoubleXY => (2, 2, 1), + Self::DoubleY => (1, 2, 1), + Self::DoubleYZ => (1, 2, 2), + Self::DoubleZ => (1, 1, 2), + _ => (1, 1, 1), + } + } +} + +pub(super) struct RoomPiece { + pub piece: MonumentPiece, + kind: RoomKind, + room: Option, +} + +impl RoomPiece { + pub const fn for_room( + kind: RoomKind, + direction: BlockDirection, + room: usize, + room_index: i32, + ) -> Self { + let dimensions = kind.dimensions(); + Self { + piece: MonumentPiece::for_room( + kind, + direction, + room_index, + dimensions.0, + dimensions.1, + dimensions.2, + ), + kind, + room: Some(room), + } + } + + pub const fn special( + kind: RoomKind, + direction: BlockDirection, + bounding_box: BlockBox, + ) -> Self { + Self { + piece: MonumentPiece::special(kind, direction, bounding_box), + kind, + room: None, + } + } + + pub fn fit( + random: &mut RandomGenerator, + direction: BlockDirection, + room: usize, + graph: &mut RoomGraph, + ) -> Self { + let room_index = graph.rooms[room].index; + let east = graph.rooms[room].connections[BlockDirection::East as usize]; + let north = graph.rooms[room].connections[BlockDirection::North as usize]; + let up = graph.rooms[room].connections[BlockDirection::Up as usize]; + let kind = if opening(&graph.rooms[room], BlockDirection::East) + && east.is_some_and(|east| !graph.rooms[east].claimed) + && opening(&graph.rooms[room], BlockDirection::Up) + && up.is_some_and(|up| !graph.rooms[up].claimed) + && east.is_some_and(|east| { + opening(&graph.rooms[east], BlockDirection::Up) + && graph.rooms[east].connections[BlockDirection::Up as usize] + .is_some_and(|up| !graph.rooms[up].claimed) + }) { + let east = east.unwrap(); + let up = up.unwrap(); + let east_up = graph.connection(east, BlockDirection::Up); + claim(graph, &[room, east, up, east_up]); + RoomKind::DoubleXY + } else if opening(&graph.rooms[room], BlockDirection::North) + && north.is_some_and(|north| !graph.rooms[north].claimed) + && opening(&graph.rooms[room], BlockDirection::Up) + && up.is_some_and(|up| !graph.rooms[up].claimed) + && north.is_some_and(|north| { + opening(&graph.rooms[north], BlockDirection::Up) + && graph.rooms[north].connections[BlockDirection::Up as usize] + .is_some_and(|up| !graph.rooms[up].claimed) + }) + { + let north = north.unwrap(); + let up = up.unwrap(); + let north_up = graph.connection(north, BlockDirection::Up); + claim(graph, &[room, north, up, north_up]); + RoomKind::DoubleYZ + } else if opening(&graph.rooms[room], BlockDirection::North) + && north.is_some_and(|north| !graph.rooms[north].claimed) + { + let north = north.unwrap(); + claim(graph, &[room, north]); + RoomKind::DoubleZ + } else if opening(&graph.rooms[room], BlockDirection::East) + && east.is_some_and(|east| !graph.rooms[east].claimed) + { + claim(graph, &[room, east.unwrap()]); + RoomKind::DoubleX + } else if opening(&graph.rooms[room], BlockDirection::Up) + && up.is_some_and(|up| !graph.rooms[up].claimed) + { + claim(graph, &[room, up.unwrap()]); + RoomKind::DoubleY + } else if ![ + BlockDirection::West, + BlockDirection::East, + BlockDirection::North, + BlockDirection::South, + BlockDirection::Up, + ] + .into_iter() + .any(|direction| opening(&graph.rooms[room], direction)) + { + claim(graph, &[room]); + RoomKind::SimpleTop + } else { + claim(graph, &[room]); + RoomKind::Simple(random.next_bounded_i32(3)) + }; + + Self::for_room(kind, direction, room, room_index) + } + + pub fn place( + &self, + chunk: &mut ProtoChunk, + random: &mut RandomGenerator, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + match self.kind { + RoomKind::Entry => self.place_entry(chunk, chunk_box, sea_level, graph), + RoomKind::Core => self.place_core(chunk, chunk_box), + RoomKind::DoubleX => self.place_double_x(chunk, chunk_box, sea_level, graph), + RoomKind::DoubleXY => self.place_double_xy(chunk, chunk_box, sea_level, graph), + RoomKind::DoubleY => self.place_double_y(chunk, chunk_box, sea_level, graph), + RoomKind::DoubleYZ => self.place_double_yz(chunk, chunk_box, sea_level, graph), + RoomKind::DoubleZ => self.place_double_z(chunk, chunk_box, sea_level, graph), + RoomKind::Simple(design) => { + self.place_simple(chunk, random, chunk_box, sea_level, graph, design); + } + RoomKind::SimpleTop => { + self.place_simple_top(chunk, random, chunk_box, sea_level, graph); + } + RoomKind::Wing(design) => { + self.place_wing(chunk, chunk_box, design); + } + RoomKind::Penthouse => { + self.place_penthouse(chunk, chunk_box, sea_level); + } + } + } + + fn room<'a>(&self, graph: &'a RoomGraph) -> &'a RoomDefinition { + &graph.rooms[self.room.unwrap()] + } + + fn place_entry( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let room = self.room(graph); + p.fill(chunk, chunk_box, 0, 3, 0, 2, 3, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 5, 3, 0, 7, 3, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 0, 2, 0, 1, 2, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 6, 2, 0, 7, 2, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 0, 1, 0, 0, 1, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 7, 1, 0, 7, 1, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 0, 1, 7, 7, 3, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 1, 0, 2, 3, 0, BASE_LIGHT); + p.fill(chunk, chunk_box, 5, 1, 0, 6, 3, 0, BASE_LIGHT); + if opening(room, BlockDirection::North) { + p.water(chunk, chunk_box, sea_level, 3, 1, 7, 4, 2, 7); + } + if opening(room, BlockDirection::West) { + p.water(chunk, chunk_box, sea_level, 0, 1, 3, 1, 2, 4); + } + if opening(room, BlockDirection::East) { + p.water(chunk, chunk_box, sea_level, 6, 1, 3, 7, 2, 4); + } + } + + fn place_core(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox) { + let p = &self.piece; + p.fill_on_water(chunk, chunk_box, 1, 8, 0, 14, 8, 14, BASE_GRAY); + p.fill(chunk, chunk_box, 0, 7, 0, 0, 7, 15, BASE_LIGHT); + p.fill(chunk, chunk_box, 15, 7, 0, 15, 7, 15, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 7, 0, 15, 7, 0, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 7, 15, 14, 7, 15, BASE_LIGHT); + for y in 1..=6 { + let block = if y == 2 || y == 6 { + BASE_GRAY + } else { + BASE_LIGHT + }; + for x in [0, 15] { + p.fill(chunk, chunk_box, x, y, 0, x, y, 1, block); + p.fill(chunk, chunk_box, x, y, 6, x, y, 9, block); + p.fill(chunk, chunk_box, x, y, 14, x, y, 15, block); + } + p.block(chunk, chunk_box, 1, y, 0, block); + p.fill(chunk, chunk_box, 6, y, 0, 9, y, 0, block); + p.block(chunk, chunk_box, 14, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 15, 14, y, 15, block); + } + p.fill(chunk, chunk_box, 6, 3, 6, 9, 6, 9, BASE_BLACK); + p.fill( + chunk, + chunk_box, + 7, + 4, + 7, + 8, + 5, + 8, + Block::GOLD_BLOCK.default_state, + ); + for y in [3, 6] { + for x in [6, 9] { + p.block(chunk, chunk_box, x, y, 6, LAMP); + p.block(chunk, chunk_box, x, y, 9, LAMP); + } + } + for (x, z) in [(5, 6), (5, 9), (10, 6), (10, 9)] { + p.fill(chunk, chunk_box, x, 1, z, x, 2, z, BASE_LIGHT); + } + for (x, z) in [(6, 5), (9, 5), (6, 10), (9, 10)] { + p.fill(chunk, chunk_box, x, 1, z, x, 2, z, BASE_LIGHT); + } + for (x, z) in [(5, 5), (5, 10), (10, 5), (10, 10)] { + p.fill(chunk, chunk_box, x, 2, z, x, 6, z, BASE_LIGHT); + } + for (x, z0, z1) in [(5, 1, 6), (10, 1, 6), (5, 9, 14), (10, 9, 14)] { + p.fill(chunk, chunk_box, x, 7, z0, x, 7, z1, BASE_LIGHT); + } + for (z, x0, x1) in [(5, 1, 6), (10, 1, 6), (5, 9, 14), (10, 9, 14)] { + p.fill(chunk, chunk_box, x0, 7, z, x1, 7, z, BASE_LIGHT); + } + for &(x, z0, z1) in &[(2, 2, 3), (13, 2, 3), (2, 12, 13), (13, 12, 13)] { + p.fill(chunk, chunk_box, x, 1, z0, x, 1, z1, BASE_LIGHT); + } + for (x, z) in [(3, 2), (12, 2), (3, 13), (12, 13)] { + p.block(chunk, chunk_box, x, 1, z, BASE_LIGHT); + } + } + + fn place_double_x( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let west = self.room.unwrap(); + let east = graph.connection(west, BlockDirection::East); + let west_room = &graph.rooms[west]; + let east_room = &graph.rooms[east]; + if west_room.index / 25 > 0 { + p.default_floor( + chunk, + chunk_box, + 8, + 0, + opening(east_room, BlockDirection::Down), + ); + p.default_floor( + chunk, + chunk_box, + 0, + 0, + opening(west_room, BlockDirection::Down), + ); + } + if west_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 4, 1, 7, 4, 6, BASE_GRAY); + } + if east_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 8, 4, 1, 14, 4, 6, BASE_GRAY); + } + for (y, block) in [(3, BASE_LIGHT), (2, BASE_GRAY), (1, BASE_LIGHT)] { + p.fill(chunk, chunk_box, 0, y, 0, 0, y, 7, block); + p.fill(chunk, chunk_box, 15, y, 0, 15, y, 7, block); + p.fill(chunk, chunk_box, 1, y, 0, 15, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 7, 14, y, 7, block); + } + p.fill(chunk, chunk_box, 5, 1, 0, 10, 1, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 6, 2, 0, 9, 2, 3, BASE_GRAY); + p.fill(chunk, chunk_box, 5, 3, 0, 10, 3, 4, BASE_LIGHT); + p.block(chunk, chunk_box, 6, 2, 3, LAMP); + p.block(chunk, chunk_box, 9, 2, 3, LAMP); + for (room, x, south, north, west_or_east) in [ + (west_room, 0, (3, 4), (3, 4), BlockDirection::West), + (east_room, 15, (11, 12), (11, 12), BlockDirection::East), + ] { + if opening(room, BlockDirection::South) { + p.water(chunk, chunk_box, sea_level, south.0, 1, 0, south.1, 2, 0); + } + if opening(room, BlockDirection::North) { + p.water(chunk, chunk_box, sea_level, north.0, 1, 7, north.1, 2, 7); + } + if opening(room, west_or_east) { + p.water(chunk, chunk_box, sea_level, x, 1, 3, x, 2, 4); + } + } + } + + #[expect(clippy::too_many_lines)] + fn place_double_xy( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let west = self.room.unwrap(); + let east = graph.connection(west, BlockDirection::East); + let west_up = graph.connection(west, BlockDirection::Up); + let east_up = graph.connection(east, BlockDirection::Up); + let west_room = &graph.rooms[west]; + let east_room = &graph.rooms[east]; + let west_up_room = &graph.rooms[west_up]; + let east_up_room = &graph.rooms[east_up]; + if west_room.index / 25 > 0 { + p.default_floor( + chunk, + chunk_box, + 8, + 0, + opening(east_room, BlockDirection::Down), + ); + p.default_floor( + chunk, + chunk_box, + 0, + 0, + opening(west_room, BlockDirection::Down), + ); + } + if west_up_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 8, 1, 7, 8, 6, BASE_GRAY); + } + if east_up_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 8, 8, 1, 14, 8, 6, BASE_GRAY); + } + for y in 1..=7 { + let block = if y == 2 || y == 6 { + BASE_GRAY + } else { + BASE_LIGHT + }; + p.fill(chunk, chunk_box, 0, y, 0, 0, y, 7, block); + p.fill(chunk, chunk_box, 15, y, 0, 15, y, 7, block); + p.fill(chunk, chunk_box, 1, y, 0, 15, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 7, 14, y, 7, block); + } + for (x, z0, z1) in [ + (2, 3, 4), + (3, 2, 2), + (3, 5, 5), + (13, 3, 4), + (11, 2, 2), + (11, 5, 5), + ] { + let x1 = if matches!(x, 3 | 11) { x + 1 } else { x }; + p.fill(chunk, chunk_box, x, 1, z0, x1, 7, z1, BASE_LIGHT); + } + p.fill(chunk, chunk_box, 5, 1, 3, 5, 3, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 10, 1, 3, 10, 3, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 5, 7, 2, 10, 7, 5, BASE_LIGHT); + for (x, z) in [(5, 2), (10, 2), (5, 5), (10, 5)] { + p.fill(chunk, chunk_box, x, 5, z, x, 7, z, BASE_LIGHT); + p.block(chunk, chunk_box, x, 4, z, LAMP); + } + for (x, z) in [(6, 2), (9, 2), (6, 5), (9, 5)] { + p.block(chunk, chunk_box, x, 6, z, BASE_LIGHT); + } + p.fill(chunk, chunk_box, 5, 4, 3, 6, 4, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 9, 4, 3, 10, 4, 4, BASE_LIGHT); + for (room, offset_x, side) in [ + (west_room, 0, BlockDirection::West), + (east_room, 8, BlockDirection::East), + ] { + if opening(room, BlockDirection::South) { + p.water( + chunk, + chunk_box, + sea_level, + offset_x + 3, + 1, + 0, + offset_x + 4, + 2, + 0, + ); + } + if opening(room, BlockDirection::North) { + p.water( + chunk, + chunk_box, + sea_level, + offset_x + 3, + 1, + 7, + offset_x + 4, + 2, + 7, + ); + } + if opening(room, side) { + let x = if side == BlockDirection::West { 0 } else { 15 }; + p.water(chunk, chunk_box, sea_level, x, 1, 3, x, 2, 4); + } + } + for (room, offset_x, side) in [ + (west_up_room, 0, BlockDirection::West), + (east_up_room, 8, BlockDirection::East), + ] { + if opening(room, BlockDirection::South) { + p.water( + chunk, + chunk_box, + sea_level, + offset_x + 3, + 5, + 0, + offset_x + 4, + 6, + 0, + ); + } + if opening(room, BlockDirection::North) { + p.water( + chunk, + chunk_box, + sea_level, + offset_x + 3, + 5, + 7, + offset_x + 4, + 6, + 7, + ); + } + if opening(room, side) { + let x = if side == BlockDirection::West { 0 } else { 15 }; + p.water(chunk, chunk_box, sea_level, x, 5, 3, x, 6, 4); + } + } + } + + #[expect(clippy::too_many_lines)] + fn place_double_y( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + _sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let lower = self.room.unwrap(); + let upper = graph.connection(lower, BlockDirection::Up); + let lower_room = &graph.rooms[lower]; + let upper_room = &graph.rooms[upper]; + if lower_room.index / 25 > 0 { + p.default_floor( + chunk, + chunk_box, + 0, + 0, + opening(lower_room, BlockDirection::Down), + ); + } + if upper_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 8, 1, 6, 8, 6, BASE_GRAY); + } + p.fill(chunk, chunk_box, 0, 4, 0, 0, 4, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 7, 4, 0, 7, 4, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 4, 0, 6, 4, 0, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 4, 7, 6, 4, 7, BASE_LIGHT); + for (x, z0, z1) in [(2, 1, 2), (5, 1, 2), (2, 5, 6), (5, 5, 6)] { + p.fill(chunk, chunk_box, x, 4, z0, x, 4, z1, BASE_LIGHT); + } + for (x, z) in [(1, 2), (6, 2), (1, 5), (6, 5)] { + p.block(chunk, chunk_box, x, 4, z, BASE_LIGHT); + } + for (level, room) in [(1, lower_room), (5, upper_room)] { + for (direction, fixed, a, b) in [ + (BlockDirection::South, 0, 2, 5), + (BlockDirection::North, 7, 2, 5), + ] { + if opening(room, direction) { + p.fill( + chunk, + chunk_box, + a, + level, + fixed, + a, + level + 2, + fixed, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + b, + level, + fixed, + b, + level + 2, + fixed, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + a + 1, + level + 2, + fixed, + b - 1, + level + 2, + fixed, + BASE_LIGHT, + ); + } else { + p.fill( + chunk, + chunk_box, + 0, + level, + fixed, + 7, + level + 2, + fixed, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 0, + level + 1, + fixed, + 7, + level + 1, + fixed, + BASE_GRAY, + ); + } + } + for (direction, fixed, a, b) in [ + (BlockDirection::West, 0, 2, 5), + (BlockDirection::East, 7, 2, 5), + ] { + if opening(room, direction) { + p.fill( + chunk, + chunk_box, + fixed, + level, + a, + fixed, + level + 2, + a, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + fixed, + level, + b, + fixed, + level + 2, + b, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + fixed, + level + 2, + a + 1, + fixed, + level + 2, + b - 1, + BASE_LIGHT, + ); + } else { + p.fill( + chunk, + chunk_box, + fixed, + level, + 0, + fixed, + level + 2, + 7, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + fixed, + level + 1, + 0, + fixed, + level + 1, + 7, + BASE_GRAY, + ); + } + } + } + } + + #[expect(clippy::too_many_lines)] + fn place_double_yz( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let south = self.room.unwrap(); + let north = graph.connection(south, BlockDirection::North); + let south_up = graph.connection(south, BlockDirection::Up); + let north_up = graph.connection(north, BlockDirection::Up); + let south_room = &graph.rooms[south]; + let north_room = &graph.rooms[north]; + let south_up_room = &graph.rooms[south_up]; + let north_up_room = &graph.rooms[north_up]; + if south_room.index / 25 > 0 { + p.default_floor( + chunk, + chunk_box, + 0, + 8, + opening(north_room, BlockDirection::Down), + ); + p.default_floor( + chunk, + chunk_box, + 0, + 0, + opening(south_room, BlockDirection::Down), + ); + } + if south_up_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 8, 1, 6, 8, 7, BASE_GRAY); + } + if north_up_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 8, 8, 6, 8, 14, BASE_GRAY); + } + for y in 1..=7 { + let block = if y == 2 || y == 6 { + BASE_GRAY + } else { + BASE_LIGHT + }; + p.fill(chunk, chunk_box, 0, y, 0, 0, y, 15, block); + p.fill(chunk, chunk_box, 7, y, 0, 7, y, 15, block); + p.fill(chunk, chunk_box, 1, y, 0, 6, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 15, 6, y, 15, block); + } + for y in 1..=7 { + let block = if y == 2 || y == 6 { LAMP } else { BASE_BLACK }; + p.fill(chunk, chunk_box, 3, y, 7, 4, y, 8, block); + } + for (room, z, cross) in [(south_room, 0, 3), (north_room, 15, 11)] { + let longitudinal = if z == 0 { + BlockDirection::South + } else { + BlockDirection::North + }; + if opening(room, longitudinal) { + p.water(chunk, chunk_box, sea_level, 3, 1, z, 4, 2, z); + } + if opening(room, BlockDirection::West) { + p.water(chunk, chunk_box, sea_level, 0, 1, cross, 0, 2, cross + 1); + } + if opening(room, BlockDirection::East) { + p.water(chunk, chunk_box, sea_level, 7, 1, cross, 7, 2, cross + 1); + } + } + for (room, z, cross) in [(south_up_room, 0, 3), (north_up_room, 15, 11)] { + let longitudinal = if z == 0 { + BlockDirection::South + } else { + BlockDirection::North + }; + if opening(room, longitudinal) { + p.water(chunk, chunk_box, sea_level, 3, 5, z, 4, 6, z); + } + if opening(room, BlockDirection::West) { + p.water(chunk, chunk_box, sea_level, 0, 5, cross, 0, 6, cross + 1); + p.fill( + chunk, + chunk_box, + 1, + 4, + cross - 1, + 2, + 4, + cross + 2, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 1, + 1, + cross - 1, + 1, + 3, + cross - 1, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 1, + 1, + cross + 2, + 1, + 3, + cross + 2, + BASE_LIGHT, + ); + } + if opening(room, BlockDirection::East) { + p.water(chunk, chunk_box, sea_level, 7, 5, cross, 7, 6, cross + 1); + p.fill( + chunk, + chunk_box, + 5, + 4, + cross - 1, + 6, + 4, + cross + 2, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 6, + 1, + cross - 1, + 6, + 3, + cross - 1, + BASE_LIGHT, + ); + p.fill( + chunk, + chunk_box, + 6, + 1, + cross + 2, + 6, + 3, + cross + 2, + BASE_LIGHT, + ); + } + } + } + + fn place_double_z( + &self, + chunk: &mut ProtoChunk, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let south = self.room.unwrap(); + let north = graph.connection(south, BlockDirection::North); + let south_room = &graph.rooms[south]; + let north_room = &graph.rooms[north]; + if south_room.index / 25 > 0 { + p.default_floor( + chunk, + chunk_box, + 0, + 8, + opening(north_room, BlockDirection::Down), + ); + p.default_floor( + chunk, + chunk_box, + 0, + 0, + opening(south_room, BlockDirection::Down), + ); + } + if south_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 4, 1, 6, 4, 7, BASE_GRAY); + } + if north_room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 4, 8, 6, 4, 14, BASE_GRAY); + } + for (y, block) in [(3, BASE_LIGHT), (2, BASE_GRAY), (1, BASE_LIGHT)] { + p.fill(chunk, chunk_box, 0, y, 0, 0, y, 15, block); + p.fill(chunk, chunk_box, 7, y, 0, 7, y, 15, block); + p.fill(chunk, chunk_box, 1, y, 0, 7, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 15, 6, y, 15, block); + } + for (x, z0, z1) in [(1, 1, 2), (6, 1, 2), (1, 13, 14), (6, 13, 14)] { + p.fill(chunk, chunk_box, x, 1, z0, x, 1, z1, BASE_LIGHT); + p.fill(chunk, chunk_box, x, 3, z0, x, 3, z1, BASE_LIGHT); + } + for (x, z) in [(2, 6), (5, 6), (2, 9), (5, 9)] { + p.fill(chunk, chunk_box, x, 1, z, x, 3, z, BASE_LIGHT); + } + p.fill(chunk, chunk_box, 3, 2, 6, 4, 2, 6, BASE_LIGHT); + p.fill(chunk, chunk_box, 3, 2, 9, 4, 2, 9, BASE_LIGHT); + p.fill(chunk, chunk_box, 2, 2, 7, 2, 2, 8, BASE_LIGHT); + p.fill(chunk, chunk_box, 5, 2, 7, 5, 2, 8, BASE_LIGHT); + for (x, z) in [(2, 5), (5, 5), (2, 10), (5, 10)] { + p.block(chunk, chunk_box, x, 2, z, LAMP); + p.block(chunk, chunk_box, x, 3, z, BASE_LIGHT); + } + for (room, z, cross) in [(south_room, 0, 3), (north_room, 15, 11)] { + let longitudinal = if z == 0 { + BlockDirection::South + } else { + BlockDirection::North + }; + if opening(room, longitudinal) { + p.water(chunk, chunk_box, sea_level, 3, 1, z, 4, 2, z); + } + if opening(room, BlockDirection::West) { + p.water(chunk, chunk_box, sea_level, 0, 1, cross, 0, 2, cross + 1); + } + if opening(room, BlockDirection::East) { + p.water(chunk, chunk_box, sea_level, 7, 1, cross, 7, 2, cross + 1); + } + } + } + + #[expect(clippy::too_many_lines)] + fn place_simple( + &self, + chunk: &mut ProtoChunk, + random: &mut RandomGenerator, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + design: i32, + ) { + let p = &self.piece; + let room = self.room(graph); + if room.index / 25 > 0 { + p.default_floor(chunk, chunk_box, 0, 0, opening(room, BlockDirection::Down)); + } + if room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 4, 1, 6, 4, 6, BASE_GRAY); + } + let center_pillar = design != 0 + && random.next_bool() + && !opening(room, BlockDirection::Down) + && !opening(room, BlockDirection::Up) + && room.count_openings() > 1; + match design { + 0 => { + for (x0, z0, lamp_x, lamp_z) in + [(0, 0, 1, 1), (5, 0, 6, 1), (0, 5, 1, 6), (5, 5, 6, 6)] + { + p.fill(chunk, chunk_box, x0, 1, z0, x0 + 2, 1, z0 + 2, BASE_LIGHT); + p.fill(chunk, chunk_box, x0, 3, z0, x0 + 2, 3, z0 + 2, BASE_LIGHT); + let outer_x = if x0 == 0 { x0 } else { x0 + 2 }; + let outer_z = if z0 == 0 { z0 } else { z0 + 2 }; + p.fill( + chunk, + chunk_box, + outer_x, + 2, + z0, + outer_x, + 2, + z0 + 2, + BASE_GRAY, + ); + p.fill( + chunk, + chunk_box, + x0 + 1 - i32::from(x0 != 0), + 2, + outer_z, + x0 + 2 - i32::from(x0 != 0), + 2, + outer_z, + BASE_GRAY, + ); + p.block(chunk, chunk_box, lamp_x, 2, lamp_z, LAMP); + } + if opening(room, BlockDirection::South) { + p.fill(chunk, chunk_box, 3, 3, 0, 4, 3, 0, BASE_LIGHT); + } else { + p.fill(chunk, chunk_box, 3, 3, 0, 4, 3, 1, BASE_LIGHT); + p.fill(chunk, chunk_box, 3, 2, 0, 4, 2, 0, BASE_GRAY); + p.fill(chunk, chunk_box, 3, 1, 0, 4, 1, 1, BASE_LIGHT); + } + if opening(room, BlockDirection::North) { + p.fill(chunk, chunk_box, 3, 3, 7, 4, 3, 7, BASE_LIGHT); + } else { + p.fill(chunk, chunk_box, 3, 3, 6, 4, 3, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 3, 2, 7, 4, 2, 7, BASE_GRAY); + p.fill(chunk, chunk_box, 3, 1, 6, 4, 1, 7, BASE_LIGHT); + } + if opening(room, BlockDirection::West) { + p.fill(chunk, chunk_box, 0, 3, 3, 0, 3, 4, BASE_LIGHT); + } else { + p.fill(chunk, chunk_box, 0, 3, 3, 1, 3, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 0, 2, 3, 0, 2, 4, BASE_GRAY); + p.fill(chunk, chunk_box, 0, 1, 3, 1, 1, 4, BASE_LIGHT); + } + if opening(room, BlockDirection::East) { + p.fill(chunk, chunk_box, 7, 3, 3, 7, 3, 4, BASE_LIGHT); + } else { + p.fill(chunk, chunk_box, 6, 3, 3, 7, 3, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 7, 2, 3, 7, 2, 4, BASE_GRAY); + p.fill(chunk, chunk_box, 6, 1, 3, 7, 1, 4, BASE_LIGHT); + } + } + 1 => { + for (x, z) in [(2, 2), (2, 5), (5, 5), (5, 2)] { + p.fill(chunk, chunk_box, x, 1, z, x, 3, z, BASE_LIGHT); + p.block(chunk, chunk_box, x, 2, z, LAMP); + } + for (x0, z0, x1, z1) in [ + (0, 0, 1, 0), + (0, 1, 0, 1), + (0, 7, 1, 7), + (0, 6, 0, 6), + (6, 7, 7, 7), + (7, 6, 7, 6), + (6, 0, 7, 0), + (7, 1, 7, 1), + ] { + p.fill(chunk, chunk_box, x0, 1, z0, x1, 3, z1, BASE_LIGHT); + } + for (x, z) in [ + (1, 0), + (0, 1), + (1, 7), + (0, 6), + (6, 7), + (7, 6), + (6, 0), + (7, 1), + ] { + p.block(chunk, chunk_box, x, 2, z, BASE_GRAY); + } + for (direction, x0, z0, x1, z1) in [ + (BlockDirection::South, 1, 0, 6, 0), + (BlockDirection::North, 1, 7, 6, 7), + (BlockDirection::West, 0, 1, 0, 6), + (BlockDirection::East, 7, 1, 7, 6), + ] { + if !opening(room, direction) { + p.fill(chunk, chunk_box, x0, 1, z0, x1, 1, z1, BASE_LIGHT); + p.fill(chunk, chunk_box, x0, 2, z0, x1, 2, z1, BASE_GRAY); + p.fill(chunk, chunk_box, x0, 3, z0, x1, 3, z1, BASE_LIGHT); + } + } + } + _ => { + for (y, block) in [(1, BASE_LIGHT), (2, BASE_BLACK), (3, BASE_LIGHT)] { + p.fill(chunk, chunk_box, 0, y, 0, 0, y, 7, block); + p.fill(chunk, chunk_box, 7, y, 0, 7, y, 7, block); + p.fill(chunk, chunk_box, 1, y, 0, 6, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 7, 6, y, 7, block); + } + for (direction, x0, z0, x1, z1) in [ + (BlockDirection::South, 3, 0, 4, 0), + (BlockDirection::North, 3, 7, 4, 7), + (BlockDirection::West, 0, 3, 0, 4), + (BlockDirection::East, 7, 3, 7, 4), + ] { + p.fill(chunk, chunk_box, x0, 1, z0, x1, 2, z1, BASE_BLACK); + if opening(room, direction) { + p.water(chunk, chunk_box, sea_level, x0, 1, z0, x1, 2, z1); + } + } + } + } + if center_pillar { + p.fill(chunk, chunk_box, 3, 1, 3, 4, 1, 4, BASE_LIGHT); + p.fill(chunk, chunk_box, 3, 2, 3, 4, 2, 4, BASE_GRAY); + p.fill(chunk, chunk_box, 3, 3, 3, 4, 3, 4, BASE_LIGHT); + } + } + + fn place_simple_top( + &self, + chunk: &mut ProtoChunk, + random: &mut RandomGenerator, + chunk_box: &BlockBox, + sea_level: i32, + graph: &RoomGraph, + ) { + let p = &self.piece; + let room = self.room(graph); + if room.index / 25 > 0 { + p.default_floor(chunk, chunk_box, 0, 0, opening(room, BlockDirection::Down)); + } + if room.connections[BlockDirection::Up as usize].is_none() { + p.fill_on_water(chunk, chunk_box, 1, 4, 1, 6, 4, 6, BASE_GRAY); + } + for x in 1..=6 { + for z in 1..=6 { + if random.next_bounded_i32(3) != 0 { + let y = if random.next_bounded_i32(4) == 0 { + 2 + } else { + 3 + }; + p.fill( + chunk, + chunk_box, + x, + y, + z, + x, + 3, + z, + Block::WET_SPONGE.default_state, + ); + } + } + } + for (y, block) in [(1, BASE_LIGHT), (2, BASE_BLACK), (3, BASE_LIGHT)] { + p.fill(chunk, chunk_box, 0, y, 0, 0, y, 7, block); + p.fill(chunk, chunk_box, 7, y, 0, 7, y, 7, block); + p.fill(chunk, chunk_box, 1, y, 0, 6, y, 0, block); + p.fill(chunk, chunk_box, 1, y, 7, 6, y, 7, block); + } + p.fill(chunk, chunk_box, 0, 1, 3, 0, 2, 4, BASE_BLACK); + p.fill(chunk, chunk_box, 7, 1, 3, 7, 2, 4, BASE_BLACK); + p.fill(chunk, chunk_box, 3, 1, 0, 4, 2, 0, BASE_BLACK); + p.fill(chunk, chunk_box, 3, 1, 7, 4, 2, 7, BASE_BLACK); + if opening(room, BlockDirection::South) { + p.water(chunk, chunk_box, sea_level, 3, 1, 0, 4, 2, 0); + } + } + + fn place_wing(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox, design: i32) { + let p = &self.piece; + if design & 1 == 0 { + for i in 0..4 { + p.fill( + chunk, + chunk_box, + 10 - i, + 3 - i, + 20 - i, + 12 + i, + 3 - i, + 20, + BASE_LIGHT, + ); + } + p.fill(chunk, chunk_box, 7, 0, 6, 15, 0, 16, BASE_LIGHT); + p.fill(chunk, chunk_box, 6, 0, 6, 6, 3, 20, BASE_LIGHT); + p.fill(chunk, chunk_box, 16, 0, 6, 16, 3, 20, BASE_LIGHT); + p.fill(chunk, chunk_box, 7, 1, 7, 7, 1, 20, BASE_LIGHT); + p.fill(chunk, chunk_box, 15, 1, 7, 15, 1, 20, BASE_LIGHT); + p.fill(chunk, chunk_box, 7, 1, 6, 9, 3, 6, BASE_LIGHT); + p.fill(chunk, chunk_box, 13, 1, 6, 15, 3, 6, BASE_LIGHT); + p.fill(chunk, chunk_box, 8, 1, 7, 9, 1, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 13, 1, 7, 14, 1, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, 9, 0, 5, 13, 0, 5, BASE_LIGHT); + p.fill(chunk, chunk_box, 10, 0, 7, 12, 0, 7, BASE_BLACK); + p.fill(chunk, chunk_box, 8, 0, 10, 8, 0, 12, BASE_BLACK); + p.fill(chunk, chunk_box, 14, 0, 10, 14, 0, 12, BASE_BLACK); + for z in (7..=18).rev().step_by(3) { + p.block(chunk, chunk_box, 6, 3, z, LAMP); + p.block(chunk, chunk_box, 16, 3, z, LAMP); + } + for (x, z) in [(10, 10), (12, 10), (10, 12), (12, 12)] { + p.block(chunk, chunk_box, x, 0, z, LAMP); + } + p.block(chunk, chunk_box, 8, 3, 6, LAMP); + p.block(chunk, chunk_box, 14, 3, 6, LAMP); + for (x, z) in [(4, 4), (18, 4), (4, 18), (18, 18)] { + p.block(chunk, chunk_box, x, 2, z, BASE_LIGHT); + p.block(chunk, chunk_box, x, 1, z, LAMP); + p.block(chunk, chunk_box, x, 0, z, BASE_LIGHT); + } + p.block(chunk, chunk_box, 9, 7, 20, BASE_LIGHT); + p.block(chunk, chunk_box, 13, 7, 20, BASE_LIGHT); + p.fill(chunk, chunk_box, 6, 0, 21, 7, 4, 21, BASE_LIGHT); + p.fill(chunk, chunk_box, 15, 0, 21, 16, 4, 21, BASE_LIGHT); + p.spawn_elder(chunk, chunk_box, 11, 2, 16); + } else { + p.fill(chunk, chunk_box, 9, 3, 18, 13, 3, 20, BASE_LIGHT); + p.fill(chunk, chunk_box, 9, 0, 18, 9, 2, 18, BASE_LIGHT); + p.fill(chunk, chunk_box, 13, 0, 18, 13, 2, 18, BASE_LIGHT); + for x in [9, 13] { + p.block(chunk, chunk_box, x, 6, 20, BASE_LIGHT); + p.block(chunk, chunk_box, x, 5, 20, LAMP); + p.block(chunk, chunk_box, x, 4, 20, BASE_LIGHT); + } + p.fill(chunk, chunk_box, 7, 3, 7, 15, 3, 14, BASE_LIGHT); + for x in [10, 12] { + p.fill(chunk, chunk_box, x, 0, 10, x, 6, 10, BASE_LIGHT); + p.fill(chunk, chunk_box, x, 0, 12, x, 6, 12, BASE_LIGHT); + for z in [10, 12] { + p.block(chunk, chunk_box, x, 0, z, LAMP); + p.block(chunk, chunk_box, x, 4, z, LAMP); + } + } + for x in [8, 14] { + p.fill(chunk, chunk_box, x, 0, 7, x, 2, 7, BASE_LIGHT); + p.fill(chunk, chunk_box, x, 0, 14, x, 2, 14, BASE_LIGHT); + } + p.fill(chunk, chunk_box, 8, 3, 8, 8, 3, 13, BASE_BLACK); + p.fill(chunk, chunk_box, 14, 3, 8, 14, 3, 13, BASE_BLACK); + p.spawn_elder(chunk, chunk_box, 11, 5, 13); + } + } + + fn place_penthouse(&self, chunk: &mut ProtoChunk, chunk_box: &BlockBox, sea_level: i32) { + let p = &self.piece; + p.fill(chunk, chunk_box, 2, -1, 2, 11, -1, 11, BASE_LIGHT); + p.fill(chunk, chunk_box, 0, -1, 0, 1, -1, 11, BASE_GRAY); + p.fill(chunk, chunk_box, 12, -1, 0, 13, -1, 11, BASE_GRAY); + p.fill(chunk, chunk_box, 2, -1, 0, 11, -1, 1, BASE_GRAY); + p.fill(chunk, chunk_box, 2, -1, 12, 11, -1, 13, BASE_GRAY); + p.fill(chunk, chunk_box, 0, 0, 0, 0, 0, 13, BASE_LIGHT); + p.fill(chunk, chunk_box, 13, 0, 0, 13, 0, 13, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 0, 0, 12, 0, 0, BASE_LIGHT); + p.fill(chunk, chunk_box, 1, 0, 13, 12, 0, 13, BASE_LIGHT); + for i in (2..=11).step_by(3) { + p.block(chunk, chunk_box, 0, 0, i, LAMP); + p.block(chunk, chunk_box, 13, 0, i, LAMP); + p.block(chunk, chunk_box, i, 0, 0, LAMP); + } + p.fill(chunk, chunk_box, 2, 0, 3, 4, 0, 9, BASE_LIGHT); + p.fill(chunk, chunk_box, 9, 0, 3, 11, 0, 9, BASE_LIGHT); + p.fill(chunk, chunk_box, 4, 0, 9, 9, 0, 11, BASE_LIGHT); + for (x, z) in [(5, 8), (8, 8), (10, 10), (3, 10)] { + p.block(chunk, chunk_box, x, 0, z, BASE_LIGHT); + } + p.fill(chunk, chunk_box, 3, 0, 3, 3, 0, 7, BASE_BLACK); + p.fill(chunk, chunk_box, 10, 0, 3, 10, 0, 7, BASE_BLACK); + p.fill(chunk, chunk_box, 6, 0, 10, 7, 0, 10, BASE_BLACK); + for x in [3, 10] { + for z in (2..=8).step_by(3) { + p.fill(chunk, chunk_box, x, 0, z, x, 2, z, BASE_LIGHT); + } + } + p.fill(chunk, chunk_box, 5, 0, 10, 5, 2, 10, BASE_LIGHT); + p.fill(chunk, chunk_box, 8, 0, 10, 8, 2, 10, BASE_LIGHT); + p.fill(chunk, chunk_box, 6, -1, 7, 7, -1, 8, BASE_BLACK); + p.water(chunk, chunk_box, sea_level, 6, -1, 3, 7, -1, 4); + p.spawn_elder(chunk, chunk_box, 6, 1, 6); + } +} + +const fn opening(room: &RoomDefinition, direction: BlockDirection) -> bool { + room.openings[direction as usize] +} + +fn claim(graph: &mut RoomGraph, rooms: &[usize]) { + for &room in rooms { + graph.rooms[room].claimed = true; + } +}