veloren_world/layer/
cave.rs

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