veloren_world/layer/
cave.rs

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 there is water here there most likely isn't an accessible entrance.
86            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    // #[inline_tweak::tweak_fn]
192    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        // Below the ground
198        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        // TODO think about making rate of change of humidity and temp noise higher to
203        // effectively increase biome size
204        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            // Default biome, no other conditions apply
247            let barren = 0.01;
248            // Mushrooms grow underground and thrive in a humid environment with moderate
249            // temperatures
250            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            // Extremely hot and dry areas deep underground
255            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            // Overgrown with plants that need a moderate climate to survive
260            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            // Cool temperature, dry and devoid of value
265            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            // Deep underground and freezing cold
269            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            // Rocky cold cave that appear near the surface
274            let snowy = close(temp, -1.8, 1.3, 4) * close(depth, 0.0, 0.6, 4);
275            // Crystals grow deep underground in areas rich with minerals. They are present
276            // in areas with colder temperatures and low humidity
277            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            // Hot, dry and shallow
283            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                // Avoid cave entrances intersecting water
407                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            // First, clear out tunnels
479            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            // Compute structure samples only once for each column.
491            // TODO Use iter function in StructureGen2d to compute samples for the whole
492            // chunk once
493            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    // rotation: Mat3<f32>,
571}
572
573// #[inline_tweak::tweak_fn]
574fn 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    // Exposed to the sky, or some other void above
591    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    // Exposed to the sky
594    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    // Get the range, if there is any, where the current cave overlaps with other
600    // caves. Right now this is only used to prevent ceiling cover from being
601    // place
602    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            // No stalactites near entrances
620            .mul(((col.alt - z_range.end as f32) / 32.0).clamped(0.0, 1.0))
621            // Prevent stalactites from blocking cave
622            .mul(((cave_width + max_height) / 40.0).clamped(0.0, 1.0))
623            // Scale with cavern height, sandy and icy biomes have bigger stalactites
624            .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 because at some point we maybe want to use some noise value here instead
635        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((biome.temp as f64 - 1.5).mul(30.0).clamped(0.0, 1.0))
679            .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    // Don't cover stalgmites in ice biome with surface block
735    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            // TODO Some way to not clone here?
931            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                        // Head
978                        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                            // Stalk
1010                            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) // Hanging orbs
1012                    && 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                        // pollen
1147                        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                        // Moss
1176                        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                        // Same as barren
1353                        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                        // Here as an example of using sprite_cfg to override
1538                        // default loot table
1539                        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                        //let deep = close(biome.depth, 1.0, 0.4); // TODO: Use this for deep only
1550                        // things
1551                        [
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
1623// #[inline_tweak::tweak_fn]
1624fn 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            // Mushroom biome
1628            (
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            // Leafy biome
1665            (
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            // Dusty biome
1743            (
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            // Icy biome
1779            (
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            // Lava biome
1804            (
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            // Crystal biome
1829            (
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            // Sandy biome
1842            (
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            // Snowy biome
1861            (
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            // With depth
1874            (
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    // FIXME: Add back waypoints once caves are not impossible to escape.
1915    /* // Occasionally place down a waypoint
1916    if RandomField::new(canvas.info().index().seed).chance(wpos, 0.000005) {
1917        canvas.spawn(EntityInfo::at(wpos.map(|e| e as f32)).into_waypoint());
1918    } */
1919}