veloren_world/site2/plot/
adlet.rs

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