veloren_world/site/plot/
adlet.rs

1use super::*;
2use crate::{
3    IndexRef, Land,
4    assets::AssetHandle,
5    site::{gen::PrimitiveTransform, util::Dir},
6    util::{
7        FastNoise, NEIGHBORS, NEIGHBORS3, RandomField, attempt, sampler::Sampler, within_distance,
8    },
9};
10use common::{
11    generation::{ChunkSupplement, EntityInfo},
12    terrain::{Structure as PrefabStructure, StructuresGroup},
13};
14use lazy_static::lazy_static;
15use rand::prelude::*;
16use std::{
17    f32::consts::{PI, TAU},
18    sync::Arc,
19};
20use vek::*;
21
22pub struct AdletStronghold {
23    name: String,
24    entrance: Vec2<i32>,
25    surface_radius: i32,
26    // Structure indicates the kind of structure it is, vec2 is relative position of structure
27    // compared to wall_center, dir tells which way structure should face
28    outer_structures: Vec<(AdletStructure, Vec2<i32>, Dir)>,
29    tunnel_length: i32,
30    cavern_center: Vec2<i32>,
31    cavern_alt: f32,
32    cavern_radius: i32,
33    // Structure indicates the kind of structure it is, vec2 is relative position of structure
34    // compared to cavern_center, dir tells which way structure should face
35    cavern_structures: Vec<(AdletStructure, Vec2<i32>, Dir)>,
36}
37
38#[derive(Copy, Clone)]
39enum AdletStructure {
40    Igloo,
41    TunnelEntrance,
42    SpeleothemCluster,
43    Bonfire,
44    YetiPit,
45    Tannery,
46    AnimalPen,
47    CookFire,
48    RockHut,
49    BoneHut,
50    BossBoneHut,
51}
52
53impl AdletStructure {
54    fn required_separation(&self, other: &Self) -> i32 {
55        let radius = |structure: &Self| match structure {
56            Self::Igloo => 20,
57            Self::TunnelEntrance => 32,
58            Self::SpeleothemCluster => 4,
59            Self::YetiPit => 32,
60            Self::Tannery => 6,
61            Self::AnimalPen => 8,
62            Self::CookFire => 3,
63            Self::RockHut => 4,
64            Self::BoneHut => 11,
65            Self::BossBoneHut => 14,
66            Self::Bonfire => 10,
67        };
68
69        let additional_padding = match (self, other) {
70            (Self::Igloo, Self::Igloo) => 3,
71            (Self::BoneHut, Self::BoneHut) => 3,
72            (Self::Tannery, Self::Tannery) => 5,
73            (Self::CookFire, Self::CookFire) => 20,
74            (Self::AnimalPen, Self::AnimalPen) => 8,
75            // Keep these last
76            (Self::SpeleothemCluster, Self::SpeleothemCluster) => 0,
77            (Self::SpeleothemCluster, _) | (_, Self::SpeleothemCluster) => 5,
78            _ => 0,
79        };
80
81        radius(self) + radius(other) + additional_padding
82    }
83}
84
85impl AdletStronghold {
86    pub fn generate(wpos: Vec2<i32>, land: &Land, rng: &mut impl Rng, _index: IndexRef) -> Self {
87        let name = NameGen::location(rng).generate_adlet();
88        let entrance = wpos;
89
90        let surface_radius: i32 = {
91            let unit_size = rng.gen_range(10..12);
92            let num_units = rng.gen_range(4..8);
93            let variation = rng.gen_range(20..30);
94            unit_size * num_units + variation
95        };
96
97        // Find direction that allows for deep enough site
98        let angle_samples = (0..64).map(|x| x as f32 / 64.0 * TAU);
99        // Sample blocks 40-50 away, use angle where these positions are highest
100        // relative to entrance
101        let angle = angle_samples
102            .max_by_key(|theta| {
103                let entrance_height = land.get_alt_approx(entrance);
104                let height =
105                    |pos: Vec2<f32>| land.get_alt_approx(pos.as_() + entrance) - entrance_height;
106                let (x, y) = (theta.cos(), theta.sin());
107                (40..=50)
108                    .map(|r| {
109                        let rpos = Vec2::new(r as f32 * x, r as f32 * y);
110                        height(rpos) as i32
111                    })
112                    .sum::<i32>()
113            })
114            .unwrap_or(0.0);
115
116        let cavern_radius: i32 = {
117            let unit_size = rng.gen_range(10..15);
118            let num_units = rng.gen_range(5..8);
119            let variation = rng.gen_range(20..40);
120            unit_size * num_units + variation
121        };
122
123        let tunnel_length = rng.gen_range(35_i32..50);
124
125        let cavern_center = entrance
126            + (Vec2::new(angle.cos(), angle.sin()) * (tunnel_length as f32 + cavern_radius as f32))
127                .as_();
128
129        let cavern_alt = (land.get_alt_approx(cavern_center) - cavern_radius as f32)
130            .min(land.get_alt_approx(entrance));
131
132        let mut outer_structures = Vec::<(AdletStructure, Vec2<i32>, Dir)>::new();
133
134        let entrance_dir = Dir::from_vec2(entrance - cavern_center);
135        outer_structures.push((AdletStructure::TunnelEntrance, Vec2::zero(), entrance_dir));
136
137        let desired_structures = surface_radius.pow(2) / 100;
138        for _ in 0..desired_structures {
139            if let Some((rpos, kind)) = attempt(50, || {
140                let structure_kind = AdletStructure::Igloo;
141                /*
142                // Choose structure kind
143                let structure_kind = match rng.gen_range(0..10) {
144                    // TODO: Add more variants
145                    _ => AdletStructure::Igloo,
146                };
147                 */
148                // Choose relative position
149                let structure_center = {
150                    let theta = rng.gen::<f32>() * TAU;
151                    // 0.8 to keep structures not directly against wall
152                    let radius = surface_radius as f32 * rng.gen::<f32>().sqrt() * 0.8;
153                    let x = radius * theta.sin();
154                    let y = radius * theta.cos();
155                    Vec2::new(x, y).as_()
156                };
157
158                let tunnel_line = LineSegment2 {
159                    start: entrance,
160                    end: entrance - entrance_dir.to_vec2() * 100,
161                };
162
163                // Check that structure not in the water or too close to another structure
164                if land
165                    .get_chunk_wpos(structure_center.as_() + entrance)
166                    .is_some_and(|c| c.is_underwater())
167                    || outer_structures.iter().any(|(kind, rpos, _dir)| {
168                        structure_center.distance_squared(*rpos)
169                            < structure_kind.required_separation(kind).pow(2)
170                    })
171                    || tunnel_line
172                        .as_::<f32>()
173                        .distance_to_point((structure_center + entrance).as_::<f32>())
174                        < 25.0
175                {
176                    None
177                } else {
178                    Some((structure_center, structure_kind))
179                }
180            }) {
181                let dir_to_wall = Dir::from_vec2(rpos);
182                let door_rng: u32 = rng.gen_range(0..9);
183                let door_dir = match door_rng {
184                    0..=3 => dir_to_wall,
185                    4..=5 => dir_to_wall.rotated_cw(),
186                    6..=7 => dir_to_wall.rotated_ccw(),
187                    // Should only be 8
188                    _ => dir_to_wall.opposite(),
189                };
190                outer_structures.push((kind, rpos, door_dir));
191            }
192        }
193
194        let mut cavern_structures = Vec::<(AdletStructure, Vec2<i32>, Dir)>::new();
195
196        fn valid_cavern_struct_pos(
197            structures: &[(AdletStructure, Vec2<i32>, Dir)],
198            structure: AdletStructure,
199            rpos: Vec2<i32>,
200        ) -> bool {
201            structures.iter().all(|(kind, rpos2, _dir)| {
202                rpos.distance_squared(*rpos2) > structure.required_separation(kind).pow(2)
203            })
204        }
205
206        // Add speleothem clusters (stalagmites/stalactites)
207        let desired_speleothem_clusters = cavern_radius.pow(2) / 2500;
208        for _ in 0..desired_speleothem_clusters {
209            if let Some(mut rpos) = attempt(25, || {
210                let rpos = {
211                    let theta = rng.gen_range(0.0..TAU);
212                    // sqrt biases radius away from center, leading to even distribution in circle
213                    let radius = rng.gen::<f32>().sqrt() * cavern_radius as f32;
214                    Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
215                };
216                valid_cavern_struct_pos(&cavern_structures, AdletStructure::SpeleothemCluster, rpos)
217                    .then_some(rpos)
218            }) {
219                // Dir doesn't matter since these are directionless
220                cavern_structures.push((AdletStructure::SpeleothemCluster, rpos, Dir::X));
221                let desired_adjacent_clusters = rng.gen_range(1..5);
222                for _ in 0..desired_adjacent_clusters {
223                    // Choose a relative position adjacent to initial speleothem cluster
224                    let adj_rpos = {
225                        let theta = rng.gen_range(0.0..TAU);
226                        let radius = rng.gen_range(1.0..5.0);
227                        let rrpos = Vec2::new(theta.cos() * radius, theta.sin() * radius).as_();
228                        rpos + rrpos
229                    };
230                    if valid_cavern_struct_pos(
231                        &cavern_structures,
232                        AdletStructure::SpeleothemCluster,
233                        adj_rpos,
234                    ) {
235                        cavern_structures.push((
236                            AdletStructure::SpeleothemCluster,
237                            adj_rpos,
238                            Dir::X,
239                        ));
240                        // Set new rpos to next cluster is adjacent to most recently placed
241                        rpos = adj_rpos;
242                    } else {
243                        // If any cluster ever fails to place, break loop and stop creating cluster
244                        // chain
245                        break;
246                    }
247                }
248            }
249        }
250
251        // Attempt to place central boss bone hut
252        if let Some(rpos) = attempt(50, || {
253            let rpos = {
254                let theta = rng.gen_range(0.0..TAU);
255                let radius = rng.gen::<f32>() * cavern_radius as f32 * 0.5;
256                Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
257            };
258            valid_cavern_struct_pos(&cavern_structures, AdletStructure::BossBoneHut, rpos)
259                .then_some(rpos)
260        })
261        .or_else(|| {
262            attempt(100, || {
263                let rpos = {
264                    let theta = rng.gen_range(0.0..TAU);
265                    // If selecting a spot near the center failed, find a spot anywhere in the
266                    // cavern
267                    let radius = rng.gen::<f32>().sqrt() * cavern_radius as f32;
268                    Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
269                };
270                valid_cavern_struct_pos(&cavern_structures, AdletStructure::BossBoneHut, rpos)
271                    .then_some(rpos)
272            })
273        }) {
274            // Direction doesn't matter for boss bonehut
275            cavern_structures.push((AdletStructure::BossBoneHut, rpos, Dir::X));
276        }
277
278        // Attempt to place yetipit near the cavern edge
279        if let Some(rpos) = attempt(50, || {
280            let rpos = {
281                let theta = rng.gen_range(0.0..TAU);
282                let radius = cavern_radius as f32;
283                Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
284            };
285            valid_cavern_struct_pos(&cavern_structures, AdletStructure::YetiPit, rpos)
286                .then_some(rpos)
287        })
288        .or_else(|| {
289            attempt(100, || {
290                let rpos = {
291                    let theta = rng.gen_range(0.0..TAU);
292                    // If selecting a spot near the cavern edge failed, find a spot anywhere in the
293                    // cavern
294                    let radius = rng.gen::<f32>().sqrt() * cavern_radius as f32;
295                    Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
296                };
297                valid_cavern_struct_pos(&cavern_structures, AdletStructure::YetiPit, rpos)
298                    .then_some(rpos)
299            })
300        }) {
301            // Direction doesn't matter for yetipit
302            cavern_structures.push((AdletStructure::YetiPit, rpos, Dir::X));
303        }
304
305        // Attempt to place big bonfire
306        if let Some(rpos) = attempt(50, || {
307            let rpos = {
308                let theta = rng.gen_range(0.0..TAU);
309                let radius = rng.gen::<f32>().sqrt() * cavern_radius as f32 * 0.9;
310                Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
311            };
312            valid_cavern_struct_pos(&cavern_structures, AdletStructure::Bonfire, rpos)
313                .then_some(rpos)
314        }) {
315            // Direction doesn't matter for central bonfire
316            cavern_structures.push((AdletStructure::Bonfire, rpos, Dir::X));
317        }
318
319        // Attempt to place some rock huts around the outer edge
320        let desired_rock_huts = cavern_radius / 5;
321        for _ in 0..desired_rock_huts {
322            if let Some(rpos) = attempt(25, || {
323                let rpos = {
324                    let theta = rng.gen_range(0.0..TAU);
325                    let radius = cavern_radius as f32 - 1.0;
326                    Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
327                };
328                valid_cavern_struct_pos(&cavern_structures, AdletStructure::RockHut, rpos)
329                    .then_some(rpos)
330            }) {
331                // Rock huts need no direction
332                cavern_structures.push((AdletStructure::RockHut, rpos, Dir::X));
333            }
334        }
335
336        // Attempt to place some general structures
337        let desired_structures = cavern_radius.pow(2) / 200;
338        for _ in 0..desired_structures {
339            if let Some((structure, rpos)) = attempt(45, || {
340                let rpos = {
341                    let theta = rng.gen_range(0.0..TAU);
342                    // sqrt biases radius away from center, leading to even distribution in circle
343                    let radius = rng.gen::<f32>().sqrt() * cavern_radius as f32 * 0.9;
344                    Vec2::new(theta.cos() * radius, theta.sin() * radius).as_()
345                };
346                let structure = match rng.gen_range(0..7) {
347                    0..=2 => AdletStructure::BoneHut,
348                    3..=4 => AdletStructure::CookFire,
349                    5 => AdletStructure::Tannery,
350                    _ => AdletStructure::AnimalPen,
351                };
352
353                valid_cavern_struct_pos(&cavern_structures, structure, rpos)
354                    .then_some((structure, rpos))
355            }) {
356                // Direction facing the central bonfire
357                let dir = Dir::from_vec2(rpos).opposite();
358                cavern_structures.push((structure, rpos, dir));
359            }
360        }
361
362        Self {
363            name,
364            entrance,
365            surface_radius,
366            outer_structures,
367            tunnel_length,
368            cavern_center,
369            cavern_radius,
370            cavern_alt,
371            cavern_structures,
372        }
373    }
374
375    pub fn name(&self) -> &str { &self.name }
376
377    // pub fn origin(&self) -> Vec2<i32> { self.cavern_center }
378
379    pub fn radius(&self) -> i32 { self.cavern_radius + self.tunnel_length + 5 }
380
381    pub fn plot_tiles(&self, origin: Vec2<i32>) -> (Aabr<i32>, Aabr<i32>) {
382        // Cavern
383        let size = self.cavern_radius / tile::TILE_SIZE as i32;
384        let offset = (self.cavern_center - origin) / tile::TILE_SIZE as i32;
385        let cavern_aabr = Aabr {
386            min: Vec2::broadcast(-size) + offset,
387            max: Vec2::broadcast(size) + offset,
388        };
389        // Surface
390        let size = (self.surface_radius * 5 / 4) / tile::TILE_SIZE as i32;
391        let offset = (self.entrance - origin) / tile::TILE_SIZE as i32;
392        let surface_aabr = Aabr {
393            min: Vec2::broadcast(-size) + offset,
394            max: Vec2::broadcast(size) + offset,
395        };
396        (cavern_aabr, surface_aabr)
397    }
398
399    pub fn spawn_rules(&self, wpos: Vec2<i32>) -> SpawnRules {
400        SpawnRules {
401            waypoints: false,
402            trees: !within_distance(wpos, self.entrance, self.surface_radius * 5 / 4),
403            ..SpawnRules::default()
404        }
405    }
406
407    // TODO: Find a better way of spawning entities in site
408    pub fn apply_supplement(
409        &self,
410        // NOTE: Used only for dynamic elements like chests and entities!
411        _dynamic_rng: &mut impl Rng,
412        wpos2d: Vec2<i32>,
413        _supplement: &mut ChunkSupplement,
414    ) {
415        let rpos = wpos2d - self.cavern_center;
416        let _area = Aabr {
417            min: rpos,
418            max: rpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
419        };
420    }
421}
422
423impl Structure for AdletStronghold {
424    #[cfg(feature = "use-dyn-lib")]
425    const UPDATE_FN: &'static [u8] = b"render_adletstronghold\0";
426
427    #[cfg_attr(feature = "be-dyn-lib", unsafe(export_name = "render_adletstronghold"))]
428    fn render_inner(&self, _site: &Site, land: &Land, painter: &Painter) {
429        let snow_ice_fill = Fill::Sampling(Arc::new(|wpos| {
430            Some(match (RandomField::new(0).get(wpos)) % 250 {
431                0..=2 => Block::new(BlockKind::Ice, Rgb::new(120, 160, 255)),
432                3..=10 => Block::new(BlockKind::ArtSnow, Rgb::new(138, 147, 217)),
433                11..=20 => Block::new(BlockKind::ArtSnow, Rgb::new(213, 213, 242)),
434                21..=35 => Block::new(BlockKind::ArtSnow, Rgb::new(231, 230, 247)),
435                36..=62 => Block::new(BlockKind::ArtSnow, Rgb::new(180, 181, 227)),
436                _ => Block::new(BlockKind::ArtSnow, Rgb::new(209, 212, 238)),
437            })
438        }));
439        let snow_ice_air_fill = Fill::Sampling(Arc::new(|wpos| {
440            Some(match (RandomField::new(0).get(wpos)) % 250 {
441                0..=2 => Block::new(BlockKind::Ice, Rgb::new(120, 160, 255)),
442                3..=5 => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
443                6..=10 => Block::new(BlockKind::ArtSnow, Rgb::new(138, 147, 217)),
444                11..=20 => Block::new(BlockKind::ArtSnow, Rgb::new(213, 213, 242)),
445                21..=35 => Block::new(BlockKind::ArtSnow, Rgb::new(231, 230, 247)),
446                36..=62 => Block::new(BlockKind::ArtSnow, Rgb::new(180, 181, 227)),
447                _ => Block::new(BlockKind::ArtSnow, Rgb::new(209, 212, 238)),
448            })
449        }));
450        let bone_fill = Fill::Brick(BlockKind::Misc, Rgb::new(200, 160, 140), 1);
451        let ice_fill = Fill::Block(Block::new(BlockKind::Ice, Rgb::new(120, 160, 255)));
452        let dirt_fill = Fill::Brick(BlockKind::Earth, Rgb::new(55, 25, 8), 24);
453        let grass_fill = Fill::Sampling(Arc::new(|wpos| {
454            Some(match (RandomField::new(0).get(wpos)) % 5 {
455                1 => Block::air(SpriteKind::ShortGrass),
456                _ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
457            })
458        }));
459        let rock_fill = Fill::Sampling(Arc::new(|wpos| {
460            Some(match (RandomField::new(0).get(wpos)) % 4 {
461                0 => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
462                _ => Block::new(BlockKind::Rock, Rgb::new(90, 110, 150)),
463            })
464        }));
465        let bone_shrub = Fill::Sampling(Arc::new(|wpos| {
466            Some(match (RandomField::new(0).get(wpos)) % 40 {
467                0 => Block::new(BlockKind::Misc, Rgb::new(200, 160, 140)),
468                _ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
469            })
470        }));
471        let yetipit_sprites = Fill::Sampling(Arc::new(|wpos| {
472            Some(match (RandomField::new(0).get(wpos)) % 60 {
473                0..=2 => Block::air(SpriteKind::Bones),
474                4..=5 => Block::air(SpriteKind::GlowIceCrystal),
475                6..=8 => Block::air(SpriteKind::IceCrystal),
476                _ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
477            })
478        }));
479        let yetipit_sprites_deep = Fill::Sampling(Arc::new(|wpos| {
480            Some(match (RandomField::new(0).get(wpos)) % 275 {
481                0..=8 => Block::air(SpriteKind::Bones),
482                9..=19 => Block::air(SpriteKind::GlowIceCrystal),
483                20..=28 => Block::air(SpriteKind::IceCrystal),
484                29..=30 => Block::air(SpriteKind::DungeonChest1),
485                _ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
486            })
487        }));
488        let yeti_bones_fill = Fill::Sampling(Arc::new(|wpos| {
489            Some(match (RandomField::new(0).get(wpos)) % 20 {
490                0 => Block::air(SpriteKind::Bones),
491                _ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
492            })
493        }));
494        let mut rng = thread_rng();
495
496        // Tunnel
497        let dist: f32 = self.cavern_center.as_().distance(self.entrance.as_());
498        let dir = Dir::from_vec2(self.entrance - self.cavern_center);
499        let tunnel_start: Vec3<f32> = match dir {
500            Dir::X => Vec2::new(self.entrance.x + 7, self.entrance.y),
501            Dir::Y => Vec2::new(self.entrance.x, self.entrance.y + 7),
502            Dir::NegX => Vec2::new(self.entrance.x - 7, self.entrance.y),
503            Dir::NegY => Vec2::new(self.entrance.x, self.entrance.y - 7),
504        }
505        .as_()
506        .with_z(self.cavern_alt - 1.0);
507        // Adds cavern radius to ensure that tunnel fully bores into cavern
508        let raw_tunnel_end =
509            ((self.cavern_center.as_() - self.entrance.as_()) * self.tunnel_length as f32 / dist)
510                .with_z(self.cavern_alt - 1.0)
511                + self.entrance.as_();
512
513        let offset = 15.0;
514        let tunnel_end = match dir {
515            Dir::X => Vec3::new(raw_tunnel_end.x - offset, tunnel_start.y, raw_tunnel_end.z),
516            Dir::Y => Vec3::new(tunnel_start.x, raw_tunnel_end.y - offset, raw_tunnel_end.z),
517            Dir::NegX => Vec3::new(raw_tunnel_end.x + offset, tunnel_start.y, raw_tunnel_end.z),
518            Dir::NegY => Vec3::new(tunnel_start.x, raw_tunnel_end.y + offset, raw_tunnel_end.z),
519        };
520        // Platform
521        painter
522            .line(
523                tunnel_start + Vec3::new(0.0, 0.0, 5.0),
524                {
525                    let end = tunnel_start + (dir.to_vec2().as_().with_z(1.0) * 20.0);
526                    end.with_z(end.z + 5.0)
527                },
528                10.0,
529            )
530            .clear();
531        painter
532            .sphere(Aabb {
533                min: (self.entrance - 15).with_z(self.cavern_alt as i32 - 15),
534                max: (self.entrance + 15).with_z(self.cavern_alt as i32 + 15),
535            })
536            .fill(snow_ice_fill.clone());
537
538        painter
539            .cylinder(Aabb {
540                min: (self.entrance - 15).with_z(self.cavern_alt as i32),
541                max: (self.entrance + 15).with_z(self.cavern_alt as i32 + 20),
542            })
543            .clear();
544        painter
545            .cylinder(Aabb {
546                min: (self.entrance - 14).with_z(self.cavern_alt as i32 - 1),
547                max: (self.entrance + 14).with_z(self.cavern_alt as i32),
548            })
549            .clear();
550        painter
551            .cylinder(Aabb {
552                min: (self.entrance - 12).with_z(self.cavern_alt as i32 - 40),
553                max: (self.entrance + 12).with_z(self.cavern_alt as i32 - 10),
554            })
555            .fill(snow_ice_fill.clone());
556
557        let valid_entrance = painter.segment_prism(tunnel_start, tunnel_end, 20.0, 30.0);
558        painter
559            .segment_prism(tunnel_start, tunnel_end, 10.0, 10.0)
560            .clear();
561        painter
562            .line(
563                tunnel_start + Vec3::new(0.0, 0.0, 10.0),
564                tunnel_end + Vec3::new(0.0, 0.0, 10.0),
565                10.0,
566            )
567            .clear();
568        painter
569            .line(
570                tunnel_start
571                    + match dir {
572                        Dir::X => Vec3::new(0.0, 4.0, 7.0),
573                        Dir::Y => Vec3::new(4.0, 0.0, 7.0),
574                        Dir::NegX => Vec3::new(0.0, 4.0, 7.0),
575                        Dir::NegY => Vec3::new(4.0, 0.0, 7.0),
576                    },
577                tunnel_end
578                    + match dir {
579                        Dir::X => Vec3::new(0.0, 4.0, 7.0),
580                        Dir::Y => Vec3::new(4.0, 0.0, 7.0),
581                        Dir::NegX => Vec3::new(0.0, 4.0, 7.0),
582                        Dir::NegY => Vec3::new(4.0, 0.0, 7.0),
583                    },
584                8.0,
585            )
586            .intersect(valid_entrance)
587            .clear();
588        painter
589            .line(
590                tunnel_start
591                    + match dir {
592                        Dir::X => Vec3::new(0.0, -4.0, 7.0),
593                        Dir::Y => Vec3::new(-4.0, 0.0, 7.0),
594                        Dir::NegX => Vec3::new(0.0, -4.0, 7.0),
595                        Dir::NegY => Vec3::new(-4.0, 0.0, 7.0),
596                    },
597                tunnel_end
598                    + match dir {
599                        Dir::X => Vec3::new(0.0, -4.0, 7.0),
600                        Dir::Y => Vec3::new(-4.0, 0.0, 7.0),
601                        Dir::NegX => Vec3::new(0.0, -4.0, 7.0),
602                        Dir::NegY => Vec3::new(-4.0, 0.0, 7.0),
603                    },
604                8.0,
605            )
606            .intersect(valid_entrance)
607            .clear();
608        // Ensure there is a path to the cave if the above is weird (e.g. when it is at
609        // or near a 45 degrees angle)
610        painter
611            .line(
612                tunnel_end.with_z(tunnel_end.z + 4.0),
613                raw_tunnel_end.with_z(raw_tunnel_end.z + 4.0),
614                4.0,
615            )
616            .clear();
617
618        // Cavern
619        let cavern = painter
620            .sphere_with_radius(
621                self.cavern_center.with_z(self.cavern_alt as i32),
622                self.cavern_radius as f32,
623            )
624            .intersect(painter.aabb(Aabb {
625                min: (self.cavern_center - self.cavern_radius).with_z(self.cavern_alt as i32),
626                max: self.cavern_center.with_z(self.cavern_alt as i32) + self.cavern_radius,
627            }))
628            .sample_with_column({
629                let origin = self.cavern_center.with_z(self.cavern_alt as i32);
630                let radius_sqr = self.cavern_radius.pow(2);
631                move |pos, col| {
632                    let alt = col.basement - col.cliff_offset;
633                    let sphere_alt = ((radius_sqr - origin.xy().distance_squared(pos.xy())) as f32)
634                        .sqrt()
635                        + origin.z as f32;
636                    // Some sort of smooth min
637                    let alt = if alt < sphere_alt {
638                        alt
639                    } else if sphere_alt - alt < 10.0 {
640                        f32::lerp(sphere_alt, alt, 1.0 / (alt - sphere_alt).max(1.0))
641                    } else {
642                        sphere_alt
643                    };
644
645                    let noise = FastNoise::new(333);
646                    let alt_offset = noise.get(pos.with_z(0).as_() / 5.0).powi(2) * 15.0;
647
648                    let alt = alt - alt_offset;
649
650                    pos.z < alt as i32
651                }
652            });
653        let alt = self.cavern_alt;
654        cavern.clear();
655
656        // snow cylinder for cavern ground and to carve out yetipit
657        painter
658            .cylinder(Aabb {
659                min: (self.cavern_center - self.cavern_radius).with_z(alt as i32 - 200),
660                max: (self.cavern_center + self.cavern_radius).with_z(alt as i32),
661            })
662            .fill(snow_ice_fill.clone());
663
664        for (structure, wpos, alt, dir) in self
665            .outer_structures
666            .iter()
667            .map(|(structure, rpos, dir)| {
668                let wpos = rpos + self.entrance;
669                (structure, wpos, land.get_alt_approx(wpos), dir)
670            })
671            .chain(self.cavern_structures.iter().map(|(structure, rpos, dir)| {
672                (structure, rpos + self.cavern_center, self.cavern_alt, dir)
673            }))
674        {
675            match structure {
676                AdletStructure::TunnelEntrance => {
677                    let rib_width_curve = |i: f32| 0.5 * (0.4 * i + 1.0).log2() + 5.5;
678                    let spine_curve_amplitude = 0.0;
679                    let spine_curve_wavelength = 1.0;
680                    let spine_curve_function = |i: f32, amplitude: f32, wavelength: f32| {
681                        amplitude * (2.0 * PI * (1.0 / wavelength) * i).sin()
682                    };
683                    let rib_cage_config = RibCageGenerator {
684                        dir: *dir,
685                        spine_radius: 2.5,
686                        length: 40,
687                        spine_curve_function,
688                        spine_curve_amplitude,
689                        spine_curve_wavelength,
690                        spine_height: self.cavern_alt + 16.0,
691                        spine_start_z_offset: 2.0,
692                        spine_ctrl0_z_offset: 3.0,
693                        spine_ctrl1_z_offset: 5.0,
694                        spine_end_z_offset: 1.0,
695                        spine_ctrl0_length_fraction: 0.3,
696                        spine_ctrl1_length_fraction: 0.7,
697                        rib_base_alt: self.cavern_alt - 1.0,
698                        rib_spacing: 7,
699                        rib_radius: 1.7,
700                        rib_run: 5.0,
701                        rib_ctrl0_run_fraction: 0.3,
702                        rib_ctrl1_run_fraction: 0.5,
703                        rib_ctrl0_width_offset: 5.0,
704                        rib_ctrl1_width_offset: 3.0,
705                        rib_width_curve,
706                        rib_ctrl0_height_fraction: 0.8,
707                        rib_ctrl1_height_fraction: 0.4,
708                        vertebra_radius: 4.0,
709                        vertebra_width: 1.0,
710                        vertebra_z_offset: 0.3,
711                    };
712                    let rib_cage =
713                        rib_cage_config.bones(wpos + 40 * dir.opposite().to_vec2(), painter);
714                    for bone in rib_cage {
715                        bone.fill(bone_fill.clone());
716                    }
717                },
718                AdletStructure::Igloo => {
719                    let igloo_pos = wpos;
720                    let igloo_size = 8.0;
721                    let height_handle = 0;
722                    let bones_size = igloo_size as i32;
723                    painter
724                        .cylinder_with_radius(
725                            (igloo_pos).with_z(alt as i32 - 5 + height_handle),
726                            11.0,
727                            45.0,
728                        )
729                        .clear();
730                    // Foundation
731                    let foundation = match RandomField::new(0).get((wpos).with_z(alt as i32)) % 5 {
732                        0 => painter
733                            .sphere(Aabb {
734                                min: (igloo_pos - 15).with_z(alt as i32 - 45 + height_handle),
735                                max: (igloo_pos + 15).with_z(alt as i32 - 15 + height_handle),
736                            })
737                            .union(painter.sphere(Aabb {
738                                min: (igloo_pos - 10).with_z(alt as i32 - 20 + height_handle),
739                                max: (igloo_pos + 10).with_z(alt as i32 - 5 + height_handle),
740                            })),
741                        _ => painter
742                            .sphere(Aabb {
743                                min: (igloo_pos - 15).with_z(alt as i32 - 60 + height_handle),
744                                max: (igloo_pos + 15).with_z(alt as i32 - 30 + height_handle),
745                            })
746                            .union(painter.cone(Aabb {
747                                min: (igloo_pos - 15).with_z(alt as i32 - 45 + height_handle),
748                                max: (igloo_pos + 15).with_z(alt as i32 + 8 + height_handle),
749                            })),
750                    };
751                    foundation.fill(snow_ice_fill.clone());
752                    foundation.intersect(cavern).clear();
753                    // Platform
754                    painter
755                        .sphere(Aabb {
756                            min: (igloo_pos - 13).with_z(alt as i32 - 11 + height_handle),
757                            max: (igloo_pos + 13).with_z(alt as i32 + 11 + height_handle),
758                        })
759                        .fill(snow_ice_air_fill.clone());
760
761                    painter
762                        .cylinder(Aabb {
763                            min: (igloo_pos - 13).with_z(alt as i32 - 4 + height_handle),
764                            max: (igloo_pos + 13).with_z(alt as i32 + 16 + height_handle),
765                        })
766                        .clear();
767                    // 2 igloo variants
768                    match RandomField::new(0).get((igloo_pos).with_z(alt as i32)) % 4 {
769                        0 => {
770                            // clear room
771                            painter
772                                .sphere_with_radius(
773                                    igloo_pos.with_z(alt as i32 - 1 + height_handle),
774                                    (igloo_size as i32 - 2) as f32,
775                                )
776                                .clear();
777                            let pos_var = RandomField::new(0).get(igloo_pos.with_z(alt as i32)) % 5;
778                            let radius = 8 + pos_var;
779                            let bones = 8.0 + pos_var as f32;
780                            let phi = TAU / bones;
781                            for n in 1..=bones as i32 {
782                                let bone_hide_fill = Fill::Sampling(Arc::new(|pos| {
783                                    Some(match (RandomField::new(0).get(pos)) % 35 {
784                                        0 => Block::new(BlockKind::Wood, Rgb::new(73, 29, 0)),
785                                        1 => Block::new(BlockKind::Wood, Rgb::new(78, 67, 43)),
786                                        2 => Block::new(BlockKind::Wood, Rgb::new(83, 74, 41)),
787                                        3 => Block::new(BlockKind::Wood, Rgb::new(14, 36, 34)),
788                                        _ => Block::new(BlockKind::Misc, Rgb::new(200, 160, 140)),
789                                    })
790                                }));
791
792                                let pos = Vec2::new(
793                                    igloo_pos.x + (radius as f32 * ((n as f32 * phi).cos())) as i32,
794                                    igloo_pos.y + (radius as f32 * ((n as f32 * phi).sin())) as i32,
795                                );
796                                let bone_var = RandomField::new(0).get(pos.with_z(alt as i32)) % 5;
797
798                                match RandomField::new(0).get((igloo_pos - 1).with_z(alt as i32))
799                                    % 3
800                                {
801                                    0 => {
802                                        painter
803                                            .line(
804                                                pos.with_z(alt as i32 - 6 + height_handle),
805                                                igloo_pos.with_z(alt as i32 + 8 + height_handle),
806                                                1.0,
807                                            )
808                                            .fill(bone_hide_fill.clone());
809                                    },
810                                    _ => {
811                                        painter
812                                            .cubic_bezier(
813                                                pos.with_z(alt as i32 - 6 + height_handle),
814                                                (pos - ((igloo_pos - pos) / 2)).with_z(
815                                                    alt as i32
816                                                        + 12
817                                                        + bone_var as i32
818                                                        + height_handle,
819                                                ),
820                                                (pos + ((igloo_pos - pos) / 2))
821                                                    .with_z(alt as i32 + 9 + height_handle),
822                                                igloo_pos.with_z(alt as i32 + 5 + height_handle),
823                                                1.0,
824                                            )
825                                            .fill(bone_hide_fill.clone());
826                                    },
827                                };
828                            }
829                            let outside_wolfs = 2
830                                + (RandomField::new(0)
831                                    .get((igloo_pos - 1).with_z(alt as i32 - 5 + height_handle))
832                                    % 3) as i32;
833                            for _ in 0..outside_wolfs {
834                                let igloo_mob_spawn =
835                                    (igloo_pos - 1).with_z(alt as i32 - 5 + height_handle);
836                                painter.spawn(wolf(igloo_mob_spawn.as_(), &mut rng))
837                            }
838                        },
839                        _ => {
840                            // top decor bone with some hide
841                            painter
842                                .aabb(Aabb {
843                                    min: igloo_pos
844                                        .with_z((alt as i32) + bones_size + height_handle),
845                                    max: (igloo_pos + 1)
846                                        .with_z((alt as i32) + bones_size + 3 + height_handle),
847                                })
848                                .fill(bone_fill.clone());
849                            painter
850                                .aabb(Aabb {
851                                    min: Vec2::new(igloo_pos.x, igloo_pos.y - 1)
852                                        .with_z((alt as i32) + bones_size + 3 + height_handle),
853                                    max: Vec2::new(igloo_pos.x + 1, igloo_pos.y + 2)
854                                        .with_z((alt as i32) + bones_size + 4 + height_handle),
855                                })
856                                .fill(bone_fill.clone());
857                            painter
858                                .aabb(Aabb {
859                                    min: igloo_pos
860                                        .with_z((alt as i32) + bones_size + 3 + height_handle),
861                                    max: (igloo_pos + 1)
862                                        .with_z((alt as i32) + bones_size + 4 + height_handle),
863                                })
864                                .clear();
865                            let top_color = Fill::Sampling(Arc::new(|igloo_pos| {
866                                Some(match (RandomField::new(0).get(igloo_pos)) % 10 {
867                                    0 => Block::new(BlockKind::Wood, Rgb::new(73, 29, 0)),
868                                    1 => Block::new(BlockKind::Wood, Rgb::new(78, 67, 43)),
869                                    2 => Block::new(BlockKind::Wood, Rgb::new(83, 74, 41)),
870                                    3 => Block::new(BlockKind::Wood, Rgb::new(14, 36, 34)),
871                                    _ => Block::new(BlockKind::Rock, Rgb::new(200, 160, 140)),
872                                })
873                            }));
874                            painter
875                                .aabb(Aabb {
876                                    min: (igloo_pos - 1)
877                                        .with_z((alt as i32) + bones_size - 1 + height_handle),
878                                    max: (igloo_pos + 2)
879                                        .with_z((alt as i32) + bones_size + 1 + height_handle),
880                                })
881                                .fill(top_color.clone());
882                            // igloo snow
883                            painter
884                                .sphere_with_radius(igloo_pos.with_z(alt as i32 - 1), igloo_size)
885                                .fill(snow_ice_fill.clone());
886                            // 4 hide pieces
887                            for dir in CARDINALS {
888                                let hide_size = 5
889                                    + (RandomField::new(0)
890                                        .get((igloo_pos + dir).with_z(alt as i32))
891                                        % 4);
892                                let hide_color = match RandomField::new(0)
893                                    .get((igloo_pos + dir).with_z(alt as i32))
894                                    % 4
895                                {
896                                    0 => Fill::Block(Block::new(
897                                        BlockKind::Wood,
898                                        Rgb::new(73, 29, 0),
899                                    )),
900                                    1 => Fill::Block(Block::new(
901                                        BlockKind::Wood,
902                                        Rgb::new(78, 67, 43),
903                                    )),
904                                    2 => Fill::Block(Block::new(
905                                        BlockKind::Wood,
906                                        Rgb::new(83, 74, 41),
907                                    )),
908                                    _ => Fill::Block(Block::new(
909                                        BlockKind::Wood,
910                                        Rgb::new(14, 36, 34),
911                                    )),
912                                };
913                                painter
914                                    .sphere_with_radius(
915                                        (igloo_pos + (2 * dir))
916                                            .with_z((alt as i32) + 1 + height_handle),
917                                        hide_size as f32,
918                                    )
919                                    .fill(hide_color.clone());
920                            }
921                            // clear room
922                            painter
923                                .sphere_with_radius(
924                                    igloo_pos.with_z(alt as i32 - 1 + height_handle),
925                                    (igloo_size as i32 - 2) as f32,
926                                )
927                                .clear();
928                            // clear entries
929                            painter
930                                .aabb(Aabb {
931                                    min: Vec2::new(
932                                        igloo_pos.x - 1,
933                                        igloo_pos.y - igloo_size as i32 - 2,
934                                    )
935                                    .with_z(alt as i32 - 4 + height_handle),
936                                    max: Vec2::new(
937                                        igloo_pos.x + 1,
938                                        igloo_pos.y + igloo_size as i32 + 2,
939                                    )
940                                    .with_z(alt as i32 - 2 + height_handle),
941                                })
942                                .clear();
943                            painter
944                                .aabb(Aabb {
945                                    min: Vec2::new(
946                                        igloo_pos.x - igloo_size as i32 - 2,
947                                        igloo_pos.y - 1,
948                                    )
949                                    .with_z(alt as i32 - 4 + height_handle),
950                                    max: Vec2::new(
951                                        igloo_pos.x + igloo_size as i32 + 2,
952                                        igloo_pos.y + 1,
953                                    )
954                                    .with_z(alt as i32 - 2 + height_handle),
955                                })
956                                .clear();
957                            // bones
958                            for h in 0..(bones_size + 4) {
959                                painter
960                                    .line(
961                                        (igloo_pos - bones_size)
962                                            .with_z((alt as i32) - 5 + h + height_handle),
963                                        (igloo_pos + bones_size)
964                                            .with_z((alt as i32) - 5 + h + height_handle),
965                                        0.5,
966                                    )
967                                    .intersect(painter.sphere_with_radius(
968                                        igloo_pos.with_z((alt as i32) - 2 + height_handle),
969                                        9.0,
970                                    ))
971                                    .fill(bone_fill.clone());
972
973                                painter
974                                    .line(
975                                        Vec2::new(
976                                            igloo_pos.x - bones_size,
977                                            igloo_pos.y + bones_size,
978                                        )
979                                        .with_z((alt as i32) - 4 + h + height_handle),
980                                        Vec2::new(
981                                            igloo_pos.x + bones_size,
982                                            igloo_pos.y - bones_size,
983                                        )
984                                        .with_z((alt as i32) - 4 + h + height_handle),
985                                        0.5,
986                                    )
987                                    .intersect(painter.sphere_with_radius(
988                                        igloo_pos.with_z((alt as i32) - 2 + height_handle),
989                                        9.0,
990                                    ))
991                                    .fill(bone_fill.clone());
992                            }
993                            painter
994                                .sphere_with_radius(
995                                    igloo_pos.with_z((alt as i32) - 2 + height_handle),
996                                    5.0,
997                                )
998                                .clear();
999
1000                            // WallSconce
1001                            painter.rotated_sprite(
1002                                Vec2::new(
1003                                    igloo_pos.x - bones_size + 4,
1004                                    igloo_pos.y + bones_size - 5,
1005                                )
1006                                .with_z((alt as i32) - 1 + height_handle),
1007                                SpriteKind::WallSconce,
1008                                0_u8,
1009                            );
1010                            let igloo_mobs = 1
1011                                + (RandomField::new(0)
1012                                    .get((igloo_pos - 1).with_z(alt as i32 - 5 + height_handle))
1013                                    % 2) as i32;
1014
1015                            for _ in 0..igloo_mobs {
1016                                let igloo_mob_spawn =
1017                                    (igloo_pos - 1).with_z(alt as i32 - 5 + height_handle);
1018                                painter.spawn(random_adlet(igloo_mob_spawn.as_(), &mut rng));
1019                            }
1020                        },
1021                    };
1022                    // igloo floor
1023                    painter
1024                        .cylinder_with_radius(
1025                            (igloo_pos).with_z(alt as i32 - 7 + height_handle),
1026                            (igloo_size as i32 - 4) as f32,
1027                            2.0,
1028                        )
1029                        .fill(snow_ice_fill.clone());
1030
1031                    // FireBowl
1032                    painter.sprite(
1033                        igloo_pos.with_z(alt as i32 - 5 + height_handle),
1034                        SpriteKind::FireBowlGround,
1035                    );
1036                },
1037                AdletStructure::SpeleothemCluster => {
1038                    let layer_color = Fill::Sampling(Arc::new(|wpos| {
1039                        Some(
1040                            match (RandomField::new(0).get(Vec3::new(wpos.z, 0, 0))) % 6 {
1041                                0 => Block::new(BlockKind::Rock, Rgb::new(100, 128, 179)),
1042                                1 => Block::new(BlockKind::Rock, Rgb::new(95, 127, 178)),
1043                                2 => Block::new(BlockKind::Rock, Rgb::new(101, 121, 169)),
1044                                3 => Block::new(BlockKind::Rock, Rgb::new(61, 109, 145)),
1045                                4 => Block::new(BlockKind::Rock, Rgb::new(74, 128, 168)),
1046                                _ => Block::new(BlockKind::Rock, Rgb::new(69, 123, 162)),
1047                            },
1048                        )
1049                    }));
1050                    for dir in NEIGHBORS {
1051                        let cone_radius = 3
1052                            + (RandomField::new(0).get((wpos + dir).with_z(alt as i32)) % 3) as i32;
1053                        let cone_offset = 3
1054                            + (RandomField::new(0).get((wpos + 1 + dir).with_z(alt as i32)) % 4)
1055                                as i32;
1056                        let cone_height = 15
1057                            + (RandomField::new(0).get((wpos + 2 + dir).with_z(alt as i32)) % 50)
1058                                as i32;
1059                        // cones
1060                        painter
1061                            .cone_with_radius(
1062                                (wpos + (dir * cone_offset)).with_z(alt as i32 - (cone_height / 8)),
1063                                cone_radius as f32,
1064                                cone_height as f32,
1065                            )
1066                            .fill(layer_color.clone());
1067                        // small cone tops
1068                        let top_pos = (RandomField::new(0).get((wpos + 3 + dir).with_z(alt as i32))
1069                            % 2) as i32;
1070                        painter
1071                            .aabb(Aabb {
1072                                min: (wpos + (dir * cone_offset) - top_pos).with_z(alt as i32),
1073                                max: (wpos + (dir * cone_offset) + 1 - top_pos)
1074                                    .with_z((alt as i32) + cone_height - (cone_height / 6)),
1075                            })
1076                            .fill(layer_color.clone());
1077                    }
1078                },
1079                AdletStructure::Bonfire => {
1080                    let bonfire_pos = wpos;
1081                    let fire_fill = Fill::Sampling(Arc::new(|bonfire_pos| {
1082                        Some(match (RandomField::new(0).get(bonfire_pos)) % 200 {
1083                            0 => Block::air(SpriteKind::Ember),
1084                            _ => Block::air(SpriteKind::FireBlock),
1085                        })
1086                    }));
1087                    let fire_pos = bonfire_pos.with_z(alt as i32 + 2);
1088                    lazy_static! {
1089                        pub static ref FIRE: AssetHandle<StructuresGroup> =
1090                            PrefabStructure::load_group("site_structures.adlet.bonfire");
1091                    }
1092                    let fire_rng = RandomField::new(0).get(fire_pos) % 10;
1093                    let fire = FIRE.read();
1094                    let fire = fire[fire_rng as usize % fire.len()].clone();
1095                    painter
1096                        .prim(Primitive::Prefab(Box::new(fire.clone())))
1097                        .translate(fire_pos)
1098                        .fill(Fill::Prefab(Box::new(fire), fire_pos, fire_rng));
1099                    painter
1100                        .sphere_with_radius((bonfire_pos).with_z(alt as i32 + 5), 4.0)
1101                        .fill(fire_fill.clone());
1102                    painter
1103                        .cylinder_with_radius((bonfire_pos).with_z(alt as i32 + 2), 6.5, 1.0)
1104                        .fill(fire_fill);
1105                },
1106                AdletStructure::YetiPit => {
1107                    let yetipit_center = self.cavern_center;
1108                    let yetipit_entrance_pos = wpos;
1109                    let storeys = (3 + RandomField::new(0).get((yetipit_center).with_z(alt as i32))
1110                        % 2) as i32;
1111                    for s in 0..storeys {
1112                        let down = 10_i32;
1113                        let level = (alt as i32) - 50 - (s * (3 * down));
1114                        let room_size = (25
1115                            + RandomField::new(0).get((yetipit_center * s).with_z(level)) % 5)
1116                            as i32;
1117                        let sprites_fill = match s {
1118                            0..=1 => yetipit_sprites.clone(),
1119                            _ => yetipit_sprites_deep.clone(),
1120                        };
1121                        if s == (storeys - 1) {
1122                            // yeti room
1123                            painter
1124                                .cylinder_with_radius(
1125                                    yetipit_center.with_z(level - (3 * down) - 5),
1126                                    room_size as f32,
1127                                    ((room_size / 3) + 5) as f32,
1128                                )
1129                                .clear();
1130                            painter
1131                                .cylinder_with_radius(
1132                                    yetipit_center.with_z(level - (3 * down) - 6),
1133                                    room_size as f32,
1134                                    1.0,
1135                                )
1136                                .fill(snow_ice_fill.clone());
1137                            // sprites: icecrystals, bones
1138                            for r in 0..4 {
1139                                painter
1140                                    .cylinder_with_radius(
1141                                        yetipit_center.with_z(level - (3 * down) - 2 - r),
1142                                        (room_size + 2 - r) as f32,
1143                                        1.0,
1144                                    )
1145                                    .fill(snow_ice_fill.clone());
1146                                painter
1147                                    .cylinder_with_radius(
1148                                        yetipit_center.with_z(level - (3 * down) - 1 - r),
1149                                        (room_size - r) as f32,
1150                                        1.0,
1151                                    )
1152                                    .fill(sprites_fill.clone());
1153                                painter
1154                                    .cylinder_with_radius(
1155                                        yetipit_center.with_z(level - (3 * down) - 2 - r),
1156                                        (room_size - 1 - r) as f32,
1157                                        2.0,
1158                                    )
1159                                    .clear();
1160                            }
1161                            painter
1162                                .cylinder_with_radius(
1163                                    yetipit_center.with_z(level - (3 * down) - 5),
1164                                    (room_size - 4) as f32,
1165                                    1.0,
1166                                )
1167                                .fill(yeti_bones_fill.clone());
1168                            painter
1169                                .cone_with_radius(
1170                                    yetipit_center.with_z(level - (3 * down) + (room_size / 3) - 2),
1171                                    room_size as f32,
1172                                    (room_size / 3) as f32,
1173                                )
1174                                .clear();
1175                            // snow covered speleothem cluster
1176                            for dir in NEIGHBORS {
1177                                let cluster_pos = yetipit_center + dir * room_size - 3;
1178                                for dir in NEIGHBORS3 {
1179                                    let cone_radius = 3
1180                                        + (RandomField::new(0)
1181                                            .get((cluster_pos + dir).with_z(alt as i32))
1182                                            % 3) as i32;
1183                                    let cone_offset = 3
1184                                        + (RandomField::new(0)
1185                                            .get((cluster_pos + 1 + dir).with_z(alt as i32))
1186                                            % 4) as i32;
1187                                    let cone_height = 15
1188                                        + (RandomField::new(0)
1189                                            .get((cluster_pos + 2 + dir).with_z(alt as i32))
1190                                            % 10) as i32;
1191                                    // cones
1192                                    painter
1193                                        .cone_with_radius(
1194                                            (cluster_pos + (dir * cone_offset))
1195                                                .with_z(level - (3 * down) - 4 - (cone_height / 8)),
1196                                            cone_radius as f32,
1197                                            cone_height as f32,
1198                                        )
1199                                        .fill(snow_ice_fill.clone());
1200                                    // small cone tops
1201                                    let top_pos = (RandomField::new(0)
1202                                        .get((cluster_pos + 3 + dir).with_z(level))
1203                                        % 2)
1204                                        as i32;
1205                                    painter
1206                                        .aabb(Aabb {
1207                                            min: (cluster_pos + (dir * cone_offset) - top_pos)
1208                                                .with_z(level - (3 * down) - 3),
1209                                            max: (cluster_pos + (dir * cone_offset) + 1 - top_pos)
1210                                                .with_z(
1211                                                    (level - (3 * down) - 2) + cone_height
1212                                                        - (cone_height / 6)
1213                                                        + 3,
1214                                                ),
1215                                        })
1216                                        .fill(snow_ice_fill.clone());
1217                                }
1218                            }
1219                            // ceiling snow covered speleothem cluster
1220                            for dir in NEIGHBORS {
1221                                for c in 0..8 {
1222                                    let cluster_pos = yetipit_center + dir * (c * (room_size / 5));
1223                                    for dir in NEIGHBORS3 {
1224                                        let cone_radius = 3
1225                                            + (RandomField::new(0)
1226                                                .get((cluster_pos + dir).with_z(alt as i32))
1227                                                % 3)
1228                                                as i32;
1229                                        let cone_offset = 3
1230                                            + (RandomField::new(0)
1231                                                .get((cluster_pos + 1 + dir).with_z(alt as i32))
1232                                                % 4)
1233                                                as i32;
1234                                        let cone_height = 15
1235                                            + (RandomField::new(0)
1236                                                .get((cluster_pos + 2 + dir).with_z(alt as i32))
1237                                                % 10)
1238                                                as i32;
1239                                        // cones
1240                                        painter
1241                                            .cone_with_radius(
1242                                                (cluster_pos + (dir * cone_offset)).with_z(
1243                                                    level - (3 * down) - 4 - (cone_height / 8),
1244                                                ),
1245                                                cone_radius as f32,
1246                                                (cone_height - 1) as f32,
1247                                            )
1248                                            .rotate_about(
1249                                                Mat3::rotation_x(PI).as_(),
1250                                                yetipit_center.with_z(level - (2 * down) - 1),
1251                                            )
1252                                            .fill(snow_ice_fill.clone());
1253                                        // small cone tops
1254                                        let top_pos = (RandomField::new(0)
1255                                            .get((cluster_pos + 3 + dir).with_z(level))
1256                                            % 2)
1257                                            as i32;
1258                                        painter
1259                                            .aabb(Aabb {
1260                                                min: (cluster_pos + (dir * cone_offset) - top_pos)
1261                                                    .with_z(level - (3 * down) - 3),
1262                                                max: (cluster_pos + (dir * cone_offset) + 1
1263                                                    - top_pos)
1264                                                    .with_z(
1265                                                        (level - (3 * down) - 2) + cone_height
1266                                                            - (cone_height / 6)
1267                                                            - 2,
1268                                                    ),
1269                                            })
1270                                            .rotate_about(
1271                                                Mat3::rotation_x(PI).as_(),
1272                                                yetipit_center.with_z(level - (2 * down)),
1273                                            )
1274                                            .fill(snow_ice_fill.clone());
1275                                    }
1276                                }
1277                            }
1278                            // frozen ponds
1279                            for dir in NEIGHBORS3 {
1280                                let pond_radius = (RandomField::new(0)
1281                                    .get((yetipit_center + dir).with_z(alt as i32))
1282                                    % 8) as i32;
1283                                let pond_pos =
1284                                    yetipit_center + (dir * ((room_size / 4) + (pond_radius)));
1285                                painter
1286                                    .cylinder_with_radius(
1287                                        pond_pos.with_z(level - (3 * down) - 6),
1288                                        pond_radius as f32,
1289                                        1.0,
1290                                    )
1291                                    .fill(ice_fill.clone());
1292                            }
1293                            // yeti
1294                            let yeti_spawn = yetipit_center.with_z(level - (3 * down) - 4);
1295                            painter.spawn(yeti(yeti_spawn.as_(), &mut rng));
1296                        } else {
1297                            // mob rooms
1298                            painter
1299                                .cylinder_with_radius(
1300                                    yetipit_center.with_z(level - (3 * down) - 5),
1301                                    room_size as f32,
1302                                    ((room_size / 3) + 5) as f32,
1303                                )
1304                                .clear();
1305                            // sprites: icecrystals, bones
1306                            for r in 0..4 {
1307                                painter
1308                                    .cylinder_with_radius(
1309                                        yetipit_center.with_z(level - (3 * down) - 2 - r),
1310                                        (room_size + 2 - r) as f32,
1311                                        1.0,
1312                                    )
1313                                    .fill(snow_ice_fill.clone());
1314                                painter
1315                                    .cylinder_with_radius(
1316                                        yetipit_center.with_z(level - (3 * down) - 1 - r),
1317                                        (room_size - r) as f32,
1318                                        1.0,
1319                                    )
1320                                    .fill(sprites_fill.clone());
1321                                painter
1322                                    .cylinder_with_radius(
1323                                        yetipit_center.with_z(level - (3 * down) - 2 - r),
1324                                        (room_size - 1 - r) as f32,
1325                                        2.0,
1326                                    )
1327                                    .clear();
1328                            }
1329                            let yetipit_mobs = 1
1330                                + (RandomField::new(0)
1331                                    .get(yetipit_center.with_z(level - (3 * down) - 3))
1332                                    % 2) as i32;
1333                            for _ in 0..yetipit_mobs {
1334                                let yetipit_mob_spawn =
1335                                    yetipit_center.with_z(level - (3 * down) - 3);
1336                                painter
1337                                    .spawn(random_yetipit_mob(yetipit_mob_spawn.as_(), &mut rng));
1338                            }
1339                            painter
1340                                .cone_with_radius(
1341                                    yetipit_center.with_z(level - (3 * down) + (room_size / 3) - 2),
1342                                    room_size as f32,
1343                                    (room_size / 3) as f32,
1344                                )
1345                                .clear();
1346                            // snow covered speleothem cluster
1347                            for dir in NEIGHBORS {
1348                                let cluster_pos = yetipit_center + dir * room_size;
1349                                for dir in NEIGHBORS {
1350                                    let cone_radius = 3
1351                                        + (RandomField::new(0)
1352                                            .get((cluster_pos + dir).with_z(alt as i32))
1353                                            % 3) as i32;
1354                                    let cone_offset = 3
1355                                        + (RandomField::new(0)
1356                                            .get((cluster_pos + 1 + dir).with_z(alt as i32))
1357                                            % 4) as i32;
1358                                    let cone_height = 15
1359                                        + (RandomField::new(0)
1360                                            .get((cluster_pos + 2 + dir).with_z(alt as i32))
1361                                            % 10) as i32;
1362                                    // cones
1363                                    painter
1364                                        .cone_with_radius(
1365                                            (cluster_pos + (dir * cone_offset))
1366                                                .with_z(level - (3 * down) - 4 - (cone_height / 8)),
1367                                            cone_radius as f32,
1368                                            cone_height as f32,
1369                                        )
1370                                        .fill(snow_ice_fill.clone());
1371                                    // small cone tops
1372                                    let top_pos = (RandomField::new(0)
1373                                        .get((cluster_pos + 3 + dir).with_z(level))
1374                                        % 2)
1375                                        as i32;
1376                                    painter
1377                                        .aabb(Aabb {
1378                                            min: (cluster_pos + (dir * cone_offset) - top_pos)
1379                                                .with_z(level - (3 * down) - 3),
1380                                            max: (cluster_pos + (dir * cone_offset) + 1 - top_pos)
1381                                                .with_z(
1382                                                    (level - (3 * down) - 2) + cone_height
1383                                                        - (cone_height / 6)
1384                                                        + 3,
1385                                                ),
1386                                        })
1387                                        .fill(snow_ice_fill.clone());
1388                                }
1389                            }
1390                            // ceiling snow covered speleothem cluster
1391                            for dir in NEIGHBORS {
1392                                for c in 0..5 {
1393                                    let cluster_pos = yetipit_center + dir * (c * (room_size / 3));
1394                                    for dir in NEIGHBORS {
1395                                        let cone_radius = 3
1396                                            + (RandomField::new(0)
1397                                                .get((cluster_pos + dir).with_z(alt as i32))
1398                                                % 3)
1399                                                as i32;
1400                                        let cone_offset = 3
1401                                            + (RandomField::new(0)
1402                                                .get((cluster_pos + 1 + dir).with_z(alt as i32))
1403                                                % 4)
1404                                                as i32;
1405                                        let cone_height = 15
1406                                            + (RandomField::new(0)
1407                                                .get((cluster_pos + 2 + dir).with_z(alt as i32))
1408                                                % 10)
1409                                                as i32;
1410                                        // cones
1411                                        painter
1412                                            .cone_with_radius(
1413                                                (cluster_pos + (dir * cone_offset)).with_z(
1414                                                    level - (3 * down) - 4 - (cone_height / 8),
1415                                                ),
1416                                                cone_radius as f32,
1417                                                (cone_height - 1) as f32,
1418                                            )
1419                                            .rotate_about(
1420                                                Mat3::rotation_x(PI).as_(),
1421                                                yetipit_center.with_z(level - (2 * down) - 1),
1422                                            )
1423                                            .fill(snow_ice_fill.clone());
1424                                        // small cone tops
1425                                        let top_pos = (RandomField::new(0)
1426                                            .get((cluster_pos + 3 + dir).with_z(level))
1427                                            % 2)
1428                                            as i32;
1429                                        painter
1430                                            .aabb(Aabb {
1431                                                min: (cluster_pos + (dir * cone_offset) - top_pos)
1432                                                    .with_z(level - (3 * down) - 3),
1433                                                max: (cluster_pos + (dir * cone_offset) + 1
1434                                                    - top_pos)
1435                                                    .with_z(
1436                                                        (level - (3 * down) - 2) + cone_height
1437                                                            - (cone_height / 6)
1438                                                            - 2,
1439                                                    ),
1440                                            })
1441                                            .rotate_about(
1442                                                Mat3::rotation_x(PI).as_(),
1443                                                yetipit_center.with_z(level - (2 * down)),
1444                                            )
1445                                            .fill(snow_ice_fill.clone());
1446                                    }
1447                                }
1448                            }
1449                            // frozen pond
1450                            painter
1451                                .cylinder_with_radius(
1452                                    yetipit_center.with_z(level - (3 * down) - 4),
1453                                    (room_size / 8) as f32,
1454                                    1.0,
1455                                )
1456                                .fill(ice_fill.clone());
1457                        }
1458                        let tunnels = (2 + RandomField::new(0)
1459                            .get((yetipit_center + s).with_z(level))
1460                            % 2) as i32;
1461                        for t in 1..tunnels {
1462                            let away1 = (50
1463                                + RandomField::new(0).get((yetipit_center * (s + t)).with_z(level))
1464                                    % 20) as i32;
1465                            let away2 = (50
1466                                + RandomField::new(0)
1467                                    .get((yetipit_center * (s + (2 * t))).with_z(level))
1468                                    % 20) as i32;
1469                            let away3 = (50
1470                                + RandomField::new(0)
1471                                    .get((yetipit_center * (s + (3 * t))).with_z(level))
1472                                    % 20) as i32;
1473                            let away4 = (50
1474                                + RandomField::new(0)
1475                                    .get((yetipit_center * (s + (4 * t))).with_z(level))
1476                                    % 20) as i32;
1477
1478                            let dir1 = 1 - 2
1479                                * (RandomField::new(0).get((yetipit_center).with_z(t * level)) % 2)
1480                                    as i32;
1481                            let dir2 = 1 - 2
1482                                * (RandomField::new(0)
1483                                    .get((yetipit_center).with_z((2 * t) * level))
1484                                    % 2) as i32;
1485                            // caves
1486                            painter
1487                                .cubic_bezier(
1488                                    yetipit_center.with_z(level - 3),
1489                                    Vec2::new(
1490                                        yetipit_center.x + ((away1 + (s * (down / 4))) * dir1),
1491                                        yetipit_center.y + ((away2 + (s * (down / 4))) * dir2),
1492                                    )
1493                                    .with_z(level - (2 * down)),
1494                                    Vec2::new(
1495                                        yetipit_center.x + ((away3 + (s * (down / 4))) * dir1),
1496                                        yetipit_center.y + ((away4 + (s * (down / 4))) * dir2),
1497                                    )
1498                                    .with_z(level - (3 * down)),
1499                                    yetipit_center.with_z(level - (3 * down)),
1500                                    6.0,
1501                                )
1502                                .clear();
1503                        }
1504                    }
1505                    // yetipit entrance
1506                    // rocks
1507                    painter
1508                        .sphere(Aabb {
1509                            min: (yetipit_entrance_pos - 8).with_z(alt as i32 - 8),
1510                            max: (yetipit_entrance_pos + 8).with_z(alt as i32 + 8),
1511                        })
1512                        .fill(rock_fill.clone());
1513                    // repaint ground
1514                    painter
1515                        .cylinder(Aabb {
1516                            min: (yetipit_entrance_pos - 8).with_z(alt as i32 - 20),
1517                            max: (yetipit_entrance_pos + 8).with_z(alt as i32),
1518                        })
1519                        .fill(snow_ice_fill.clone());
1520                    // tunnel
1521                    let door_dist = self.cavern_center - yetipit_entrance_pos;
1522                    let door_dir = door_dist.map(|e| e.checked_div(e.abs()).unwrap_or(0));
1523                    painter
1524                        .cubic_bezier(
1525                            (yetipit_entrance_pos + door_dir * 10).with_z(alt as i32 + 2),
1526                            (yetipit_entrance_pos - door_dir * 16).with_z(alt as i32 - 10),
1527                            (yetipit_entrance_pos + door_dir * 20).with_z((alt as i32) - 30),
1528                            self.cavern_center.with_z((alt as i32) - 50),
1529                            4.0,
1530                        )
1531                        .clear();
1532                    // bone door
1533                    painter
1534                        .cylinder(Aabb {
1535                            min: Vec2::new(yetipit_entrance_pos.x - 7, yetipit_entrance_pos.y - 7)
1536                                .with_z(alt as i32 - 8),
1537                            max: Vec2::new(yetipit_entrance_pos.x + 7, yetipit_entrance_pos.y + 7)
1538                                .with_z((alt as i32) - 7),
1539                        })
1540                        .fill(snow_ice_fill.clone());
1541
1542                    painter
1543                        .cylinder(Aabb {
1544                            min: Vec2::new(yetipit_entrance_pos.x - 3, yetipit_entrance_pos.y - 3)
1545                                .with_z(alt as i32 - 8),
1546                            max: Vec2::new(yetipit_entrance_pos.x + 3, yetipit_entrance_pos.y + 3)
1547                                .with_z((alt as i32) - 7),
1548                        })
1549                        .fill(Fill::Block(Block::air(SpriteKind::BoneKeyDoor)));
1550                    painter
1551                        .aabb(Aabb {
1552                            min: Vec2::new(yetipit_entrance_pos.x - 1, yetipit_entrance_pos.y)
1553                                .with_z(alt as i32 - 8),
1554                            max: Vec2::new(yetipit_entrance_pos.x, yetipit_entrance_pos.y + 1)
1555                                .with_z((alt as i32) - 7),
1556                        })
1557                        .fill(Fill::Block(Block::air(SpriteKind::BoneKeyhole)));
1558                },
1559                AdletStructure::Tannery => {
1560                    // shattered bone pieces
1561                    painter
1562                        .cylinder_with_radius(wpos.with_z(alt as i32), 7.0, 1.0)
1563                        .fill(bone_shrub.clone());
1564                    // bones upright
1565                    painter
1566                        .aabb(Aabb {
1567                            min: Vec2::new(wpos.x - 6, wpos.y).with_z(alt as i32),
1568                            max: Vec2::new(wpos.x + 6, wpos.y + 1).with_z((alt as i32) + 8),
1569                        })
1570                        .fill(bone_fill.clone());
1571                    painter
1572                        .aabb(Aabb {
1573                            min: Vec2::new(wpos.x - 5, wpos.y).with_z(alt as i32),
1574                            max: Vec2::new(wpos.x + 5, wpos.y + 1).with_z((alt as i32) + 8),
1575                        })
1576                        .clear();
1577                    painter
1578                        .aabb(Aabb {
1579                            min: Vec2::new(wpos.x - 6, wpos.y - 1).with_z(alt as i32 + 8),
1580                            max: Vec2::new(wpos.x + 6, wpos.y + 2).with_z((alt as i32) + 9),
1581                        })
1582                        .fill(bone_fill.clone());
1583                    painter
1584                        .aabb(Aabb {
1585                            min: Vec2::new(wpos.x - 5, wpos.y - 1).with_z(alt as i32 + 8),
1586                            max: Vec2::new(wpos.x + 5, wpos.y + 2).with_z((alt as i32) + 9),
1587                        })
1588                        .clear();
1589                    painter
1590                        .aabb(Aabb {
1591                            min: Vec2::new(wpos.x - 6, wpos.y).with_z(alt as i32 + 8),
1592                            max: Vec2::new(wpos.x + 6, wpos.y + 1).with_z((alt as i32) + 9),
1593                        })
1594                        .clear();
1595                    // bones lying
1596                    painter
1597                        .aabb(Aabb {
1598                            min: Vec2::new(wpos.x - 6, wpos.y + 3).with_z(alt as i32),
1599                            max: Vec2::new(wpos.x + 6, wpos.y + 4).with_z((alt as i32) + 2),
1600                        })
1601                        .fill(bone_fill.clone());
1602                    painter
1603                        .aabb(Aabb {
1604                            min: Vec2::new(wpos.x - 5, wpos.y + 3).with_z(alt as i32),
1605                            max: Vec2::new(wpos.x - 3, wpos.y + 4).with_z((alt as i32) + 1),
1606                        })
1607                        .clear();
1608                    painter
1609                        .aabb(Aabb {
1610                            min: Vec2::new(wpos.x + 3, wpos.y + 3).with_z(alt as i32),
1611                            max: Vec2::new(wpos.x + 5, wpos.y + 4).with_z((alt as i32) + 1),
1612                        })
1613                        .clear();
1614                    painter
1615                        .aabb(Aabb {
1616                            min: Vec2::new(wpos.x - 2, wpos.y + 3).with_z(alt as i32),
1617                            max: Vec2::new(wpos.x + 2, wpos.y + 4).with_z((alt as i32) + 1),
1618                        })
1619                        .clear();
1620                    // hide
1621                    for n in 0..10 {
1622                        let hide_color =
1623                            match RandomField::new(0).get((wpos + n).with_z(alt as i32)) % 4 {
1624                                0 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(73, 29, 0))),
1625                                1 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(78, 67, 43))),
1626                                2 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(83, 74, 41))),
1627                                _ => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(14, 36, 34))),
1628                            };
1629                        let rand_length =
1630                            (RandomField::new(0).get((wpos - n).with_z(alt as i32)) % 7) as i32;
1631                        painter
1632                            .aabb(Aabb {
1633                                min: Vec2::new(wpos.x - 5, wpos.y).with_z(alt as i32 + rand_length),
1634                                max: Vec2::new(wpos.x - 4 + n, wpos.y + 1).with_z((alt as i32) + 8),
1635                            })
1636                            .fill(hide_color.clone());
1637                    }
1638                    let tannery_mobs =
1639                        1 + (RandomField::new(0).get(wpos.with_z(alt as i32)) % 2) as i32;
1640                    for _ in 0..tannery_mobs {
1641                        let tannery_mob_spawn = wpos.with_z(alt as i32);
1642                        painter.spawn(random_adlet(tannery_mob_spawn.as_(), &mut rng));
1643                    }
1644                },
1645                AdletStructure::AnimalPen => {
1646                    let pen_size = 8.0;
1647                    painter
1648                        .sphere_with_radius(
1649                            wpos.with_z(alt as i32 + (pen_size as i32 / 4) - 1),
1650                            pen_size as f32,
1651                        )
1652                        .fill(bone_fill.clone());
1653                    painter
1654                        .sphere_with_radius(
1655                            wpos.with_z(alt as i32 + (pen_size as i32 / 2) - 1),
1656                            pen_size as f32,
1657                        )
1658                        .clear();
1659                    painter
1660                        .cylinder(Aabb {
1661                            min: (wpos - (pen_size as i32)).with_z(alt as i32 - (pen_size as i32)),
1662                            max: (wpos + (pen_size as i32)).with_z(alt as i32),
1663                        })
1664                        .fill(dirt_fill.clone());
1665                    painter
1666                        .cylinder(Aabb {
1667                            min: (wpos - (pen_size as i32) + 1).with_z(alt as i32),
1668                            max: (wpos + (pen_size as i32) - 1).with_z(alt as i32 + 1),
1669                        })
1670                        .fill(grass_fill.clone());
1671                    enum AnimalPenKind {
1672                        Rat,
1673                        Wolf,
1674                        Bear,
1675                    }
1676                    let (kind, num) = {
1677                        let rand_field = RandomField::new(1).get(wpos.with_z(alt as i32));
1678                        match RandomField::new(0).get(wpos.with_z(alt as i32)) % 4 {
1679                            0 => (AnimalPenKind::Bear, 1 + rand_field % 2),
1680                            1 => (AnimalPenKind::Wolf, 2 + rand_field % 2),
1681                            _ => (AnimalPenKind::Rat, 3 + rand_field % 3),
1682                        }
1683                    };
1684                    for _ in 0..num {
1685                        let animalpen_mob_spawn = wpos.with_z(alt as i32);
1686                        match kind {
1687                            AnimalPenKind::Rat => {
1688                                painter.spawn(rat(animalpen_mob_spawn.as_(), &mut rng))
1689                            },
1690                            AnimalPenKind::Wolf => {
1691                                painter.spawn(wolf(animalpen_mob_spawn.as_(), &mut rng))
1692                            },
1693                            AnimalPenKind::Bear => {
1694                                painter.spawn(bear(animalpen_mob_spawn.as_(), &mut rng))
1695                            },
1696                        }
1697                    }
1698                },
1699                AdletStructure::CookFire => {
1700                    painter
1701                        .cylinder(Aabb {
1702                            min: (wpos - 3).with_z(alt as i32),
1703                            max: (wpos + 4).with_z(alt as i32 + 1),
1704                        })
1705                        .fill(bone_fill.clone());
1706                    let cook_sprites = Fill::Sampling(Arc::new(|wpos| {
1707                        Some(match (RandomField::new(0).get(wpos)) % 20 {
1708                            0 => Block::air(SpriteKind::FlowerpotWoodWoodlandS),
1709                            1 => Block::air(SpriteKind::Bowl),
1710                            2 => Block::air(SpriteKind::FlowerpotWoodWoodlandS),
1711                            3 => Block::air(SpriteKind::VialEmpty),
1712                            4 => Block::air(SpriteKind::Lantern),
1713                            _ => Block::air(SpriteKind::Empty),
1714                        })
1715                    }));
1716                    painter
1717                        .cylinder(Aabb {
1718                            min: (wpos - 3).with_z(alt as i32 + 1),
1719                            max: (wpos + 4).with_z(alt as i32 + 2),
1720                        })
1721                        .fill(cook_sprites);
1722                    painter
1723                        .cylinder(Aabb {
1724                            min: (wpos - 2).with_z(alt as i32),
1725                            max: (wpos + 3).with_z(alt as i32 + 2),
1726                        })
1727                        .clear();
1728                    painter
1729                        .aabb(Aabb {
1730                            min: (wpos).with_z(alt as i32),
1731                            max: (wpos + 1).with_z(alt as i32 + 1),
1732                        })
1733                        .fill(bone_fill.clone());
1734                    painter.sprite(wpos.with_z(alt as i32 + 1), SpriteKind::FireBowlGround);
1735                    let cookfire_mobs =
1736                        1 + (RandomField::new(0).get(wpos.with_z(alt as i32)) % 2) as i32;
1737                    for _ in 0..cookfire_mobs {
1738                        let cookfire_mob_spawn = wpos.with_z(alt as i32);
1739                        painter.spawn(random_adlet(cookfire_mob_spawn.as_(), &mut rng));
1740                    }
1741                },
1742                AdletStructure::RockHut => {
1743                    painter
1744                        .sphere_with_radius(wpos.with_z(alt as i32), 5.0)
1745                        .fill(rock_fill.clone());
1746                    painter
1747                        .sphere_with_radius(wpos.with_z(alt as i32), 4.0)
1748                        .clear();
1749                    // clear entries
1750                    painter
1751                        .aabb(Aabb {
1752                            min: Vec2::new(wpos.x - 6, wpos.y - 1).with_z(alt as i32),
1753                            max: Vec2::new(wpos.x + 6, wpos.y + 1).with_z(alt as i32 + 2),
1754                        })
1755                        .clear();
1756                    painter
1757                        .aabb(Aabb {
1758                            min: Vec2::new(wpos.x - 1, wpos.y - 6).with_z(alt as i32),
1759                            max: Vec2::new(wpos.x + 1, wpos.y + 6).with_z(alt as i32 + 2),
1760                        })
1761                        .clear();
1762                    // fill with dirt
1763                    painter
1764                        .cylinder(Aabb {
1765                            min: (wpos - 5).with_z((alt as i32) - 5),
1766                            max: (wpos + 5).with_z(alt as i32 - 1),
1767                        })
1768                        .fill(dirt_fill.clone());
1769                    painter.sprite(wpos.with_z(alt as i32) - 1, SpriteKind::FireBowlGround);
1770                    let rockhut_mobs =
1771                        1 + (RandomField::new(0).get(wpos.with_z(alt as i32)) % 2) as i32;
1772                    for _ in 0..rockhut_mobs {
1773                        let rockhut_mob_spawn = wpos.with_z(alt as i32);
1774                        painter.spawn(random_adlet(rockhut_mob_spawn.as_(), &mut rng));
1775                    }
1776                },
1777                AdletStructure::BoneHut => {
1778                    let hut_radius = 5;
1779                    // top decor bone with some hide
1780                    painter
1781                        .aabb(Aabb {
1782                            min: wpos.with_z((alt as i32) + hut_radius + 4),
1783                            max: (wpos + 1).with_z((alt as i32) + hut_radius + 7),
1784                        })
1785                        .fill(bone_fill.clone());
1786                    painter
1787                        .aabb(Aabb {
1788                            min: Vec2::new(wpos.x, wpos.y - 1)
1789                                .with_z((alt as i32) + hut_radius + 7),
1790                            max: Vec2::new(wpos.x + 1, wpos.y + 2)
1791                                .with_z((alt as i32) + hut_radius + 8),
1792                        })
1793                        .fill(bone_fill.clone());
1794                    painter
1795                        .aabb(Aabb {
1796                            min: wpos.with_z((alt as i32) + hut_radius + 7),
1797                            max: (wpos + 1).with_z((alt as i32) + hut_radius + 8),
1798                        })
1799                        .clear();
1800                    let top_color = Fill::Sampling(Arc::new(|wpos| {
1801                        Some(match (RandomField::new(0).get(wpos)) % 10 {
1802                            0 => Block::new(BlockKind::Wood, Rgb::new(73, 29, 0)),
1803                            1 => Block::new(BlockKind::Wood, Rgb::new(78, 67, 43)),
1804                            2 => Block::new(BlockKind::Wood, Rgb::new(83, 74, 41)),
1805                            3 => Block::new(BlockKind::Wood, Rgb::new(14, 36, 34)),
1806                            _ => Block::new(BlockKind::Rock, Rgb::new(200, 160, 140)),
1807                        })
1808                    }));
1809                    painter
1810                        .aabb(Aabb {
1811                            min: (wpos - 1).with_z((alt as i32) + hut_radius + 3),
1812                            max: (wpos + 2).with_z((alt as i32) + hut_radius + 5),
1813                        })
1814                        .fill(top_color.clone());
1815                    // 4 hide pieces
1816                    for dir in CARDINALS {
1817                        let hide_size =
1818                            6 + (RandomField::new(0).get((wpos + dir).with_z(alt as i32)) % 2);
1819                        let hide_color =
1820                            match RandomField::new(0).get((wpos + dir).with_z(alt as i32)) % 4 {
1821                                0 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(73, 29, 0))),
1822                                1 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(78, 67, 43))),
1823                                2 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(83, 74, 41))),
1824                                _ => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(14, 36, 34))),
1825                            };
1826                        painter
1827                            .sphere_with_radius(
1828                                (wpos + (2 * dir)).with_z((alt as i32) + 2),
1829                                hide_size as f32,
1830                            )
1831                            .fill(hide_color.clone());
1832                    }
1833                    // clear room
1834                    painter
1835                        .sphere_with_radius(wpos.with_z((alt as i32) + 2), 6.0)
1836                        .intersect(painter.aabb(Aabb {
1837                            min: (wpos - 6).with_z(alt as i32),
1838                            max: (wpos + 6).with_z(alt as i32 + 2 * hut_radius),
1839                        }))
1840                        .clear();
1841                    //clear entries
1842                    painter
1843                        .aabb(Aabb {
1844                            min: Vec2::new(wpos.x - 1, wpos.y - hut_radius - 6).with_z(alt as i32),
1845                            max: Vec2::new(wpos.x + 1, wpos.y + hut_radius + 6)
1846                                .with_z((alt as i32) + 3),
1847                        })
1848                        .clear();
1849
1850                    // bones
1851                    for h in 0..(hut_radius + 4) {
1852                        painter
1853                            .line(
1854                                (wpos - hut_radius).with_z((alt as i32) + h),
1855                                (wpos + hut_radius).with_z((alt as i32) + h),
1856                                0.5,
1857                            )
1858                            .intersect(
1859                                painter.sphere_with_radius(wpos.with_z((alt as i32) + 2), 9.0),
1860                            )
1861                            .fill(bone_fill.clone());
1862
1863                        painter
1864                            .line(
1865                                Vec2::new(wpos.x - hut_radius, wpos.y + hut_radius)
1866                                    .with_z((alt as i32) + h),
1867                                Vec2::new(wpos.x + hut_radius, wpos.y - hut_radius)
1868                                    .with_z((alt as i32) + h),
1869                                0.5,
1870                            )
1871                            .intersect(
1872                                painter.sphere_with_radius(wpos.with_z((alt as i32) + 2), 9.0),
1873                            )
1874                            .fill(bone_fill.clone());
1875                    }
1876                    painter
1877                        .sphere_with_radius(wpos.with_z((alt as i32) + 2), 5.0)
1878                        .intersect(painter.aabb(Aabb {
1879                            min: (wpos - 5).with_z(alt as i32),
1880                            max: (wpos + 5).with_z((alt as i32) + 2 * hut_radius),
1881                        }))
1882                        .clear();
1883
1884                    // WallSconce
1885                    painter.rotated_sprite(
1886                        Vec2::new(wpos.x - hut_radius + 1, wpos.y + hut_radius - 2)
1887                            .with_z((alt as i32) + 3),
1888                        SpriteKind::WallSconce,
1889                        0_u8,
1890                    );
1891                    // FireBowl
1892                    painter.sprite(wpos.with_z(alt as i32), SpriteKind::FireBowlGround);
1893                    let bonehut_mobs =
1894                        1 + (RandomField::new(0).get(wpos.with_z(alt as i32)) % 2) as i32;
1895                    for _ in 0..bonehut_mobs {
1896                        let bonehut_mob_spawn = wpos.with_z(alt as i32);
1897                        painter.spawn(random_adlet(bonehut_mob_spawn.as_(), &mut rng));
1898                    }
1899                    // chests
1900                    let chest = Fill::Sampling(Arc::new(|wpos| {
1901                        Some(match (RandomField::new(0).get(wpos)) % 2 {
1902                            0 => Block::air(SpriteKind::DungeonChest1),
1903                            _ => Block::air(SpriteKind::Empty),
1904                        })
1905                    }));
1906                    painter
1907                        .aabb(Aabb {
1908                            min: (wpos - 3).with_z(alt as i32),
1909                            max: (wpos - 2).with_z((alt as i32) + 1),
1910                        })
1911                        .fill(chest);
1912                },
1913                AdletStructure::BossBoneHut => {
1914                    let bosshut_pos = wpos;
1915                    let hut_radius = 10;
1916                    // 4 hide pieces
1917                    for dir in CARDINALS {
1918                        let hide_size = 10
1919                            + (RandomField::new(0).get((bosshut_pos + dir).with_z(alt as i32)) % 4);
1920                        let hide_color = match RandomField::new(0)
1921                            .get((bosshut_pos + dir).with_z(alt as i32))
1922                            % 4
1923                        {
1924                            0 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(73, 29, 0))),
1925                            1 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(78, 67, 43))),
1926                            2 => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(83, 74, 41))),
1927                            _ => Fill::Block(Block::new(BlockKind::Wood, Rgb::new(14, 36, 34))),
1928                        };
1929                        painter
1930                            .sphere_with_radius(
1931                                (bosshut_pos + (3 * dir)).with_z((alt as i32) + 2),
1932                                hide_size as f32,
1933                            )
1934                            .fill(hide_color.clone());
1935                    }
1936                    // bones
1937                    for h in 0..(hut_radius + 4) {
1938                        painter
1939                            .line(
1940                                (bosshut_pos - hut_radius + 1).with_z((alt as i32) + h),
1941                                (bosshut_pos + hut_radius - 1).with_z((alt as i32) + h),
1942                                1.5,
1943                            )
1944                            .intersect(
1945                                painter
1946                                    .sphere_with_radius(bosshut_pos.with_z((alt as i32) + 2), 14.0),
1947                            )
1948                            .intersect(
1949                                painter.aabb(Aabb {
1950                                    min: (bosshut_pos - 2 * hut_radius).with_z(alt as i32),
1951                                    max: (bosshut_pos + 2 * hut_radius)
1952                                        .with_z((alt as i32) + 2 * hut_radius),
1953                                }),
1954                            )
1955                            .fill(bone_fill.clone());
1956
1957                        painter
1958                            .line(
1959                                Vec2::new(
1960                                    bosshut_pos.x - hut_radius + 1,
1961                                    bosshut_pos.y + hut_radius - 2,
1962                                )
1963                                .with_z((alt as i32) + h),
1964                                Vec2::new(
1965                                    bosshut_pos.x + hut_radius - 1,
1966                                    bosshut_pos.y - hut_radius + 2,
1967                                )
1968                                .with_z((alt as i32) + h),
1969                                1.5,
1970                            )
1971                            .intersect(
1972                                painter
1973                                    .sphere_with_radius(bosshut_pos.with_z((alt as i32) + 2), 14.0),
1974                            )
1975                            .intersect(
1976                                painter.aabb(Aabb {
1977                                    min: (bosshut_pos - 2 * hut_radius).with_z(alt as i32),
1978                                    max: (bosshut_pos + 2 * hut_radius)
1979                                        .with_z((alt as i32) + 2 * hut_radius),
1980                                }),
1981                            )
1982                            .fill(bone_fill.clone());
1983                    }
1984                    painter
1985                        .sphere_with_radius(bosshut_pos.with_z((alt as i32) + 2), 9.0)
1986                        .intersect(painter.aabb(Aabb {
1987                            min: (bosshut_pos - 9).with_z(alt as i32),
1988                            max: (bosshut_pos + 9).with_z(alt as i32 + 11),
1989                        }))
1990                        .clear();
1991
1992                    for n in 0..2 {
1993                        // large entries
1994
1995                        painter
1996                            .sphere_with_radius(
1997                                (Vec2::new(
1998                                    bosshut_pos.x,
1999                                    bosshut_pos.y - hut_radius + (2 * (hut_radius * n)),
2000                                ))
2001                                .with_z((alt as i32) + 2),
2002                                7.0,
2003                            )
2004                            .intersect(
2005                                painter.aabb(Aabb {
2006                                    min: Vec2::new(
2007                                        bosshut_pos.x - 7,
2008                                        bosshut_pos.y - hut_radius + (2 * (hut_radius * n) - 7),
2009                                    )
2010                                    .with_z(alt as i32),
2011                                    max: Vec2::new(
2012                                        bosshut_pos.x + 7,
2013                                        bosshut_pos.y - hut_radius + (2 * (hut_radius * n) + 7),
2014                                    )
2015                                    .with_z(alt as i32 + 9),
2016                                }),
2017                            )
2018                            .clear();
2019                        let entry_start = Vec2::new(
2020                            bosshut_pos.x - hut_radius + 3,
2021                            bosshut_pos.y - hut_radius - 2 + (n * ((2 * hut_radius) + 4)),
2022                        )
2023                        .with_z(alt as i32);
2024                        let entry_peak = Vec2::new(
2025                            bosshut_pos.x,
2026                            bosshut_pos.y - hut_radius + (n * (2 * hut_radius)),
2027                        )
2028                        .with_z(alt as i32 + hut_radius + 2);
2029                        let entry_end = Vec2::new(
2030                            bosshut_pos.x + hut_radius - 3,
2031                            bosshut_pos.y - hut_radius - 2 + (n * ((2 * hut_radius) + 4)),
2032                        )
2033                        .with_z(alt as i32);
2034                        painter
2035                            .cubic_bezier(entry_start, entry_peak, entry_peak, entry_end, 1.0)
2036                            .fill(bone_fill.clone());
2037                    }
2038
2039                    // top decor bone with some hide
2040                    painter
2041                        .aabb(Aabb {
2042                            min: bosshut_pos.with_z((alt as i32) + hut_radius + 5),
2043                            max: (bosshut_pos + 1).with_z((alt as i32) + hut_radius + 8),
2044                        })
2045                        .fill(bone_fill.clone());
2046                    painter
2047                        .aabb(Aabb {
2048                            min: Vec2::new(bosshut_pos.x, bosshut_pos.y - 1)
2049                                .with_z((alt as i32) + hut_radius + 8),
2050                            max: Vec2::new(bosshut_pos.x + 1, bosshut_pos.y + 2)
2051                                .with_z((alt as i32) + hut_radius + 9),
2052                        })
2053                        .fill(bone_fill.clone());
2054                    painter
2055                        .aabb(Aabb {
2056                            min: bosshut_pos.with_z((alt as i32) + hut_radius + 8),
2057                            max: (bosshut_pos + 1).with_z((alt as i32) + hut_radius + 9),
2058                        })
2059                        .clear();
2060
2061                    let top_color = Fill::Sampling(Arc::new(|bosshut_pos| {
2062                        Some(match (RandomField::new(0).get(bosshut_pos)) % 10 {
2063                            0 => Block::new(BlockKind::Wood, Rgb::new(73, 29, 0)),
2064                            1 => Block::new(BlockKind::Wood, Rgb::new(78, 67, 43)),
2065                            2 => Block::new(BlockKind::Wood, Rgb::new(83, 74, 41)),
2066                            3 => Block::new(BlockKind::Wood, Rgb::new(14, 36, 34)),
2067                            _ => Block::new(BlockKind::Rock, Rgb::new(200, 160, 140)),
2068                        })
2069                    }));
2070                    painter
2071                        .aabb(Aabb {
2072                            min: (bosshut_pos - 1).with_z((alt as i32) + hut_radius + 5),
2073                            max: (bosshut_pos + 2).with_z((alt as i32) + hut_radius + 6),
2074                        })
2075                        .fill(top_color);
2076                    // WallSconces
2077                    for dir in SQUARE_4 {
2078                        let corner_pos = Vec2::new(
2079                            bosshut_pos.x - (hut_radius / 2) - 1,
2080                            bosshut_pos.y - (hut_radius / 2) - 2,
2081                        );
2082                        let sprite_pos = Vec2::new(
2083                            corner_pos.x + dir.x * (hut_radius + 1),
2084                            corner_pos.y + dir.y * (hut_radius + 3),
2085                        )
2086                        .with_z(alt as i32 + 3);
2087                        painter.rotated_sprite(
2088                            sprite_pos,
2089                            SpriteKind::WallSconce,
2090                            (2 + (4 * dir.x)) as u8,
2091                        );
2092                    }
2093                    let boss_spawn = wpos.with_z(alt as i32);
2094                    painter.spawn(adlet_elder(boss_spawn.as_(), &mut rng));
2095                },
2096            }
2097        }
2098    }
2099}
2100
2101struct RibCageGenerator {
2102    dir: Dir,
2103    length: u32,
2104    spine_height: f32,
2105    spine_radius: f32,
2106    /// Defines how the spine curves given the ratio along the spine from 0.0 to
2107    /// 1.0, the amplitude, and the wavelength
2108    spine_curve_function: fn(f32, f32, f32) -> f32,
2109    spine_curve_amplitude: f32,
2110    // FIXME: CAN CAUSE DIV BY 0 IF VALUE IS 0.0
2111    spine_curve_wavelength: f32,
2112    spine_start_z_offset: f32,
2113    spine_ctrl0_z_offset: f32,
2114    spine_ctrl1_z_offset: f32,
2115    spine_end_z_offset: f32,
2116    spine_ctrl0_length_fraction: f32,
2117    spine_ctrl1_length_fraction: f32,
2118    rib_base_alt: f32,
2119    rib_spacing: usize,
2120    rib_radius: f32,
2121    rib_run: f32,
2122    rib_ctrl0_run_fraction: f32,
2123    rib_ctrl1_run_fraction: f32,
2124    rib_ctrl0_width_offset: f32,
2125    rib_ctrl1_width_offset: f32,
2126    /// Defines how much ribs flare out as you go along the rib cage given the
2127    /// ratio along the spine from 0.0 to 1.0
2128    rib_width_curve: fn(f32) -> f32,
2129    rib_ctrl0_height_fraction: f32,
2130    rib_ctrl1_height_fraction: f32,
2131    vertebra_radius: f32,
2132    vertebra_width: f32,
2133    vertebra_z_offset: f32,
2134}
2135
2136impl RibCageGenerator {
2137    fn bones<'a>(&self, origin: Vec2<i32>, painter: &'a Painter) -> Vec<PrimitiveRef<'a>> {
2138        let RibCageGenerator {
2139            dir,
2140            length,
2141            spine_height,
2142            spine_radius,
2143            spine_curve_function,
2144            spine_curve_amplitude,
2145            spine_curve_wavelength,
2146            spine_start_z_offset,
2147            spine_ctrl0_z_offset,
2148            spine_ctrl1_z_offset,
2149            spine_end_z_offset,
2150            spine_ctrl0_length_fraction,
2151            spine_ctrl1_length_fraction,
2152            rib_base_alt,
2153            rib_spacing,
2154            rib_radius,
2155            rib_run,
2156            rib_ctrl0_run_fraction,
2157            rib_ctrl1_run_fraction,
2158            rib_ctrl0_width_offset,
2159            rib_ctrl1_width_offset,
2160            rib_width_curve,
2161            rib_ctrl0_height_fraction,
2162            rib_ctrl1_height_fraction,
2163            vertebra_radius,
2164            vertebra_width,
2165            vertebra_z_offset,
2166        } = self;
2167        let length_f32 = *length as f32;
2168
2169        let mut bones = Vec::new();
2170
2171        let spine_start = origin
2172            .map(|e| e as f32)
2173            .with_z(spine_height + spine_start_z_offset)
2174            + spine_curve_function(0.0, *spine_curve_amplitude, *spine_curve_wavelength)
2175                * Vec3::unit_y();
2176        let spine_ctrl0 = origin
2177            .map(|e| e as f32)
2178            .with_z(spine_height + spine_ctrl0_z_offset)
2179            + length_f32 * spine_ctrl0_length_fraction * Vec3::unit_x()
2180            + spine_curve_function(
2181                length_f32 * spine_ctrl0_length_fraction,
2182                *spine_curve_amplitude,
2183                *spine_curve_wavelength,
2184            ) * Vec3::unit_y();
2185        let spine_ctrl1 = origin
2186            .map(|e| e as f32)
2187            .with_z(spine_height + spine_ctrl1_z_offset)
2188            + length_f32 * spine_ctrl1_length_fraction * Vec3::unit_x()
2189            + spine_curve_function(
2190                length_f32 * spine_ctrl1_length_fraction,
2191                *spine_curve_amplitude,
2192                *spine_curve_wavelength,
2193            ) * Vec3::unit_y();
2194        let spine_end = origin
2195            .map(|e| e as f32)
2196            .with_z(spine_height + spine_end_z_offset)
2197            + length_f32 * Vec3::unit_x()
2198            + spine_curve_function(length_f32, *spine_curve_amplitude, *spine_curve_wavelength)
2199                * Vec3::unit_y();
2200        let spine_bezier = CubicBezier3 {
2201            start: spine_start,
2202            ctrl0: spine_ctrl0,
2203            ctrl1: spine_ctrl1,
2204            end: spine_end,
2205        };
2206        let spine = painter.cubic_bezier(
2207            spine_start,
2208            spine_ctrl0,
2209            spine_ctrl1,
2210            spine_end,
2211            *spine_radius,
2212        );
2213
2214        let rotation_origin = Vec3::new(spine_start.x, spine_start.y + 0.5, spine_start.z);
2215        let rotate = |prim: PrimitiveRef<'a>, dir: &Dir| -> PrimitiveRef<'a> {
2216            match dir {
2217                Dir::X => prim,
2218                Dir::Y => prim.rotate_about(Mat3::rotation_z(0.5 * PI).as_(), rotation_origin),
2219                Dir::NegX => prim.rotate_about(Mat3::rotation_z(PI).as_(), rotation_origin),
2220                Dir::NegY => prim.rotate_about(Mat3::rotation_z(1.5 * PI).as_(), rotation_origin),
2221            }
2222        };
2223
2224        let spine_rotated = rotate(spine, dir);
2225        bones.push(spine_rotated);
2226
2227        for i in (0..*length).step_by(*rib_spacing) {
2228            enum Side {
2229                Left,
2230                Right,
2231            }
2232
2233            let rib = |side| -> PrimitiveRef {
2234                let y_offset_multiplier = match side {
2235                    Side::Left => 1.0,
2236                    Side::Right => -1.0,
2237                };
2238                let rib_start: Vec3<f32> = spine_bezier
2239                    .evaluate((i as f32 / length_f32).clamped(0.0, 1.0))
2240                    + y_offset_multiplier * Vec3::unit_y();
2241                let rib_ctrl0 = Vec3::new(
2242                    rib_start.x + rib_ctrl0_run_fraction * rib_run,
2243                    rib_start.y
2244                        + y_offset_multiplier * rib_ctrl0_width_offset
2245                        + y_offset_multiplier * rib_width_curve(i as f32),
2246                    rib_base_alt + rib_ctrl0_height_fraction * (rib_start.z - rib_base_alt),
2247                );
2248                let rib_ctrl1 = Vec3::new(
2249                    rib_start.x + rib_ctrl1_run_fraction * rib_run,
2250                    rib_start.y
2251                        + y_offset_multiplier * rib_ctrl1_width_offset
2252                        + y_offset_multiplier * rib_width_curve(i as f32),
2253                    rib_base_alt + rib_ctrl1_height_fraction * (rib_start.z - rib_base_alt),
2254                );
2255                let rib_end = Vec3::new(
2256                    rib_start.x + rib_run,
2257                    rib_start.y + y_offset_multiplier * rib_width_curve(i as f32),
2258                    *rib_base_alt,
2259                );
2260                painter.cubic_bezier(rib_start, rib_ctrl0, rib_ctrl1, rib_end, *rib_radius)
2261            };
2262            let l_rib = rib(Side::Left);
2263            let l_rib_rotated = rotate(l_rib, dir);
2264            bones.push(l_rib_rotated);
2265
2266            let r_rib = rib(Side::Right);
2267            let r_rib_rotated = rotate(r_rib, dir);
2268            bones.push(r_rib_rotated);
2269
2270            let vertebra_start: Vec3<f32> =
2271                spine_bezier.evaluate((i as f32 / length_f32).clamped(0.0, 1.0)) + Vec3::unit_y();
2272            let vertebra = painter.ellipsoid(Aabb {
2273                min: Vec3::new(
2274                    vertebra_start.x - vertebra_width,
2275                    vertebra_start.y - 1.0 - vertebra_radius,
2276                    vertebra_start.z - vertebra_radius + vertebra_z_offset,
2277                )
2278                .map(|e| e.round() as i32),
2279                max: Vec3::new(
2280                    vertebra_start.x + vertebra_width,
2281                    vertebra_start.y - 1.0 + vertebra_radius,
2282                    vertebra_start.z + vertebra_radius + vertebra_z_offset,
2283                )
2284                .map(|e| e.round() as i32),
2285            });
2286            let vertebra_rotated = rotate(vertebra, dir);
2287            bones.push(vertebra_rotated);
2288        }
2289
2290        bones
2291    }
2292}
2293
2294fn adlet_hunter<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2295    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2296        "common.entity.dungeon.adlet.hunter",
2297        rng,
2298        None,
2299    )
2300}
2301
2302fn adlet_icepicker<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2303    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2304        "common.entity.dungeon.adlet.icepicker",
2305        rng,
2306        None,
2307    )
2308}
2309
2310fn adlet_tracker<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2311    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2312        "common.entity.dungeon.adlet.tracker",
2313        rng,
2314        None,
2315    )
2316}
2317
2318fn random_adlet<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2319    match rng.gen_range(0..3) {
2320        0 => adlet_hunter(pos, rng),
2321        1 => adlet_icepicker(pos, rng),
2322        _ => adlet_tracker(pos, rng),
2323    }
2324}
2325
2326fn adlet_elder<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2327    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2328        "common.entity.dungeon.adlet.elder",
2329        rng,
2330        None,
2331    )
2332}
2333
2334fn rat<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2335    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2336        "common.entity.wild.peaceful.rat",
2337        rng,
2338        None,
2339    )
2340}
2341
2342fn wolf<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2343    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2344        "common.entity.wild.aggressive.wolf",
2345        rng,
2346        None,
2347    )
2348}
2349
2350fn bear<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2351    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2352        "common.entity.wild.aggressive.bear",
2353        rng,
2354        None,
2355    )
2356}
2357
2358fn frostfang<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2359    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2360        "common.entity.wild.aggressive.frostfang",
2361        rng,
2362        None,
2363    )
2364}
2365
2366fn roshwalr<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2367    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2368        "common.entity.wild.aggressive.roshwalr",
2369        rng,
2370        None,
2371    )
2372}
2373
2374fn icedrake<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2375    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2376        "common.entity.wild.aggressive.icedrake",
2377        rng,
2378        None,
2379    )
2380}
2381
2382fn tursus<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2383    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2384        "common.entity.wild.aggressive.tursus",
2385        rng,
2386        None,
2387    )
2388}
2389
2390fn random_yetipit_mob<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2391    match rng.gen_range(0..4) {
2392        0 => frostfang(pos, rng),
2393        1 => roshwalr(pos, rng),
2394        2 => icedrake(pos, rng),
2395        _ => tursus(pos, rng),
2396    }
2397}
2398
2399fn yeti<R: Rng>(pos: Vec3<i32>, rng: &mut R) -> EntityInfo {
2400    EntityInfo::at(pos.map(|x| x as f32)).with_asset_expect(
2401        "common.entity.dungeon.adlet.yeti",
2402        rng,
2403        None,
2404    )
2405}
2406
2407#[cfg(test)]
2408mod tests {
2409    use super::*;
2410
2411    #[test]
2412    fn test_creating_entities() {
2413        let pos = Vec3::zero();
2414        let mut rng = thread_rng();
2415
2416        adlet_hunter(pos, &mut rng);
2417        adlet_icepicker(pos, &mut rng);
2418        adlet_tracker(pos, &mut rng);
2419        random_adlet(pos, &mut rng);
2420        adlet_elder(pos, &mut rng);
2421        rat(pos, &mut rng);
2422        wolf(pos, &mut rng);
2423        bear(pos, &mut rng);
2424        frostfang(pos, &mut rng);
2425        roshwalr(pos, &mut rng);
2426        icedrake(pos, &mut rng);
2427        tursus(pos, &mut rng);
2428    }
2429}