1use crate::{
2 Canvas, CanvasInfo, ColumnSample, Land,
3 site::SiteKind,
4 util::{
5 FastNoise2d, LOCALITY, RandomField, RandomPerm, SQUARE_4, SmallCache, StructureGen2d,
6 close_fast as close, sampler::Sampler,
7 },
8};
9use common::{
10 generation::EntityInfo,
11 terrain::{
12 Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize,
13 quadratic_nearest_point, river_spline_coeffs,
14 },
15 vol::RectVolSize,
16};
17use itertools::Itertools;
18use noise::NoiseFn;
19use rand::prelude::*;
20use std::{
21 cmp::Ordering,
22 f64::consts::PI,
23 ops::{Add, Mul, Range, Sub},
24};
25use vek::*;
26
27const CELL_SIZE: i32 = 1536;
28
29#[derive(Copy, Clone)]
30pub struct Node {
31 pub wpos: Vec2<i32>,
32 pub depth: i32,
33}
34
35fn to_cell(wpos: Vec2<i32>, level: u32) -> Vec2<i32> {
36 (wpos + (level & 1) as i32 * CELL_SIZE / 4).map(|e| e.div_euclid(CELL_SIZE))
37}
38fn to_wpos(cell: Vec2<i32>, level: u32) -> Vec2<i32> {
39 (cell * CELL_SIZE) - (level & 1) as i32 * CELL_SIZE / 4
40}
41
42const AVG_LEVEL_DEPTH: i32 = 120;
43pub const LAYERS: u32 = 5;
44const MIN_RADIUS: f32 = 8.0;
45const MAX_RADIUS: f32 = 64.0;
46
47fn node_at(cell: Vec2<i32>, level: u32, land: &Land) -> Option<Node> {
48 let rand = RandomField::new(37 + level);
49
50 if rand.chance(cell.with_z(0), 0.75) || level == 0 {
51 let dx = RandomField::new(38 + level);
52 let dy = RandomField::new(39 + level);
53 let wpos = to_wpos(cell, level)
54 + CELL_SIZE / 4
55 + (Vec2::new(dx.get(cell.with_z(0)), dy.get(cell.with_z(0))) % CELL_SIZE as u32 / 2)
56 .map(|e| e as i32);
57 land.get_chunk_wpos(wpos).and_then(|chunk| {
58 let depth = AVG_LEVEL_DEPTH * level as i32 - 6;
59
60 if level > 0
61 || (!chunk.near_cliffs()
62 && !chunk.river.near_water()
63 && chunk.sites.is_empty()
64 && land.get_gradient_approx(wpos) < 0.75)
65 {
66 Some(Node { wpos, depth })
67 } else {
68 None
69 }
70 })
71 } else {
72 None
73 }
74}
75
76pub fn surface_entrances<'a>(land: &'a Land) -> impl Iterator<Item = Vec2<i32>> + 'a {
77 let sz_cells = to_cell(land.size().as_::<i32>().cpos_to_wpos(), 0);
78 (0..sz_cells.x + 1)
79 .flat_map(move |x| (0..sz_cells.y + 1).map(move |y| Vec2::new(x, y)))
80 .filter_map(|cell| Some(tunnel_below_from_cell(cell, 0, land)?.a.wpos))
81}
82
83#[derive(Copy, Clone)]
84pub struct Tunnel {
85 a: Node,
86 b: Node,
87 curve: f32,
88}
89
90impl Tunnel {
91 fn ctrl_offset(&self) -> Vec2<f32> {
92 let start = self.a.wpos.map(|e| e as f64 + 0.5);
93 let end = self.b.wpos.map(|e| e as f64 + 0.5);
94
95 ((end - start) * 0.5 + ((end - start) * 0.5).rotated_z(PI / 2.0) * 6.0 * self.curve as f64)
96 .map(|e| e as f32)
97 }
98
99 fn possibly_near(&self, wposf: Vec2<f64>, threshold: f64) -> Option<(f64, Vec2<f64>, f64)> {
100 let start = self.a.wpos.map(|e| e as f64 + 0.5);
101 let end = self.b.wpos.map(|e| e as f64 + 0.5);
102 if let Some((t, closest, _)) = quadratic_nearest_point(
103 &river_spline_coeffs(start, self.ctrl_offset(), end),
104 wposf,
105 Vec2::new(start, end),
106 ) {
107 let dist2 = closest.distance_squared(wposf);
108 if dist2 < (MAX_RADIUS as f64 + threshold).powi(2) {
109 Some((t, closest, dist2.sqrt()))
110 } else {
111 None
112 }
113 } else {
114 None
115 }
116 }
117
118 fn z_range_at(
119 &self,
120 wposf: Vec2<f64>,
121 info: CanvasInfo,
122 ) -> Option<(Range<i32>, f32, f32, f32)> {
123 let _start = self.a.wpos.map(|e| e as f64 + 0.5);
124 let _end = self.b.wpos.map(|e| e as f64 + 0.5);
125 if let Some((t, closest, dist)) = self.possibly_near(wposf, 1.0) {
126 let horizontal = Lerp::lerp_unclamped(
127 MIN_RADIUS as f64,
128 MAX_RADIUS as f64,
129 (info.index().noise.cave_fbm_nz.get(
130 (closest.with_z(info.land().get_alt_approx(self.a.wpos) as f64) / 256.0)
131 .into_array(),
132 ) + 0.5)
133 .clamped(0.0, 1.0)
134 .powf(3.0),
135 );
136 let vertical = Lerp::lerp_unclamped(
137 MIN_RADIUS as f64,
138 MAX_RADIUS as f64,
139 (info.index().noise.cave_fbm_nz.get(
140 (closest.with_z(info.land().get_alt_approx(self.b.wpos) as f64) / 256.0)
141 .into_array(),
142 ) + 0.5)
143 .clamped(0.0, 1.0)
144 .powf(3.0),
145 );
146 let height_here = (1.0 - dist / horizontal).max(0.0).powf(0.3) * vertical;
147
148 if height_here > 0.0 {
149 let z_offs = info
150 .index()
151 .noise
152 .cave_fbm_nz
153 .get((wposf / 512.0).into_array())
154 * 96.0
155 * ((1.0 - (t - 0.5).abs() * 2.0) * 8.0).min(1.0);
156 let alt_here = info.land().get_alt_approx(closest.map(|e| e as i32));
157 let base = (Lerp::lerp_unclamped(
158 alt_here as f64 - self.a.depth as f64,
159 alt_here as f64 - self.b.depth as f64,
160 t,
161 ) + z_offs)
162 .min(alt_here as f64);
163 Some((
164 (base - height_here * 0.3) as i32..(base + height_here * 1.35) as i32,
165 horizontal as f32,
166 vertical as f32,
167 dist as f32,
168 ))
169 } else {
170 None
171 }
172 } else {
173 None
174 }
175 }
176
177 pub fn biome_at(&self, wpos: Vec3<i32>, info: &CanvasInfo) -> Biome {
179 let Some(col) = info.col_or_gen(wpos.xy()) else {
180 return Biome::default();
181 };
182
183 let below = ((col.alt - wpos.z as f32) / (AVG_LEVEL_DEPTH as f32 * 2.0)).clamped(0.0, 1.0);
185 let depth = (col.alt - wpos.z as f32) / (AVG_LEVEL_DEPTH as f32 * LAYERS as f32);
186 let underground = ((col.alt - wpos.z as f32) / 80.0 - 1.0).clamped(0.0, 1.0);
187
188 let humidity = Lerp::lerp_unclamped(
191 col.humidity,
192 FastNoise2d::new(41)
193 .get(wpos.xy().map(|e| e as f64 / 768.0))
194 .mul(1.2),
195 below,
196 );
197
198 let temp = Lerp::lerp_unclamped(
199 col.temp * 2.0,
200 FastNoise2d::new(42)
201 .get(wpos.xy().map(|e| e as f64 / 2048.0))
202 .mul(1.15)
203 .mul(2.0)
204 .sub(0.5)
205 .add(
206 ((col.alt - wpos.z as f32) / (AVG_LEVEL_DEPTH as f32 * LAYERS as f32))
207 .clamped(0.0, 1.0),
208 ),
209 below,
210 );
211
212 let mineral = FastNoise2d::new(43)
213 .get(wpos.xy().map(|e| e as f64 / 256.0))
214 .mul(1.15)
215 .mul(0.5)
216 .add(
217 ((col.alt - wpos.z as f32) / (AVG_LEVEL_DEPTH as f32 * LAYERS as f32))
218 .clamped(0.0, 1.0),
219 );
220
221 let [
222 barren,
223 mushroom,
224 fire,
225 leafy,
226 dusty,
227 icy,
228 snowy,
229 crystal,
230 sandy,
231 ] = {
232 let barren = 0.01;
234 let mushroom = underground
237 * close(humidity, 1.0, 0.7, 4)
238 * close(temp, 2.0, 1.8, 4)
239 * close(depth, 0.9, 0.7, 4);
240 let fire = underground
242 * close(humidity, 0.0, 0.6, 4)
243 * close(temp, 2.5, 2.2, 4)
244 * close(depth, 1.0, 0.4, 4);
245 let leafy = underground
247 * close(humidity, 0.8, 0.8, 4)
248 * close(temp, 1.2 + depth, 1.5, 4)
249 * close(depth, 0.1, 0.7, 4);
250 let dusty = close(humidity, 0.0, 0.5, 4)
252 * close(temp, -0.3, 0.6, 4)
253 * close(depth, 0.5, 0.5, 4);
254 let icy = underground
256 * close(temp, -2.3 + (depth - 0.5) * 0.5, 2.0, 4)
257 * close(depth, 0.8, 0.6, 4)
258 * close(humidity, 1.0, 0.85, 4);
259 let snowy = close(temp, -1.8, 1.3, 4) * close(depth, 0.0, 0.6, 4);
261 let crystal = underground
264 * close(humidity, 0.0, 0.6, 4)
265 * close(temp, -1.6, 1.3, 4)
266 * close(depth, 1.0, 0.5, 4)
267 * close(mineral, 1.5, 1.0, 4);
268 let sandy = close(humidity, 0.0, 0.4, 4)
270 * close(temp, 0.7, 0.8, 4)
271 * close(depth, 0.0, 0.65, 4);
272
273 let biomes = [
274 barren, mushroom, fire, leafy, dusty, icy, snowy, crystal, sandy,
275 ];
276 let max = biomes
277 .into_iter()
278 .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
279 .unwrap();
280 biomes.map(|e| (e / max).powf(3.0))
281 };
282
283 Biome {
284 humidity,
285 mineral,
286 barren,
287 mushroom,
288 fire,
289 leafy,
290 dusty,
291 icy,
292 snowy,
293 crystal,
294 sandy,
295 depth,
296 }
297 }
298
299 pub fn nodes(&self) -> (&Node, &Node) { (&self.a, &self.b) }
300}
301
302pub(crate) fn tunnels_at<'a>(
303 wpos: Vec2<i32>,
304 level: u32,
305 land: &'a Land,
306) -> impl Iterator<Item = Tunnel> + 'a {
307 let rand = RandomField::new(37 + level);
308 let col_cell = to_cell(wpos - CELL_SIZE / 4, level);
309 LOCALITY
310 .into_iter()
311 .filter_map(move |rpos| {
312 let current_cell_pos = col_cell + rpos;
313 Some(current_cell_pos).zip(node_at(current_cell_pos, level, land))
314 })
315 .flat_map(move |(current_cell_pos, current_cell)| {
316 [Vec2::new(1, 1), Vec2::new(1, -1)]
317 .into_iter()
318 .filter(move |rpos| {
319 let mid = (current_cell_pos * 2 + rpos) / 2;
320 rand.chance(mid.with_z(0), 0.5) ^ (rpos.y == -1)
321 })
322 .chain([Vec2::new(1, 0), Vec2::new(0, 1)])
323 .filter_map(move |rpos| {
324 let other_cell_pos = current_cell_pos + rpos;
325 Some(other_cell_pos).zip(node_at(other_cell_pos, level, land))
326 })
327 .filter(move |(other_cell_pos, _)| {
328 rand.chance((current_cell_pos + other_cell_pos).with_z(7), 0.3)
329 })
330 .map(move |(_other_cell_pos, other_cell)| Tunnel {
331 a: current_cell,
332 b: other_cell,
333 curve: RandomField::new(13)
334 .get_f32(current_cell.wpos.with_z(0))
335 .powf(0.25)
336 .mul(
337 if RandomField::new(14).chance(current_cell.wpos.with_z(0), 0.5) {
338 1.0
339 } else {
340 -1.0
341 },
342 ),
343 })
344 })
345}
346
347fn tunnel_below_from_cell(cell: Vec2<i32>, level: u32, land: &Land) -> Option<Tunnel> {
348 let wpos = to_wpos(cell, level);
349 Some(Tunnel {
350 a: node_at(to_cell(wpos, level), level, land)?,
351 b: node_at(to_cell(wpos + CELL_SIZE / 2, level + 1), level + 1, land)?,
352 curve: 0.0,
353 })
354}
355
356fn tunnels_down_from<'a>(
357 wpos: Vec2<i32>,
358 level: u32,
359 land: &'a Land,
360) -> impl Iterator<Item = Tunnel> + 'a {
361 let col_cell = to_cell(wpos, level);
362 LOCALITY
363 .into_iter()
364 .filter_map(move |rpos| tunnel_below_from_cell(col_cell + rpos, level, land))
365}
366
367fn all_tunnels_at<'a>(
368 wpos2d: Vec2<i32>,
369 _info: &'a CanvasInfo,
370 land: &'a Land,
371) -> impl Iterator<Item = (u32, Tunnel)> + 'a {
372 (1..LAYERS + 1).flat_map(move |level| {
373 tunnels_at(wpos2d, level, land)
374 .chain(tunnels_down_from(wpos2d, level - 1, land))
375 .map(move |tunnel| (level, tunnel))
376 })
377}
378
379fn tunnel_bounds_at_from<'a>(
380 wpos2d: Vec2<i32>,
381 info: &'a CanvasInfo,
382 _land: &'a Land,
383 tunnels: impl Iterator<Item = (u32, Tunnel)> + 'a,
384) -> impl Iterator<Item = (u32, Range<i32>, f32, f32, f32, Tunnel)> + 'a {
385 let wposf = wpos2d.map(|e| e as f64 + 0.5);
386 info.col_or_gen(wpos2d)
387 .map(move |col| {
388 let col_alt = col.alt;
389 let col_water_dist = col.water_dist;
390 tunnels.filter_map(move |(level, tunnel)| {
391 let (z_range, horizontal, vertical, dist) = tunnel.z_range_at(wposf, *info)?;
392 let z_range = Lerp::lerp_unclamped(
394 z_range.end,
395 z_range.start,
396 1.0 - (1.0
397 - ((col_water_dist.unwrap_or(1000.0) - 4.0).max(0.0) / 32.0)
398 .clamped(0.0, 1.0))
399 * (1.0 - ((col_alt - z_range.end as f32 - 4.0) / 8.0).clamped(0.0, 1.0)),
400 )..z_range.end;
401 if z_range.end - z_range.start > 0 {
402 Some((level, z_range, horizontal, vertical, dist, tunnel))
403 } else {
404 None
405 }
406 })
407 })
408 .into_iter()
409 .flatten()
410}
411
412pub fn tunnel_bounds_at<'a>(
413 wpos2d: Vec2<i32>,
414 info: &'a CanvasInfo,
415 land: &'a Land,
416) -> impl Iterator<Item = (u32, Range<i32>, f32, f32, f32, Tunnel)> + 'a {
417 tunnel_bounds_at_from(wpos2d, info, land, all_tunnels_at(wpos2d, info, land))
418}
419
420pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) {
421 let info = canvas.info();
422 let land = info.land();
423
424 let diagonal = (TerrainChunkSize::RECT_SIZE.map(|e| e * e).sum() as f32).sqrt() as f64;
425 let tunnels = all_tunnels_at(
426 info.wpos() + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) / 2,
427 &info,
428 &land,
429 )
430 .filter(|(_, tunnel)| {
431 SQUARE_4
432 .into_iter()
433 .map(|rpos| info.wpos() + rpos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32))
434 .any(|wpos| {
435 tunnel
436 .possibly_near(wpos.map(|e| e as f64), diagonal + 1.0)
437 .is_some()
438 })
439 })
440 .collect::<Vec<_>>();
441
442 if !tunnels.is_empty() {
443 let giant_tree_dist = info
444 .chunk
445 .sites
446 .iter()
447 .filter_map(|site| {
448 if let SiteKind::GiantTree(t) = &info.index.sites.get(*site).kind {
449 Some(t.origin.distance_squared(info.wpos) as f32 / t.radius().powi(2))
450 } else {
451 None
452 }
453 })
454 .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
455 .unwrap_or(0.0)
456 .clamp(0.0, 1.0);
457 let mut structure_cache = SmallCache::default();
458 canvas.foreach_col(|canvas, wpos2d, col| {
459 let tunnel_bounds =
460 tunnel_bounds_at_from(wpos2d, &info, &land, tunnels.iter().copied())
461 .collect::<Vec<_>>();
462
463 for (_, z_range, _, _, _, _) in &tunnel_bounds {
465 for z in z_range.start..z_range.end.min(col.alt as i32 + 1) {
466 canvas.set(wpos2d.with_z(z), Block::empty());
467 }
468 }
469
470 let z_ranges = &tunnel_bounds
471 .iter()
472 .map(|(_, z_range, _, _, _, _)| z_range.clone())
473 .collect_vec();
474
475 let structure_seeds = StructureGen2d::new(34537, 24, 8).get(wpos2d);
479 for (level, z_range, horizontal, vertical, dist, tunnel) in tunnel_bounds {
480 write_column(
481 canvas,
482 col,
483 level,
484 wpos2d,
485 z_range.clone(),
486 z_ranges,
487 tunnel,
488 (horizontal, vertical, dist),
489 giant_tree_dist,
490 &mut structure_cache,
491 &structure_seeds,
492 rng,
493 );
494 }
495 });
496 }
497}
498
499#[derive(Default, Clone)]
500pub struct Biome {
501 humidity: f32,
502 pub barren: f32,
503 mineral: f32,
504 pub mushroom: f32,
505 pub fire: f32,
506 pub leafy: f32,
507 pub dusty: f32,
508 pub icy: f32,
509 pub snowy: f32,
510 pub crystal: f32,
511 pub sandy: f32,
512 depth: f32,
513}
514
515#[derive(Clone)]
516enum CaveStructure {
517 Mushroom(Mushroom),
518 Crystal(CrystalCluster),
519 Flower(Flower),
520 GiantRoot {
521 pos: Vec3<i32>,
522 radius: f32,
523 height: f32,
524 },
525}
526
527#[derive(Clone)]
528struct Mushroom {
529 pos: Vec3<i32>,
530 stalk: f32,
531 head_color: Rgb<u8>,
532}
533
534#[derive(Clone)]
535struct Crystal {
536 dir: Vec3<f32>,
537 length: f32,
538 radius: f32,
539}
540
541#[derive(Clone)]
542struct CrystalCluster {
543 pos: Vec3<i32>,
544 crystals: [Crystal; 5],
545 color: Rgb<u8>,
546}
547
548#[derive(Clone)]
549struct Flower {
550 pos: Vec3<i32>,
551 stalk: f32,
552 petals: usize,
553 petal_height: f32,
554 petal_radius: f32,
555 }
557
558fn write_column<R: Rng>(
560 canvas: &mut Canvas,
561 col: &ColumnSample,
562 level: u32,
563 wpos2d: Vec2<i32>,
564 z_range: Range<i32>,
565 z_ranges: &[Range<i32>],
566 tunnel: Tunnel,
567 dimensions: (f32, f32, f32),
568 giant_tree_dist: f32,
569 structure_cache: &mut SmallCache<Vec3<i32>, Option<CaveStructure>>,
570 structure_seeds: &[(Vec2<i32>, u32); 9],
571 rng: &mut R,
572) {
573 let info = canvas.info();
574
575 let void_above = !canvas.get(wpos2d.with_z(z_range.end)).is_filled();
577 let void_below = !canvas.get(wpos2d.with_z(z_range.start - 1)).is_filled();
578 let sky_above = z_range.end as f32 > col.alt;
580 let cavern_height = (z_range.end - z_range.start) as f32;
581 let (cave_width, max_height, dist_cave_center) = dimensions;
582 let biome = tunnel.biome_at(wpos2d.with_z(z_range.start), &info);
583
584 let overlap = z_ranges.iter().find_map(|other_z_range| {
588 if *other_z_range == z_range {
589 return None;
590 }
591 let start = z_range.start.max(other_z_range.start);
592 let end = z_range.end.min(other_z_range.end);
593 let min = z_range.start.min(other_z_range.start);
594 if start < end { Some(min..end) } else { None }
595 });
596
597 let stalactite = {
598 FastNoise2d::new(35)
599 .get(wpos2d.map(|e| e as f64 / 8.0))
600 .sub(0.5)
601 .max(0.0)
602 .mul(2.0)
603 .powi(if biome.icy > 0.7 {2} else {1})
604 .mul(((col.alt - z_range.end as f32) / 32.0).clamped(0.0, 1.0))
606 .mul(((cave_width + max_height) / 40.0).clamped(0.0, 1.0))
608 .mul(8.0 + cavern_height * (0.4 + (biome.sandy - 0.5).max(biome.icy - 0.6).max(0.0)))
610 };
611
612 let ceiling_cover = if biome.leafy > 0.3
613 || biome.mushroom > 0.5
614 || biome.icy > 0.6
615 || biome.sandy > 0.4
616 || biome.fire > 0.4
617 || biome.crystal > 0.75
618 {
619 1.0.mul(((col.alt - z_range.end as f32) / 32.0).clamped(0.0, 1.0))
621 .mul(max_height * (dist_cave_center / cave_width).powf(3.33))
622 .max(1.0)
623 .sub(
624 if col.marble_mid
625 > biome
626 .fire
627 .max(biome.icy - 0.6)
628 .max(biome.sandy - 0.3)
629 .max(biome.leafy - 0.4)
630 .max(biome.mushroom - 0.4)
631 .max(biome.crystal - 0.5)
632 {
633 max_height * col.marble_mid
634 } else {
635 0.0
636 },
637 )
638 .max(0.0)
639 } else {
640 0.0
641 };
642
643 let basalt = if biome.fire > 0.5 {
644 FastNoise2d::new(36)
645 .get(wpos2d.map(|e| e as f64 / 16.0))
646 .mul(1.25)
647 .sub(0.75)
648 .max(0.0)
649 .mul(((cave_width + max_height) / 64.0).clamped(0.0, 1.0))
650 .mul(6.0 + cavern_height * 0.5)
651 .mul((biome.fire - 0.5).powi(3) * 8.0)
652 } else {
653 0.0
654 };
655
656 let lava = if biome.fire > 0.5 {
657 FastNoise2d::new(37)
658 .get(wpos2d.map(|e| e as f64 / 32.0))
659 .mul(0.5)
660 .abs()
661 .sub(0.2)
662 .min(0.0)
663 .mul((cave_width / 16.0).clamped(0.5, 1.0))
665 .mul((biome.fire - 0.5).mul(30.0).clamped(0.0, 1.0))
666 .mul(64.0)
667 .max(-32.0)
668 } else {
669 0.0
670 };
671
672 let bump = if biome
673 .sandy
674 .max(biome.dusty)
675 .max(biome.leafy)
676 .max(biome.barren)
677 > 0.0
678 {
679 FastNoise2d::new(38)
680 .get(wpos2d.map(|e| e as f64 / 4.0))
681 .mul(1.15)
682 .add(1.0)
683 .mul(0.5)
684 .mul(((col.alt - z_range.end as f32) / 16.0).clamped(0.0, 1.0))
685 .mul({
686 let (val, total) = [
687 (biome.sandy, 0.9),
688 (biome.dusty, 0.5),
689 (biome.leafy, 0.6),
690 (biome.barren, 0.6),
691 ]
692 .into_iter()
693 .fold((0.0, 0.0), |a, x| (a.0 + x.0.max(0.0) * x.1, a.1 + x.1));
694 val / total
695 })
696 .mul(cavern_height * 0.2)
697 .clamped(0.0, 4.0)
698 } else {
699 0.0
700 };
701
702 let rand = RandomField::new(37 + level);
703
704 let is_ice = biome.icy + col.marble * 0.2 > 0.5 && col.marble > 0.6;
705 let is_snow = biome.snowy + col.marble_mid * 0.2 > 0.5 && col.marble_mid > 0.6;
706
707 let (has_stalagmite, has_stalactite) = if biome.icy > 0.5 {
708 (col.marble_mid > 0.6, col.marble_mid < 0.5)
709 } else {
710 (true, true)
711 };
712
713 let stalagmite_only = if has_stalagmite && !has_stalactite {
714 0.6f32
715 } else {
716 0.0f32
717 };
718
719 let dirt = if biome.icy > 0.5 && has_stalagmite && stalactite > 3.0 {
721 0
722 } else {
723 1 + (!is_ice) as i32 + is_snow as i32
724 };
725 let bedrock = z_range.start + lava as i32;
726 let base = bedrock + (stalactite * (0.4 + stalagmite_only)) as i32;
727 let floor = base + dirt + bump as i32;
728 let ceiling =
729 z_range.end - (stalactite * has_stalactite as i32 as f32).max(ceiling_cover) as i32;
730
731 let get_ceiling_drip = |wpos: Vec2<i32>, freq: f64, length: f32| {
732 let wposf = wpos.map(|e| e as f32);
733 let wposf = wposf + wposf.yx() * 1.1;
734 let dims = Vec2::new(11.0, 32.0);
735 let posf = wposf + Vec2::unit_y() * (wposf.x / dims.x).floor() * 89.0 / dims;
736 let pos = posf.map(|e| e.floor() as i32);
737 if rand.chance(pos.with_z(73), freq as f32) {
738 let drip_length = ((posf.y.fract() - 0.5).abs() * 2.0 * dims.y)
739 .mul(length * 0.01)
740 .powf(2.0)
741 .min(length);
742
743 if (posf.x.fract() * 2.0 - 1.0).powi(2) < 1.0 {
744 drip_length
745 } else {
746 0.0
747 }
748 } else {
749 0.0
750 }
751 };
752
753 let ceiling_drip = ceiling
754 - if !void_above && !sky_above && overlap.is_none() {
755 let c = if biome.mushroom > 0.9 && ceiling_cover > 0.0 {
756 Some((0.07, 7.0))
757 } else if biome.icy > 0.9 {
758 Some((0.1 * col.marble_mid as f64, 9.0))
759 } else {
760 None
761 };
762 if let Some((freq, length)) = c {
763 get_ceiling_drip(wpos2d, freq, length).max(get_ceiling_drip(
764 wpos2d.yx(),
765 freq,
766 length,
767 )) as i32
768 } else {
769 0
770 }
771 } else {
772 0
773 };
774
775 let structures = structure_seeds
776 .iter()
777 .filter_map(|(wpos2d, seed)| {
778 let structure = structure_cache.get(wpos2d.with_z(tunnel.a.depth), |_| {
779 let mut rng = RandomPerm::new(*seed);
780 let (z_range, horizontal, vertical, _) =
781 tunnel.z_range_at(wpos2d.map(|e| e as f64 + 0.5), info)?;
782 let pos = wpos2d.with_z(z_range.start);
783 let biome = tunnel.biome_at(pos, &info);
784 let tunnel_intersection = || {
785 tunnel_bounds_at(pos.xy(), &info, &info.land())
786 .any(|(_, z_range, _, _, _, _)| z_range.contains(&(z_range.start - 1)))
787 };
788
789 if biome.mushroom > 0.7
790 && vertical > 16.0
791 && rng.gen_bool(
792 0.5 * close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2) as f64
793 * close(biome.mushroom, 1.0, 0.7, 1) as f64,
794 )
795 {
796 if tunnel_intersection() {
797 return None;
798 }
799 let purp = rng.gen_range(0..50);
800 Some(CaveStructure::Mushroom(Mushroom {
801 pos,
802 stalk: 8.0
803 + rng.gen::<f32>().powf(2.0)
804 * (z_range.end - z_range.start - 8) as f32
805 * 0.75,
806 head_color: Rgb::new(
807 40 + purp,
808 rng.gen_range(60..120),
809 rng.gen_range(80..200) + purp,
810 ),
811 }))
812 } else if biome.crystal > 0.5
813 && rng.gen_bool(0.4 * close(biome.crystal, 1.0, 0.7, 2) as f64)
814 {
815 if tunnel_intersection() {
816 return None;
817 }
818 let on_ground = rng.gen_bool(0.6);
819 let pos = wpos2d.with_z(if on_ground {
820 z_range.start
821 } else {
822 z_range.end
823 });
824
825 let max_length = (48.0 * close(vertical, MAX_RADIUS, MAX_RADIUS, 1)).max(12.0);
826 let length = rng.gen_range(8.0..max_length);
827 let radius =
828 Lerp::lerp(2.0, 4.5, length / max_length + rng.gen_range(-0.1..0.1));
829 let dir = Vec3::new(
830 rng.gen_range(-3.0..3.0),
831 rng.gen_range(-3.0..3.0),
832 rng.gen_range(0.5..10.0) * if on_ground { 1.0 } else { -1.0 },
833 )
834 .normalized();
835
836 let mut gen_crystal = || Crystal {
837 dir: Vec3::new(
838 rng.gen_range(-1.0..1.0),
839 rng.gen_range(-1.0..1.0),
840 (dir.z + rng.gen_range(-0.2..0.2)).clamped(0.0, 1.0),
841 ),
842 length: length * rng.gen_range(0.3..0.8),
843 radius: (radius * rng.gen_range(0.5..0.8)).max(1.0),
844 };
845
846 let crystals = [
847 Crystal {
848 dir,
849 length,
850 radius,
851 },
852 gen_crystal(),
853 gen_crystal(),
854 gen_crystal(),
855 gen_crystal(),
856 ];
857
858 let purple = rng.gen_range(25..75);
859 let blue = (rng.gen_range(45.0..75.0) * biome.icy) as u8;
860 Some(CaveStructure::Crystal(CrystalCluster {
861 pos,
862 crystals,
863 color: Rgb::new(
864 255 - blue * 2,
865 255 - blue - purple,
866 200 + rng.gen_range(25..55),
867 ),
868 }))
869 } else if biome.leafy > 0.8
870 && vertical > 16.0
871 && horizontal > 16.0
872 && rng.gen_bool(
873 0.25 * (close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2)
874 * close(horizontal, MAX_RADIUS, MAX_RADIUS - 16.0, 2)
875 * biome.leafy) as f64,
876 )
877 {
878 if tunnel_intersection() {
879 return None;
880 }
881 let petal_radius = rng.gen_range(8.0..16.0);
882 Some(CaveStructure::Flower(Flower {
883 pos,
884 stalk: 6.0
885 + rng.gen::<f32>().powf(2.0)
886 * (z_range.end - z_range.start - 8) as f32
887 * 0.75,
888 petals: rng.gen_range(1..5) * 2 + 1,
889 petal_height: 0.4 * petal_radius * (1.0 + rng.gen::<f32>().powf(2.0)),
890 petal_radius,
891 }))
892 } else if (biome.leafy > 0.7 || giant_tree_dist > 0.0)
893 && rng.gen_bool(
894 (0.5 * close(biome.leafy, 1.0, 0.5, 1).max(1.0 + giant_tree_dist) as f64)
895 .clamped(0.0, 1.0),
896 )
897 {
898 if tunnel_intersection() {
899 return None;
900 }
901 Some(CaveStructure::GiantRoot {
902 pos,
903 radius: rng.gen_range(
904 2.5..(3.5
905 + close(vertical, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0
906 + close(horizontal, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0),
907 ),
908 height: (z_range.end - z_range.start) as f32,
909 })
910 } else {
911 None
912 }
913 });
914
915 structure
917 .as_ref()
918 .map(|structure| (*seed, structure.clone()))
919 })
920 .collect_vec();
921 let get_structure = |wpos: Vec3<i32>, dynamic_rng: &mut R| {
922 let warp = |wposf: Vec3<f64>, freq: f64, amp: Vec3<f32>, seed: u32| -> Vec3<f32> {
923 let xy = wposf.xy();
924 let xz = Vec2::new(wposf.x, wposf.z);
925 let yz = Vec2::new(wposf.y, wposf.z);
926 Vec3::new(
927 FastNoise2d::new(seed).get(yz * freq),
928 FastNoise2d::new(seed).get(xz * freq),
929 FastNoise2d::new(seed).get(xy * freq),
930 ) * amp
931 };
932 for (seed, structure) in structures.iter() {
933 let seed = *seed;
934 match structure {
935 CaveStructure::Mushroom(mushroom) => {
936 let wposf = wpos.map(|e| e as f64);
937 let warp_freq = 1.0 / 32.0;
938 let warp_amp = Vec3::new(12.0, 12.0, 12.0);
939 let warp_offset = warp(wposf, warp_freq, warp_amp, seed);
940 let wposf_warped = wposf.map(|e| e as f32)
941 + warp_offset
942 * (wposf.z as f32 - mushroom.pos.z as f32)
943 .mul(0.1)
944 .clamped(0.0, 1.0);
945
946 let rpos = wposf_warped - mushroom.pos.map(|e| e as f32);
947
948 let stalk_radius = 2.5f32;
949 let head_radius = 12.0f32;
950 let head_height = 14.0;
951
952 let dist_sq = rpos.xy().magnitude_squared();
953 if dist_sq < head_radius.powi(2) {
954 let dist = dist_sq.sqrt();
955 let head_dist = ((rpos - Vec3::unit_z() * mushroom.stalk)
956 / Vec2::broadcast(head_radius).with_z(head_height))
957 .magnitude();
958
959 let stalk = mushroom.stalk
960 + Lerp::lerp_unclamped(head_height * 0.5, 0.0, dist / head_radius);
961
962 if rpos.z > stalk
964 && rpos.z <= mushroom.stalk + head_height
965 && dist
966 < head_radius
967 * (1.0 - (rpos.z - mushroom.stalk) / head_height).powf(0.125)
968 {
969 if head_dist < 0.85 {
970 let radial = (rpos.x.atan2(rpos.y) * 10.0).sin() * 0.5 + 0.5;
971 let block_kind = if dynamic_rng.gen_bool(0.1) {
972 BlockKind::GlowingMushroom
973 } else {
974 BlockKind::Rock
975 };
976 return Some(Block::new(
977 block_kind,
978 Rgb::new(
979 30,
980 120 + (radial * 40.0) as u8,
981 180 - (radial * 40.0) as u8,
982 ),
983 ));
984 } else if head_dist < 1.0 {
985 return Some(Block::new(BlockKind::Wood, mushroom.head_color));
986 }
987 }
988
989 if rpos.z <= mushroom.stalk + head_height - 1.0
990 && dist_sq
991 < (stalk_radius * Lerp::lerp(1.5, 0.75, rpos.z / mushroom.stalk))
992 .powi(2)
993 {
994 return Some(Block::new(BlockKind::Wood, Rgb::new(25, 60, 90)));
996 } else if ((mushroom.stalk - 0.1)..(mushroom.stalk + 0.9)).contains(&rpos.z) && dist > head_radius * 0.85
998 && dynamic_rng.gen_bool(0.1)
999 {
1000 use SpriteKind::*;
1001 let sprites = if dynamic_rng.gen_bool(0.1) {
1002 &[Beehive, Lantern] as &[_]
1003 } else {
1004 &[MycelBlue, MycelBlue] as &[_]
1005 };
1006 return Some(Block::air(*sprites.choose(dynamic_rng).unwrap()));
1007 }
1008 }
1009 },
1010 CaveStructure::Crystal(cluster) => {
1011 let wposf = wpos.map(|e| e as f32);
1012 let cluster_pos = cluster.pos.map(|e| e as f32);
1013 for crystal in &cluster.crystals {
1014 let line = LineSegment3 {
1015 start: cluster_pos,
1016 end: cluster_pos + crystal.dir * crystal.length,
1017 };
1018
1019 let projected = line.projected_point(wposf);
1020 let dist_sq = projected.distance_squared(wposf);
1021 if dist_sq < crystal.radius.powi(2) {
1022 let rpos = wposf - cluster_pos;
1023 let line_length = line.start.distance_squared(line.end);
1024 let taper = if line_length < 0.001 {
1025 0.0
1026 } else {
1027 rpos.dot(line.end - line.start) / line_length
1028 };
1029
1030 let peak_cutoff = 0.8;
1031 let taper_factor = 0.55;
1032 let peak_taper = 0.3;
1033
1034 let crystal_radius = if taper > peak_cutoff {
1035 let taper = (taper - peak_cutoff) * 5.0;
1036 Lerp::lerp_unclamped(
1037 crystal.radius * taper_factor,
1038 crystal.radius * peak_taper,
1039 taper,
1040 )
1041 } else {
1042 let taper = taper * 1.25;
1043 Lerp::lerp_unclamped(
1044 crystal.radius,
1045 crystal.radius * taper_factor,
1046 taper,
1047 )
1048 };
1049
1050 if dist_sq < crystal_radius.powi(2) {
1051 if dist_sq / crystal_radius.powi(2) > 0.75 {
1052 return Some(Block::new(BlockKind::GlowingRock, cluster.color));
1053 } else {
1054 return Some(Block::new(BlockKind::Rock, cluster.color));
1055 }
1056 }
1057 }
1058 }
1059 },
1060 CaveStructure::Flower(flower) => {
1061 let wposf = wpos.map(|e| e as f64);
1062 let warp_freq = 1.0 / 16.0;
1063 let warp_amp = Vec3::new(8.0, 8.0, 8.0);
1064 let warp_offset = warp(wposf, warp_freq, warp_amp, seed);
1065 let wposf_warped = wposf.map(|e| e as f32)
1066 + warp_offset
1067 * (wposf.z as f32 - flower.pos.z as f32)
1068 .mul(1.0 / flower.stalk)
1069 .sub(1.0)
1070 .min(0.0)
1071 .abs()
1072 .clamped(0.0, 1.0);
1073 let rpos = wposf_warped - flower.pos.map(|e| e as f32);
1074
1075 let stalk_radius = 2.5f32;
1076 let petal_thickness = 2.5;
1077 let dist_sq = rpos.xy().magnitude_squared();
1078 if rpos.z < flower.stalk
1079 && dist_sq
1080 < (stalk_radius
1081 * Lerp::lerp_unclamped(1.0, 0.75, rpos.z / flower.stalk))
1082 .powi(2)
1083 {
1084 return Some(Block::new(BlockKind::Wood, Rgb::new(0, 108, 0)));
1085 }
1086
1087 let rpos = rpos - Vec3::unit_z() * flower.stalk;
1088 let dist_sq = rpos.xy().magnitude_squared();
1089 let petal_radius_sq = flower.petal_radius.powi(2);
1090 if dist_sq < petal_radius_sq {
1091 let petal_height_at = (dist_sq / petal_radius_sq) * flower.petal_height;
1092 if rpos.z > petal_height_at - 1.0
1093 && rpos.z <= petal_height_at + petal_thickness
1094 {
1095 let dist_ratio = dist_sq / petal_radius_sq;
1096 let yellow = (60.0 * dist_ratio) as u8;
1097 let near = rpos
1098 .x
1099 .atan2(rpos.y)
1100 .rem_euclid(std::f32::consts::TAU / flower.petals as f32);
1101 if dist_ratio < 0.175 {
1102 let red = close(near, 0.0, 0.5, 1);
1103 let purple = close(near, 0.0, 0.35, 1);
1104 if dist_ratio > red || rpos.z < petal_height_at {
1105 return Some(Block::new(
1106 BlockKind::ArtLeaves,
1107 Rgb::new(240, 80 - yellow, 80 - yellow),
1108 ));
1109 } else if dist_ratio > purple {
1110 return Some(Block::new(
1111 BlockKind::ArtLeaves,
1112 Rgb::new(200, 14, 132),
1113 ));
1114 } else {
1115 return Some(Block::new(
1116 BlockKind::ArtLeaves,
1117 Rgb::new(249, 156, 218),
1118 ));
1119 }
1120 } else {
1121 let inset = close(near, -1.0, 1.0, 2).max(close(near, 1.0, 1.0, 2));
1122 if dist_ratio < inset {
1123 return Some(Block::new(
1124 BlockKind::ArtLeaves,
1125 Rgb::new(240, 80 - yellow, 80 - yellow),
1126 ));
1127 }
1128 }
1129 }
1130
1131 let pollen_height = 5.0;
1133 if rpos.z > 0.0
1134 && rpos.z < pollen_height
1135 && dist_sq
1136 < (stalk_radius
1137 * Lerp::lerp_unclamped(0.5, 1.25, rpos.z / pollen_height))
1138 .powi(2)
1139 {
1140 return Some(Block::new(
1141 BlockKind::GlowingWeakRock,
1142 Rgb::new(239, 192, 0),
1143 ));
1144 }
1145 }
1146 },
1147 CaveStructure::GiantRoot {
1148 pos,
1149 radius,
1150 height,
1151 } => {
1152 let wposf = wpos.map(|e| e as f64);
1153 let warp_freq = 1.0 / 16.0;
1154 let warp_amp = Vec3::new(8.0, 8.0, 8.0);
1155 let warp_offset = warp(wposf, warp_freq, warp_amp, seed);
1156 let wposf_warped = wposf.map(|e| e as f32) + warp_offset;
1157 let rpos = wposf_warped - pos.map(|e| e as f32);
1158 let dist_sq = rpos.xy().magnitude_squared();
1159 if dist_sq < radius.powi(2) {
1160 if col.marble_mid
1162 > (std::f32::consts::PI * rpos.z / *height)
1163 .sin()
1164 .powf(2.0)
1165 .mul(0.25)
1166 .add(col.marble_small)
1167 {
1168 return Some(Block::new(BlockKind::Wood, Rgb::new(48, 70, 25)));
1169 }
1170 return Some(Block::new(BlockKind::Wood, Rgb::new(66, 41, 26)));
1171 }
1172 },
1173 }
1174 }
1175 None
1176 };
1177
1178 for z in bedrock..z_range.end {
1179 let wpos = wpos2d.with_z(z);
1180 let mut try_spawn_entity = false;
1181 canvas.set(wpos, {
1182 if z < z_range.start - 4 && !void_below {
1183 Block::new(BlockKind::Lava, Rgb::new(255, 65, 0))
1184 } else if basalt > 0.0
1185 && z < bedrock / 6 * 6
1186 + 2
1187 + basalt as i32 / 4 * 4
1188 + (RandomField::new(77)
1189 .get_f32(((wpos2d + Vec2::new(wpos2d.y, -wpos2d.x) / 2) / 4).with_z(0))
1190 * 6.0)
1191 .floor() as i32
1192 && !void_below
1193 {
1194 Block::new(BlockKind::Rock, Rgb::new(50, 35, 75))
1195 } else if (z < base && !void_below)
1196 || ((z >= ceiling && !void_above)
1197 && !(ceiling_cover > 0.0 && overlap.as_ref().is_some_and(|o| o.contains(&z))))
1198 {
1199 let stalactite: Rgb<i16> = Lerp::lerp_unclamped(
1200 Lerp::lerp_unclamped(
1201 Lerp::lerp_unclamped(
1202 Lerp::lerp_unclamped(
1203 Lerp::lerp_unclamped(
1204 Lerp::lerp_unclamped(
1205 Rgb::new(80, 100, 150),
1206 Rgb::new(23, 44, 88),
1207 biome.mushroom,
1208 ),
1209 Lerp::lerp_unclamped(
1210 Rgb::new(100, 40, 40),
1211 Rgb::new(100, 75, 100),
1212 col.marble_small,
1213 ),
1214 biome.fire,
1215 ),
1216 Lerp::lerp_unclamped(
1217 Rgb::new(238, 198, 139),
1218 Rgb::new(111, 99, 64),
1219 col.marble_mid,
1220 ),
1221 biome.sandy,
1222 ),
1223 Lerp::lerp_unclamped(
1224 Rgb::new(0, 73, 12),
1225 Rgb::new(49, 63, 12),
1226 col.marble_small,
1227 ),
1228 biome.leafy,
1229 ),
1230 Lerp::lerp_unclamped(
1231 Rgb::new(100, 150, 255),
1232 Rgb::new(100, 120, 255),
1233 col.marble,
1234 ),
1235 biome.icy,
1236 ),
1237 Lerp::lerp_unclamped(
1238 Rgb::new(105, 25, 131),
1239 Rgb::new(251, 238, 255),
1240 col.marble_mid,
1241 ),
1242 biome.crystal,
1243 );
1244 Block::new(
1245 if rand.chance(
1246 wpos,
1247 (biome.mushroom * 0.01)
1248 .max(biome.icy * 0.1)
1249 .max(biome.crystal * 0.005),
1250 ) {
1251 BlockKind::GlowingWeakRock
1252 } else if rand.chance(wpos, biome.sandy) {
1253 BlockKind::Sand
1254 } else if rand.chance(wpos, biome.leafy) {
1255 BlockKind::ArtLeaves
1256 } else if ceiling_cover > 0.0 {
1257 BlockKind::Rock
1258 } else {
1259 BlockKind::WeakRock
1260 },
1261 stalactite.map(|e| e as u8),
1262 )
1263 } else if z < ceiling && z >= ceiling_drip {
1264 if biome.mushroom > 0.9 {
1265 let block = if rand.chance(wpos2d.with_z(89), 0.05) {
1266 BlockKind::GlowingMushroom
1267 } else {
1268 BlockKind::GlowingWeakRock
1269 };
1270 Block::new(block, Rgb::new(10, 70, 148))
1271 } else if biome.icy > 0.9 {
1272 Block::new(BlockKind::GlowingWeakRock, Rgb::new(120, 140, 255))
1273 } else {
1274 Block::new(BlockKind::WeakRock, Rgb::new(80, 100, 150))
1275 }
1276 } else if z >= base && z < floor && !void_below && !sky_above {
1277 let (net_col, total) = [
1278 (
1279 Lerp::lerp_unclamped(
1280 Rgb::new(68, 62, 58),
1281 Rgb::new(97, 95, 85),
1282 col.marble_small,
1283 ),
1284 0.05,
1285 ),
1286 (
1287 Lerp::lerp_unclamped(
1288 Rgb::new(66, 37, 30),
1289 Rgb::new(88, 62, 45),
1290 col.marble_mid,
1291 ),
1292 biome.dusty,
1293 ),
1294 (
1295 Lerp::lerp_unclamped(
1296 Rgb::new(20, 65, 175),
1297 Rgb::new(20, 100, 80),
1298 col.marble_mid,
1299 ),
1300 biome.mushroom,
1301 ),
1302 (
1303 Lerp::lerp_unclamped(
1304 Rgb::new(120, 50, 20),
1305 Rgb::new(50, 5, 40),
1306 col.marble_small,
1307 ),
1308 biome.fire,
1309 ),
1310 (
1311 Lerp::lerp_unclamped(
1312 Rgb::new(0, 100, 50),
1313 Rgb::new(80, 100, 20),
1314 col.marble_small,
1315 ),
1316 biome.leafy,
1317 ),
1318 (Rgb::new(170, 195, 255), biome.icy),
1319 (
1320 Lerp::lerp_unclamped(
1321 Rgb::new(105, 25, 131),
1322 Rgb::new(251, 238, 255),
1323 col.marble_mid,
1324 ),
1325 biome.crystal,
1326 ),
1327 (
1328 Lerp::lerp_unclamped(
1329 Rgb::new(201, 174, 116),
1330 Rgb::new(244, 239, 227),
1331 col.marble_small,
1332 ),
1333 biome.sandy,
1334 ),
1335 (
1336 Lerp::lerp_unclamped(
1338 Rgb::new(68, 62, 58),
1339 Rgb::new(97, 95, 85),
1340 col.marble_small,
1341 ),
1342 biome.snowy,
1343 ),
1344 ]
1345 .into_iter()
1346 .fold((Rgb::<f32>::zero(), 0.0), |a, x| {
1347 (a.0 + x.0.map(|e| e as f32) * x.1, a.1 + x.1)
1348 });
1349 let surf_color = net_col.map(|e| (e / total) as u8);
1350
1351 if is_ice {
1352 Block::new(BlockKind::Ice, Rgb::new(120, 160, 255))
1353 } else if is_snow {
1354 Block::new(BlockKind::ArtSnow, Rgb::new(170, 195, 255))
1355 } else {
1356 Block::new(
1357 if biome.mushroom.max(biome.leafy) > 0.5 {
1358 BlockKind::Grass
1359 } else if biome.icy > 0.5 {
1360 BlockKind::ArtSnow
1361 } else if biome.fire.max(biome.snowy) > 0.5 {
1362 BlockKind::Rock
1363 } else if biome.crystal > 0.5 {
1364 if rand.chance(wpos, biome.crystal * 0.02) {
1365 BlockKind::GlowingRock
1366 } else {
1367 BlockKind::Rock
1368 }
1369 } else {
1370 BlockKind::Sand
1371 },
1372 surf_color,
1373 )
1374 }
1375 } else if let Some(sprite) = (z == floor && !void_below && !sky_above)
1376 .then(|| {
1377 if col.marble_mid > 0.55
1378 && biome.mushroom > 0.6
1379 && rand.chance(
1380 wpos2d.with_z(1),
1381 biome.mushroom.powi(2) * 0.2 * col.marble_mid,
1382 )
1383 {
1384 [
1385 (SpriteKind::GlowMushroom, 0.5),
1386 (SpriteKind::Mushroom, 0.25),
1387 (SpriteKind::GrassBlueMedium, 1.5),
1388 (SpriteKind::GrassBlueLong, 2.0),
1389 (SpriteKind::Moonbell, 0.01),
1390 (SpriteKind::SporeReed, 2.5),
1391 ]
1392 .choose_weighted(rng, |(_, w)| *w)
1393 .ok()
1394 .map(|s| s.0)
1395 } else if col.marble_mid > 0.6
1396 && biome.leafy > 0.4
1397 && rand.chance(
1398 wpos2d.with_z(15),
1399 biome.leafy.powi(2) * col.marble_mid * (biome.humidity * 1.3) * 0.25,
1400 )
1401 {
1402 let mixed = col.marble.add(col.marble_small.sub(0.5).mul(0.25));
1403 if (0.25..0.45).contains(&mixed) || (0.55..0.75).contains(&mixed) {
1404 return [
1405 (SpriteKind::LongGrass, 1.0),
1406 (SpriteKind::MediumGrass, 2.0),
1407 (SpriteKind::JungleFern, 0.5),
1408 (SpriteKind::JungleRedGrass, 0.35),
1409 (SpriteKind::Fern, 0.75),
1410 (SpriteKind::LeafyPlant, 0.8),
1411 (SpriteKind::JungleLeafyPlant, 0.5),
1412 (SpriteKind::LanternPlant, 0.1),
1413 (SpriteKind::LanternFlower, 0.1),
1414 (SpriteKind::LushFlower, 0.2),
1415 ]
1416 .choose_weighted(rng, |(_, w)| *w)
1417 .ok()
1418 .map(|s| s.0);
1419 } else if (0.0..0.25).contains(&mixed) {
1420 return [(Some(SpriteKind::LanternPlant), 0.5), (None, 0.5)]
1421 .choose_weighted(rng, |(_, w)| *w)
1422 .ok()
1423 .and_then(|s| s.0);
1424 } else if (0.75..1.0).contains(&mixed) {
1425 return [(Some(SpriteKind::LushFlower), 0.6), (None, 0.4)]
1426 .choose_weighted(rng, |(_, w)| *w)
1427 .ok()
1428 .and_then(|s| s.0);
1429 } else {
1430 return [
1431 (SpriteKind::LongGrass, 1.0),
1432 (SpriteKind::MediumGrass, 2.0),
1433 (SpriteKind::JungleFern, 0.5),
1434 (SpriteKind::JungleLeafyPlant, 0.5),
1435 (SpriteKind::JungleRedGrass, 0.35),
1436 (SpriteKind::Mushroom, 0.15),
1437 (SpriteKind::EnsnaringVines, 0.2),
1438 (SpriteKind::Fern, 0.75),
1439 (SpriteKind::LeafyPlant, 0.8),
1440 (SpriteKind::Twigs, 0.07),
1441 (SpriteKind::Wood, 0.03),
1442 (SpriteKind::LanternPlant, 0.3),
1443 (SpriteKind::LanternFlower, 0.3),
1444 (SpriteKind::LushFlower, 0.5),
1445 (SpriteKind::LushMushroom, 1.0),
1446 ]
1447 .choose_weighted(rng, |(_, w)| *w)
1448 .ok()
1449 .map(|s| s.0);
1450 }
1451 } else if rand.chance(wpos2d.with_z(2), biome.dusty.max(biome.sandy) * 0.01) {
1452 [
1453 (SpriteKind::Bones, 0.5),
1454 (SpriteKind::Stones, 1.5),
1455 (SpriteKind::DeadBush, 1.0),
1456 (SpriteKind::DeadPlant, 1.5),
1457 (SpriteKind::EnsnaringWeb, 0.5),
1458 (SpriteKind::Mud, 0.025),
1459 ]
1460 .choose_weighted(rng, |(_, w)| *w)
1461 .ok()
1462 .map(|s| s.0)
1463 } else if rand.chance(wpos2d.with_z(14), biome.barren * 0.003) {
1464 [
1465 (SpriteKind::Bones, 0.5),
1466 (SpriteKind::Welwitch, 0.5),
1467 (SpriteKind::DeadBush, 1.5),
1468 (SpriteKind::DeadPlant, 1.5),
1469 (SpriteKind::RockyMushroom, 1.5),
1470 (SpriteKind::Crate, 0.005),
1471 ]
1472 .choose_weighted(rng, |(_, w)| *w)
1473 .ok()
1474 .map(|s| s.0)
1475 } else if rand.chance(wpos2d.with_z(3), biome.crystal * 0.005) {
1476 Some(SpriteKind::CrystalLow)
1477 } else if rand.chance(wpos2d.with_z(13), biome.fire * 0.0006) {
1478 [
1479 (SpriteKind::Pyrebloom, 0.3),
1480 (SpriteKind::Bloodstone, 0.3),
1481 (SpriteKind::Gold, 0.2),
1482 ]
1483 .choose_weighted(rng, |(_, w)| *w)
1484 .ok()
1485 .map(|(s, _)| *s)
1486 } else if biome.icy > 0.5 && rand.chance(wpos2d.with_z(23), biome.icy * 0.005) {
1487 Some(SpriteKind::IceCrystal)
1488 } else if biome.icy > 0.5
1489 && rand.chance(wpos2d.with_z(31), biome.icy * biome.mineral * 0.005)
1490 {
1491 Some(SpriteKind::GlowIceCrystal)
1492 } else if rand.chance(wpos2d.with_z(5), 0.0015) {
1493 [
1494 (Some(SpriteKind::VeloriteFrag), 0.3),
1495 (Some(SpriteKind::Velorite), 0.15),
1496 (Some(SpriteKind::Amethyst), 0.15),
1497 (Some(SpriteKind::Topaz), 0.15),
1498 (Some(SpriteKind::Diamond), 0.02),
1499 (Some(SpriteKind::Ruby), 0.05),
1500 (Some(SpriteKind::Emerald), 0.04),
1501 (Some(SpriteKind::Sapphire), 0.04),
1502 (None, 10.0),
1503 ]
1504 .choose_weighted(rng, |(_, w)| *w)
1505 .ok()
1506 .and_then(|s| s.0)
1507 } else if rand.chance(wpos2d.with_z(6), 0.0002) {
1508 [
1509 (Some(SpriteKind::DungeonChest0), 1.0),
1510 (Some(SpriteKind::DungeonChest1), 0.3),
1511 (Some(SpriteKind::DungeonChest2), 0.1),
1512 (Some(SpriteKind::DungeonChest3), 0.03),
1513 (Some(SpriteKind::DungeonChest4), 0.01),
1514 (Some(SpriteKind::DungeonChest5), 0.003),
1515 (None, 1.0),
1516 ]
1517 .choose_weighted(rng, |(_, w)| *w)
1518 .ok()
1519 .and_then(|s| s.0)
1520 } else if rand.chance(wpos2d.with_z(7), 0.007) {
1521 let shallow = close(biome.depth, 0.0, 0.4, 3);
1522 let middle = close(biome.depth, 0.5, 0.4, 3);
1523 [
1526 (Some(SpriteKind::Stones), 1.5),
1527 (Some(SpriteKind::Copper), shallow),
1528 (Some(SpriteKind::Tin), shallow),
1529 (Some(SpriteKind::Iron), shallow * 0.6),
1530 (Some(SpriteKind::Coal), middle * 0.25),
1531 (Some(SpriteKind::Cobalt), middle * 0.1),
1532 (Some(SpriteKind::Silver), middle * 0.05),
1533 (None, 10.0),
1534 ]
1535 .choose_weighted(rng, |(_, w)| *w)
1536 .ok()
1537 .and_then(|s| s.0)
1538 } else {
1539 try_spawn_entity = true;
1540 None
1541 }
1542 })
1543 .flatten()
1544 {
1545 Block::air(sprite)
1546 } else if let Some(sprite) = (z == ceiling - 1 && !void_above)
1547 .then(|| {
1548 if biome.mushroom > 0.5
1549 && rand.chance(wpos2d.with_z(3), biome.mushroom.powi(2) * 0.01)
1550 {
1551 [(SpriteKind::MycelBlue, 0.75), (SpriteKind::Mold, 1.0)]
1552 .choose_weighted(rng, |(_, w)| *w)
1553 .ok()
1554 .map(|s| s.0)
1555 } else if biome.leafy > 0.4
1556 && rand.chance(
1557 wpos2d.with_z(4),
1558 biome.leafy * (biome.humidity * 1.3) * 0.015,
1559 )
1560 {
1561 [
1562 (SpriteKind::Liana, 1.5),
1563 (SpriteKind::CeilingLanternPlant, 1.25),
1564 (SpriteKind::CeilingLanternFlower, 1.0),
1565 (SpriteKind::CeilingJungleLeafyPlant, 1.5),
1566 ]
1567 .choose_weighted(rng, |(_, w)| *w)
1568 .ok()
1569 .map(|s| s.0)
1570 } else if rand.chance(wpos2d.with_z(5), biome.barren * 0.015) {
1571 Some(SpriteKind::Root)
1572 } else if rand.chance(wpos2d.with_z(5), biome.crystal * 0.005) {
1573 Some(SpriteKind::CrystalHigh)
1574 } else {
1575 None
1576 }
1577 })
1578 .flatten()
1579 {
1580 Block::air(sprite)
1581 } else if let Some(structure_block) = get_structure(wpos, rng) {
1582 structure_block
1583 } else {
1584 Block::empty()
1585 }
1586 });
1587
1588 if try_spawn_entity {
1589 apply_entity_spawns(canvas, wpos, &biome, rng);
1590 }
1591 }
1592}
1593
1594fn apply_entity_spawns<R: Rng>(canvas: &mut Canvas, wpos: Vec3<i32>, biome: &Biome, rng: &mut R) {
1596 if RandomField::new(canvas.info().index().seed).chance(wpos, 0.035) {
1597 if let Some(entity_asset) = [
1598 (
1600 Some("common.entity.wild.aggressive.goblin_thug"),
1601 biome.mushroom + 0.02,
1602 0.35,
1603 0.5,
1604 ),
1605 (
1606 Some("common.entity.wild.aggressive.goblin_chucker"),
1607 biome.mushroom + 0.02,
1608 0.35,
1609 0.5,
1610 ),
1611 (
1612 Some("common.entity.wild.aggressive.goblin_ruffian"),
1613 biome.mushroom + 0.02,
1614 0.35,
1615 0.5,
1616 ),
1617 (
1618 Some("common.entity.wild.peaceful.truffler"),
1619 biome.mushroom + 0.02,
1620 0.35,
1621 0.5,
1622 ),
1623 (
1624 Some("common.entity.wild.peaceful.fungome"),
1625 biome.mushroom + 0.02,
1626 0.5,
1627 0.5,
1628 ),
1629 (
1630 Some("common.entity.wild.peaceful.bat"),
1631 biome.mushroom + 0.1,
1632 0.25,
1633 0.5,
1634 ),
1635 (
1637 Some("common.entity.wild.aggressive.goblin_thug"),
1638 biome.leafy.max(biome.dusty) + 0.05,
1639 0.25,
1640 0.5,
1641 ),
1642 (
1643 Some("common.entity.wild.aggressive.goblin_chucker"),
1644 biome.leafy.max(biome.dusty) + 0.05,
1645 0.25,
1646 0.5,
1647 ),
1648 (
1649 Some("common.entity.wild.aggressive.goblin_ruffian"),
1650 biome.leafy.max(biome.dusty) + 0.05,
1651 0.25,
1652 0.5,
1653 ),
1654 (
1655 Some("common.entity.wild.peaceful.holladon"),
1656 biome.leafy.max(biome.dusty) + 0.05,
1657 0.25,
1658 0.5,
1659 ),
1660 (
1661 Some("common.entity.dungeon.gnarling.mandragora"),
1662 biome.leafy + 0.05,
1663 0.2,
1664 0.5,
1665 ),
1666 (
1667 Some("common.entity.wild.aggressive.rootsnapper"),
1668 biome.leafy + 0.05,
1669 0.075,
1670 0.5,
1671 ),
1672 (
1673 Some("common.entity.wild.aggressive.maneater"),
1674 biome.leafy + 0.05,
1675 0.075,
1676 0.5,
1677 ),
1678 (
1679 Some("common.entity.wild.aggressive.batfox"),
1680 biome
1681 .leafy
1682 .max(biome.barren)
1683 .max(biome.sandy)
1684 .max(biome.snowy)
1685 + 0.3,
1686 0.25,
1687 0.5,
1688 ),
1689 (
1690 Some("common.entity.wild.peaceful.crawler_moss"),
1691 biome.leafy + 0.05,
1692 0.25,
1693 0.5,
1694 ),
1695 (
1696 Some("common.entity.wild.aggressive.asp"),
1697 biome.leafy.max(biome.sandy) + 0.1,
1698 0.2,
1699 0.5,
1700 ),
1701 (
1702 Some("common.entity.wild.aggressive.swamp_troll"),
1703 biome.leafy + 0.0,
1704 0.05,
1705 0.5,
1706 ),
1707 (
1708 Some("common.entity.wild.peaceful.bat"),
1709 biome.leafy + 0.1,
1710 0.25,
1711 0.5,
1712 ),
1713 (
1715 Some("common.entity.wild.aggressive.dodarock"),
1716 biome
1717 .dusty
1718 .max(biome.barren)
1719 .max(biome.crystal)
1720 .max(biome.snowy)
1721 + 0.05,
1722 0.05,
1723 0.5,
1724 ),
1725 (
1726 Some("common.entity.wild.aggressive.cave_spider"),
1727 biome.dusty + 0.0,
1728 0.05,
1729 0.5,
1730 ),
1731 (
1732 Some("common.entity.wild.aggressive.cave_troll"),
1733 biome.dusty + 0.1,
1734 0.05,
1735 0.5,
1736 ),
1737 (
1738 Some("common.entity.wild.peaceful.rat"),
1739 biome.dusty + 0.1,
1740 0.3,
1741 0.5,
1742 ),
1743 (
1744 Some("common.entity.wild.peaceful.bat"),
1745 biome.dusty.max(biome.sandy).max(biome.snowy) + 0.1,
1746 0.25,
1747 0.5,
1748 ),
1749 (
1751 Some("common.entity.wild.aggressive.icedrake"),
1752 biome.icy + 0.0,
1753 0.1,
1754 0.5,
1755 ),
1756 (
1757 Some("common.entity.wild.aggressive.wendigo"),
1758 biome.icy.min(biome.depth) + 0.0,
1759 0.02,
1760 0.5,
1761 ),
1762 (
1763 Some("common.entity.wild.aggressive.frostfang"),
1764 biome.icy + 0.0,
1765 0.25,
1766 0.5,
1767 ),
1768 (
1769 Some("common.entity.wild.aggressive.tursus"),
1770 biome.icy + 0.0,
1771 0.03,
1772 0.5,
1773 ),
1774 (
1776 Some("common.entity.wild.aggressive.lavadrake"),
1777 biome.fire + 0.0,
1778 0.15,
1779 0.5,
1780 ),
1781 (
1782 Some("common.entity.wild.peaceful.crawler_molten"),
1783 biome.fire + 0.0,
1784 0.2,
1785 0.5,
1786 ),
1787 (
1788 Some("common.entity.wild.aggressive.cave_salamander"),
1789 biome.fire + 0.0,
1790 0.4,
1791 0.5,
1792 ),
1793 (
1794 Some("common.entity.wild.aggressive.red_oni"),
1795 biome.fire + 0.0,
1796 0.03,
1797 0.5,
1798 ),
1799 (
1801 Some("common.entity.wild.aggressive.basilisk"),
1802 biome.crystal + 0.1,
1803 0.1,
1804 0.5,
1805 ),
1806 (
1807 Some("common.entity.wild.aggressive.blue_oni"),
1808 biome.crystal + 0.0,
1809 0.03,
1810 0.5,
1811 ),
1812 (
1814 Some("common.entity.wild.aggressive.antlion"),
1815 biome.sandy.max(biome.dusty) + 0.1,
1816 0.025,
1817 0.5,
1818 ),
1819 (
1820 Some("common.entity.wild.aggressive.sandshark"),
1821 biome.sandy + 0.1,
1822 0.025,
1823 0.5,
1824 ),
1825 (
1826 Some("common.entity.wild.peaceful.crawler_sand"),
1827 biome.sandy + 0.1,
1828 0.25,
1829 0.5,
1830 ),
1831 (
1833 Some("common.entity.wild.aggressive.akhlut"),
1834 (biome.snowy.max(biome.icy) + 0.1),
1835 0.01,
1836 0.5,
1837 ),
1838 (
1839 Some("common.entity.wild.aggressive.rocksnapper"),
1840 biome.barren.max(biome.crystal).max(biome.snowy) + 0.1,
1841 0.1,
1842 0.5,
1843 ),
1844 (
1846 Some("common.entity.wild.aggressive.black_widow"),
1847 biome.depth + 0.0,
1848 0.02,
1849 0.5,
1850 ),
1851 (
1852 Some("common.entity.wild.aggressive.ogre"),
1853 biome.depth + 0.0,
1854 0.02,
1855 0.5,
1856 ),
1857 (None, 100.0, 0.0, 0.0),
1858 ]
1859 .iter()
1860 .filter_map(|(entity, biome_modifier, chance, cutoff)| {
1861 if let Some(entity) = entity {
1862 if *biome_modifier > *cutoff {
1863 let close = close(1.0, *biome_modifier, *cutoff, 2);
1864 (close > 0.0).then(|| (Some(entity), close * chance))
1865 } else {
1866 None
1867 }
1868 } else {
1869 Some((None, 100.0))
1870 }
1871 })
1872 .collect_vec()
1873 .choose_weighted(rng, |(_, w)| *w)
1874 .ok()
1875 .and_then(|s| s.0)
1876 {
1877 canvas.spawn(EntityInfo::at(wpos.map(|e| e as f32)).with_asset_expect(
1878 entity_asset,
1879 rng,
1880 None,
1881 ));
1882 }
1883 }
1884
1885 }