steel_core/worldgen/feature/features/dripstone/
large.rs1use 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}