Skip to main content

steel_core/worldgen/feature/features/
iceberg.rs

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}