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::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::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                if let SiteKind::GiantTree(t) = &info.index.sites.get(*site).kind {
463                    Some(t.origin.distance_squared(info.wpos) as f32 / t.radius().powi(2))
464                } else {
465                    None
466                }
467            })
468            .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
469            .unwrap_or(0.0)
470            .clamp(0.0, 1.0);
471        let mut structure_cache = SmallCache::default();
472        canvas.foreach_col(|canvas, wpos2d, col| {
473            let tunnel_bounds =
474                tunnel_bounds_at_from(wpos2d, &info, &land, tunnels.iter().copied())
475                    .collect::<Vec<_>>();
476
477            // First, clear out tunnels
478            for (_, z_range, _, _, _, _) in &tunnel_bounds {
479                for z in z_range.start..z_range.end.min(col.alt as i32 + 1) {
480                    canvas.set(wpos2d.with_z(z), Block::empty());
481                }
482            }
483
484            let z_ranges = &tunnel_bounds
485                .iter()
486                .map(|(_, z_range, _, _, _, _)| z_range.clone())
487                .collect_vec();
488
489            // Compute structure samples only once for each column.
490            // TODO Use iter function in StructureGen2d to compute samples for the whole
491            // chunk once
492            let structure_seeds = StructureGen2d::new(34537, 24, 8).get(wpos2d);
493            for (level, z_range, horizontal, vertical, dist, tunnel) in tunnel_bounds {
494                write_column(
495                    canvas,
496                    col,
497                    level,
498                    wpos2d,
499                    z_range.clone(),
500                    z_ranges,
501                    tunnel,
502                    (horizontal, vertical, dist),
503                    giant_tree_dist,
504                    &mut structure_cache,
505                    &structure_seeds,
506                    rng,
507                );
508            }
509        });
510    }
511}
512
513#[derive(Default, Clone)]
514pub struct Biome {
515    humidity: f32,
516    pub barren: f32,
517    mineral: f32,
518    pub mushroom: f32,
519    pub fire: f32,
520    pub leafy: f32,
521    pub dusty: f32,
522    pub icy: f32,
523    pub snowy: f32,
524    pub crystal: f32,
525    pub sandy: f32,
526    depth: f32,
527}
528
529#[derive(Clone)]
530enum CaveStructure {
531    Mushroom(Mushroom),
532    Crystal(CrystalCluster),
533    Flower(Flower),
534    GiantRoot {
535        pos: Vec3<i32>,
536        radius: f32,
537        height: f32,
538    },
539}
540
541#[derive(Clone)]
542struct Mushroom {
543    pos: Vec3<i32>,
544    stalk: f32,
545    head_color: Rgb<u8>,
546}
547
548#[derive(Clone)]
549struct Crystal {
550    dir: Vec3<f32>,
551    length: f32,
552    radius: f32,
553}
554
555#[derive(Clone)]
556struct CrystalCluster {
557    pos: Vec3<i32>,
558    crystals: [Crystal; 5],
559    color: Rgb<u8>,
560}
561
562#[derive(Clone)]
563struct Flower {
564    pos: Vec3<i32>,
565    stalk: f32,
566    petals: usize,
567    petal_height: f32,
568    petal_radius: f32,
569    // rotation: Mat3<f32>,
570}
571
572// #[inline_tweak::tweak_fn]
573fn write_column<R: Rng>(
574    canvas: &mut Canvas,
575    col: &ColumnSample,
576    level: u32,
577    wpos2d: Vec2<i32>,
578    z_range: Range<i32>,
579    z_ranges: &[Range<i32>],
580    tunnel: Tunnel,
581    dimensions: (f32, f32, f32),
582    giant_tree_dist: f32,
583    structure_cache: &mut SmallCache<Vec3<i32>, Option<CaveStructure>>,
584    structure_seeds: &[(Vec2<i32>, u32); 9],
585    rng: &mut R,
586) {
587    let info = canvas.info();
588
589    // Exposed to the sky, or some other void above
590    let void_above = !canvas.get(wpos2d.with_z(z_range.end)).is_filled();
591    let void_below = !canvas.get(wpos2d.with_z(z_range.start - 1)).is_filled();
592    // Exposed to the sky
593    let sky_above = z_range.end as f32 > col.alt;
594    let cavern_height = (z_range.end - z_range.start) as f32;
595    let (cave_width, max_height, dist_cave_center) = dimensions;
596    let biome = tunnel.biome_at(wpos2d.with_z(z_range.start), &info);
597
598    // Get the range, if there is any, where the current cave overlaps with other
599    // caves. Right now this is only used to prevent ceiling cover from being
600    // place
601    let overlap = z_ranges.iter().find_map(|other_z_range| {
602        if *other_z_range == z_range {
603            return None;
604        }
605        let start = z_range.start.max(other_z_range.start);
606        let end = z_range.end.min(other_z_range.end);
607        let min = z_range.start.min(other_z_range.start);
608        if start < end { Some(min..end) } else { None }
609    });
610
611    let stalactite = {
612        FastNoise2d::new(35)
613            .get(wpos2d.map(|e| e as f64 / 8.0))
614            .sub(0.5)
615            .max(0.0)
616            .mul(2.0)
617            .powi(if biome.icy > 0.7 {2} else {1})
618            // No stalactites near entrances
619            .mul(((col.alt - z_range.end as f32) / 32.0).clamped(0.0, 1.0))
620            // Prevent stalactites from blocking cave
621            .mul(((cave_width + max_height) / 40.0).clamped(0.0, 1.0))
622            // Scale with cavern height, sandy and icy biomes have bigger stalactites
623            .mul(8.0 + cavern_height * (0.4 + (biome.sandy - 0.5).max(biome.icy - 0.6).max(0.0)))
624    };
625
626    let ceiling_cover = if biome.leafy > 0.3
627        || biome.mushroom > 0.5
628        || biome.icy > 0.6
629        || biome.sandy > 0.4
630        || biome.fire > 0.4
631        || biome.crystal > 0.75
632    {
633        // 1.0 because at some point we maybe want to use some noise value here instead
634        1.0.mul(((col.alt - z_range.end as f32) / 32.0).clamped(0.0, 1.0))
635            .mul(max_height * (dist_cave_center / cave_width).powf(3.33))
636            .max(1.0)
637            .sub(
638                if col.marble_mid
639                    > biome
640                        .fire
641                        .max(biome.icy - 0.6)
642                        .max(biome.sandy - 0.3)
643                        .max(biome.leafy - 0.4)
644                        .max(biome.mushroom - 0.4)
645                        .max(biome.crystal - 0.5)
646                {
647                    max_height * col.marble_mid
648                } else {
649                    0.0
650                },
651            )
652            .max(0.0)
653    } else {
654        0.0
655    };
656
657    let basalt = if biome.fire > 0.5 {
658        FastNoise2d::new(36)
659            .get(wpos2d.map(|e| e as f64 / 16.0))
660            .mul(1.25)
661            .sub(0.75)
662            .max(0.0)
663            .mul(((cave_width + max_height) / 64.0).clamped(0.0, 1.0))
664            .mul(6.0 + cavern_height * 0.5)
665            .mul((biome.fire - 0.5).powi(3) * 8.0)
666    } else {
667        0.0
668    };
669
670    let lava = if biome.fire > 0.5 {
671        FastNoise2d::new(37)
672            .get(wpos2d.map(|e| e as f64 / 32.0))
673            .mul(0.5)
674            .abs()
675            .sub(0.2)
676            .min(0.0)
677            // .mul((biome.temp as f64 - 1.5).mul(30.0).clamped(0.0, 1.0))
678            .mul((cave_width / 16.0).clamped(0.5, 1.0))
679            .mul((biome.fire - 0.5).mul(30.0).clamped(0.0, 1.0))
680            .mul(64.0)
681            .max(-32.0)
682    } else {
683        0.0
684    };
685
686    let bump = if biome
687        .sandy
688        .max(biome.dusty)
689        .max(biome.leafy)
690        .max(biome.barren)
691        > 0.0
692    {
693        FastNoise2d::new(38)
694            .get(wpos2d.map(|e| e as f64 / 4.0))
695            .mul(1.15)
696            .add(1.0)
697            .mul(0.5)
698            .mul(((col.alt - z_range.end as f32) / 16.0).clamped(0.0, 1.0))
699            .mul({
700                let (val, total) = [
701                    (biome.sandy, 0.9),
702                    (biome.dusty, 0.5),
703                    (biome.leafy, 0.6),
704                    (biome.barren, 0.6),
705                ]
706                .into_iter()
707                .fold((0.0, 0.0), |a, x| (a.0 + x.0.max(0.0) * x.1, a.1 + x.1));
708                val / total
709            })
710            .mul(cavern_height * 0.2)
711            .clamped(0.0, 4.0)
712    } else {
713        0.0
714    };
715
716    let rand = RandomField::new(37 + level);
717
718    let is_ice = biome.icy + col.marble * 0.2 > 0.5 && col.marble > 0.6;
719    let is_snow = biome.snowy + col.marble_mid * 0.2 > 0.5 && col.marble_mid > 0.6;
720
721    let (has_stalagmite, has_stalactite) = if biome.icy > 0.5 {
722        (col.marble_mid > 0.6, col.marble_mid < 0.5)
723    } else {
724        (true, true)
725    };
726
727    let stalagmite_only = if has_stalagmite && !has_stalactite {
728        0.6f32
729    } else {
730        0.0f32
731    };
732
733    // Don't cover stalgmites in ice biome with surface block
734    let dirt = if biome.icy > 0.5 && has_stalagmite && stalactite > 3.0 {
735        0
736    } else {
737        1 + (!is_ice) as i32 + is_snow as i32
738    };
739    let bedrock = z_range.start + lava as i32;
740    let base = bedrock + (stalactite * (0.4 + stalagmite_only)) as i32;
741    let floor = base + dirt + bump as i32;
742    let ceiling =
743        z_range.end - (stalactite * has_stalactite as i32 as f32).max(ceiling_cover) as i32;
744
745    let get_ceiling_drip = |wpos: Vec2<i32>, freq: f64, length: f32| {
746        let wposf = wpos.map(|e| e as f32);
747        let wposf = wposf + wposf.yx() * 1.1;
748        let dims = Vec2::new(11.0, 32.0);
749        let posf = wposf + Vec2::unit_y() * (wposf.x / dims.x).floor() * 89.0 / dims;
750        let pos = posf.map(|e| e.floor() as i32);
751        if rand.chance(pos.with_z(73), freq as f32) {
752            let drip_length = ((posf.y.fract() - 0.5).abs() * 2.0 * dims.y)
753                .mul(length * 0.01)
754                .powf(2.0)
755                .min(length);
756
757            if (posf.x.fract() * 2.0 - 1.0).powi(2) < 1.0 {
758                drip_length
759            } else {
760                0.0
761            }
762        } else {
763            0.0
764        }
765    };
766
767    let ceiling_drip = ceiling
768        - if !void_above && !sky_above && overlap.is_none() {
769            let c = if biome.mushroom > 0.9 && ceiling_cover > 0.0 {
770                Some((0.07, 7.0))
771            } else if biome.icy > 0.9 {
772                Some((0.1 * col.marble_mid as f64, 9.0))
773            } else {
774                None
775            };
776            if let Some((freq, length)) = c {
777                get_ceiling_drip(wpos2d, freq, length).max(get_ceiling_drip(
778                    wpos2d.yx(),
779                    freq,
780                    length,
781                )) as i32
782            } else {
783                0
784            }
785        } else {
786            0
787        };
788
789    let structures = structure_seeds
790        .iter()
791        .filter_map(|(wpos2d, seed)| {
792            let structure = structure_cache.get(wpos2d.with_z(tunnel.a.depth), |_| {
793                let mut rng = RandomPerm::new(*seed);
794                let (z_range, horizontal, vertical, _) =
795                    tunnel.z_range_at(wpos2d.map(|e| e as f64 + 0.5), info)?;
796                let pos = wpos2d.with_z(z_range.start);
797                let biome = tunnel.biome_at(pos, &info);
798                let tunnel_intersection = || {
799                    tunnel_bounds_at(pos.xy(), &info, &info.land())
800                        .any(|(_, z_range, _, _, _, _)| z_range.contains(&(z_range.start - 1)))
801                };
802
803                if biome.mushroom > 0.7
804                    && vertical > 16.0
805                    && rng.gen_bool(
806                        0.5 * close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2) as f64
807                            * close(biome.mushroom, 1.0, 0.7, 1) as f64,
808                    )
809                {
810                    if tunnel_intersection() {
811                        return None;
812                    }
813                    let purp = rng.gen_range(0..50);
814                    Some(CaveStructure::Mushroom(Mushroom {
815                        pos,
816                        stalk: 8.0
817                            + rng.gen::<f32>().powf(2.0)
818                                * (z_range.end - z_range.start - 8) as f32
819                                * 0.75,
820                        head_color: Rgb::new(
821                            40 + purp,
822                            rng.gen_range(60..120),
823                            rng.gen_range(80..200) + purp,
824                        ),
825                    }))
826                } else if biome.crystal > 0.5
827                    && rng.gen_bool(0.4 * close(biome.crystal, 1.0, 0.7, 2) as f64)
828                {
829                    if tunnel_intersection() {
830                        return None;
831                    }
832                    let on_ground = rng.gen_bool(0.6);
833                    let pos = wpos2d.with_z(if on_ground {
834                        z_range.start
835                    } else {
836                        z_range.end
837                    });
838
839                    let max_length = (48.0 * close(vertical, MAX_RADIUS, MAX_RADIUS, 1)).max(12.0);
840                    let length = rng.gen_range(8.0..max_length);
841                    let radius =
842                        Lerp::lerp(2.0, 4.5, length / max_length + rng.gen_range(-0.1..0.1));
843                    let dir = Vec3::new(
844                        rng.gen_range(-3.0..3.0),
845                        rng.gen_range(-3.0..3.0),
846                        rng.gen_range(0.5..10.0) * if on_ground { 1.0 } else { -1.0 },
847                    )
848                    .normalized();
849
850                    let mut gen_crystal = || Crystal {
851                        dir: Vec3::new(
852                            rng.gen_range(-1.0..1.0),
853                            rng.gen_range(-1.0..1.0),
854                            (dir.z + rng.gen_range(-0.2..0.2)).clamped(0.0, 1.0),
855                        ),
856                        length: length * rng.gen_range(0.3..0.8),
857                        radius: (radius * rng.gen_range(0.5..0.8)).max(1.0),
858                    };
859
860                    let crystals = [
861                        Crystal {
862                            dir,
863                            length,
864                            radius,
865                        },
866                        gen_crystal(),
867                        gen_crystal(),
868                        gen_crystal(),
869                        gen_crystal(),
870                    ];
871
872                    let purple = rng.gen_range(25..75);
873                    let blue = (rng.gen_range(45.0..75.0) * biome.icy) as u8;
874                    Some(CaveStructure::Crystal(CrystalCluster {
875                        pos,
876                        crystals,
877                        color: Rgb::new(
878                            255 - blue * 2,
879                            255 - blue - purple,
880                            200 + rng.gen_range(25..55),
881                        ),
882                    }))
883                } else if biome.leafy > 0.8
884                    && vertical > 16.0
885                    && horizontal > 16.0
886                    && rng.gen_bool(
887                        0.25 * (close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2)
888                            * close(horizontal, MAX_RADIUS, MAX_RADIUS - 16.0, 2)
889                            * biome.leafy) as f64,
890                    )
891                {
892                    if tunnel_intersection() {
893                        return None;
894                    }
895                    let petal_radius = rng.gen_range(8.0..16.0);
896                    Some(CaveStructure::Flower(Flower {
897                        pos,
898                        stalk: 6.0
899                            + rng.gen::<f32>().powf(2.0)
900                                * (z_range.end - z_range.start - 8) as f32
901                                * 0.75,
902                        petals: rng.gen_range(1..5) * 2 + 1,
903                        petal_height: 0.4 * petal_radius * (1.0 + rng.gen::<f32>().powf(2.0)),
904                        petal_radius,
905                    }))
906                } else if (biome.leafy > 0.7 || giant_tree_dist > 0.0)
907                    && rng.gen_bool(
908                        (0.5 * close(biome.leafy, 1.0, 0.5, 1).max(1.0 + giant_tree_dist) as f64)
909                            .clamped(0.0, 1.0),
910                    )
911                {
912                    if tunnel_intersection() {
913                        return None;
914                    }
915                    Some(CaveStructure::GiantRoot {
916                        pos,
917                        radius: rng.gen_range(
918                            2.5..(3.5
919                                + close(vertical, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0
920                                + close(horizontal, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0),
921                        ),
922                        height: (z_range.end - z_range.start) as f32,
923                    })
924                } else {
925                    None
926                }
927            });
928
929            // TODO Some way to not clone here?
930            structure
931                .as_ref()
932                .map(|structure| (*seed, structure.clone()))
933        })
934        .collect_vec();
935    let get_structure = |wpos: Vec3<i32>, dynamic_rng: &mut R| {
936        let warp = |wposf: Vec3<f64>, freq: f64, amp: Vec3<f32>, seed: u32| -> Vec3<f32> {
937            let xy = wposf.xy();
938            let xz = Vec2::new(wposf.x, wposf.z);
939            let yz = Vec2::new(wposf.y, wposf.z);
940            Vec3::new(
941                FastNoise2d::new(seed).get(yz * freq),
942                FastNoise2d::new(seed).get(xz * freq),
943                FastNoise2d::new(seed).get(xy * freq),
944            ) * amp
945        };
946        for (seed, structure) in structures.iter() {
947            let seed = *seed;
948            match structure {
949                CaveStructure::Mushroom(mushroom) => {
950                    let wposf = wpos.map(|e| e as f64);
951                    let warp_freq = 1.0 / 32.0;
952                    let warp_amp = Vec3::new(12.0, 12.0, 12.0);
953                    let warp_offset = warp(wposf, warp_freq, warp_amp, seed);
954                    let wposf_warped = wposf.map(|e| e as f32)
955                        + warp_offset
956                            * (wposf.z as f32 - mushroom.pos.z as f32)
957                                .mul(0.1)
958                                .clamped(0.0, 1.0);
959
960                    let rpos = wposf_warped - mushroom.pos.map(|e| e as f32);
961
962                    let stalk_radius = 2.5f32;
963                    let head_radius = 12.0f32;
964                    let head_height = 14.0;
965
966                    let dist_sq = rpos.xy().magnitude_squared();
967                    if dist_sq < head_radius.powi(2) {
968                        let dist = dist_sq.sqrt();
969                        let head_dist = ((rpos - Vec3::unit_z() * mushroom.stalk)
970                            / Vec2::broadcast(head_radius).with_z(head_height))
971                        .magnitude();
972
973                        let stalk = mushroom.stalk
974                            + Lerp::lerp_unclamped(head_height * 0.5, 0.0, dist / head_radius);
975
976                        // Head
977                        if rpos.z > stalk
978                            && rpos.z <= mushroom.stalk + head_height
979                            && dist
980                                < head_radius
981                                    * (1.0 - (rpos.z - mushroom.stalk) / head_height).powf(0.125)
982                        {
983                            if head_dist < 0.85 {
984                                let radial = (rpos.x.atan2(rpos.y) * 10.0).sin() * 0.5 + 0.5;
985                                let block_kind = if dynamic_rng.gen_bool(0.1) {
986                                    BlockKind::GlowingMushroom
987                                } else {
988                                    BlockKind::Rock
989                                };
990                                return Some(Block::new(
991                                    block_kind,
992                                    Rgb::new(
993                                        30,
994                                        120 + (radial * 40.0) as u8,
995                                        180 - (radial * 40.0) as u8,
996                                    ),
997                                ));
998                            } else if head_dist < 1.0 {
999                                return Some(Block::new(BlockKind::Wood, mushroom.head_color));
1000                            }
1001                        }
1002
1003                        if rpos.z <= mushroom.stalk + head_height - 1.0
1004                            && dist_sq
1005                                < (stalk_radius * Lerp::lerp(1.5, 0.75, rpos.z / mushroom.stalk))
1006                                    .powi(2)
1007                        {
1008                            // Stalk
1009                            return Some(Block::new(BlockKind::Wood, Rgb::new(25, 60, 90)));
1010                        } else if ((mushroom.stalk - 0.1)..(mushroom.stalk + 0.9)).contains(&rpos.z) // Hanging orbs
1011                    && dist > head_radius * 0.85
1012                    && dynamic_rng.gen_bool(0.1)
1013                        {
1014                            use SpriteKind::*;
1015                            let sprites = if dynamic_rng.gen_bool(0.1) {
1016                                &[Beehive, Lantern] as &[_]
1017                            } else {
1018                                &[MycelBlue, MycelBlue] as &[_]
1019                            };
1020                            return Some(Block::air(*sprites.choose(dynamic_rng).unwrap()));
1021                        }
1022                    }
1023                },
1024                CaveStructure::Crystal(cluster) => {
1025                    let wposf = wpos.map(|e| e as f32);
1026                    let cluster_pos = cluster.pos.map(|e| e as f32);
1027                    for crystal in &cluster.crystals {
1028                        let line = LineSegment3 {
1029                            start: cluster_pos,
1030                            end: cluster_pos + crystal.dir * crystal.length,
1031                        };
1032
1033                        let projected = line.projected_point(wposf);
1034                        let dist_sq = projected.distance_squared(wposf);
1035                        if dist_sq < crystal.radius.powi(2) {
1036                            let rpos = wposf - cluster_pos;
1037                            let line_length = line.start.distance_squared(line.end);
1038                            let taper = if line_length < 0.001 {
1039                                0.0
1040                            } else {
1041                                rpos.dot(line.end - line.start) / line_length
1042                            };
1043
1044                            let peak_cutoff = 0.8;
1045                            let taper_factor = 0.55;
1046                            let peak_taper = 0.3;
1047
1048                            let crystal_radius = if taper > peak_cutoff {
1049                                let taper = (taper - peak_cutoff) * 5.0;
1050                                Lerp::lerp_unclamped(
1051                                    crystal.radius * taper_factor,
1052                                    crystal.radius * peak_taper,
1053                                    taper,
1054                                )
1055                            } else {
1056                                let taper = taper * 1.25;
1057                                Lerp::lerp_unclamped(
1058                                    crystal.radius,
1059                                    crystal.radius * taper_factor,
1060                                    taper,
1061                                )
1062                            };
1063
1064                            if dist_sq < crystal_radius.powi(2) {
1065                                if dist_sq / crystal_radius.powi(2) > 0.75 {
1066                                    return Some(Block::new(BlockKind::GlowingRock, cluster.color));
1067                                } else {
1068                                    return Some(Block::new(BlockKind::Rock, cluster.color));
1069                                }
1070                            }
1071                        }
1072                    }
1073                },
1074                CaveStructure::Flower(flower) => {
1075                    let wposf = wpos.map(|e| e as f64);
1076                    let warp_freq = 1.0 / 16.0;
1077                    let warp_amp = Vec3::new(8.0, 8.0, 8.0);
1078                    let warp_offset = warp(wposf, warp_freq, warp_amp, seed);
1079                    let wposf_warped = wposf.map(|e| e as f32)
1080                        + warp_offset
1081                            * (wposf.z as f32 - flower.pos.z as f32)
1082                                .mul(1.0 / flower.stalk)
1083                                .sub(1.0)
1084                                .min(0.0)
1085                                .abs()
1086                                .clamped(0.0, 1.0);
1087                    let rpos = wposf_warped - flower.pos.map(|e| e as f32);
1088
1089                    let stalk_radius = 2.5f32;
1090                    let petal_thickness = 2.5;
1091                    let dist_sq = rpos.xy().magnitude_squared();
1092                    if rpos.z < flower.stalk
1093                        && dist_sq
1094                            < (stalk_radius
1095                                * Lerp::lerp_unclamped(1.0, 0.75, rpos.z / flower.stalk))
1096                            .powi(2)
1097                    {
1098                        return Some(Block::new(BlockKind::Wood, Rgb::new(0, 108, 0)));
1099                    }
1100
1101                    let rpos = rpos - Vec3::unit_z() * flower.stalk;
1102                    let dist_sq = rpos.xy().magnitude_squared();
1103                    let petal_radius_sq = flower.petal_radius.powi(2);
1104                    if dist_sq < petal_radius_sq {
1105                        let petal_height_at = (dist_sq / petal_radius_sq) * flower.petal_height;
1106                        if rpos.z > petal_height_at - 1.0
1107                            && rpos.z <= petal_height_at + petal_thickness
1108                        {
1109                            let dist_ratio = dist_sq / petal_radius_sq;
1110                            let yellow = (60.0 * dist_ratio) as u8;
1111                            let near = rpos
1112                                .x
1113                                .atan2(rpos.y)
1114                                .rem_euclid(std::f32::consts::TAU / flower.petals as f32);
1115                            if dist_ratio < 0.175 {
1116                                let red = close(near, 0.0, 0.5, 1);
1117                                let purple = close(near, 0.0, 0.35, 1);
1118                                if dist_ratio > red || rpos.z < petal_height_at {
1119                                    return Some(Block::new(
1120                                        BlockKind::ArtLeaves,
1121                                        Rgb::new(240, 80 - yellow, 80 - yellow),
1122                                    ));
1123                                } else if dist_ratio > purple {
1124                                    return Some(Block::new(
1125                                        BlockKind::ArtLeaves,
1126                                        Rgb::new(200, 14, 132),
1127                                    ));
1128                                } else {
1129                                    return Some(Block::new(
1130                                        BlockKind::ArtLeaves,
1131                                        Rgb::new(249, 156, 218),
1132                                    ));
1133                                }
1134                            } else {
1135                                let inset = close(near, -1.0, 1.0, 2).max(close(near, 1.0, 1.0, 2));
1136                                if dist_ratio < inset {
1137                                    return Some(Block::new(
1138                                        BlockKind::ArtLeaves,
1139                                        Rgb::new(240, 80 - yellow, 80 - yellow),
1140                                    ));
1141                                }
1142                            }
1143                        }
1144
1145                        // pollen
1146                        let pollen_height = 5.0;
1147                        if rpos.z > 0.0
1148                            && rpos.z < pollen_height
1149                            && dist_sq
1150                                < (stalk_radius
1151                                    * Lerp::lerp_unclamped(0.5, 1.25, rpos.z / pollen_height))
1152                                .powi(2)
1153                        {
1154                            return Some(Block::new(
1155                                BlockKind::GlowingWeakRock,
1156                                Rgb::new(239, 192, 0),
1157                            ));
1158                        }
1159                    }
1160                },
1161                CaveStructure::GiantRoot {
1162                    pos,
1163                    radius,
1164                    height,
1165                } => {
1166                    let wposf = wpos.map(|e| e as f64);
1167                    let warp_freq = 1.0 / 16.0;
1168                    let warp_amp = Vec3::new(8.0, 8.0, 8.0);
1169                    let warp_offset = warp(wposf, warp_freq, warp_amp, seed);
1170                    let wposf_warped = wposf.map(|e| e as f32) + warp_offset;
1171                    let rpos = wposf_warped - pos.map(|e| e as f32);
1172                    let dist_sq = rpos.xy().magnitude_squared();
1173                    if dist_sq < radius.powi(2) {
1174                        // Moss
1175                        if col.marble_mid
1176                            > (std::f32::consts::PI * rpos.z / *height)
1177                                .sin()
1178                                .powf(2.0)
1179                                .mul(0.25)
1180                                .add(col.marble_small)
1181                        {
1182                            return Some(Block::new(BlockKind::Wood, Rgb::new(48, 70, 25)));
1183                        }
1184                        return Some(Block::new(BlockKind::Wood, Rgb::new(66, 41, 26)));
1185                    }
1186                },
1187            }
1188        }
1189        None
1190    };
1191
1192    for z in bedrock..z_range.end {
1193        let wpos = wpos2d.with_z(z);
1194        let mut try_spawn_entity = false;
1195        let mut sprite_cfg_to_set = None;
1196        canvas.set(wpos, {
1197            if z < z_range.start - 4 && !void_below {
1198                Block::new(BlockKind::Lava, Rgb::new(255, 65, 0))
1199            } else if basalt > 0.0
1200                && z < bedrock / 6 * 6
1201                    + 2
1202                    + basalt as i32 / 4 * 4
1203                    + (RandomField::new(77)
1204                        .get_f32(((wpos2d + Vec2::new(wpos2d.y, -wpos2d.x) / 2) / 4).with_z(0))
1205                        * 6.0)
1206                        .floor() as i32
1207                && !void_below
1208            {
1209                Block::new(BlockKind::Rock, Rgb::new(50, 35, 75))
1210            } else if (z < base && !void_below)
1211                || ((z >= ceiling && !void_above)
1212                    && !(ceiling_cover > 0.0 && overlap.as_ref().is_some_and(|o| o.contains(&z))))
1213            {
1214                let stalactite: Rgb<i16> = Lerp::lerp_unclamped(
1215                    Lerp::lerp_unclamped(
1216                        Lerp::lerp_unclamped(
1217                            Lerp::lerp_unclamped(
1218                                Lerp::lerp_unclamped(
1219                                    Lerp::lerp_unclamped(
1220                                        Rgb::new(80, 100, 150),
1221                                        Rgb::new(23, 44, 88),
1222                                        biome.mushroom,
1223                                    ),
1224                                    Lerp::lerp_unclamped(
1225                                        Rgb::new(100, 40, 40),
1226                                        Rgb::new(100, 75, 100),
1227                                        col.marble_small,
1228                                    ),
1229                                    biome.fire,
1230                                ),
1231                                Lerp::lerp_unclamped(
1232                                    Rgb::new(238, 198, 139),
1233                                    Rgb::new(111, 99, 64),
1234                                    col.marble_mid,
1235                                ),
1236                                biome.sandy,
1237                            ),
1238                            Lerp::lerp_unclamped(
1239                                Rgb::new(0, 73, 12),
1240                                Rgb::new(49, 63, 12),
1241                                col.marble_small,
1242                            ),
1243                            biome.leafy,
1244                        ),
1245                        Lerp::lerp_unclamped(
1246                            Rgb::new(100, 150, 255),
1247                            Rgb::new(100, 120, 255),
1248                            col.marble,
1249                        ),
1250                        biome.icy,
1251                    ),
1252                    Lerp::lerp_unclamped(
1253                        Rgb::new(105, 25, 131),
1254                        Rgb::new(251, 238, 255),
1255                        col.marble_mid,
1256                    ),
1257                    biome.crystal,
1258                );
1259                Block::new(
1260                    if rand.chance(
1261                        wpos,
1262                        (biome.mushroom * 0.01)
1263                            .max(biome.icy * 0.1)
1264                            .max(biome.crystal * 0.005),
1265                    ) {
1266                        BlockKind::GlowingWeakRock
1267                    } else if rand.chance(wpos, biome.sandy) {
1268                        BlockKind::Sand
1269                    } else if rand.chance(wpos, biome.leafy) {
1270                        BlockKind::ArtLeaves
1271                    } else if ceiling_cover > 0.0 {
1272                        BlockKind::Rock
1273                    } else {
1274                        BlockKind::WeakRock
1275                    },
1276                    stalactite.map(|e| e as u8),
1277                )
1278            } else if z < ceiling && z >= ceiling_drip {
1279                if biome.mushroom > 0.9 {
1280                    let block = if rand.chance(wpos2d.with_z(89), 0.05) {
1281                        BlockKind::GlowingMushroom
1282                    } else {
1283                        BlockKind::GlowingWeakRock
1284                    };
1285                    Block::new(block, Rgb::new(10, 70, 148))
1286                } else if biome.icy > 0.9 {
1287                    Block::new(BlockKind::GlowingWeakRock, Rgb::new(120, 140, 255))
1288                } else {
1289                    Block::new(BlockKind::WeakRock, Rgb::new(80, 100, 150))
1290                }
1291            } else if z >= base && z < floor && !void_below && !sky_above {
1292                let (net_col, total) = [
1293                    (
1294                        Lerp::lerp_unclamped(
1295                            Rgb::new(68, 62, 58),
1296                            Rgb::new(97, 95, 85),
1297                            col.marble_small,
1298                        ),
1299                        0.05,
1300                    ),
1301                    (
1302                        Lerp::lerp_unclamped(
1303                            Rgb::new(66, 37, 30),
1304                            Rgb::new(88, 62, 45),
1305                            col.marble_mid,
1306                        ),
1307                        biome.dusty,
1308                    ),
1309                    (
1310                        Lerp::lerp_unclamped(
1311                            Rgb::new(20, 65, 175),
1312                            Rgb::new(20, 100, 80),
1313                            col.marble_mid,
1314                        ),
1315                        biome.mushroom,
1316                    ),
1317                    (
1318                        Lerp::lerp_unclamped(
1319                            Rgb::new(120, 50, 20),
1320                            Rgb::new(50, 5, 40),
1321                            col.marble_small,
1322                        ),
1323                        biome.fire,
1324                    ),
1325                    (
1326                        Lerp::lerp_unclamped(
1327                            Rgb::new(0, 100, 50),
1328                            Rgb::new(80, 100, 20),
1329                            col.marble_small,
1330                        ),
1331                        biome.leafy,
1332                    ),
1333                    (Rgb::new(170, 195, 255), biome.icy),
1334                    (
1335                        Lerp::lerp_unclamped(
1336                            Rgb::new(105, 25, 131),
1337                            Rgb::new(251, 238, 255),
1338                            col.marble_mid,
1339                        ),
1340                        biome.crystal,
1341                    ),
1342                    (
1343                        Lerp::lerp_unclamped(
1344                            Rgb::new(201, 174, 116),
1345                            Rgb::new(244, 239, 227),
1346                            col.marble_small,
1347                        ),
1348                        biome.sandy,
1349                    ),
1350                    (
1351                        // Same as barren
1352                        Lerp::lerp_unclamped(
1353                            Rgb::new(68, 62, 58),
1354                            Rgb::new(97, 95, 85),
1355                            col.marble_small,
1356                        ),
1357                        biome.snowy,
1358                    ),
1359                ]
1360                .into_iter()
1361                .fold((Rgb::<f32>::zero(), 0.0), |a, x| {
1362                    (a.0 + x.0.map(|e| e as f32) * x.1, a.1 + x.1)
1363                });
1364                let surf_color = net_col.map(|e| (e / total) as u8);
1365
1366                if is_ice {
1367                    Block::new(BlockKind::Ice, Rgb::new(120, 160, 255))
1368                } else if is_snow {
1369                    Block::new(BlockKind::ArtSnow, Rgb::new(170, 195, 255))
1370                } else {
1371                    Block::new(
1372                        if biome.mushroom.max(biome.leafy) > 0.5 {
1373                            BlockKind::Grass
1374                        } else if biome.icy > 0.5 {
1375                            BlockKind::ArtSnow
1376                        } else if biome.fire.max(biome.snowy) > 0.5 {
1377                            BlockKind::Rock
1378                        } else if biome.crystal > 0.5 {
1379                            if rand.chance(wpos, biome.crystal * 0.02) {
1380                                BlockKind::GlowingRock
1381                            } else {
1382                                BlockKind::Rock
1383                            }
1384                        } else {
1385                            BlockKind::Sand
1386                        },
1387                        surf_color,
1388                    )
1389                }
1390            } else if let Some(sprite) = (z == floor && !void_below && !sky_above)
1391                .then(|| {
1392                    if col.marble_mid > 0.55
1393                        && biome.mushroom > 0.6
1394                        && rand.chance(
1395                            wpos2d.with_z(1),
1396                            biome.mushroom.powi(2) * 0.2 * col.marble_mid,
1397                        )
1398                    {
1399                        [
1400                            (SpriteKind::GlowMushroom, 0.5),
1401                            (SpriteKind::Mushroom, 0.25),
1402                            (SpriteKind::GrassBlueMedium, 1.5),
1403                            (SpriteKind::GrassBlueLong, 2.0),
1404                            (SpriteKind::Moonbell, 0.01),
1405                            (SpriteKind::SporeReed, 2.5),
1406                        ]
1407                        .choose_weighted(rng, |(_, w)| *w)
1408                        .ok()
1409                        .map(|s| s.0)
1410                    } else if col.marble_mid > 0.6
1411                        && biome.leafy > 0.4
1412                        && rand.chance(
1413                            wpos2d.with_z(15),
1414                            biome.leafy.powi(2) * col.marble_mid * (biome.humidity * 1.3) * 0.25,
1415                        )
1416                    {
1417                        let mixed = col.marble.add(col.marble_small.sub(0.5).mul(0.25));
1418                        if (0.25..0.45).contains(&mixed) || (0.55..0.75).contains(&mixed) {
1419                            return [
1420                                (SpriteKind::LongGrass, 1.0),
1421                                (SpriteKind::MediumGrass, 2.0),
1422                                (SpriteKind::JungleFern, 0.5),
1423                                (SpriteKind::JungleRedGrass, 0.35),
1424                                (SpriteKind::Fern, 0.75),
1425                                (SpriteKind::LeafyPlant, 0.8),
1426                                (SpriteKind::JungleLeafyPlant, 0.5),
1427                                (SpriteKind::LanternPlant, 0.1),
1428                                (SpriteKind::LanternFlower, 0.1),
1429                                (SpriteKind::LushFlower, 0.2),
1430                            ]
1431                            .choose_weighted(rng, |(_, w)| *w)
1432                            .ok()
1433                            .map(|s| s.0);
1434                        } else if (0.0..0.25).contains(&mixed) {
1435                            return [(Some(SpriteKind::LanternPlant), 0.5), (None, 0.5)]
1436                                .choose_weighted(rng, |(_, w)| *w)
1437                                .ok()
1438                                .and_then(|s| s.0);
1439                        } else if (0.75..1.0).contains(&mixed) {
1440                            return [(Some(SpriteKind::LushFlower), 0.6), (None, 0.4)]
1441                                .choose_weighted(rng, |(_, w)| *w)
1442                                .ok()
1443                                .and_then(|s| s.0);
1444                        } else {
1445                            return [
1446                                (SpriteKind::LongGrass, 1.0),
1447                                (SpriteKind::MediumGrass, 2.0),
1448                                (SpriteKind::JungleFern, 0.5),
1449                                (SpriteKind::JungleLeafyPlant, 0.5),
1450                                (SpriteKind::JungleRedGrass, 0.35),
1451                                (SpriteKind::Mushroom, 0.15),
1452                                (SpriteKind::EnsnaringVines, 0.2),
1453                                (SpriteKind::Fern, 0.75),
1454                                (SpriteKind::LeafyPlant, 0.8),
1455                                (SpriteKind::Twigs, 0.07),
1456                                (SpriteKind::Wood, 0.03),
1457                                (SpriteKind::LanternPlant, 0.3),
1458                                (SpriteKind::LanternFlower, 0.3),
1459                                (SpriteKind::LushFlower, 0.5),
1460                                (SpriteKind::LushMushroom, 1.0),
1461                            ]
1462                            .choose_weighted(rng, |(_, w)| *w)
1463                            .ok()
1464                            .map(|s| s.0);
1465                        }
1466                    } else if rand.chance(wpos2d.with_z(2), biome.dusty.max(biome.sandy) * 0.01) {
1467                        [
1468                            (SpriteKind::Bones, 0.5),
1469                            (SpriteKind::Stones, 1.5),
1470                            (SpriteKind::DeadBush, 1.0),
1471                            (SpriteKind::DeadPlant, 1.5),
1472                            (SpriteKind::EnsnaringWeb, 0.5),
1473                            (SpriteKind::Mud, 0.025),
1474                        ]
1475                        .choose_weighted(rng, |(_, w)| *w)
1476                        .ok()
1477                        .map(|s| s.0)
1478                    } else if rand.chance(wpos2d.with_z(14), biome.barren * 0.003) {
1479                        [
1480                            (SpriteKind::Bones, 0.5),
1481                            (SpriteKind::Welwitch, 0.5),
1482                            (SpriteKind::DeadBush, 1.5),
1483                            (SpriteKind::DeadPlant, 1.5),
1484                            (SpriteKind::RockyMushroom, 1.5),
1485                            (SpriteKind::Crate, 0.005),
1486                        ]
1487                        .choose_weighted(rng, |(_, w)| *w)
1488                        .ok()
1489                        .map(|s| s.0)
1490                    } else if rand.chance(wpos2d.with_z(3), biome.crystal * 0.005) {
1491                        Some(SpriteKind::CrystalLow)
1492                    } else if rand.chance(wpos2d.with_z(13), biome.fire * 0.0006) {
1493                        [
1494                            (SpriteKind::Pyrebloom, 0.3),
1495                            (SpriteKind::Bloodstone, 0.3),
1496                            (SpriteKind::Gold, 0.2),
1497                        ]
1498                        .choose_weighted(rng, |(_, w)| *w)
1499                        .ok()
1500                        .map(|(s, _)| *s)
1501                    } else if biome.icy > 0.5 && rand.chance(wpos2d.with_z(23), biome.icy * 0.005) {
1502                        Some(SpriteKind::IceCrystal)
1503                    } else if biome.icy > 0.5
1504                        && rand.chance(wpos2d.with_z(31), biome.icy * biome.mineral * 0.005)
1505                    {
1506                        Some(SpriteKind::GlowIceCrystal)
1507                    } else if rand.chance(wpos2d.with_z(5), 0.0015) {
1508                        [
1509                            (Some(SpriteKind::VeloriteFrag), 0.3),
1510                            (Some(SpriteKind::Velorite), 0.15),
1511                            (Some(SpriteKind::Amethyst), 0.15),
1512                            (Some(SpriteKind::Topaz), 0.15),
1513                            (Some(SpriteKind::Diamond), 0.02),
1514                            (Some(SpriteKind::Ruby), 0.05),
1515                            (Some(SpriteKind::Emerald), 0.04),
1516                            (Some(SpriteKind::Sapphire), 0.04),
1517                            (None, 10.0),
1518                        ]
1519                        .choose_weighted(rng, |(_, w)| *w)
1520                        .ok()
1521                        .and_then(|s| s.0)
1522                    } else if rand.chance(wpos2d.with_z(6), 0.0002) {
1523                        let chest = [
1524                            (Some(SpriteKind::DungeonChest0), 1.0),
1525                            (Some(SpriteKind::DungeonChest1), 0.3),
1526                            (Some(SpriteKind::DungeonChest2), 0.1),
1527                            (Some(SpriteKind::DungeonChest3), 0.03),
1528                            (Some(SpriteKind::DungeonChest4), 0.01),
1529                            (Some(SpriteKind::DungeonChest5), 0.003),
1530                            (None, 1.0),
1531                        ]
1532                        .choose_weighted(rng, |(_, w)| *w)
1533                        .ok()
1534                        .and_then(|s| s.0);
1535
1536                        // Here as an example of using sprite_cfg to override
1537                        // default loot table
1538                        if chest == Some(SpriteKind::DungeonChest3) {
1539                            sprite_cfg_to_set = Some(SpriteCfg {
1540                                loot_table: Some("common.loot_tables.cave_large".to_owned()),
1541                                ..Default::default()
1542                            });
1543                        }
1544                        chest
1545                    } else if rand.chance(wpos2d.with_z(7), 0.007) {
1546                        let shallow = close(biome.depth, 0.0, 0.4, 3);
1547                        let middle = close(biome.depth, 0.5, 0.4, 3);
1548                        //let deep = close(biome.depth, 1.0, 0.4); // TODO: Use this for deep only
1549                        // things
1550                        [
1551                            (Some(SpriteKind::Stones), 1.5),
1552                            (Some(SpriteKind::Copper), shallow),
1553                            (Some(SpriteKind::Tin), shallow),
1554                            (Some(SpriteKind::Iron), shallow * 0.6),
1555                            (Some(SpriteKind::Coal), middle * 0.25),
1556                            (Some(SpriteKind::Cobalt), middle * 0.1),
1557                            (Some(SpriteKind::Silver), middle * 0.05),
1558                            (None, 10.0),
1559                        ]
1560                        .choose_weighted(rng, |(_, w)| *w)
1561                        .ok()
1562                        .and_then(|s| s.0)
1563                    } else {
1564                        try_spawn_entity = true;
1565                        None
1566                    }
1567                })
1568                .flatten()
1569            {
1570                Block::air(sprite)
1571            } else if let Some(sprite) = (z == ceiling - 1 && !void_above)
1572                .then(|| {
1573                    if biome.mushroom > 0.5
1574                        && rand.chance(wpos2d.with_z(3), biome.mushroom.powi(2) * 0.01)
1575                    {
1576                        [(SpriteKind::MycelBlue, 0.75), (SpriteKind::Mold, 1.0)]
1577                            .choose_weighted(rng, |(_, w)| *w)
1578                            .ok()
1579                            .map(|s| s.0)
1580                    } else if biome.leafy > 0.4
1581                        && rand.chance(
1582                            wpos2d.with_z(4),
1583                            biome.leafy * (biome.humidity * 1.3) * 0.015,
1584                        )
1585                    {
1586                        [
1587                            (SpriteKind::Liana, 1.5),
1588                            (SpriteKind::CeilingLanternPlant, 1.25),
1589                            (SpriteKind::CeilingLanternFlower, 1.0),
1590                            (SpriteKind::CeilingJungleLeafyPlant, 1.5),
1591                        ]
1592                        .choose_weighted(rng, |(_, w)| *w)
1593                        .ok()
1594                        .map(|s| s.0)
1595                    } else if rand.chance(wpos2d.with_z(5), biome.barren * 0.015) {
1596                        Some(SpriteKind::Root)
1597                    } else if rand.chance(wpos2d.with_z(5), biome.crystal * 0.005) {
1598                        Some(SpriteKind::CrystalHigh)
1599                    } else {
1600                        None
1601                    }
1602                })
1603                .flatten()
1604            {
1605                Block::air(sprite)
1606            } else if let Some(structure_block) = get_structure(wpos, rng) {
1607                structure_block
1608            } else {
1609                Block::empty()
1610            }
1611        });
1612        if let Some(sprite_cfg) = sprite_cfg_to_set {
1613            canvas.set_sprite_cfg(wpos, sprite_cfg);
1614        }
1615
1616        if try_spawn_entity {
1617            apply_entity_spawns(canvas, wpos, &biome, rng);
1618        }
1619    }
1620}
1621
1622// #[inline_tweak::tweak_fn]
1623fn apply_entity_spawns<R: Rng>(canvas: &mut Canvas, wpos: Vec3<i32>, biome: &Biome, rng: &mut R) {
1624    if RandomField::new(canvas.info().index().seed).chance(wpos, 0.035) {
1625        if let Some(entity_asset) = [
1626            // Mushroom biome
1627            (
1628                Some("common.entity.wild.aggressive.goblin_thug"),
1629                biome.mushroom + 0.02,
1630                0.35,
1631                0.5,
1632            ),
1633            (
1634                Some("common.entity.wild.aggressive.goblin_chucker"),
1635                biome.mushroom + 0.02,
1636                0.35,
1637                0.5,
1638            ),
1639            (
1640                Some("common.entity.wild.aggressive.goblin_ruffian"),
1641                biome.mushroom + 0.02,
1642                0.35,
1643                0.5,
1644            ),
1645            (
1646                Some("common.entity.wild.peaceful.truffler"),
1647                biome.mushroom + 0.02,
1648                0.35,
1649                0.5,
1650            ),
1651            (
1652                Some("common.entity.wild.peaceful.fungome"),
1653                biome.mushroom + 0.02,
1654                0.5,
1655                0.5,
1656            ),
1657            (
1658                Some("common.entity.wild.peaceful.bat"),
1659                biome.mushroom + 0.1,
1660                0.25,
1661                0.5,
1662            ),
1663            // Leafy biome
1664            (
1665                Some("common.entity.wild.aggressive.goblin_thug"),
1666                biome.leafy.max(biome.dusty) + 0.05,
1667                0.25,
1668                0.5,
1669            ),
1670            (
1671                Some("common.entity.wild.aggressive.goblin_chucker"),
1672                biome.leafy.max(biome.dusty) + 0.05,
1673                0.25,
1674                0.5,
1675            ),
1676            (
1677                Some("common.entity.wild.aggressive.goblin_ruffian"),
1678                biome.leafy.max(biome.dusty) + 0.05,
1679                0.25,
1680                0.5,
1681            ),
1682            (
1683                Some("common.entity.wild.peaceful.holladon"),
1684                biome.leafy.max(biome.dusty) + 0.05,
1685                0.25,
1686                0.5,
1687            ),
1688            (
1689                Some("common.entity.dungeon.gnarling.mandragora"),
1690                biome.leafy + 0.05,
1691                0.2,
1692                0.5,
1693            ),
1694            (
1695                Some("common.entity.wild.aggressive.rootsnapper"),
1696                biome.leafy + 0.05,
1697                0.075,
1698                0.5,
1699            ),
1700            (
1701                Some("common.entity.wild.aggressive.maneater"),
1702                biome.leafy + 0.05,
1703                0.075,
1704                0.5,
1705            ),
1706            (
1707                Some("common.entity.wild.aggressive.batfox"),
1708                biome
1709                    .leafy
1710                    .max(biome.barren)
1711                    .max(biome.sandy)
1712                    .max(biome.snowy)
1713                    + 0.3,
1714                0.25,
1715                0.5,
1716            ),
1717            (
1718                Some("common.entity.wild.peaceful.crawler_moss"),
1719                biome.leafy + 0.05,
1720                0.25,
1721                0.5,
1722            ),
1723            (
1724                Some("common.entity.wild.aggressive.asp"),
1725                biome.leafy.max(biome.sandy) + 0.1,
1726                0.2,
1727                0.5,
1728            ),
1729            (
1730                Some("common.entity.wild.aggressive.swamp_troll"),
1731                biome.leafy + 0.0,
1732                0.05,
1733                0.5,
1734            ),
1735            (
1736                Some("common.entity.wild.peaceful.bat"),
1737                biome.leafy + 0.1,
1738                0.25,
1739                0.5,
1740            ),
1741            // Dusty biome
1742            (
1743                Some("common.entity.wild.aggressive.dodarock"),
1744                biome
1745                    .dusty
1746                    .max(biome.barren)
1747                    .max(biome.crystal)
1748                    .max(biome.snowy)
1749                    + 0.05,
1750                0.05,
1751                0.5,
1752            ),
1753            (
1754                Some("common.entity.wild.aggressive.cave_spider"),
1755                biome.dusty + 0.0,
1756                0.05,
1757                0.5,
1758            ),
1759            (
1760                Some("common.entity.wild.aggressive.cave_troll"),
1761                biome.dusty + 0.1,
1762                0.05,
1763                0.5,
1764            ),
1765            (
1766                Some("common.entity.wild.peaceful.rat"),
1767                biome.dusty + 0.1,
1768                0.3,
1769                0.5,
1770            ),
1771            (
1772                Some("common.entity.wild.peaceful.bat"),
1773                biome.dusty.max(biome.sandy).max(biome.snowy) + 0.1,
1774                0.25,
1775                0.5,
1776            ),
1777            // Icy biome
1778            (
1779                Some("common.entity.wild.aggressive.icedrake"),
1780                biome.icy + 0.0,
1781                0.1,
1782                0.5,
1783            ),
1784            (
1785                Some("common.entity.wild.aggressive.wendigo"),
1786                biome.icy.min(biome.depth) + 0.0,
1787                0.02,
1788                0.5,
1789            ),
1790            (
1791                Some("common.entity.wild.aggressive.frostfang"),
1792                biome.icy + 0.0,
1793                0.25,
1794                0.5,
1795            ),
1796            (
1797                Some("common.entity.wild.aggressive.tursus"),
1798                biome.icy + 0.0,
1799                0.03,
1800                0.5,
1801            ),
1802            // Lava biome
1803            (
1804                Some("common.entity.wild.aggressive.lavadrake"),
1805                biome.fire + 0.0,
1806                0.15,
1807                0.5,
1808            ),
1809            (
1810                Some("common.entity.wild.peaceful.crawler_molten"),
1811                biome.fire + 0.0,
1812                0.2,
1813                0.5,
1814            ),
1815            (
1816                Some("common.entity.wild.aggressive.cave_salamander"),
1817                biome.fire + 0.0,
1818                0.4,
1819                0.5,
1820            ),
1821            (
1822                Some("common.entity.wild.aggressive.red_oni"),
1823                biome.fire + 0.0,
1824                0.03,
1825                0.5,
1826            ),
1827            // Crystal biome
1828            (
1829                Some("common.entity.wild.aggressive.basilisk"),
1830                biome.crystal + 0.1,
1831                0.1,
1832                0.5,
1833            ),
1834            (
1835                Some("common.entity.wild.aggressive.blue_oni"),
1836                biome.crystal + 0.0,
1837                0.03,
1838                0.5,
1839            ),
1840            // Sandy biome
1841            (
1842                Some("common.entity.wild.aggressive.antlion"),
1843                biome.sandy.max(biome.dusty) + 0.1,
1844                0.025,
1845                0.5,
1846            ),
1847            (
1848                Some("common.entity.wild.aggressive.sandshark"),
1849                biome.sandy + 0.1,
1850                0.025,
1851                0.5,
1852            ),
1853            (
1854                Some("common.entity.wild.peaceful.crawler_sand"),
1855                biome.sandy + 0.1,
1856                0.25,
1857                0.5,
1858            ),
1859            // Snowy biome
1860            (
1861                Some("common.entity.wild.aggressive.akhlut"),
1862                (biome.snowy.max(biome.icy) + 0.1),
1863                0.01,
1864                0.5,
1865            ),
1866            (
1867                Some("common.entity.wild.aggressive.rocksnapper"),
1868                biome.barren.max(biome.crystal).max(biome.snowy) + 0.1,
1869                0.1,
1870                0.5,
1871            ),
1872            // With depth
1873            (
1874                Some("common.entity.wild.aggressive.black_widow"),
1875                biome.depth + 0.0,
1876                0.02,
1877                0.5,
1878            ),
1879            (
1880                Some("common.entity.wild.aggressive.ogre"),
1881                biome.depth + 0.0,
1882                0.02,
1883                0.5,
1884            ),
1885            (None, 100.0, 0.0, 0.0),
1886        ]
1887        .iter()
1888        .filter_map(|(entity, biome_modifier, chance, cutoff)| {
1889            if let Some(entity) = entity {
1890                if *biome_modifier > *cutoff {
1891                    let close = close(1.0, *biome_modifier, *cutoff, 2);
1892                    (close > 0.0).then(|| (Some(entity), close * chance))
1893                } else {
1894                    None
1895                }
1896            } else {
1897                Some((None, 100.0))
1898            }
1899        })
1900        .collect_vec()
1901        .choose_weighted(rng, |(_, w)| *w)
1902        .ok()
1903        .and_then(|s| s.0)
1904        {
1905            canvas.spawn(EntityInfo::at(wpos.map(|e| e as f32)).with_asset_expect(
1906                entity_asset,
1907                rng,
1908                None,
1909            ));
1910        }
1911    }
1912
1913    // FIXME: Add back waypoints once caves are not impossible to escape.
1914    /* // Occasionally place down a waypoint
1915    if RandomField::new(canvas.info().index().seed).chance(wpos, 0.000005) {
1916        canvas.spawn(EntityInfo::at(wpos.map(|e| e as f32)).into_waypoint());
1917    } */
1918}