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", unsafe(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 - c).with_z(ground_floor + (room_size / 3));
446                painter.spawn(EntityInfo::at(crab_pos.as_()).with_asset_expect(
447                    "common.entity.dungeon.sahagin.soldier_crab",
448                    &mut thread_rng,
449                    None,
450                ));
451            }
452            if r == key_chest_index_2 {
453                painter.sprite(
454                    (room_center - 1).with_z(ground_floor + (room_size / 3)),
455                    SpriteKind::SahaginChest,
456                );
457            }
458
459            let center_entry = RandomField::new(0).get(center.with_z(base)) % 4;
460
461            if r > 0 {
462                // overground - keep center clear
463                let rooms_center =
464                    place_circular(center, (outer_room_radius - 15) as f32, room_size / 2);
465                let room_base = base - (room_size / 2) + (room_size / 2);
466                for room_center in rooms_center {
467                    let room_var =
468                        RandomField::new(0).get(room_center.with_z(room_base)) as i32 % 10;
469                    let room_var_size = room_size - room_var;
470                    painter
471                        .rounded_aabb(Aabb {
472                            min: (room_center - room_var_size).with_z(room_base),
473                            max: (room_center + room_var_size).with_z(room_base + room_var_size),
474                        })
475                        .fill(brick.clone());
476                }
477                if r == (center_entry + 1) as usize {
478                    painter
479                        .line(
480                            room_center.with_z(ground_floor + room_size),
481                            center.with_z(ground_floor + room_size),
482                            15.0,
483                        )
484                        .clear();
485                }
486            }
487        }
488        // tunnels
489        for p in 0..tunnel_points.len() {
490            if p == tunnel_points.len() - 1 {
491                painter
492                    .line(
493                        tunnel_points[p].with_z(ground_floor + (room_size / 2)),
494                        tunnel_points[0].with_z(ground_floor + (room_size / 2)),
495                        5.0,
496                    )
497                    .clear();
498            } else {
499                painter
500                    .line(
501                        tunnel_points[p].with_z(ground_floor + (room_size / 2)),
502                        tunnel_points[p + 1].with_z(ground_floor + (room_size / 2)),
503                        5.0,
504                    )
505                    .clear();
506            }
507        }
508        // boss room
509        painter
510            .rounded_aabb(Aabb {
511                min: (center - room_size - 10).with_z(base - 2),
512                max: (center + room_size + 10).with_z(base + room_size),
513            })
514            .fill(brick.clone());
515        let clear_limiter = painter.aabb(Aabb {
516            min: (center - room_size - 8).with_z(base + (room_size / 5)),
517            max: (center + room_size + 8).with_z(base + room_size - 1),
518        });
519        painter
520            .rounded_aabb(Aabb {
521                min: (center - room_size - 8).with_z(base),
522                max: (center + room_size + 8).with_z(base + room_size - 1),
523            })
524            .intersect(clear_limiter)
525            .clear();
526
527        // lamps
528        let var_lamps = 25 + RandomField::new(0).get(center.with_z(base)) as i32 % 5;
529        let lamp_positions = place_circular(center, (room_size + 5) as f32, var_lamps);
530
531        for lamp_pos in lamp_positions {
532            painter.sprite(
533                lamp_pos.with_z(base + (room_size / 5)),
534                SpriteKind::FireBowlGround,
535            );
536        }
537
538        // top entry and stairs
539        let stair_radius = room_size / 3;
540        for e in 0..=1 {
541            let stairs_pos = center - (room_size / 2) + ((room_size * 2) * e);
542            // top entry foundation and door
543            if e > 0 {
544                painter
545                    .rounded_aabb(Aabb {
546                        min: (stairs_pos - stair_radius - 5).with_z(base - (room_size / 2)),
547                        max: (stairs_pos + stair_radius + 5)
548                            .with_z(base + (room_size / 5) + (3 * (room_size / 2))),
549                    })
550                    .fill(brick.clone());
551                // door clear
552                painter
553                    .aabb(Aabb {
554                        min: Vec2::new(stairs_pos.x - stair_radius - 8, stairs_pos.y - 2)
555                            .with_z(base + (room_size / 5) + (2 * (room_size / 2))),
556                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 2)
557                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
558                    })
559                    .clear();
560                painter
561                    .aabb(Aabb {
562                        min: Vec2::new(stairs_pos.x - stair_radius - 8, stairs_pos.y - 1)
563                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
564                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
565                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 8),
566                    })
567                    .clear();
568                // door
569                painter
570                    .aabb(Aabb {
571                        min: Vec2::new(stairs_pos.x - stair_radius - 1, stairs_pos.y - 2)
572                            .with_z(base + (room_size / 5) + (2 * (room_size / 2))),
573                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 2)
574                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
575                    })
576                    .fill(key_door.clone());
577                painter
578                    .aabb(Aabb {
579                        min: Vec2::new(stairs_pos.x - stair_radius - 1, stairs_pos.y - 1)
580                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
581                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
582                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 8),
583                    })
584                    .fill(key_door.clone());
585                painter
586                    .aabb(Aabb {
587                        min: Vec2::new(stairs_pos.x - stair_radius - 1, stairs_pos.y)
588                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 2),
589                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
590                            .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 3),
591                    })
592                    .fill(key_hole.clone());
593                // steps
594                for s in 0..4 {
595                    painter
596                        .aabb(Aabb {
597                            min: Vec2::new(stairs_pos.x - stair_radius - 2 - s, stairs_pos.y - 2)
598                                .with_z(base + (room_size / 5) + (2 * (room_size / 2)) - 1 - s),
599                            max: Vec2::new(stairs_pos.x - stair_radius - 1 - s, stairs_pos.y + 2)
600                                .with_z(base + (room_size / 5) + (2 * (room_size / 2)) + 7),
601                        })
602                        .clear();
603                }
604            } else {
605                // boss entry 1
606                // tube
607                painter
608                    .cylinder(Aabb {
609                        min: (stairs_pos - stair_radius - 4).with_z(base + (room_size / 5)),
610                        max: (stairs_pos + stair_radius + 4).with_z(base + room_size - 2),
611                    })
612                    .fill(brick.clone());
613                painter
614                    .cylinder(Aabb {
615                        min: (stairs_pos - stair_radius).with_z(base + (room_size / 5)),
616                        max: (stairs_pos + stair_radius).with_z(base + room_size - 3),
617                    })
618                    .clear();
619                // door clear
620                painter
621                    .aabb(Aabb {
622                        min: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y - 2)
623                            .with_z(base + (room_size / 5) + 2),
624                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 2)
625                            .with_z(base + (room_size / 5) + 9),
626                    })
627                    .clear();
628                painter
629                    .aabb(Aabb {
630                        min: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y - 1)
631                            .with_z(base + (room_size / 5) + 9),
632                        max: Vec2::new(stairs_pos.x - stair_radius, stairs_pos.y + 1)
633                            .with_z(base + (room_size / 5) + 10),
634                    })
635                    .clear();
636                // door
637                painter
638                    .aabb(Aabb {
639                        min: Vec2::new(stairs_pos.x - stair_radius - 4, stairs_pos.y - 2)
640                            .with_z(base + (room_size / 5) + 2),
641                        max: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y + 2)
642                            .with_z(base + (room_size / 5) + 9),
643                    })
644                    .fill(key_door.clone());
645                painter
646                    .aabb(Aabb {
647                        min: Vec2::new(stairs_pos.x - stair_radius - 4, stairs_pos.y - 1)
648                            .with_z(base + (room_size / 5) + 9),
649                        max: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y + 1)
650                            .with_z(base + (room_size / 5) + 10),
651                    })
652                    .fill(key_door.clone());
653                painter
654                    .aabb(Aabb {
655                        min: Vec2::new(stairs_pos.x - stair_radius - 4, stairs_pos.y)
656                            .with_z(base + (room_size / 5) + 3),
657                        max: Vec2::new(stairs_pos.x - stair_radius - 3, stairs_pos.y + 1)
658                            .with_z(base + (room_size / 5) + 4),
659                    })
660                    .fill(key_hole.clone());
661            }
662
663            let stairs_clear = painter.cylinder(Aabb {
664                min: (stairs_pos - stair_radius).with_z(ground_floor + (room_size / 3)),
665                max: (stairs_pos + stair_radius)
666                    .with_z(base + (room_size / 5) + (((3 * (room_size / 2)) - 6) * e)),
667            });
668            stairs_clear.clear();
669            stairs_clear
670                .sample(spiral_staircase(
671                    stairs_pos.with_z(ground_floor + (room_size / 3)),
672                    (stair_radius + 1) as f32,
673                    2.5,
674                    (room_size - 5) as f32,
675                ))
676                .fill(wood.clone());
677        }
678
679        // boss entry 2
680        let boss_entry_pos = center + (room_size / 3);
681        let rope_pos = center + (room_size / 3) - 2;
682        let spike_pos = center + (room_size / 3) - 1;
683
684        painter
685            .cylinder(Aabb {
686                min: (boss_entry_pos - stair_radius).with_z(base + room_size - 5),
687                max: (boss_entry_pos + stair_radius).with_z(base + (room_size * 2) - 10),
688            })
689            .fill(wood.clone());
690        painter
691            .cylinder(Aabb {
692                min: (boss_entry_pos - 3).with_z(base + (room_size * 2) - 10),
693                max: (boss_entry_pos + 4).with_z(base + (room_size * 2) - 7),
694            })
695            .fill(wood.clone());
696        painter
697            .cylinder(Aabb {
698                min: (boss_entry_pos - 2).with_z(base + room_size - 5),
699                max: (boss_entry_pos + 3).with_z(base + (room_size * 2) - 7),
700            })
701            .clear();
702
703        painter
704            .aabb(Aabb {
705                min: rope_pos.with_z(base + (room_size / 4) + 1),
706                max: (rope_pos + 1).with_z(base + room_size - 5),
707            })
708            .fill(rope.clone());
709
710        painter
711            .cylinder(Aabb {
712                min: (spike_pos - 3).with_z(base + (room_size * 2) - 7),
713                max: (spike_pos + 4).with_z(base + (room_size * 2) - 5),
714            })
715            .fill(Fill::Block(Block::air(SpriteKind::IronSpike)));
716        // top room npcs
717        let npc_pos = boss_entry_pos;
718        painter.spawn(
719            EntityInfo::at((npc_pos.with_z(base + (room_size / 4))).as_()).with_asset_expect(
720                "common.entity.dungeon.sahagin.tidalwarrior",
721                &mut thread_rng,
722                None,
723            ),
724        );
725        painter.spawn(
726            EntityInfo::at(((npc_pos - 2).with_z(base + (room_size / 4))).as_()).with_asset_expect(
727                "common.entity.dungeon.sahagin.hakulaq",
728                &mut thread_rng,
729                None,
730            ),
731        );
732        for c in 0..5 {
733            let crab_pos = (npc_pos + c).with_z(base + (room_size / 4));
734            painter.spawn(EntityInfo::at(crab_pos.as_()).with_asset_expect(
735                "common.entity.dungeon.sahagin.soldier_crab",
736                &mut thread_rng,
737                None,
738            ));
739        }
740
741        // room npcs
742        for m in 0..2 {
743            let mini_boss_pos = center.with_z(base + room_size + 5);
744            painter.spawn(
745                EntityInfo::at((mini_boss_pos + (1 * m)).as_()).with_asset_expect(
746                    "common.entity.dungeon.sahagin.hakulaq",
747                    &mut thread_rng,
748                    None,
749                ),
750            );
751        }
752
753        for c in 0..5 {
754            let crab_pos = (center - c).with_z(base + room_size + 5);
755            painter.spawn(EntityInfo::at(crab_pos.as_()).with_asset_expect(
756                "common.entity.dungeon.sahagin.soldier_crab",
757                &mut thread_rng,
758                None,
759            ));
760        }
761
762        for pos in random_npcs {
763            let entities = [
764                "common.entity.dungeon.sahagin.sniper",
765                "common.entity.dungeon.sahagin.sniper",
766                "common.entity.dungeon.sahagin.sniper",
767                "common.entity.dungeon.sahagin.sorcerer",
768                "common.entity.dungeon.sahagin.spearman",
769            ];
770            let npc = entities[(RandomField::new(0).get(pos) % entities.len() as u32) as usize];
771            painter.spawn(EntityInfo::at(pos.as_()).with_asset_expect(npc, &mut thread_rng, None));
772        }
773    }
774}