steel_core/worldgen/structure/piece_placer/
ruined_portal.rs1use steel_registry::blocks::BlockRef;
2use steel_registry::blocks::block_state_ext::BlockStateExt;
3use steel_registry::blocks::properties::{BlockStateProperties, BoolProperty};
4use steel_registry::blocks::shapes;
5use steel_registry::structure::RuinedPortalPlacementData;
6use steel_registry::vanilla_block_tags::BlockTag;
7use steel_registry::vanilla_blocks;
8use steel_utils::random::Random;
9use steel_utils::random::worldgen_random::WorldgenRandom;
10use steel_utils::{BlockPos, BoundingBox, Direction, types::UpdateFlags};
11
12use crate::chunk::heightmap::HeightmapType;
13use crate::worldgen::region::WorldGenRegion;
14use steel_worldgen::structure::RuinedPortalProperties;
15
16use super::StructurePiecePlacer;
17
18const NETHERRACK_PROBABILITY_BY_DISTANCE: [f32; 14] = [
19 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.9, 0.8, 0.7, 0.6, 0.4, 0.2,
20];
21const RUINED_PORTAL_HORIZONTAL_DIRECTIONS: [Direction; 4] = [
22 Direction::North,
23 Direction::East,
24 Direction::South,
25 Direction::West,
26];
27
28impl StructurePiecePlacer {
29 pub(super) fn post_process_ruined_portal(
30 region: &mut WorldGenRegion<'_>,
31 vertical_placement: RuinedPortalPlacementData,
32 properties: RuinedPortalProperties,
33 portal_box: BoundingBox,
34 random: &mut WorldgenRandom,
35 ) {
36 Self::spread_ruined_portal_netherrack(
37 region,
38 vertical_placement,
39 properties,
40 portal_box,
41 random,
42 );
43 Self::add_ruined_portal_netherrack_drip_columns(region, properties, portal_box, random);
44
45 if !properties.vines && !properties.overgrown {
46 return;
47 }
48
49 for z in portal_box.min_z()..=portal_box.max_z() {
50 for y in portal_box.min_y()..=portal_box.max_y() {
51 for x in portal_box.min_x()..=portal_box.max_x() {
52 let pos = BlockPos::new(x, y, z);
53 if properties.vines {
54 Self::maybe_add_ruined_portal_vines(region, pos, random);
55 }
56 if properties.overgrown {
57 Self::maybe_add_ruined_portal_leaves(region, pos, random);
58 }
59 }
60 }
61 }
62 }
63
64 fn maybe_add_ruined_portal_vines(
65 region: &mut WorldGenRegion<'_>,
66 pos: BlockPos,
67 random: &mut WorldgenRandom,
68 ) {
69 let state = region.block_state(pos);
70 if state.is_air() || state.get_block() == &vanilla_blocks::VINE {
71 return;
72 }
73
74 let direction = Self::random_ruined_portal_horizontal_direction(random);
75 let neighbor_pos = direction.relative(pos);
76 if !region.block_state(neighbor_pos).is_air() {
77 return;
78 }
79 if !shapes::is_offset_face_full(state.get_collision_shape_at(pos), direction) {
80 return;
81 }
82
83 let vine_state = vanilla_blocks::VINE
84 .default_state()
85 .set_value(Self::vine_face_property(direction.opposite()), true);
86 let _ = region.set_block_state(neighbor_pos, vine_state, UpdateFlags::UPDATE_ALL);
87 }
88
89 fn maybe_add_ruined_portal_leaves(
90 region: &mut WorldGenRegion<'_>,
91 pos: BlockPos,
92 random: &mut WorldgenRandom,
93 ) {
94 if random.next_f32() >= 0.5 {
95 return;
96 }
97 if region.block_state(pos).get_block() != &vanilla_blocks::NETHERRACK {
98 return;
99 }
100 let above = pos.above();
101 if !region.block_state(above).is_air() {
102 return;
103 }
104
105 let leaves = vanilla_blocks::JUNGLE_LEAVES
106 .default_state()
107 .set_value(&BlockStateProperties::PERSISTENT, true);
108 let _ = region.set_block_state(above, leaves, UpdateFlags::UPDATE_ALL);
109 }
110
111 fn add_ruined_portal_netherrack_drip_columns(
112 region: &mut WorldGenRegion<'_>,
113 properties: RuinedPortalProperties,
114 portal_box: BoundingBox,
115 random: &mut WorldgenRandom,
116 ) {
117 for x in portal_box.min_x() + 1..portal_box.max_x() {
118 for z in portal_box.min_z() + 1..portal_box.max_z() {
119 let pos = BlockPos::new(x, portal_box.min_y(), z);
120 if region.block_state(pos).get_block() == &vanilla_blocks::NETHERRACK {
121 Self::add_ruined_portal_netherrack_drip_column(
122 region,
123 properties,
124 pos.below(),
125 random,
126 );
127 }
128 }
129 }
130 }
131
132 fn add_ruined_portal_netherrack_drip_column(
133 region: &mut WorldGenRegion<'_>,
134 properties: RuinedPortalProperties,
135 pos: BlockPos,
136 random: &mut WorldgenRandom,
137 ) {
138 let mut current = pos;
139 Self::place_ruined_portal_netherrack_or_magma(region, properties, current, random);
140 let mut remaining_cap = 8;
141 while remaining_cap > 0 && random.next_f32() < 0.5 {
142 current = current.below();
143 remaining_cap -= 1;
144 Self::place_ruined_portal_netherrack_or_magma(region, properties, current, random);
145 }
146 }
147
148 fn spread_ruined_portal_netherrack(
149 region: &mut WorldGenRegion<'_>,
150 vertical_placement: RuinedPortalPlacementData,
151 properties: RuinedPortalProperties,
152 portal_box: BoundingBox,
153 random: &mut WorldgenRandom,
154 ) {
155 let follow_ground_surface = matches!(
156 vertical_placement,
157 RuinedPortalPlacementData::OnLandSurface | RuinedPortalPlacementData::OnOceanFloor
158 );
159 let center = portal_box.center();
160 let average_width = i32::midpoint(portal_box.width(), portal_box.depth());
161 let distance_adjustment = random.next_i32_bounded(1.max(8 - average_width / 2));
162 let max_distance =
163 i32::try_from(NETHERRACK_PROBABILITY_BY_DISTANCE.len()).unwrap_or(i32::MAX);
164
165 for x in center.x - max_distance..=center.x + max_distance {
166 for z in center.z - max_distance..=center.z + max_distance {
167 let distance = (x - center.x).abs() + (z - center.z).abs();
168 let adjusted_distance = 0.max(distance + distance_adjustment);
169 if adjusted_distance >= max_distance {
170 continue;
171 }
172
173 let probability = NETHERRACK_PROBABILITY_BY_DISTANCE[adjusted_distance as usize];
174 if random.next_f64() >= f64::from(probability) {
175 continue;
176 }
177
178 let surface_y = Self::ruined_portal_surface_y(region, vertical_placement, x, z);
179 let y = if follow_ground_surface {
180 surface_y
181 } else {
182 portal_box.min_y().min(surface_y)
183 };
184 let pos = BlockPos::new(x, y, z);
185 if (y - portal_box.min_y()).abs() > 3
186 || !Self::can_replace_with_ruined_portal_netherrack_or_magma(
187 region,
188 vertical_placement,
189 pos,
190 )
191 {
192 continue;
193 }
194
195 Self::place_ruined_portal_netherrack_or_magma(region, properties, pos, random);
196 if properties.overgrown {
197 Self::maybe_add_ruined_portal_leaves(region, pos, random);
198 }
199 Self::add_ruined_portal_netherrack_drip_column(
200 region,
201 properties,
202 pos.below(),
203 random,
204 );
205 }
206 }
207 }
208
209 fn can_replace_with_ruined_portal_netherrack_or_magma(
210 region: &WorldGenRegion<'_>,
211 vertical_placement: RuinedPortalPlacementData,
212 pos: BlockPos,
213 ) -> bool {
214 let state = region.block_state(pos);
215 Self::can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
216 vertical_placement,
217 state.get_block(),
218 )
219 }
220
221 fn can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
222 vertical_placement: RuinedPortalPlacementData,
223 block: BlockRef,
224 ) -> bool {
225 block != &vanilla_blocks::AIR
226 && block != &vanilla_blocks::OBSIDIAN
227 && !block.has_tag(&BlockTag::FEATURES_CANNOT_REPLACE)
228 && (vertical_placement == RuinedPortalPlacementData::InNether
229 || block != &vanilla_blocks::LAVA)
230 }
231
232 fn place_ruined_portal_netherrack_or_magma(
233 region: &mut WorldGenRegion<'_>,
234 properties: RuinedPortalProperties,
235 pos: BlockPos,
236 random: &mut WorldgenRandom,
237 ) {
238 let state = if !properties.cold && random.next_f32() < 0.07 {
239 vanilla_blocks::MAGMA_BLOCK.default_state()
240 } else {
241 vanilla_blocks::NETHERRACK.default_state()
242 };
243 let _ = region.set_block_state(pos, state, UpdateFlags::UPDATE_ALL);
244 }
245
246 fn ruined_portal_surface_y(
247 region: &WorldGenRegion<'_>,
248 vertical_placement: RuinedPortalPlacementData,
249 x: i32,
250 z: i32,
251 ) -> i32 {
252 let heightmap = if vertical_placement == RuinedPortalPlacementData::OnOceanFloor {
253 HeightmapType::OceanFloorWg
254 } else {
255 HeightmapType::WorldSurfaceWg
256 };
257 region.height_at(heightmap, x, z) - 1
258 }
259
260 fn random_ruined_portal_horizontal_direction(random: &mut WorldgenRandom) -> Direction {
261 RUINED_PORTAL_HORIZONTAL_DIRECTIONS
262 [random.next_i32_bounded(RUINED_PORTAL_HORIZONTAL_DIRECTIONS.len() as i32) as usize]
263 }
264
265 const fn vine_face_property(direction: Direction) -> &'static BoolProperty {
266 match direction {
267 Direction::Up => &BlockStateProperties::UP,
268 Direction::North => &BlockStateProperties::NORTH,
269 Direction::East => &BlockStateProperties::EAST,
270 Direction::South => &BlockStateProperties::SOUTH,
271 Direction::West => &BlockStateProperties::WEST,
272 Direction::Down => panic!("vine has no down face property"),
273 }
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use steel_registry::init_vanilla_registry;
280
281 use super::*;
282
283 #[test]
284 fn ruined_portal_netherrack_replacement_matches_vanilla_air_checks() {
285 init_vanilla_registry();
286
287 assert!(
288 StructurePiecePlacer::can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
289 RuinedPortalPlacementData::InNether,
290 &vanilla_blocks::CAVE_AIR,
291 )
292 );
293 assert!(
294 !StructurePiecePlacer::can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
295 RuinedPortalPlacementData::InNether,
296 &vanilla_blocks::AIR,
297 )
298 );
299 assert!(
300 !StructurePiecePlacer::can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
301 RuinedPortalPlacementData::InNether,
302 &vanilla_blocks::OBSIDIAN,
303 )
304 );
305 assert!(
306 StructurePiecePlacer::can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
307 RuinedPortalPlacementData::InNether,
308 &vanilla_blocks::LAVA,
309 )
310 );
311 assert!(
312 !StructurePiecePlacer::can_block_be_replaced_with_ruined_portal_netherrack_or_magma(
313 RuinedPortalPlacementData::OnLandSurface,
314 &vanilla_blocks::LAVA,
315 )
316 );
317 }
318}