Skip to main content

steel_worldgen/structure/
ocean_ruin.rs

1//! Ocean ruin: a base piece from a warm/cold × small/large pool, plus — when large
2//! and the cluster check passes — a scatter of smaller ruins with collision checks.
3//! Warm uses one piece; cold stacks three (brick + cracked + mossy) from the same index.
4
5use glam::IVec3;
6use steel_registry::structure::{
7    LiquidSettingsData, OceanRuinBiomeTempData, StructureConfigData, StructureData,
8};
9use steel_utils::random::Random;
10use steel_utils::random::legacy_random::LegacyRandom;
11use steel_utils::{BoundingBox, Direction, Identifier, Rotation};
12
13use crate::structure::{
14    GenerationStub, Structure, StructureBlockIgnore, StructureGenerationContext, StructureMirror,
15    StructurePiece, StructurePiecePayload, TemplateMarkerHandling, TemplatePieceData,
16    TemplatePlacementAdjustment, TemplatePlacementClip, TemplatePostProcess, TemplateProcessorList,
17};
18
19static WARM_SMALL: &[&str] = &[
20    "underwater_ruin/warm_1",
21    "underwater_ruin/warm_2",
22    "underwater_ruin/warm_3",
23    "underwater_ruin/warm_4",
24    "underwater_ruin/warm_5",
25    "underwater_ruin/warm_6",
26    "underwater_ruin/warm_7",
27    "underwater_ruin/warm_8",
28];
29static WARM_LARGE: &[&str] = &[
30    "underwater_ruin/big_warm_4",
31    "underwater_ruin/big_warm_5",
32    "underwater_ruin/big_warm_6",
33    "underwater_ruin/big_warm_7",
34];
35static COLD_BRICK: &[&str] = &[
36    "underwater_ruin/brick_1",
37    "underwater_ruin/brick_2",
38    "underwater_ruin/brick_3",
39    "underwater_ruin/brick_4",
40    "underwater_ruin/brick_5",
41    "underwater_ruin/brick_6",
42    "underwater_ruin/brick_7",
43    "underwater_ruin/brick_8",
44];
45static COLD_CRACKED: &[&str] = &[
46    "underwater_ruin/cracked_1",
47    "underwater_ruin/cracked_2",
48    "underwater_ruin/cracked_3",
49    "underwater_ruin/cracked_4",
50    "underwater_ruin/cracked_5",
51    "underwater_ruin/cracked_6",
52    "underwater_ruin/cracked_7",
53    "underwater_ruin/cracked_8",
54];
55static COLD_MOSSY: &[&str] = &[
56    "underwater_ruin/mossy_1",
57    "underwater_ruin/mossy_2",
58    "underwater_ruin/mossy_3",
59    "underwater_ruin/mossy_4",
60    "underwater_ruin/mossy_5",
61    "underwater_ruin/mossy_6",
62    "underwater_ruin/mossy_7",
63    "underwater_ruin/mossy_8",
64];
65static COLD_BIG_BRICK: &[&str] = &[
66    "underwater_ruin/big_brick_1",
67    "underwater_ruin/big_brick_2",
68    "underwater_ruin/big_brick_3",
69    "underwater_ruin/big_brick_8",
70];
71static COLD_BIG_CRACKED: &[&str] = &[
72    "underwater_ruin/big_cracked_1",
73    "underwater_ruin/big_cracked_2",
74    "underwater_ruin/big_cracked_3",
75    "underwater_ruin/big_cracked_8",
76];
77static COLD_BIG_MOSSY: &[&str] = &[
78    "underwater_ruin/big_mossy_1",
79    "underwater_ruin/big_mossy_2",
80    "underwater_ruin/big_mossy_3",
81    "underwater_ruin/big_mossy_8",
82];
83
84fn template_bb(position: IVec3, size: IVec3, rotation: Rotation) -> BoundingBox {
85    rotation.get_bounding_box(position, size)
86}
87
88/// `(x_base, z_base, x_between, z_between)` for a single candidate.
89type ClusterOffset = (i32, i32, (i32, i32), (i32, i32));
90
91/// Vanilla's 8 candidate offsets around a parent ruin.
92#[rustfmt::skip]
93const CLUSTER_OFFSETS: [ClusterOffset; 8] = [
94    (-16,  16, (1, 8), (1, 7)),
95    (-16,   0, (1, 8), (1, 7)),
96    (-16, -16, (1, 8), (4, 8)),
97    (  0,  16, (1, 7), (1, 7)),
98    (  0, -16, (1, 7), (4, 6)),
99    ( 16,  16, (1, 7), (3, 8)),
100    ( 16,   0, (1, 7), (1, 7)),
101    ( 16, -16, (1, 7), (4, 8)),
102];
103
104fn ocean_ruin_piece(
105    template_id: Identifier,
106    position: IVec3,
107    size: IVec3,
108    rotation: Rotation,
109    biome_temp: OceanRuinBiomeTempData,
110    is_large: bool,
111    integrity: f32,
112) -> StructurePiece {
113    StructurePiece {
114        piece_type: Identifier::new_static("minecraft", "orp"),
115        bounding_box: template_bb(position, size, rotation),
116        gen_depth: 0,
117        orientation: Some(Direction::North),
118        payload: StructurePiecePayload::Template(TemplatePieceData {
119            template_id,
120            template_position: position,
121            rotation,
122            mirror: StructureMirror::None,
123            rotation_pivot: IVec3::ZERO,
124            block_ignore: StructureBlockIgnore::None,
125            late_block_ignore: StructureBlockIgnore::StructureAndAir,
126            processors: TemplateProcessorList::OceanRuin {
127                biome_temp,
128                integrity,
129            },
130            liquid_settings: LiquidSettingsData::ApplyWaterlogging,
131            marker_handling: TemplateMarkerHandling::OceanRuin { is_large },
132            placement_adjustment: TemplatePlacementAdjustment::OceanRuin,
133            placement_clip: TemplatePlacementClip::CenterChunk,
134            post_process: TemplatePostProcess::None,
135        }),
136        ground_level_delta: 0,
137        junctions: Vec::new(),
138        projection: None,
139    }
140}
141
142/// Registered under `"minecraft:ocean_ruin"`. Warm/cold are distinguished by
143/// `entry.structure.path`.
144pub struct OceanRuinStructure;
145
146impl Structure for OceanRuinStructure {
147    #[expect(
148        clippy::too_many_lines,
149        reason = "keeps vanilla's warm/cold large-ruin cluster generation in one RNG-ordered flow"
150    )]
151    fn find_generation_point(
152        &self,
153        ctx: &mut dyn StructureGenerationContext,
154        structure: &StructureData,
155        rng: &mut LegacyRandom,
156    ) -> Option<GenerationStub> {
157        let ocean_floor_y = ctx.base_height(ctx.center_block_x(), ctx.center_block_z(), true) - 1;
158        let biome = ctx.biome_at(ctx.center_block_x(), ocean_floor_y, ctx.center_block_z());
159        if !structure.allowed_biomes.contains(&biome.key) {
160            return None;
161        }
162
163        let StructureConfigData::OceanRuin {
164            biome_temp,
165            large_probability,
166            cluster_probability,
167        } = &structure.config
168        else {
169            return None;
170        };
171        let is_warm = matches!(biome_temp, OceanRuinBiomeTempData::Warm);
172        let rotation = Rotation::get_random(rng);
173        let is_large = rng.next_f32() <= *large_probability;
174        let (pos_x, pos_z) = (ctx.chunk_min_x(), ctx.chunk_min_z());
175
176        let mut pieces: Vec<StructurePiece> = Vec::new();
177        let push_piece = |pieces: &mut Vec<StructurePiece>,
178                          name: &str,
179                          x: i32,
180                          z: i32,
181                          rot: Rotation,
182                          is_large_piece: bool,
183                          integrity: f32| {
184            let template_id = Identifier::new("minecraft", name.to_string());
185            if let Some(template) = ctx.templates().get(&template_id) {
186                let pos = IVec3::new(x, 90, z);
187                let size = IVec3::from(template.size);
188                pieces.push(ocean_ruin_piece(
189                    template_id,
190                    pos,
191                    size,
192                    rot,
193                    *biome_temp,
194                    is_large_piece,
195                    integrity,
196                ));
197            }
198        };
199        let base_integrity = if is_large { 0.9 } else { 0.8 };
200
201        if is_warm {
202            let arr = if is_large { WARM_LARGE } else { WARM_SMALL };
203            let idx = rng.next_i32_bounded(arr.len() as i32) as usize;
204            push_piece(
205                &mut pieces,
206                arr[idx],
207                pos_x,
208                pos_z,
209                rotation,
210                is_large,
211                base_integrity,
212            );
213        } else {
214            let (bricks, cracked, mossy) = if is_large {
215                (COLD_BIG_BRICK, COLD_BIG_CRACKED, COLD_BIG_MOSSY)
216            } else {
217                (COLD_BRICK, COLD_CRACKED, COLD_MOSSY)
218            };
219            let idx = rng.next_i32_bounded(bricks.len() as i32) as usize;
220            push_piece(
221                &mut pieces,
222                bricks[idx],
223                pos_x,
224                pos_z,
225                rotation,
226                is_large,
227                base_integrity,
228            );
229            push_piece(
230                &mut pieces,
231                cracked[idx],
232                pos_x,
233                pos_z,
234                rotation,
235                is_large,
236                0.7,
237            );
238            push_piece(
239                &mut pieces,
240                mossy[idx],
241                pos_x,
242                pos_z,
243                rotation,
244                is_large,
245                0.5,
246            );
247        }
248
249        if is_large && rng.next_f32() <= *cluster_probability {
250            let pc = rotation.transform_pos(IVec3::new(15, 0, 15), IVec3::ZERO);
251            let parent_corner_x = pos_x + pc.x;
252            let parent_corner_z = pos_z + pc.z;
253            let parent_min = IVec3::new(pos_x.min(parent_corner_x), 0, pos_z.min(parent_corner_z));
254            let parent_max =
255                IVec3::new(pos_x.max(parent_corner_x), 255, pos_z.max(parent_corner_z));
256            let parent_bb = BoundingBox::new(parent_min, parent_max);
257            let bl_x = pos_x.min(parent_corner_x);
258            let bl_z = pos_z.min(parent_corner_z);
259
260            let mut candidates: Vec<(i32, i32)> = CLUSTER_OFFSETS
261                .iter()
262                .map(|&(ox, oz, (xa, xb), (za, zb))| {
263                    (
264                        bl_x + ox + rng.next_i32_between(xa, xb),
265                        bl_z + oz + rng.next_i32_between(za, zb),
266                    )
267                })
268                .collect();
269
270            for _ in 0..rng.next_i32_between(4, 8) {
271                if candidates.is_empty() {
272                    break;
273                }
274                let idx = rng.next_i32_bounded(candidates.len() as i32) as usize;
275                let (cx, cz) = candidates.remove(idx);
276                let cluster_rot = Rotation::get_random(rng);
277                let nc = cluster_rot.transform_pos(IVec3::new(5, 0, 6), IVec3::ZERO);
278                let cluster_min = IVec3::new(cx.min(cx + nc.x), 0, cz.min(cz + nc.z));
279                let cluster_max = IVec3::new(cx.max(cx + nc.x), 255, cz.max(cz + nc.z));
280                let cluster_bb = BoundingBox::new(cluster_min, cluster_max);
281                if !cluster_bb.intersects(parent_bb) {
282                    if is_warm {
283                        let tidx = rng.next_i32_bounded(WARM_SMALL.len() as i32) as usize;
284                        push_piece(
285                            &mut pieces,
286                            WARM_SMALL[tidx],
287                            cx,
288                            cz,
289                            cluster_rot,
290                            false,
291                            0.8,
292                        );
293                    } else {
294                        let tidx = rng.next_i32_bounded(COLD_BRICK.len() as i32) as usize;
295                        push_piece(
296                            &mut pieces,
297                            COLD_BRICK[tidx],
298                            cx,
299                            cz,
300                            cluster_rot,
301                            false,
302                            0.8,
303                        );
304                        push_piece(
305                            &mut pieces,
306                            COLD_CRACKED[tidx],
307                            cx,
308                            cz,
309                            cluster_rot,
310                            false,
311                            0.7,
312                        );
313                        push_piece(
314                            &mut pieces,
315                            COLD_MOSSY[tidx],
316                            cx,
317                            cz,
318                            cluster_rot,
319                            false,
320                            0.5,
321                        );
322                    }
323                }
324            }
325        }
326
327        Some(GenerationStub {
328            position: (ctx.center_block_x(), ocean_floor_y, ctx.center_block_z()),
329            pieces,
330        })
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn ocean_ruin_piece_uses_template_payload_with_height_adjustment_and_processors() {
340        let template_id = Identifier::vanilla_static("underwater_ruin/warm_1");
341        let position = IVec3::new(32, 90, -48);
342        let size = IVec3::new(9, 7, 9);
343        let piece = ocean_ruin_piece(
344            template_id.clone(),
345            position,
346            size,
347            Rotation::Clockwise90,
348            OceanRuinBiomeTempData::Warm,
349            false,
350            0.8,
351        );
352
353        assert_eq!(piece.piece_type, Identifier::new_static("minecraft", "orp"));
354        assert_eq!(piece.gen_depth, 0);
355        assert_eq!(piece.orientation, Some(Direction::North));
356        assert_eq!(
357            piece.bounding_box,
358            Rotation::Clockwise90.get_bounding_box(position, size)
359        );
360
361        let StructurePiecePayload::Template(data) = piece.payload else {
362            panic!("ocean ruin piece should be template-backed");
363        };
364        assert_eq!(data.template_id, template_id);
365        assert_eq!(data.template_position, position);
366        assert_eq!(data.rotation, Rotation::Clockwise90);
367        assert_eq!(data.mirror, StructureMirror::None);
368        assert_eq!(data.rotation_pivot, IVec3::ZERO);
369        assert_eq!(data.block_ignore, StructureBlockIgnore::None);
370        assert_eq!(
371            data.late_block_ignore,
372            StructureBlockIgnore::StructureAndAir
373        );
374        assert_eq!(
375            data.processors,
376            TemplateProcessorList::OceanRuin {
377                biome_temp: OceanRuinBiomeTempData::Warm,
378                integrity: 0.8,
379            }
380        );
381        assert_eq!(data.liquid_settings, LiquidSettingsData::ApplyWaterlogging);
382        assert_eq!(
383            data.marker_handling,
384            TemplateMarkerHandling::OceanRuin { is_large: false }
385        );
386        assert_eq!(
387            data.placement_adjustment,
388            TemplatePlacementAdjustment::OceanRuin
389        );
390        assert_eq!(data.placement_clip, TemplatePlacementClip::CenterChunk);
391        assert_eq!(data.post_process, TemplatePostProcess::None);
392    }
393}