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