Skip to main content

veloren_world/site/plot/
adlet.rs

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