Skip to main content

steel_core/worldgen/feature/features/dripstone/
cluster.rs

1#![expect(
2    clippy::too_many_lines,
3    reason = "dripstone cluster placement follows vanilla's linear algorithm"
4)]
5
6use steel_registry::vanilla_block_tags::BlockTag;
7
8use super::super::super::prelude::*;
9use super::super::super::runner::FeatureDecorationRunner;
10
11#[derive(Clone, Copy)]
12pub(in crate::worldgen::feature) struct DripstoneColumn {
13    pub(in crate::worldgen::feature) floor: Option<i32>,
14    pub(in crate::worldgen::feature) ceiling: Option<i32>,
15}
16
17impl DripstoneColumn {
18    const fn with_floor(self, floor: Option<i32>) -> Self {
19        Self {
20            floor,
21            ceiling: self.ceiling,
22        }
23    }
24
25    const fn height(self) -> Option<i32> {
26        match (self.floor, self.ceiling) {
27            (Some(floor), Some(ceiling)) => Some(ceiling - floor - 1),
28            _ => None,
29        }
30    }
31}
32
33impl FeatureDecorationRunner {
34    pub(in crate::worldgen::feature) fn place_dripstone_cluster_feature(
35        region: &mut WorldGenRegion<'_>,
36        random: &mut WorldgenRandom,
37        config: &DripstoneClusterConfiguration,
38        origin: BlockPos,
39    ) -> bool {
40        if !Self::is_empty_or_water(region.block_state(origin)) {
41            return false;
42        }
43
44        let height = config.height.sample(random);
45        let wetness = config.wetness.sample(random);
46        let density = config.density.sample(random);
47        let x_radius = config.radius.sample(random);
48        let z_radius = config.radius.sample(random);
49
50        for dx in -x_radius..=x_radius {
51            for dz in -z_radius..=z_radius {
52                let chance =
53                    Self::chance_of_stalagmite_or_stalactite(x_radius, z_radius, dx, dz, config);
54                let pos = origin.offset(dx, 0, dz);
55                Self::place_dripstone_cluster_column(
56                    region, random, pos, dx, dz, wetness, chance, height, density, config,
57                );
58            }
59        }
60
61        true
62    }
63
64    #[expect(
65        clippy::too_many_arguments,
66        reason = "mirrors vanilla DripstoneClusterFeature.placeColumn state"
67    )]
68    fn place_dripstone_cluster_column(
69        region: &mut WorldGenRegion<'_>,
70        random: &mut WorldgenRandom,
71        pos: BlockPos,
72        dx: i32,
73        dz: i32,
74        chance_of_water: f32,
75        chance_of_stalagmite_or_stalactite: f64,
76        cluster_height: i32,
77        density: f32,
78        config: &DripstoneClusterConfiguration,
79    ) {
80        let Some(base_column) = Self::scan_dripstone_column(
81            region,
82            pos,
83            config.floor_to_ceiling_search_range,
84            Self::is_empty_or_water,
85            Self::is_neither_empty_nor_water,
86        ) else {
87            return;
88        };
89
90        let ceiling = base_column.ceiling;
91        let base_floor = base_column.floor;
92        if ceiling.is_none() && base_floor.is_none() {
93            return;
94        }
95
96        let want_pool = random.next_f32() < chance_of_water;
97        let column = if let Some(base_floor_y) = base_floor {
98            if want_pool && Self::can_place_dripstone_pool(region, pos.at_y(base_floor_y)) {
99                let _ = region.set_block_state(
100                    pos.at_y(base_floor_y),
101                    vanilla_blocks::WATER.default_state(),
102                    UpdateFlags::UPDATE_CLIENTS,
103                );
104                base_column.with_floor(Some(base_floor_y - 1))
105            } else {
106                base_column
107            }
108        } else {
109            base_column
110        };
111
112        let floor = column.floor;
113        let want_stalactite = random.next_f64() < chance_of_stalagmite_or_stalactite;
114        let stalactite_height = if let Some(ceiling_y) = ceiling {
115            if want_stalactite && !Self::is_lava(region, pos.at_y(ceiling_y)) {
116                let ceiling_thickness = config.dripstone_block_layer_thickness.sample(random);
117                Self::replace_blocks_with_dripstone_blocks(
118                    region,
119                    pos.at_y(ceiling_y),
120                    ceiling_thickness,
121                    Direction::Up,
122                );
123                let max_height = if let Some(floor_y) = floor {
124                    cluster_height.min(ceiling_y - floor_y)
125                } else {
126                    cluster_height
127                };
128                Self::dripstone_cluster_height(random, dx, dz, density, max_height, config)
129            } else {
130                0
131            }
132        } else {
133            0
134        };
135
136        let want_stalagmite = random.next_f64() < chance_of_stalagmite_or_stalactite;
137        let stalagmite_height = if let Some(floor_y) = floor {
138            if want_stalagmite && !Self::is_lava(region, pos.at_y(floor_y)) {
139                let floor_thickness = config.dripstone_block_layer_thickness.sample(random);
140                Self::replace_blocks_with_dripstone_blocks(
141                    region,
142                    pos.at_y(floor_y),
143                    floor_thickness,
144                    Direction::Down,
145                );
146                if ceiling.is_some() {
147                    (stalactite_height
148                        + random.next_i32_between(
149                            -config.max_stalagmite_stalactite_height_diff,
150                            config.max_stalagmite_stalactite_height_diff,
151                        ))
152                    .max(0)
153                } else {
154                    Self::dripstone_cluster_height(random, dx, dz, density, cluster_height, config)
155                }
156            } else {
157                0
158            }
159        } else {
160            0
161        };
162
163        let (actual_stalactite_height, actual_stalagmite_height) =
164            if let (Some(ceiling_y), Some(floor_y)) = (ceiling, floor) {
165                if ceiling_y - stalactite_height <= floor_y + stalagmite_height {
166                    let lowest_stalactite_bottom = (ceiling_y - stalactite_height).max(floor_y + 1);
167                    let highest_stalagmite_top = (floor_y + stalagmite_height).min(ceiling_y - 1);
168                    let actual_stalactite_bottom = random
169                        .next_i32_between(lowest_stalactite_bottom, highest_stalagmite_top + 1);
170                    let actual_stalagmite_top = actual_stalactite_bottom - 1;
171                    (
172                        ceiling_y - actual_stalactite_bottom,
173                        actual_stalagmite_top - floor_y,
174                    )
175                } else {
176                    (stalactite_height, stalagmite_height)
177                }
178            } else {
179                (stalactite_height, stalagmite_height)
180            };
181
182        let merge_tips = random.next_bool()
183            && actual_stalactite_height > 0
184            && actual_stalagmite_height > 0
185            && column.height().is_some_and(|height| {
186                actual_stalactite_height + actual_stalagmite_height == height
187            });
188
189        if let Some(ceiling_y) = ceiling {
190            Self::grow_pointed_dripstone(
191                region,
192                pos.at_y(ceiling_y - 1),
193                Direction::Down,
194                actual_stalactite_height,
195                merge_tips,
196            );
197        }
198
199        if let Some(floor_y) = floor {
200            Self::grow_pointed_dripstone(
201                region,
202                pos.at_y(floor_y + 1),
203                Direction::Up,
204                actual_stalagmite_height,
205                merge_tips,
206            );
207        }
208    }
209
210    pub(in crate::worldgen::feature) fn scan_dripstone_column<I, V>(
211        region: &WorldGenRegion<'_>,
212        pos: BlockPos,
213        search_range: i32,
214        inside_column: I,
215        valid_edge: V,
216    ) -> Option<DripstoneColumn>
217    where
218        I: Fn(BlockStateId) -> bool,
219        V: Fn(BlockStateId) -> bool,
220    {
221        if !inside_column(region.block_state(pos)) {
222            return None;
223        }
224
225        let ceiling = Self::scan_dripstone_column_direction(
226            region,
227            pos,
228            search_range,
229            &inside_column,
230            &valid_edge,
231            Direction::Up,
232        );
233        let floor = Self::scan_dripstone_column_direction(
234            region,
235            pos,
236            search_range,
237            &inside_column,
238            &valid_edge,
239            Direction::Down,
240        );
241
242        Some(DripstoneColumn { floor, ceiling })
243    }
244
245    fn scan_dripstone_column_direction<I, V>(
246        region: &WorldGenRegion<'_>,
247        mut pos: BlockPos,
248        search_range: i32,
249        inside_column: &I,
250        valid_edge: &V,
251        direction: Direction,
252    ) -> Option<i32>
253    where
254        I: Fn(BlockStateId) -> bool,
255        V: Fn(BlockStateId) -> bool,
256    {
257        for _ in 1..search_range {
258            if !inside_column(region.block_state(pos)) {
259                break;
260            }
261            pos = pos.relative(direction);
262        }
263
264        valid_edge(region.block_state(pos)).then_some(pos.y())
265    }
266
267    fn dripstone_cluster_height(
268        random: &mut WorldgenRandom,
269        dx: i32,
270        dz: i32,
271        density: f32,
272        max_height: i32,
273        config: &DripstoneClusterConfiguration,
274    ) -> i32 {
275        if random.next_f32() > density {
276            return 0;
277        }
278
279        let distance_from_center = dx.abs() + dz.abs();
280        let height_mean = Self::clamped_map_f64(
281            f64::from(distance_from_center),
282            0.0,
283            f64::from(config.max_distance_from_center_affecting_height_bias),
284            f64::from(max_height) / 2.0,
285            0.0,
286        ) as f32;
287        Self::random_between_biased(
288            random,
289            0.0,
290            max_height as f32,
291            height_mean,
292            config.height_deviation as f32,
293        ) as i32
294    }
295
296    fn can_place_dripstone_pool(region: &WorldGenRegion<'_>, pos: BlockPos) -> bool {
297        let state = region.block_state(pos);
298        let block = state.get_block();
299        if block == &vanilla_blocks::WATER
300            || block == &vanilla_blocks::DRIPSTONE_BLOCK
301            || block == &vanilla_blocks::POINTED_DRIPSTONE
302        {
303            return false;
304        }
305
306        if get_fluid_state_from_block(region.block_state(pos.above())).is_water() {
307            return false;
308        }
309
310        for direction in Self::VANILLA_HORIZONTAL_DIRECTIONS {
311            if !Self::can_be_adjacent_to_dripstone_pool_water(region, pos.relative(direction)) {
312                return false;
313            }
314        }
315
316        Self::can_be_adjacent_to_dripstone_pool_water(region, pos.below())
317    }
318
319    fn can_be_adjacent_to_dripstone_pool_water(region: &WorldGenRegion<'_>, pos: BlockPos) -> bool {
320        let state = region.block_state(pos);
321        state.get_block().has_tag(&BlockTag::BASE_STONE_OVERWORLD)
322            || get_fluid_state_from_block(state).is_water()
323    }
324
325    fn replace_blocks_with_dripstone_blocks(
326        region: &mut WorldGenRegion<'_>,
327        mut pos: BlockPos,
328        max_count: i32,
329        direction: Direction,
330    ) {
331        for _ in 0..max_count {
332            if !Self::place_dripstone_block_if_possible(region, pos) {
333                return;
334            }
335
336            pos = pos.relative(direction);
337        }
338    }
339
340    fn chance_of_stalagmite_or_stalactite(
341        x_radius: i32,
342        z_radius: i32,
343        dx: i32,
344        dz: i32,
345        config: &DripstoneClusterConfiguration,
346    ) -> f64 {
347        let x_distance_from_edge = x_radius - dx.abs();
348        let z_distance_from_edge = z_radius - dz.abs();
349        let distance_from_edge = x_distance_from_edge.min(z_distance_from_edge);
350        f64::from(Self::clamped_map_f32(
351            distance_from_edge as f32,
352            0.0,
353            config.max_distance_from_edge_affecting_chance_of_dripstone_column as f32,
354            config.chance_of_dripstone_column_at_max_distance_from_center,
355            1.0,
356        ))
357    }
358
359    pub(in crate::worldgen::feature) fn is_neither_empty_nor_water(state: BlockStateId) -> bool {
360        !state.is_air() && state.get_block() != &vanilla_blocks::WATER
361    }
362
363    pub(in crate::worldgen::feature) fn is_empty_or_water_or_lava(state: BlockStateId) -> bool {
364        state.is_air()
365            || state.get_block() == &vanilla_blocks::WATER
366            || state.get_block() == &vanilla_blocks::LAVA
367    }
368
369    pub(in crate::worldgen::feature) fn is_lava(
370        region: &WorldGenRegion<'_>,
371        pos: BlockPos,
372    ) -> bool {
373        region.block_state(pos).get_block() == &vanilla_blocks::LAVA
374    }
375
376    pub(in crate::worldgen::feature) fn clamped_map_f32(
377        value: f32,
378        old_min: f32,
379        old_max: f32,
380        new_min: f32,
381        new_max: f32,
382    ) -> f32 {
383        if value <= old_min {
384            return new_min;
385        }
386        if value >= old_max {
387            return new_max;
388        }
389
390        let factor = (value - old_min) / (old_max - old_min);
391        new_min + factor * (new_max - new_min)
392    }
393
394    fn clamped_map_f64(value: f64, old_min: f64, old_max: f64, new_min: f64, new_max: f64) -> f64 {
395        if value <= old_min {
396            return new_min;
397        }
398        if value >= old_max {
399            return new_max;
400        }
401
402        let factor = (value - old_min) / (old_max - old_min);
403        new_min + factor * (new_max - new_min)
404    }
405
406    fn random_between_biased(
407        random: &mut WorldgenRandom,
408        min: f32,
409        max: f32,
410        mean: f32,
411        deviation: f32,
412    ) -> f32 {
413        let sample = mean + deviation * random.next_gaussian() as f32;
414        sample.clamp(min, max)
415    }
416}