Skip to main content

steel_core/worldgen/structure/piece_placer/
ruined_portal.rs

1use 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}