Skip to main content

veloren_world/site/plot/
sahagin.rs

1use super::*;
2use crate::{
3    Land,
4    site::{
5        generation::{place_circular, place_circular_as_vec, spiral_staircase},
6        util::gradient::WrapMode,
7    },
8    util::{RandomField, sampler::Sampler, within_distance},
9};
10use common::generation::EntityInfo;
11use rand::prelude::*;
12use std::sync::Arc;
13use vek::*;
14
15pub struct Sahagin {
16    bounds: Aabr<i32>,
17    pub(crate) alt: i32,
18    surface_color: Rgb<f32>,
19    sub_surface_color: Rgb<f32>,
20    pub(crate) center: Vec2<i32>,
21    pub(crate) rooms: Vec<Vec2<i32>>,
22    pub(crate) room_size: i32,
23}
24impl Sahagin {
25    pub fn generate(
26        land: &Land,
27        index: IndexRef,
28        _rng: &mut impl Rng,
29        site: &Site,
30        tile_aabr: Aabr<i32>,
31    ) -> Self {
32        let bounds = Aabr {
33            min: site.tile_wpos(tile_aabr.min),
34            max: site.tile_wpos(tile_aabr.max),
35        };
36        let (surface_color, sub_surface_color) =
37            if let Some(sample) = land.column_sample(bounds.center(), index) {
38                (sample.surface_color, sample.sub_surface_color)
39            } else {
40                (Rgb::new(161.0, 116.0, 86.0), Rgb::new(88.0, 64.0, 64.0))
41            };
42        let room_size = 30;
43        let center = bounds.center();
44        let outer_room_radius = (room_size * 2) + (room_size / 3);
45
46        let outer_rooms = place_circular(center, outer_room_radius as f32, 5);
47        let mut rooms = vec![center];
48        rooms.extend(outer_rooms);
49
50        Self {
51            bounds,
52            alt: CONFIG.sea_level as i32,
53            surface_color,
54            sub_surface_color,
55            center,
56            rooms,
57            room_size,
58        }
59    }
60}
61
62impl Structure for Sahagin {
63    #[cfg(feature = "dyn-lib")]
64    #[unsafe(export_name = "as_dyn_structure_sahagin")]
65    fn as_dyn_outer(&self) -> Option<(&dyn Structure, &'static str)> {
66        Some((Self::as_dyn_impl(self), "as_dyn_structure_sahagin"))
67    }
68
69    fn spawn_rules_inner(
70        &self,
71        spawn_rules: &mut SpawnRules,
72        _land: &Land,
73        wpos: Vec2<i32>,
74        _weight: f32,
75    ) {
76        spawn_rules.trees &= !within_distance(wpos, self.bounds.center(), 75);
77        spawn_rules.waypoints = false;
78    }
79
80    fn render_inner(&self, _site: &Site, _land: &Land, painter: &Painter) {
81        let room_size = self.room_size;
82        let center = self.center;
83        let base = self.alt - room_size + 1;
84        let rooms = &self.rooms;
85        let mut rng = rand::rng();
86        let surface_color = self.surface_color.map(|e| (e * 255.0) as u8);
87        let sub_surface_color = self.sub_surface_color.map(|e| (e * 255.0) as u8);
88        let gradient_center = Vec3::new(center.x as f32, center.y as f32, (base + 1) as f32);
89        let gradient_var_1 = RandomField::new(0).get(center.with_z(base)) as i32 % 8;
90        let gradient_var_2 = RandomField::new(0).get(center.with_z(base + 1)) as i32 % 10;
91        let mut random_npcs = vec![];
92        let brick = Fill::Gradient(
93            util::gradient::Gradient::new(
94                gradient_center,
95                8.0 + gradient_var_1 as f32,
96                util::gradient::Shape::Point,
97                (surface_color, sub_surface_color),
98            )
99            .with_repeat(if gradient_var_2 > 5 {
100                WrapMode::Repeat
101            } else {
102                WrapMode::PingPong
103            }),
104            BlockKind::Rock,
105        );
106        let jellyfish = Fill::Gradient(
107            util::gradient::Gradient::new(
108                gradient_center,
109                8.0 + gradient_var_1 as f32,
110                util::gradient::Shape::Point,
111                (Rgb::new(180, 181, 227), Rgb::new(120, 160, 255)),
112            )
113            .with_repeat(if gradient_var_2 > 5 {
114                WrapMode::Repeat
115            } else {
116                WrapMode::PingPong
117            }),
118            BlockKind::GlowingRock,
119        );
120        let white = Fill::Sampling(Arc::new(|center| {
121            Some(match (RandomField::new(0).get(center)) % 37 {
122                0..=8 => Block::new(BlockKind::Rock, Rgb::new(251, 251, 227)),
123                9..=17 => Block::new(BlockKind::Rock, Rgb::new(245, 245, 229)),
124                18..=26 => Block::new(BlockKind::Rock, Rgb::new(250, 243, 221)),
125                27..=35 => Block::new(BlockKind::Rock, Rgb::new(240, 240, 230)),
126                _ => Block::new(BlockKind::GlowingRock, Rgb::new(255, 244, 193)),
127            })
128        }));
129        let wood = Fill::Brick(BlockKind::Wood, Rgb::new(71, 33, 11), 12);
130        let key_door = Fill::Block(Block::air(SpriteKind::SahaginKeyDoor));
131        let key_hole = Fill::Block(Block::air(SpriteKind::SahaginKeyhole));
132        let rope = Fill::Block(Block::air(SpriteKind::Rope));
133        let room_size = 30;
134        let cell_size_raw = room_size / 6;
135        let ground_floor = base - (room_size * 2);
136        let outer_room_radius = (room_size * 2) + (room_size / 3);
137        let tunnel_radius = (room_size * 3) + 6;
138        let tunnel_points = place_circular_as_vec(center, tunnel_radius as f32, 25);
139        let scaler = -10;
140        let height_handle = -room_size;
141        let shell_radius = 3 * (room_size / 2) + scaler;
142        let shell_carve_radius = 6 * (room_size / 2) + scaler;
143        let shell_base = base + (room_size * 1) + height_handle;
144        let high_carve_base = base + (room_size * 7) + height_handle;
145        let low_carve_base = base + height_handle;
146        let shell_carve_limiter_1 = painter.aabb(Aabb {
147            min: (center - shell_radius - 6).with_z(shell_base),
148            max: (center + shell_radius + 6).with_z(shell_base + (5 * shell_radius)),
149        });
150
151        let shell_carve_limiter_2 = painter.aabb(Aabb {
152            min: (center - shell_radius).with_z(base + (room_size + 2) - 2),
153            max: (center + shell_radius).with_z(shell_base + (5 * shell_radius)),
154        });
155        painter
156            .cylinder_with_radius(
157                center.with_z(shell_base),
158                shell_radius as f32,
159                5.0 * shell_radius as f32,
160            )
161            .intersect(shell_carve_limiter_2)
162            .fill(white.clone());
163        // decor bubbles
164        let decor_radius = room_size / 3;
165        for b in 3..=7 {
166            let shell_decor = place_circular(center, (shell_radius - 2) as f32, 3 * b);
167
168            for pos in shell_decor {
169                let decor_var = 3 + RandomField::new(0).get(pos.with_z(base)) as i32 % 3;
170
171                painter
172                    .sphere_with_radius(
173                        pos.with_z(shell_base + (b * (shell_radius / 2))),
174                        (decor_radius - decor_var) as f32,
175                    )
176                    .fill(white.clone());
177            }
178        }
179        // shell carves
180        painter
181            .sphere_with_radius(
182                (center - room_size).with_z(high_carve_base),
183                shell_carve_radius as f32,
184            )
185            .intersect(shell_carve_limiter_1)
186            .clear();
187
188        painter
189            .sphere_with_radius(
190                (center + (room_size / 2)).with_z(low_carve_base),
191                shell_carve_radius as f32,
192            )
193            .intersect(shell_carve_limiter_2)
194            .clear();
195        // clear room
196        painter
197            .cylinder_with_radius(
198                center.with_z(shell_base + (3 * (shell_radius / 2))),
199                (shell_radius - 8) as f32,
200                shell_radius as f32,
201            )
202            .clear();
203
204        painter
205            .sphere_with_radius(
206                center.with_z(shell_base + (5 * (shell_radius / 2)) - 5),
207                (shell_radius - 8) as f32,
208            )
209            .clear();
210        let boss_pos = center.with_z(shell_base + (3 * (shell_radius / 2)));
211        painter.spawn(EntityInfo::at(boss_pos.as_()).with_asset_expect(
212            "common.entity.dungeon.sahagin.karkatha",
213            &mut rng,
214            None,
215        ));
216        // overground towers
217        let var_towers = 32 + RandomField::new(0).get(center.with_z(base)) as i32 % 6;
218        let tower_positions = place_circular(center, (5 * (room_size / 2)) as f32, var_towers);
219
220        for tower_center_pos in tower_positions {
221            for dir in CARDINALS {
222                let tower_center = tower_center_pos + dir * 5;
223                let var_height =
224                    RandomField::new(0).get(tower_center.with_z(base)) as i32 % (room_size / 2);
225                painter
226                    .rounded_aabb(Aabb {
227                        min: (tower_center - 10).with_z(base - room_size),
228                        max: (tower_center + 10).with_z(base + (3 * (room_size / 2)) + var_height),
229                    })
230                    .fill(brick.clone());
231            }
232        }
233        let bldg_base = base + room_size;
234        let bldgs = var_towers / 3;
235        let beam_th = 2.5;
236        let bldg_positions = place_circular_as_vec(center, (5 * (room_size / 2)) as f32, bldgs);
237        // buildings
238        for bldg_center in &bldg_positions {
239            let bldg_size = ((room_size / 4) + 1)
240                + RandomField::new(0).get(bldg_center.with_z(bldg_base)) as i32 % 3;
241            let points = 21;
242            let ring_0 = place_circular_as_vec(*bldg_center, (4 * (bldg_size / 2)) as f32, points);
243            let ring_1 = place_circular_as_vec(*bldg_center, (9 * (bldg_size / 2)) as f32, points);
244            let ring_2 = place_circular_as_vec(*bldg_center, (4 * (bldg_size / 2)) as f32, points);
245            let ring_3 = place_circular_as_vec(*bldg_center, (2 * (bldg_size / 2)) as f32, points);
246
247            let ring_4 = place_circular_as_vec(*bldg_center, (6 * (bldg_size / 2)) as f32, points);
248            let ring_5 = place_circular_as_vec(*bldg_center, (4 * (bldg_size / 2)) as f32, points);
249            let ring_6 = place_circular_as_vec(*bldg_center, (2 * (bldg_size / 2)) as f32, points);
250
251            for b in 0..=(ring_0.len() - 1) {
252                painter
253                    .cubic_bezier(
254                        ring_0[b].with_z(bldg_base + (3 * (bldg_size / 2))),
255                        ring_1[b].with_z(bldg_base + (5 * (bldg_size / 2))),
256                        ring_2[b].with_z(bldg_base + (10 * (bldg_size / 2))),
257                        ring_3[b].with_z(bldg_base + (14 * (bldg_size / 2))),
258                        beam_th,
259                    )
260                    .fill(jellyfish.clone());
261                if b == (ring_0.len() - 2) {
262                    painter
263                        .cubic_bezier(
264                            ring_4[b].with_z(bldg_base + (12 * (bldg_size / 2))),
265                            ring_5[b + 1].with_z(bldg_base + (14 * (bldg_size / 2))),
266                            ring_6[0].with_z(bldg_base + (16 * (bldg_size / 2))),
267                            bldg_center.with_z(bldg_base + (18 * (bldg_size / 2))),
268                            beam_th,
269                        )
270                        .fill(jellyfish.clone());
271                } else if b == (ring_0.len() - 1) {
272                    painter
273                        .cubic_bezier(
274                            ring_4[b].with_z(bldg_base + (12 * (bldg_size / 2))),
275                            ring_5[0].with_z(bldg_base + (14 * (bldg_size / 2))),
276                            ring_6[1].with_z(bldg_base + (16 * (bldg_size / 2))),
277                            bldg_center.with_z(bldg_base + (18 * (bldg_size / 2))),
278                            beam_th,
279                        )
280                        .fill(jellyfish.clone());
281                } else {
282                    painter
283                        .cubic_bezier(
284                            ring_4[b].with_z(bldg_base + (12 * (bldg_size / 2))),
285                            ring_5[b + 1].with_z(bldg_base + (14 * (bldg_size / 2))),
286                            ring_6[b + 2].with_z(bldg_base + (16 * (bldg_size / 2))),
287                            bldg_center.with_z(bldg_base + (18 * (bldg_size / 2))),
288                            beam_th,
289                        )
290                        .fill(jellyfish.clone());
291                }
292            }
293        }
294        let key_chest_index_1 =
295            RandomField::new(0).get(center.with_z(base)) as usize % bldgs as usize;
296        for (p, bldg_center) in bldg_positions.iter().enumerate() {
297            let bldg_size = ((room_size / 4) + 1)
298                + RandomField::new(0).get(bldg_center.with_z(bldg_base)) as i32 % 3;
299
300            // passage
301
302            if p == (bldg_positions.len() - 1) {
303                painter
304                    .line(
305                        bldg_positions[p].with_z(bldg_base + (5 * (bldg_size / 2))),
306                        bldg_positions[0].with_z(bldg_base + (5 * (bldg_size / 2))),
307                        beam_th * 2.0,
308                    )
309                    .clear();
310            } else {
311                painter
312                    .line(
313                        bldg_positions[p].with_z(bldg_base + (5 * (bldg_size / 2))),
314                        bldg_positions[p + 1].with_z(bldg_base + (5 * (bldg_size / 2))),
315                        beam_th * 2.0,
316                    )
317                    .clear();
318            }
319            // floor
320            painter
321                .cylinder(Aabb {
322                    min: (bldg_center - (2 * bldg_size) - 2).with_z(base),
323                    max: (bldg_center + (2 * bldg_size) + 2)
324                        .with_z(bldg_base + (5 * (bldg_size / 2)) - 4),
325                })
326                .fill(brick.clone());
327            let chest_pos = bldg_center - 4;
328            if p == key_chest_index_1 {
329                painter.sprite(
330                    chest_pos.with_z(bldg_base + (9 * (bldg_size / 2))),
331                    SpriteKind::SahaginChest,
332                );
333            }
334            painter
335                .cylinder(Aabb {
336                    min: (chest_pos - 2).with_z(bldg_base + (9 * (bldg_size / 2)) - 1),
337                    max: (chest_pos + 3).with_z(bldg_base + (9 * (bldg_size / 2))),
338                })
339                .fill(wood.clone());
340
341            random_npcs.push(chest_pos.with_z(bldg_base + (9 * (bldg_size / 2)) + 1));
342        }
343        for bldg_center in bldg_positions {
344            let bldg_size = ((room_size / 4) + 1)
345                + RandomField::new(0).get(bldg_center.with_z(bldg_base)) as i32 % 3;
346
347            // center spear
348            painter
349                .cylinder(Aabb {
350                    min: (bldg_center - 3).with_z(bldg_base),
351                    max: (bldg_center + 3).with_z(bldg_base + (20 * (bldg_size / 2))),
352                })
353                .fill(wood.clone());
354            painter
355                .cone(Aabb {
356                    min: (bldg_center - 4).with_z(bldg_base + (20 * (bldg_size / 2))),
357                    max: (bldg_center + 4).with_z(bldg_base + (30 * (bldg_size / 2))),
358                })
359                .fill(wood.clone());
360        }
361
362        // underground
363        // rooms
364        for room_center in rooms {
365            painter
366                .rounded_aabb(Aabb {
367                    min: (room_center - room_size - (room_size / 2)).with_z(ground_floor),
368                    max: (room_center + room_size + (room_size / 2)).with_z(base + 5),
369                })
370                .fill(brick.clone());
371        }
372        let key_chest_index_2 = RandomField::new(0).get(center.with_z(base)) as usize % rooms.len();
373        for (r, room_center) in rooms.iter().enumerate() {
374            painter
375                .rounded_aabb(Aabb {
376                    min: (room_center - room_size).with_z(ground_floor + 1),
377                    max: (room_center + room_size).with_z(base - 2),
378                })
379                .clear();
380            let cells = place_circular_as_vec(*room_center, room_size as f32, room_size / 2);
381            let spawns = place_circular_as_vec(*room_center, (room_size + 2) as f32, room_size / 2);
382            let cell_floors = (room_size / 6) - 1;
383            for f in 0..cell_floors {
384                let cell_floor = ground_floor + (room_size / 2) + ((cell_size_raw * 2) * f);
385                for cell_pos in &cells {
386                    let cell_var = RandomField::new(0).get(cell_pos.with_z(cell_floor)) as i32 % 2;
387                    let cell_size = cell_size_raw + cell_var;
388                    painter
389                        .rounded_aabb(Aabb {
390                            min: (cell_pos - cell_size).with_z(cell_floor - cell_size),
391                            max: (cell_pos + cell_size).with_z(cell_floor + cell_size),
392                        })
393                        .clear();
394                }
395                for spawn_pos in &spawns {
396                    painter
397                        .cylinder(Aabb {
398                            min: (spawn_pos - 3).with_z(cell_floor - cell_size_raw - 1),
399                            max: (spawn_pos + 4).with_z(cell_floor - cell_size_raw),
400                        })
401                        .fill(brick.clone());
402                    painter
403                        .cylinder(Aabb {
404                            min: (spawn_pos - 2).with_z(cell_floor - cell_size_raw),
405                            max: (spawn_pos + 3).with_z(cell_floor - cell_size_raw + 1),
406                        })
407                        .fill(brick.clone());
408                    painter
409                        .cylinder(Aabb {
410                            min: (spawn_pos - 1).with_z(cell_floor - cell_size_raw + 1),
411                            max: (spawn_pos + 2).with_z(cell_floor - cell_size_raw + 2),
412                        })
413                        .fill(brick.clone());
414                    painter.sprite(
415                        spawn_pos.with_z(cell_floor - cell_size_raw + 2),
416                        match (RandomField::new(0)
417                            .get(spawn_pos.with_z(cell_floor - cell_size_raw)))
418                            % 75
419                        {
420                            0 => SpriteKind::DungeonChest2,
421                            _ => SpriteKind::FireBowlGround,
422                        },
423                    );
424
425                    let npc_pos = spawn_pos.with_z(cell_floor - cell_size_raw + 3);
426                    if RandomField::new(0).get(npc_pos) as i32 % 5 == 1 {
427                        random_npcs.push(npc_pos);
428                    }
429                }
430            }
431            // solid floor
432            painter
433                .aabb(Aabb {
434                    min: (room_center - room_size).with_z(ground_floor),
435                    max: (room_center + room_size).with_z(ground_floor + (room_size / 3)),
436                })
437                .fill(brick.clone());
438
439            for m in 0..2 {
440                let mini_boss_pos = room_center.with_z(ground_floor + (room_size / 3));
441                painter.spawn(
442                    EntityInfo::at((mini_boss_pos + (1 * m)).as_()).with_asset_expect(
443                        "common.entity.dungeon.sahagin.hakulaq",
444                        &mut rng,
445                        None,
446                    ),
447                );
448            }
449            for c in 0..5 {
450                let crab_pos = (room_center - c).with_z(ground_floor + (room_size / 3));
451                painter.spawn(EntityInfo::at(crab_pos.as_()).with_asset_expect(
452                    "common.entity.dungeon.sahagin.soldier_crab",
453                    &mut rng,
454                    None,
455                ));
456            }
457            if r == key_chest_index_2 {
458                painter.sprite(
459                    (room_center - 1).with_z(ground_floor + (room_size / 3)),
460                    SpriteKind::SahaginChest,
461                );
462            }
463
464            let center_entry = RandomField::new(0).get(center.with_z(base)) % 4;
465
466            if r > 0 {
467                // overground - keep center clear
468                let rooms_center =
469                    place_circular(center, (outer_room_radius - 15) as f32, room_size / 2);
470                let room_base = base - (room_size / 2) + (room_size / 2);
471                for room_center in rooms_center {
472                    let room_var =
473                        RandomField::new(0).get(room_center.with_z(room_base)) as i32 % 10;
474                    let room_var_size = room_size - room_var;
475                    painter
476                        .rounded_aabb(Aabb {
477                            min: (room_center - room_var_size).with_z(room_base),
478                            max: (room_center + room_var_size).with_z(room_base + room_var_size),
479                        })
480                        .fill(brick.clone());
481                }
482                if r == (center_entry + 1) as usize {
483                    painter
484                        .line(
485                            room_center.with_z(ground_floor + room_size),
486                            center.with_z(ground_floor + room_size),
487                            15.0,
488                        )
489                        .clear();
490                }
491            }
492        }
493        // tunnels
494        for p in 0..tunnel_points.len() {
495            if p == tunnel_points.len() - 1 {
496                painter
497                    .line(
498                        tunnel_points[p].with_z(ground_floor + (room_size / 2)),
499                        tunnel_points[0].with_z(ground_floor + (room_size / 2)),
500                        5.0,
501                    )
502                    .clear();
503            } else {
504                painter
505                    .line(
506                        tunnel_points[p].with_z(ground_floor + (room_size / 2)),
507                        tunnel_points[p + 1].with_z(ground_floor + (room_size / 2)),
508                        5.0,
509                    )
510                    .clear();
511            }
512        }
513        // boss room
514        painter
515            .rounded_aabb(Aabb {
516                min: (center - room_size - 10).with_z(base - 2),
517                max: (center + room_size + 10).with_z(base + room_size),
518            })
519            .fill(brick.clone());
520        let clear_limiter = painter.aabb(Aabb {
521            min: (center - room_size - 8).with_z(base + (room_size / 5)),
522            max: (center + room_size + 8).with_z(base + room_size - 1),
523        });
524        painter
525            .rounded_aabb(Aabb {
526                min: (center - room_size - 8).with_z(base),
527                max: (center + room_size + 8).with_z(base + room_size - 1),
528            })
529            .intersect(clear_limiter)
530            .clear();
531
532        // lamps
533        let var_lamps = 25 + RandomField::new(0).get(center.with_z(base)) as i32 % 5;
534        let lamp_positions = place_circular(center, (room_size + 5) as f32, var_lamps);
535
536        for lamp_pos in lamp_positions {
537            painter.sprite(
538                lamp_pos.with_z(base + (room_size / 5)),
539                SpriteKind::FireBowlGround,
540            );
541        }
542
543        // top entry and stairs
544        let stair_radius = room_size / 3;
545        for e in 0..=1 {
546            let stairs_pos = center - (room_size / 2) + ((room_size * 2) * e);
547            // top entry foundation and door
548            if e > 0 {
549                painter
550                    .rounded_aabb(Aabb {
551                        min: (stairs_pos - stair_radius - 5).with_z(base - (room_size / 2)),
552                        max: (stairs_pos + stair_radius + 5)
553                            .with_z(base + (room_size / 5) + (3 * (room_size / 2))),
554                    })
555                    .fill(brick.clone());
556                // door clear
557                painter
558                    .aabb(Aabb {
559                        min: Vec2::new(stairs_pos.x - stair_radius - 8, stairs_pos.y - 2)
560                            .with_z(base + (room_size / 5) + (2 * (room_size / 2))),
561                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 2)
562                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
563                    })
564                    .clear();
565                painter
566                    .aabb(Aabb {
567                        min: Vec2::new(stairs_pos.x - stair_radius - 8, stairs_pos.y - 1)
568                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
569                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
570                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 8),
571                    })
572                    .clear();
573                // door
574                painter
575                    .aabb(Aabb {
576                        min: Vec2::new(stairs_pos.x - stair_radius - 1, stairs_pos.y - 2)
577                            .with_z(base + (room_size / 5) + (2 * (room_size / 2))),
578                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 2)
579                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
580                    })
581                    .fill(key_door.clone());
582                painter
583                    .aabb(Aabb {
584                        min: Vec2::new(stairs_pos.x - stair_radius - 1, stairs_pos.y - 1)
585                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
586                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
587                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 8),
588                    })
589                    .fill(key_door.clone());
590                painter
591                    .aabb(Aabb {
592                        min: Vec2::new(stairs_pos.x - stair_radius - 1, stairs_pos.y)
593                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 2),
594                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
595                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 3),
596                    })
597                    .fill(key_hole.clone());
598                // steps
599                for s in 0..4 {
600                    painter
601                        .aabb(Aabb {
602                            min: Vec2::new(stairs_pos.x - stair_radius - 2 - s, stairs_pos.y - 2)
603                                .with_z(base + (room_size / 5) + (2 * (room_size / 2)) - 1 - s),
604                            max: Vec2::new(stairs_pos.x - stair_radius - 1 - s, stairs_pos.y + 2)
605                                .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
606                        })
607                        .clear();
608                }
609            } else {
610                // boss entry 1
611                // tube
612                painter
613                    .cylinder(Aabb {
614                        min: (stairs_pos - stair_radius - 4).with_z(base + (room_size / 5)),
615                        max: (stairs_pos + stair_radius + 4).with_z(base + room_size - 2),
616                    })
617                    .fill(brick.clone());
618                painter
619                    .cylinder(Aabb {
620                        min: (stairs_pos - stair_radius).with_z(base + (room_size / 5)),
621                        max: (stairs_pos + stair_radius).with_z(base + room_size - 3),
622                    })
623                    .clear();
624                // door clear
625                painter
626                    .aabb(Aabb {
627                        min: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y - 2)
628                            .with_z(base + (room_size / 5) + 2),
629                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 2)
630                            .with_z(base + (room_size / 5) + 9),
631                    })
632                    .clear();
633                painter
634                    .aabb(Aabb {
635                        min: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y - 1)
636                            .with_z(base + (room_size / 5) + 9),
637                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
638                            .with_z(base + (room_size / 5) + 10),
639                    })
640                    .clear();
641                // door
642                painter
643                    .aabb(Aabb {
644                        min: Vec2::new(stairs_pos.x - stair_radius - 4, stairs_pos.y - 2)
645                            .with_z(base + (room_size / 5) + 2),
646                        max: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y + 2)
647                            .with_z(base + (room_size / 5) + 9),
648                    })
649                    .fill(key_door.clone());
650                painter
651                    .aabb(Aabb {
652                        min: Vec2::new(stairs_pos.x - stair_radius - 4, stairs_pos.y - 1)
653                            .with_z(base + (room_size / 5) + 9),
654                        max: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y + 1)
655                            .with_z(base + (room_size / 5) + 10),
656                    })
657                    .fill(key_door.clone());
658                painter
659                    .aabb(Aabb {
660                        min: Vec2::new(stairs_pos.x - stair_radius - 4, stairs_pos.y)
661                            .with_z(base + (room_size / 5) + 3),
662                        max: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y + 1)
663                            .with_z(base + (room_size / 5) + 4),
664                    })
665                    .fill(key_hole.clone());
666            }
667
668            let stairs_clear = painter.cylinder(Aabb {
669                min: (stairs_pos - stair_radius).with_z(ground_floor + (room_size / 3)),
670                max: (stairs_pos + stair_radius)
671                    .with_z(base + (room_size / 5) + (((3 * (room_size / 2)) - 6) * e)),
672            });
673            stairs_clear.clear();
674            stairs_clear
675                .sample(spiral_staircase(
676                    stairs_pos.with_z(ground_floor + (room_size / 3)),
677                    (stair_radius + 1) as f32,
678                    2.5,
679                    (room_size - 5) as f32,
680                ))
681                .fill(wood.clone());
682        }
683
684        // boss entry 2
685        let boss_entry_pos = center + (room_size / 3);
686        let rope_pos = center + (room_size / 3) - 2;
687        let spike_pos = center + (room_size / 3) - 1;
688
689        painter
690            .cylinder(Aabb {
691                min: (boss_entry_pos - stair_radius).with_z(base + room_size - 5),
692                max: (boss_entry_pos + stair_radius).with_z(base + (room_size * 2) - 10),
693            })
694            .fill(wood.clone());
695        painter
696            .cylinder(Aabb {
697                min: (boss_entry_pos - 3).with_z(base + (room_size * 2) - 10),
698                max: (boss_entry_pos + 4).with_z(base + (room_size * 2) - 7),
699            })
700            .fill(wood.clone());
701        painter
702            .cylinder(Aabb {
703                min: (boss_entry_pos - 2).with_z(base + room_size - 5),
704                max: (boss_entry_pos + 3).with_z(base + (room_size * 2) - 7),
705            })
706            .clear();
707
708        painter
709            .aabb(Aabb {
710                min: rope_pos.with_z(base + (room_size / 4) + 1),
711                max: (rope_pos + 1).with_z(base + room_size - 5),
712            })
713            .fill(rope.clone());
714
715        painter
716            .cylinder(Aabb {
717                min: (spike_pos - 3).with_z(base + (room_size * 2) - 7),
718                max: (spike_pos + 4).with_z(base + (room_size * 2) - 5),
719            })
720            .fill(Fill::Block(Block::air(SpriteKind::IronSpike)));
721        // top room npcs
722        let npc_pos = boss_entry_pos;
723        painter.spawn(
724            EntityInfo::at((npc_pos.with_z(base + (room_size / 4))).as_()).with_asset_expect(
725                "common.entity.dungeon.sahagin.tidalwarrior",
726                &mut rng,
727                None,
728            ),
729        );
730        painter.spawn(
731            EntityInfo::at(((npc_pos - 2).with_z(base + (room_size / 4))).as_()).with_asset_expect(
732                "common.entity.dungeon.sahagin.hakulaq",
733                &mut rng,
734                None,
735            ),
736        );
737        for c in 0..5 {
738            let crab_pos = (npc_pos + c).with_z(base + (room_size / 4));
739            painter.spawn(EntityInfo::at(crab_pos.as_()).with_asset_expect(
740                "common.entity.dungeon.sahagin.soldier_crab",
741                &mut rng,
742                None,
743            ));
744        }
745
746        // room npcs
747        for m in 0..2 {
748            let mini_boss_pos = center.with_z(base + room_size + 5);
749            painter.spawn(
750                EntityInfo::at((mini_boss_pos + (1 * m)).as_()).with_asset_expect(
751                    "common.entity.dungeon.sahagin.hakulaq",
752                    &mut rng,
753                    None,
754                ),
755            );
756        }
757
758        for c in 0..5 {
759            let crab_pos = (center - c).with_z(base + room_size + 5);
760            painter.spawn(EntityInfo::at(crab_pos.as_()).with_asset_expect(
761                "common.entity.dungeon.sahagin.soldier_crab",
762                &mut rng,
763                None,
764            ));
765        }
766
767        for pos in random_npcs {
768            let entities = [
769                "common.entity.dungeon.sahagin.sniper",
770                "common.entity.dungeon.sahagin.sniper",
771                "common.entity.dungeon.sahagin.sniper",
772                "common.entity.dungeon.sahagin.sorcerer",
773                "common.entity.dungeon.sahagin.spearman",
774            ];
775            let npc = entities[(RandomField::new(0).get(pos) % entities.len() as u32) as usize];
776            painter.spawn(EntityInfo::at(pos.as_()).with_asset_expect(npc, &mut rng, None));
777        }
778    }
779}