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}