Skip to main content

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

1use super::super::super::prelude::*;
2use super::super::super::runner::FeatureDecorationRunner;
3use std::f32::consts::{PI, TAU};
4use steel_math::trig;
5use steel_registry::vanilla_block_tags::BlockTag;
6use steel_utils::value_providers::FloatProvider;
7
8struct LargeDripstone {
9    root: BlockPos,
10    pointing_up: bool,
11    radius: i32,
12    bluntness: f64,
13    scale: f64,
14}
15
16struct WindOffsetter {
17    origin_y: i32,
18    wind_speed: Option<(f64, f64)>,
19    max_offset: i32,
20}
21
22impl FeatureDecorationRunner {
23    pub(in crate::worldgen::feature) fn place_large_dripstone_feature(
24        region: &mut WorldGenRegion<'_>,
25        random: &mut WorldgenRandom,
26        config: &LargeDripstoneConfiguration,
27        origin: BlockPos,
28    ) -> bool {
29        if !Self::is_empty_or_water(region.block_state(origin)) {
30            return false;
31        }
32
33        let Some(column) = Self::scan_dripstone_column(
34            region,
35            origin,
36            config.floor_to_ceiling_search_range,
37            Self::is_empty_or_water,
38            |state| {
39                let block = state.get_block();
40                block == &vanilla_blocks::DRIPSTONE_BLOCK
41                    || Self::block_matches_holder_set(block, &config.replaceable_blocks)
42                    || block == &vanilla_blocks::LAVA
43            },
44        ) else {
45            return false;
46        };
47
48        let (Some(floor), Some(ceiling)) = (column.floor, column.ceiling) else {
49            return false;
50        };
51        let column_height = ceiling - floor - 1;
52        if column_height < 4 {
53            return false;
54        }
55
56        let max_radius_from_height =
57            (column_height as f32 * config.max_column_radius_to_cave_height_ratio) as i32;
58        let min_radius = config.column_radius.min();
59        let max_radius = max_radius_from_height.clamp(min_radius, config.column_radius.max());
60        let radius = random.next_i32_between(min_radius, max_radius);
61
62        let mut stalactite = LargeDripstone::new(
63            origin.at_y(ceiling - 1),
64            false,
65            random,
66            radius,
67            config.stalactite_bluntness,
68            config.height_scale,
69        );
70        let mut stalagmite = LargeDripstone::new(
71            origin.at_y(floor + 1),
72            true,
73            random,
74            radius,
75            config.stalagmite_bluntness,
76            config.height_scale,
77        );
78        let wind =
79            if stalactite.is_suitable_for_wind(config) && stalagmite.is_suitable_for_wind(config) {
80                WindOffsetter::new(origin.y(), random, config.wind_speed, 16 - radius)
81            } else {
82                WindOffsetter::no_wind()
83            };
84
85        let stalactite_base_embedded =
86            stalactite.move_back_until_base_is_inside_stone(region, &wind);
87        let stalagmite_base_embedded =
88            stalagmite.move_back_until_base_is_inside_stone(region, &wind);
89        if stalactite_base_embedded {
90            stalactite.place_blocks(region, random, &wind);
91        }
92
93        if stalagmite_base_embedded {
94            stalagmite.place_blocks(region, random, &wind);
95        }
96
97        true
98    }
99
100    pub(in crate::worldgen::feature) fn dripstone_height(
101        mut xz_distance_from_center: f64,
102        dripstone_radius: f64,
103        scale: f64,
104        bluntness: f64,
105    ) -> f64 {
106        if xz_distance_from_center < bluntness {
107            xz_distance_from_center = bluntness;
108        }
109
110        let cutoff = 0.384;
111        let r = xz_distance_from_center / dripstone_radius * cutoff;
112        let part1 = 0.75 * r.powf(4.0 / 3.0);
113        let part2 = r.powf(2.0 / 3.0);
114        let part3 = (1.0 / 3.0) * r.ln();
115        let height_relative_to_max_radius = (scale * (part1 - part2 - part3)).max(0.0);
116        height_relative_to_max_radius / cutoff * dripstone_radius
117    }
118
119    fn is_circle_mostly_embedded_in_stone(
120        region: &WorldGenRegion<'_>,
121        center: BlockPos,
122        xz_radius: i32,
123    ) -> bool {
124        if Self::is_empty_or_water_or_lava(region.block_state(center)) {
125            return false;
126        }
127
128        let angle_increment = 6.0 / xz_radius as f32;
129        let mut angle = 0.0f32;
130        while angle < TAU {
131            let dx = (trig::cos(f64::from(angle)) * xz_radius as f32) as i32;
132            let dz = (trig::sin(f64::from(angle)) * xz_radius as f32) as i32;
133            if Self::is_empty_or_water_or_lava(region.block_state(center.offset(dx, 0, dz))) {
134                return false;
135            }
136            angle += angle_increment;
137        }
138
139        true
140    }
141
142    fn random_f32_between(random: &mut WorldgenRandom, min: f32, max: f32) -> f32 {
143        random.next_f32() * (max - min) + min
144    }
145}
146
147impl LargeDripstone {
148    fn new(
149        root: BlockPos,
150        pointing_up: bool,
151        random: &mut WorldgenRandom,
152        radius: i32,
153        bluntness: FloatProvider,
154        height_scale: FloatProvider,
155    ) -> Self {
156        Self {
157            root,
158            pointing_up,
159            radius,
160            bluntness: f64::from(bluntness.sample(random)),
161            scale: f64::from(height_scale.sample(random)),
162        }
163    }
164
165    fn height(&self) -> i32 {
166        self.height_at_radius(0.0)
167    }
168
169    fn move_back_until_base_is_inside_stone(
170        &mut self,
171        region: &WorldGenRegion<'_>,
172        wind: &WindOffsetter,
173    ) -> bool {
174        while self.radius > 1 {
175            let mut new_root = self.root;
176            let max_tries = 10.min(self.height());
177
178            for _ in 0..max_tries {
179                if FeatureDecorationRunner::is_lava(region, new_root) {
180                    return false;
181                }
182
183                if FeatureDecorationRunner::is_circle_mostly_embedded_in_stone(
184                    region,
185                    wind.offset(new_root),
186                    self.radius,
187                ) {
188                    self.root = new_root;
189                    return true;
190                }
191
192                new_root = new_root.relative(if self.pointing_up {
193                    Direction::Down
194                } else {
195                    Direction::Up
196                });
197            }
198
199            self.radius /= 2;
200        }
201
202        false
203    }
204
205    fn height_at_radius(&self, check_radius: f32) -> i32 {
206        FeatureDecorationRunner::dripstone_height(
207            f64::from(check_radius),
208            f64::from(self.radius),
209            self.scale,
210            self.bluntness,
211        ) as i32
212    }
213
214    fn place_blocks(
215        &self,
216        region: &mut WorldGenRegion<'_>,
217        random: &mut WorldgenRandom,
218        wind: &WindOffsetter,
219    ) {
220        for dx in -self.radius..=self.radius {
221            for dz in -self.radius..=self.radius {
222                let current_radius = ((dx * dx + dz * dz) as f32).sqrt();
223                if current_radius > self.radius as f32 {
224                    continue;
225                }
226
227                let mut height = self.height_at_radius(current_radius);
228                if height <= 0 {
229                    continue;
230                }
231
232                if random.next_f32() < 0.2 {
233                    height = (height as f32
234                        * FeatureDecorationRunner::random_f32_between(random, 0.8, 1.0))
235                        as i32;
236                }
237
238                let mut pos = self.root.offset(dx, 0, dz);
239                let mut has_been_out_of_stone = false;
240                let max_y = if self.pointing_up {
241                    region.height_at(HeightmapType::WorldSurfaceWg, pos.x(), pos.z())
242                } else {
243                    i32::MAX
244                };
245
246                for _ in 0..height {
247                    if pos.y() >= max_y {
248                        break;
249                    }
250
251                    let wind_adjusted_pos = wind.offset(pos);
252                    let state = region.block_state(wind_adjusted_pos);
253                    if FeatureDecorationRunner::is_empty_or_water_or_lava(state) {
254                        has_been_out_of_stone = true;
255                        let _ = region.set_block_state(
256                            wind_adjusted_pos,
257                            vanilla_blocks::DRIPSTONE_BLOCK.default_state(),
258                            UpdateFlags::UPDATE_CLIENTS,
259                        );
260                    } else if has_been_out_of_stone
261                        && state.get_block().has_tag(&BlockTag::BASE_STONE_OVERWORLD)
262                    {
263                        break;
264                    }
265
266                    pos = pos.relative(if self.pointing_up {
267                        Direction::Up
268                    } else {
269                        Direction::Down
270                    });
271                }
272            }
273        }
274    }
275
276    fn is_suitable_for_wind(&self, config: &LargeDripstoneConfiguration) -> bool {
277        self.radius >= config.min_radius_for_wind
278            && self.bluntness >= f64::from(config.min_bluntness_for_wind)
279    }
280}
281
282impl WindOffsetter {
283    fn new(
284        origin_y: i32,
285        random: &mut WorldgenRandom,
286        wind_speed_range: FloatProvider,
287        max_offset: i32,
288    ) -> Self {
289        let speed = wind_speed_range.sample(random);
290        let direction = FeatureDecorationRunner::random_f32_between(random, 0.0, PI);
291        Self {
292            origin_y,
293            wind_speed: Some((
294                f64::from(trig::cos(f64::from(direction)) * speed),
295                f64::from(trig::sin(f64::from(direction)) * speed),
296            )),
297            max_offset,
298        }
299    }
300
301    const fn no_wind() -> Self {
302        Self {
303            origin_y: 0,
304            wind_speed: None,
305            max_offset: 0,
306        }
307    }
308
309    fn offset(&self, pos: BlockPos) -> BlockPos {
310        let Some((wind_x, wind_z)) = self.wind_speed else {
311            return pos;
312        };
313
314        let dy = self.origin_y - pos.y();
315        pos.offset(
316            fast_floor(wind_x * f64::from(dy)).clamp(-self.max_offset, self.max_offset),
317            0,
318            fast_floor(wind_z * f64::from(dy)).clamp(-self.max_offset, self.max_offset),
319        )
320    }
321}