1use 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
88type ClusterOffset = (i32, i32, (i32, i32), (i32, i32));
90
91#[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
142pub 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}