veloren_world/layer/
cave.rs

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