veloren_world/site2/plot/
sahagin.rs

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