1#![expect(
2 clippy::too_many_lines,
3 reason = "iceberg placement is kept linear to preserve vanilla parity"
4)]
5
6use std::cmp::Ordering;
7use std::f64::consts::PI;
8
9use super::super::prelude::*;
10use super::super::runner::FeatureDecorationRunner;
11
12impl FeatureDecorationRunner {
13 pub(in crate::worldgen::feature) fn place_iceberg_feature(
14 region: &mut WorldGenRegion<'_>,
15 registry: &Registry,
16 random: &mut WorldgenRandom,
17 config: &BlockStateData,
18 origin: BlockPos,
19 ) -> bool {
20 let origin = BlockPos::new(origin.x(), region.sea_level(), origin.z());
21 let snow_on_top = random.next_f64() > 0.7;
22 let main_block_state = Self::block_state_from_data(registry, config);
23 let shape_angle = random.next_f64() * 2.0 * PI;
24 let shape_ellipse_a = 11 - random.next_i32_bounded(5);
25 let shape_ellipse_c = 3 + random.next_i32_bounded(3);
26 let is_ellipse = random.next_f64() > 0.7;
27 let mut over_water_height = if is_ellipse {
28 random.next_i32_bounded(6) + 6
29 } else {
30 random.next_i32_bounded(15) + 3
31 };
32 if !is_ellipse && random.next_f64() > 0.9 {
33 over_water_height += random.next_i32_bounded(19) + 7;
34 }
35
36 let under_water_height = (over_water_height + random.next_i32_bounded(11)).min(18);
37 let width =
38 (over_water_height + random.next_i32_bounded(7) - random.next_i32_bounded(5)).min(11);
39 let a = if is_ellipse { shape_ellipse_a } else { 11 };
40
41 for x_offset in -a..a {
42 for z_offset in -a..a {
43 for y_offset in 0..over_water_height {
44 let radius = if is_ellipse {
45 Self::height_dependent_radius_ellipse(y_offset, over_water_height, width)
46 } else {
47 Self::height_dependent_radius_round(
48 random,
49 y_offset,
50 over_water_height,
51 width,
52 )
53 };
54
55 if is_ellipse || x_offset < radius {
56 Self::generate_iceberg_block(
57 region,
58 random,
59 origin,
60 over_water_height,
61 x_offset,
62 y_offset,
63 z_offset,
64 radius,
65 a,
66 is_ellipse,
67 shape_ellipse_c,
68 shape_angle,
69 snow_on_top,
70 main_block_state,
71 );
72 }
73 }
74 }
75 }
76
77 Self::smooth_iceberg(
78 region,
79 origin,
80 width,
81 over_water_height,
82 is_ellipse,
83 shape_ellipse_a,
84 );
85
86 for x_offset in -a..a {
87 for z_offset in -a..a {
88 for y_offset in (-(under_water_height - 1)..=-1).rev() {
89 let new_a = if is_ellipse {
90 ((a as f32)
91 * (1.0 - (y_offset as f32).powi(2) / (under_water_height as f32 * 8.0)))
92 .ceil() as i32
93 } else {
94 a
95 };
96 let radius = Self::height_dependent_radius_steep(
97 random,
98 -y_offset,
99 under_water_height,
100 width,
101 );
102
103 if x_offset < radius {
104 Self::generate_iceberg_block(
105 region,
106 random,
107 origin,
108 under_water_height,
109 x_offset,
110 y_offset,
111 z_offset,
112 radius,
113 new_a,
114 is_ellipse,
115 shape_ellipse_c,
116 shape_angle,
117 snow_on_top,
118 main_block_state,
119 );
120 }
121 }
122 }
123 }
124
125 let do_cut_out = if is_ellipse {
126 random.next_f64() > 0.1
127 } else {
128 random.next_f64() > 0.7
129 };
130 if do_cut_out {
131 Self::generate_iceberg_cut_out(
132 region,
133 random,
134 width,
135 over_water_height,
136 origin,
137 is_ellipse,
138 shape_ellipse_a,
139 shape_angle,
140 shape_ellipse_c,
141 );
142 }
143
144 true
145 }
146
147 #[expect(
148 clippy::too_many_arguments,
149 reason = "Mirrors vanilla IcebergFeature call shape"
150 )]
151 fn generate_iceberg_cut_out(
152 region: &mut WorldGenRegion<'_>,
153 random: &mut WorldgenRandom,
154 width: i32,
155 height: i32,
156 global_origin: BlockPos,
157 is_ellipse: bool,
158 shape_ellipse_a: i32,
159 shape_angle: f64,
160 shape_ellipse_c: i32,
161 ) {
162 let random_sign_x = if random.next_bool() { -1 } else { 1 };
163 let random_sign_z = if random.next_bool() { -1 } else { 1 };
164 let mut x_offset = random.next_i32_bounded((width / 2 - 2).max(1));
165 if random.next_bool() {
166 x_offset = width / 2 + 1 - random.next_i32_bounded((width - width / 2 - 1).max(1));
167 }
168
169 let mut z_offset = random.next_i32_bounded((width / 2 - 2).max(1));
170 if random.next_bool() {
171 z_offset = width / 2 + 1 - random.next_i32_bounded((width - width / 2 - 1).max(1));
172 }
173
174 if is_ellipse {
175 let offset = random.next_i32_bounded((shape_ellipse_a - 5).max(1));
176 x_offset = offset;
177 z_offset = offset;
178 }
179
180 let local_origin = BlockPos::new(random_sign_x * x_offset, 0, random_sign_z * z_offset);
181 let angle = if is_ellipse {
182 shape_angle + PI / 2.0
183 } else {
184 random.next_f64() * 2.0 * PI
185 };
186
187 for y_offset in 0..height - 3 {
188 let radius = Self::height_dependent_radius_round(random, y_offset, height, width);
189 Self::carve_iceberg(
190 region,
191 radius,
192 y_offset,
193 global_origin,
194 false,
195 angle,
196 local_origin,
197 shape_ellipse_a,
198 shape_ellipse_c,
199 );
200 }
201
202 let mut y_offset = -1;
203 while y_offset > -height + random.next_i32_bounded(5) {
204 let radius = Self::height_dependent_radius_steep(random, -y_offset, height, width);
205 Self::carve_iceberg(
206 region,
207 radius,
208 y_offset,
209 global_origin,
210 true,
211 angle,
212 local_origin,
213 shape_ellipse_a,
214 shape_ellipse_c,
215 );
216 y_offset -= 1;
217 }
218 }
219
220 #[expect(
221 clippy::too_many_arguments,
222 reason = "Mirrors vanilla IcebergFeature call shape"
223 )]
224 fn carve_iceberg(
225 region: &mut WorldGenRegion<'_>,
226 radius: i32,
227 y_offset: i32,
228 global_origin: BlockPos,
229 under_water: bool,
230 angle: f64,
231 local_origin: BlockPos,
232 shape_ellipse_a: i32,
233 shape_ellipse_c: i32,
234 ) {
235 let a = radius + 1 + shape_ellipse_a / 3;
236 let c = (radius - 3).min(3) + shape_ellipse_c / 2 - 1;
237
238 for x_offset in -a..a {
239 for z_offset in -a..a {
240 let signed_distance =
241 Self::signed_distance_ellipse(x_offset, z_offset, local_origin, a, c, angle);
242 if signed_distance.partial_cmp(&0.0) != Some(Ordering::Less) {
243 continue;
244 }
245
246 let pos = global_origin.offset(x_offset, y_offset, z_offset);
247 let state = region.block_state(pos);
248 if !Self::is_iceberg_state(state)
249 && state.get_block() != &vanilla_blocks::SNOW_BLOCK
250 {
251 continue;
252 }
253
254 if under_water {
255 Self::set_iceberg_block(region, pos, vanilla_blocks::WATER.default_state());
256 } else {
257 Self::set_iceberg_block(region, pos, vanilla_blocks::AIR.default_state());
258 Self::remove_floating_snow_layer(region, pos);
259 }
260 }
261 }
262 }
263
264 fn remove_floating_snow_layer(region: &mut WorldGenRegion<'_>, pos: BlockPos) {
265 let above = pos.above();
266 if region.block_state(above).get_block() == &vanilla_blocks::SNOW {
267 Self::set_iceberg_block(region, above, vanilla_blocks::AIR.default_state());
268 }
269 }
270
271 #[expect(
272 clippy::too_many_arguments,
273 reason = "Mirrors vanilla IcebergFeature call shape"
274 )]
275 fn generate_iceberg_block(
276 region: &mut WorldGenRegion<'_>,
277 random: &mut WorldgenRandom,
278 origin: BlockPos,
279 height: i32,
280 x_offset: i32,
281 y_offset: i32,
282 z_offset: i32,
283 radius: i32,
284 a: i32,
285 is_ellipse: bool,
286 shape_ellipse_c: i32,
287 shape_angle: f64,
288 snow_on_top: bool,
289 main_block_state: BlockStateId,
290 ) {
291 let signed_distance = if is_ellipse {
292 Self::signed_distance_ellipse(
293 x_offset,
294 z_offset,
295 BlockPos::ZERO,
296 a,
297 Self::get_ellipse_c(y_offset, height, shape_ellipse_c),
298 shape_angle,
299 )
300 } else {
301 Self::signed_distance_circle(x_offset, z_offset, BlockPos::ZERO, radius, random)
302 };
303
304 if signed_distance.partial_cmp(&0.0) != Some(Ordering::Less) {
305 return;
306 }
307
308 let compare_value = if is_ellipse {
309 -0.5
310 } else {
311 -6.0 - f64::from(random.next_i32_bounded(3))
312 };
313 if signed_distance > compare_value && random.next_f64() > 0.9 {
314 return;
315 }
316
317 let pos = origin.offset(x_offset, y_offset, z_offset);
318 Self::set_iceberg_shape_block(
319 region,
320 random,
321 pos,
322 height - y_offset,
323 height,
324 is_ellipse,
325 snow_on_top,
326 main_block_state,
327 );
328 }
329
330 #[expect(
331 clippy::too_many_arguments,
332 reason = "Mirrors vanilla IcebergFeature call shape"
333 )]
334 fn set_iceberg_shape_block(
335 region: &mut WorldGenRegion<'_>,
336 random: &mut WorldgenRandom,
337 pos: BlockPos,
338 height_difference: i32,
339 height: i32,
340 is_ellipse: bool,
341 snow_on_top: bool,
342 main_block_state: BlockStateId,
343 ) {
344 let state = region.block_state(pos);
345 if !state.is_air()
346 && state.get_block() != &vanilla_blocks::SNOW_BLOCK
347 && state.get_block() != &vanilla_blocks::ICE
348 && state.get_block() != &vanilla_blocks::WATER
349 {
350 return;
351 }
352
353 let randomness = !is_ellipse || random.next_f64() > 0.05;
354 if snow_on_top && state.get_block() != &vanilla_blocks::WATER {
355 let divisor = if is_ellipse { 3 } else { 2 };
356 let limit = f64::from(random.next_i32_bounded((height / divisor).max(1)))
357 + f64::from(height) * 0.6;
358 if f64::from(height_difference) <= limit && randomness {
359 Self::set_iceberg_block(region, pos, vanilla_blocks::SNOW_BLOCK.default_state());
360 return;
361 }
362 }
363
364 Self::set_iceberg_block(region, pos, main_block_state);
365 }
366
367 const fn get_ellipse_c(y_offset: i32, height: i32, shape_ellipse_c: i32) -> i32 {
368 if y_offset > 0 && height - y_offset <= 3 {
369 shape_ellipse_c - (4 - (height - y_offset))
370 } else {
371 shape_ellipse_c
372 }
373 }
374
375 fn signed_distance_circle(
376 x_offset: i32,
377 z_offset: i32,
378 origin: BlockPos,
379 radius: i32,
380 random: &mut WorldgenRandom,
381 ) -> f64 {
382 let offset = 10.0 * random.next_f32().clamp(0.2, 0.8) / radius as f32;
383 f64::from(offset)
384 + f64::from((x_offset - origin.x()).pow(2))
385 + f64::from((z_offset - origin.z()).pow(2))
386 - f64::from(radius.pow(2))
387 }
388
389 fn signed_distance_ellipse(
390 x_offset: i32,
391 z_offset: i32,
392 origin: BlockPos,
393 a: i32,
394 c: i32,
395 angle: f64,
396 ) -> f64 {
397 ((f64::from(x_offset - origin.x()) * angle.cos()
398 - f64::from(z_offset - origin.z()) * angle.sin())
399 / f64::from(a))
400 .powi(2)
401 + ((f64::from(x_offset - origin.x()) * angle.sin()
402 + f64::from(z_offset - origin.z()) * angle.cos())
403 / f64::from(c))
404 .powi(2)
405 - 1.0
406 }
407
408 fn height_dependent_radius_round(
409 random: &mut WorldgenRandom,
410 y_offset: i32,
411 height: i32,
412 width: i32,
413 ) -> i32 {
414 let k = 3.5 - random.next_f32();
415 let mut scale = (1.0 - (y_offset as f32).powi(2) / (height as f32 * k)) * width as f32;
416 if height > 15 + random.next_i32_bounded(5) {
417 let temp_y_offset = if y_offset < 3 + random.next_i32_bounded(6) {
418 y_offset / 2
419 } else {
420 y_offset
421 };
422 scale = (1.0 - temp_y_offset as f32 / (height as f32 * k * 0.4)) * width as f32;
423 }
424
425 (scale / 2.0).ceil() as i32
426 }
427
428 fn height_dependent_radius_ellipse(y_offset: i32, height: i32, width: i32) -> i32 {
429 let scale = (1.0 - (y_offset as f32).powi(2) / height as f32) * width as f32;
430 (scale / 2.0).ceil() as i32
431 }
432
433 fn height_dependent_radius_steep(
434 random: &mut WorldgenRandom,
435 y_offset: i32,
436 height: i32,
437 width: i32,
438 ) -> i32 {
439 let k = 1.0 + random.next_f32() / 2.0;
440 let scale = (1.0 - y_offset as f32 / (height as f32 * k)) * width as f32;
441 (scale / 2.0).ceil() as i32
442 }
443
444 fn is_iceberg_state(state: BlockStateId) -> bool {
445 let block = state.get_block();
446 block == &vanilla_blocks::PACKED_ICE
447 || block == &vanilla_blocks::SNOW_BLOCK
448 || block == &vanilla_blocks::BLUE_ICE
449 }
450
451 fn smooth_iceberg(
452 region: &mut WorldGenRegion<'_>,
453 origin: BlockPos,
454 width: i32,
455 height: i32,
456 is_ellipse: bool,
457 shape_ellipse_a: i32,
458 ) {
459 let a = if is_ellipse {
460 shape_ellipse_a
461 } else {
462 width / 2
463 };
464
465 for x_offset in -a..=a {
466 for z_offset in -a..=a {
467 for y_offset in 0..=height {
468 let pos = origin.offset(x_offset, y_offset, z_offset);
469 let state = region.block_state(pos);
470 if !Self::is_iceberg_state(state) && state.get_block() != &vanilla_blocks::SNOW
471 {
472 continue;
473 }
474
475 if region.block_state(pos.below()).is_air() {
476 Self::set_iceberg_block(region, pos, vanilla_blocks::AIR.default_state());
477 Self::set_iceberg_block(
478 region,
479 pos.above(),
480 vanilla_blocks::AIR.default_state(),
481 );
482 } else if Self::is_iceberg_state(state) {
483 let side_count = [pos.west(), pos.east(), pos.north(), pos.south()]
484 .into_iter()
485 .filter(|side| !Self::is_iceberg_state(region.block_state(*side)))
486 .count();
487
488 if side_count >= 3 {
489 Self::set_iceberg_block(
490 region,
491 pos,
492 vanilla_blocks::AIR.default_state(),
493 );
494 }
495 }
496 }
497 }
498 }
499 }
500
501 fn set_iceberg_block(region: &mut WorldGenRegion<'_>, pos: BlockPos, state: BlockStateId) {
502 let _ = region.set_block_state(pos, state, UpdateFlags::UPDATE_ALL);
503 }
504}