Skip to main content

veloren_world/site/plot/
cultist.rs

1use super::*;
2use crate::{
3    Land,
4    site::generation::{inscribed_polystar, place_circular},
5    util::{DIAGONALS, RandomField, sampler::Sampler, within_distance},
6};
7use common::{
8    comp::misc::PortalData,
9    generation::{EntityInfo, SpecialEntity},
10    resources::Secs,
11    terrain::SpriteKind,
12};
13use rand::prelude::*;
14use std::sync::Arc;
15use vek::*;
16
17pub struct Room {
18    room_base: i32,
19    room_center: Vec2<i32>,
20    clear_center: Vec2<i32>,
21    mob_room: bool,
22    boss_room: bool,
23    portal_to_boss: bool,
24}
25
26pub struct Cultist {
27    base: i32,
28    bounds: Aabr<i32>,
29    pub(crate) alt: i32,
30    pub(crate) center: Vec2<i32>,
31    pub(crate) room_data: Vec<Room>,
32    room_size: i32,
33    floors: i32,
34}
35impl Cultist {
36    pub fn generate(land: &Land, _rng: &mut impl Rng, site: &Site, tile_aabr: Aabr<i32>) -> Self {
37        let bounds = Aabr {
38            min: site.tile_wpos(tile_aabr.min),
39            max: site.tile_wpos(tile_aabr.max),
40        };
41        let center = bounds.center();
42        let base = land.get_alt_approx(center) as i32;
43        let room_size = 30;
44        let mut room_data = vec![];
45
46        let floors = 3;
47        for f in 0..=floors {
48            for s in 0..=1 {
49                // rooms
50                let rooms = [1, 2];
51                let boss_portal_floor =
52                    (1 + (RandomField::new(0).get(center.with_z(base + 1)) % 2)) as i32;
53                let portal_to_boss_index =
54                    (RandomField::new(0).get(center.with_z(base)) % 4) as usize;
55                if rooms.contains(&f) {
56                    for (d, dir) in DIAGONALS.iter().enumerate() {
57                        let room_base = base - (f * (2 * (room_size))) - (s * room_size);
58                        let room_center = center + (dir * ((room_size * 2) - 5 + (10 * s)));
59                        let clear_center = center + (dir * ((room_size * 2) - 6 + (10 * s)));
60                        let mob_room = s < 1;
61                        let portal_to_boss =
62                            mob_room && d == portal_to_boss_index && f == boss_portal_floor;
63                        room_data.push(Room {
64                            room_base,
65                            room_center,
66                            clear_center,
67                            mob_room,
68                            boss_room: false,
69                            portal_to_boss,
70                        });
71                    }
72                }
73            }
74        }
75        let boss_room_base = base - (6 * room_size);
76        room_data.push(Room {
77            room_base: boss_room_base,
78            room_center: center,
79            clear_center: center,
80            mob_room: false,
81            boss_room: true,
82            portal_to_boss: false,
83        });
84
85        Self {
86            bounds,
87            alt: land.get_alt_approx(site.tile_center_wpos((tile_aabr.max - tile_aabr.min) / 2))
88                as i32
89                + 2,
90            base,
91            center,
92            room_size,
93            room_data,
94            floors,
95        }
96    }
97}
98
99impl Structure for Cultist {
100    #[cfg(feature = "dyn-lib")]
101    #[unsafe(export_name = "as_dyn_structure_cultist")]
102    fn as_dyn_outer(&self) -> Option<(&dyn Structure, &'static str)> {
103        Some((Self::as_dyn_impl(self), "as_dyn_structure_cultist"))
104    }
105
106    fn spawn_rules_inner(
107        &self,
108        spawn_rules: &mut SpawnRules,
109        _land: &Land,
110        wpos: Vec2<i32>,
111        _weight: f32,
112    ) {
113        spawn_rules.trees &= !within_distance(wpos, self.bounds.center(), 75);
114        spawn_rules.waypoints = false;
115    }
116
117    fn render_inner(&self, _site: &Site, _land: &Land, painter: &Painter) {
118        let center = self.center;
119        let base = self.base;
120        let room_size = self.room_size;
121        let floors = self.floors;
122        let mut rng = rand::rng();
123        let candles_lite = Fill::Sampling(Arc::new(|wpos| {
124            Some(match (RandomField::new(0).get(wpos)) % 30 {
125                0 => Block::air(SpriteKind::Candle),
126                _ => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
127            })
128        }));
129
130        let mut tower_positions = vec![];
131        let mut clear_positions = vec![];
132        let room_data = &self.room_data;
133        let mut star_positions = vec![];
134        let mut sprite_positions = vec![];
135        let mut random_npcs = vec![];
136
137        let rock_broken = Fill::Sampling(Arc::new(|center| {
138            Some(match (RandomField::new(0).get(center)) % 52 {
139                0..=8 => Block::new(BlockKind::Rock, Rgb::new(60, 55, 65)),
140                9..=17 => Block::new(BlockKind::Rock, Rgb::new(65, 60, 70)),
141                18..=26 => Block::new(BlockKind::Rock, Rgb::new(70, 65, 75)),
142                27..=35 => Block::new(BlockKind::Rock, Rgb::new(75, 70, 80)),
143                36..=37 => Block::new(BlockKind::Air, Rgb::new(0, 0, 0)),
144                _ => Block::new(BlockKind::Rock, Rgb::new(55, 50, 60)),
145            })
146        }));
147        let rock = Fill::Brick(BlockKind::Rock, Rgb::new(55, 50, 60), 24);
148        let water = Fill::Block(Block::new(BlockKind::Water, Rgb::zero()));
149        let key_door = Fill::Block(Block::air(SpriteKind::KeyDoor));
150        let key_hole = Fill::Block(Block::air(SpriteKind::Keyhole));
151        let gold_chain = Fill::Block(Block::air(SpriteKind::SeaDecorChain));
152
153        for room in room_data {
154            let (room_base, room_center, mob_room) =
155                (room.room_base, room.room_center, room.mob_room);
156            // rooms
157            // encapsulation
158            painter
159                .aabb(Aabb {
160                    min: (room_center - room_size - 23).with_z(room_base - room_size - 1),
161                    max: (room_center + room_size + 23).with_z(room_base - 2),
162                })
163                .fill(rock.clone());
164            if mob_room {
165                painter
166                    .aabb(Aabb {
167                        min: (room_center - room_size - 2).with_z(room_base - room_size - 1),
168                        max: (room_center + room_size + 2).with_z(room_base - 2),
169                    })
170                    .fill(rock_broken.clone());
171            }
172            // solid floor
173            painter
174                .aabb(Aabb {
175                    min: (room_center - room_size - 10).with_z(room_base - room_size - 2),
176                    max: (room_center + room_size + 10).with_z(room_base - room_size - 1),
177                })
178                .fill(rock.clone());
179            // floor candles
180            painter
181                .cylinder(Aabb {
182                    min: (room_center - room_size + 1).with_z(room_base - room_size - 1),
183                    max: (room_center + room_size - 1).with_z(room_base - room_size),
184                })
185                .fill(candles_lite.clone());
186        }
187
188        for s in 0..=floors {
189            let room_base = base - (s * (2 * room_size));
190
191            // center pit
192            for p in 3..=5 {
193                let pos = 3 * p;
194                let radius = pos * 2;
195                let amount = pos;
196                let clear_radius = radius - 8;
197                let tower_pos = place_circular(center, radius as f32, amount);
198                let clear_pos = place_circular(center, clear_radius as f32, amount);
199                tower_positions.extend(tower_pos);
200                clear_positions.extend(clear_pos);
201            }
202            for tower_center in &tower_positions {
203                let height_var =
204                    (RandomField::new(0).get(tower_center.with_z(room_base)) % 15) as i32;
205                let height = height_var * 3;
206                let size = height_var / 3;
207
208                // towers
209
210                // encapsulation if under ground
211                if room_base < base {
212                    painter
213                        .aabb(Aabb {
214                            min: (tower_center - 9 - size).with_z(room_base - 2),
215                            max: (tower_center + 9 + size).with_z(room_base + 10 + height),
216                        })
217                        .fill(rock.clone());
218                    painter
219                        .aabb(Aabb {
220                            min: (tower_center - 9 - (size / 2)).with_z(room_base + 8 + height),
221                            max: (tower_center + 9 + (size / 2))
222                                .with_z(room_base + 10 + height + 5 + (height / 2)),
223                        })
224                        .fill(rock.clone());
225                }
226                painter
227                    .aabb(Aabb {
228                        min: (tower_center - 8 - size).with_z(room_base),
229                        max: (tower_center + 8 + size).with_z(room_base + 10 + height),
230                    })
231                    .fill(rock_broken.clone());
232                painter
233                    .aabb(Aabb {
234                        min: (tower_center - 8 - (size / 2)).with_z(room_base + 10 + height),
235                        max: (tower_center + 8 + (size / 2))
236                            .with_z(room_base + 10 + height + 5 + (height / 2)),
237                    })
238                    .fill(rock_broken.clone());
239
240                // vault carves floor 0
241                painter
242                    .vault(
243                        Aabb {
244                            min: Vec2::new(tower_center.x - 8 - size, tower_center.y - 4 - size)
245                                .with_z(room_base + size),
246                            max: Vec2::new(tower_center.x + 8 + size, tower_center.y + 4 + size)
247                                .with_z(room_base + height),
248                        },
249                        Dir2::X,
250                    )
251                    .clear();
252
253                painter
254                    .vault(
255                        Aabb {
256                            min: Vec2::new(tower_center.x - 4 - size, tower_center.y - 8 - size)
257                                .with_z(room_base + size),
258                            max: Vec2::new(tower_center.x + 4 + size, tower_center.y + 8 + size)
259                                .with_z(room_base + height),
260                        },
261                        Dir2::Y,
262                    )
263                    .clear();
264                // vault carves floor 1
265                painter
266                    .vault(
267                        Aabb {
268                            min: Vec2::new(
269                                tower_center.x - 8 - (size / 2),
270                                tower_center.y - 4 - (size / 2),
271                            )
272                            .with_z(room_base + 10 + height),
273                            max: Vec2::new(
274                                tower_center.x + 8 + (size / 2),
275                                tower_center.y + 4 + (size / 2),
276                            )
277                            .with_z(room_base + 10 + height + 5 + (height / 4) + (size / 2)),
278                        },
279                        Dir2::X,
280                    )
281                    .clear();
282
283                painter
284                    .vault(
285                        Aabb {
286                            min: Vec2::new(
287                                tower_center.x - 4 - (size / 2),
288                                tower_center.y - 8 - (size / 2),
289                            )
290                            .with_z(room_base + 10 + height),
291                            max: Vec2::new(
292                                tower_center.x + 4 + (size / 2),
293                                tower_center.y + 8 + (size / 2),
294                            )
295                            .with_z(room_base + 10 + height + 5 + (height / 4) + (size / 2)),
296                        },
297                        Dir2::Y,
298                    )
299                    .clear();
300            }
301            // tower clears
302            for (tower_center, clear_center) in tower_positions.iter().zip(&clear_positions) {
303                let height_var =
304                    (RandomField::new(0).get(tower_center.with_z(room_base)) % 15) as i32;
305                let height = height_var * 3;
306                let size = height_var / 3;
307                // tower clears
308                painter
309                    .aabb(Aabb {
310                        min: (clear_center - 9 - size).with_z(room_base + size),
311                        max: (clear_center + 9 + size).with_z(room_base + 8 + height),
312                    })
313                    .clear();
314                painter
315                    .aabb(Aabb {
316                        min: (clear_center - 8 - (size / 2)).with_z(room_base + 10 + height),
317                        max: (clear_center + 8 + (size / 2))
318                            .with_z(room_base + 8 + height + 5 + (height / 2)),
319                    })
320                    .clear();
321
322                // decay
323                let decay_size = 8 + size;
324                painter
325                    .cylinder(Aabb {
326                        min: (clear_center - decay_size).with_z(room_base + 8 + height),
327                        max: (clear_center + decay_size).with_z(room_base + 10 + height),
328                    })
329                    .clear();
330
331                painter
332                    .cylinder(Aabb {
333                        min: (clear_center - decay_size + 5)
334                            .with_z(room_base + 8 + height + 5 + (height / 2)),
335                        max: (clear_center + decay_size - 5)
336                            .with_z(room_base + 10 + height + 5 + (height / 2)),
337                    })
338                    .clear();
339            }
340
341            // center clear
342            painter
343                .cylinder(Aabb {
344                    min: (center - room_size + 10).with_z(room_base),
345                    max: (center + room_size - 10).with_z(room_base + (4 * (room_size))),
346                })
347                .clear();
348        }
349        // room clears
350        for room in room_data {
351            let (room_base, room_center, clear_center, mob_room, boss_room, portal_to_boss) = (
352                room.room_base,
353                room.room_center,
354                room.clear_center,
355                room.mob_room,
356                room.boss_room,
357                room.portal_to_boss,
358            );
359            painter
360                .cylinder(Aabb {
361                    min: (clear_center - room_size - 1).with_z(room_base - room_size),
362                    max: (clear_center + room_size + 1).with_z(room_base - 4),
363                })
364                .clear();
365
366            // room decor
367            let decor_var = RandomField::new(0).get(room_center.with_z(room_base)) % 4;
368            if mob_room {
369                // room_center platform or water basin
370                if decor_var < 3 {
371                    painter
372                        .aabb(Aabb {
373                            min: (room_center - room_size + 10).with_z(room_base - room_size - 1),
374                            max: (room_center + room_size - 10).with_z(room_base - 2),
375                        })
376                        .fill(rock_broken.clone());
377                }
378
379                // carves
380                let spacing = 12;
381                let carve_length = room_size + 8;
382                let carve_width = 3;
383                for f in 0..3 {
384                    for c in 0..5 {
385                        // candles & chest & npcs
386                        let sprite_pos_1 = Vec2::new(
387                            room_center.x - room_size + (spacing / 2) + (spacing * c) - carve_width
388                                + 2,
389                            room_center.y - carve_length + 2,
390                        )
391                        .with_z(room_base - room_size - 1 + ((room_size / 3) * f));
392                        sprite_positions.push(sprite_pos_1);
393
394                        let sprite_pos_2 = Vec2::new(
395                            room_center.x - room_size + (spacing / 2) + (spacing * c) + carve_width
396                                - 2,
397                            room_center.y + carve_length - 2,
398                        )
399                        .with_z(room_base - room_size - 1 + ((room_size / 3) * f));
400                        sprite_positions.push(sprite_pos_2);
401
402                        let sprite_pos_3 = Vec2::new(
403                            room_center.x - carve_length + 2,
404                            room_center.y - room_size + (spacing / 2) + (spacing * c) - carve_width
405                                + 2,
406                        )
407                        .with_z(room_base - room_size - 1 + ((room_size / 3) * f));
408                        sprite_positions.push(sprite_pos_3);
409
410                        let sprite_pos_4 = Vec2::new(
411                            room_center.x + carve_length - 2,
412                            room_center.y - room_size + (spacing / 2) + (spacing * c) + carve_width
413                                - 2,
414                        )
415                        .with_z(room_base - room_size - 1 + ((room_size / 3) * f));
416                        sprite_positions.push(sprite_pos_4);
417
418                        let candle_limiter = painter.aabb(Aabb {
419                            min: (room_center - room_size + 10)
420                                .with_z(room_base - room_size - 2 + ((room_size / 3) * f)),
421                            max: (room_center + room_size - 10)
422                                .with_z(room_base - room_size + ((room_size / 3) * f)),
423                        });
424
425                        painter
426                            .vault(
427                                Aabb {
428                                    min: Vec2::new(
429                                        room_center.x - room_size + (spacing / 2) + (spacing * c)
430                                            - carve_width,
431                                        room_center.y - carve_length,
432                                    )
433                                    .with_z(room_base - room_size - 1 + ((room_size / 3) * f)),
434                                    max: Vec2::new(
435                                        room_center.x - room_size
436                                            + (spacing / 2)
437                                            + (spacing * c)
438                                            + carve_width,
439                                        room_center.y + carve_length,
440                                    )
441                                    .with_z(
442                                        room_base - room_size - 3
443                                            + (room_size / 3)
444                                            + ((room_size / 3) * f),
445                                    ),
446                                },
447                                Dir2::Y,
448                            )
449                            .clear();
450
451                        painter
452                            .aabb(Aabb {
453                                min: Vec2::new(
454                                    room_center.x - room_size + (spacing / 2) + (spacing * c)
455                                        - carve_width,
456                                    room_center.y - carve_length,
457                                )
458                                .with_z(room_base - room_size - 2 + ((room_size / 3) * f)),
459                                max: Vec2::new(
460                                    room_center.x - room_size
461                                        + (spacing / 2)
462                                        + (spacing * c)
463                                        + carve_width,
464                                    room_center.y + carve_length,
465                                )
466                                .with_z(room_base - room_size - 1 + ((room_size / 3) * f)),
467                            })
468                            .intersect(candle_limiter)
469                            .fill(rock.clone());
470                        painter
471                            .aabb(Aabb {
472                                min: Vec2::new(
473                                    room_center.x - room_size + (spacing / 2) + (spacing * c)
474                                        - carve_width,
475                                    room_center.y - carve_length,
476                                )
477                                .with_z(room_base - room_size - 1 + ((room_size / 3) * f)),
478                                max: Vec2::new(
479                                    room_center.x - room_size
480                                        + (spacing / 2)
481                                        + (spacing * c)
482                                        + carve_width,
483                                    room_center.y + carve_length,
484                                )
485                                .with_z(room_base - room_size + ((room_size / 3) * f)),
486                            })
487                            .intersect(candle_limiter)
488                            .fill(candles_lite.clone());
489
490                        painter
491                            .vault(
492                                Aabb {
493                                    min: Vec2::new(
494                                        room_center.x - carve_length,
495                                        room_center.y - room_size + (spacing / 2) + (spacing * c)
496                                            - carve_width,
497                                    )
498                                    .with_z(room_base - room_size - 1 + ((room_size / 3) * f)),
499                                    max: Vec2::new(
500                                        room_center.x + carve_length,
501                                        room_center.y - room_size
502                                            + (spacing / 2)
503                                            + (spacing * c)
504                                            + carve_width,
505                                    )
506                                    .with_z(
507                                        room_base - room_size - 3
508                                            + (room_size / 3)
509                                            + ((room_size / 3) * f),
510                                    ),
511                                },
512                                Dir2::X,
513                            )
514                            .clear();
515
516                        painter
517                            .aabb(Aabb {
518                                min: Vec2::new(
519                                    room_center.x - carve_length,
520                                    room_center.y - room_size + (spacing / 2) + (spacing * c)
521                                        - carve_width,
522                                )
523                                .with_z(room_base - room_size - 2 + ((room_size / 3) * f)),
524                                max: Vec2::new(
525                                    room_center.x + carve_length,
526                                    room_center.y - room_size
527                                        + (spacing / 2)
528                                        + (spacing * c)
529                                        + carve_width,
530                                )
531                                .with_z(room_base - room_size - 1 + ((room_size / 3) * f)),
532                            })
533                            .intersect(candle_limiter)
534                            .fill(rock.clone());
535                        painter
536                            .aabb(Aabb {
537                                min: Vec2::new(
538                                    room_center.x - carve_length,
539                                    room_center.y - room_size + (spacing / 2) + (spacing * c)
540                                        - carve_width,
541                                )
542                                .with_z(room_base - room_size - 1 + ((room_size / 3) * f)),
543                                max: Vec2::new(
544                                    room_center.x + carve_length,
545                                    room_center.y - room_size
546                                        + (spacing / 2)
547                                        + (spacing * c)
548                                        + carve_width,
549                                )
550                                .with_z(room_base - room_size + ((room_size / 3) * f)),
551                            })
552                            .intersect(candle_limiter)
553                            .fill(candles_lite.clone());
554                    }
555                    // mob room npcs
556                    for dir in CARDINALS {
557                        for d in 1..=4 {
558                            let npc_pos = (room_center + dir * ((spacing / 2) * d))
559                                .with_z(room_base - room_size + ((room_size / 3) * f));
560                            let pos_var = RandomField::new(0).get(npc_pos) % 10;
561                            if pos_var < 2 {
562                                painter.spawn(EntityInfo::at(npc_pos.as_()).with_asset_expect(
563                                    "common.entity.dungeon.cultist.cultist",
564                                    &mut rng,
565                                    None,
566                                ))
567                            } else if pos_var > 2 && f > 0 {
568                                painter.sphere_with_radius(npc_pos, 5_f32).clear();
569                            }
570                        }
571                    }
572                    let decor_var = RandomField::new(0).get(room_center.with_z(room_base)) % 4;
573                    if decor_var < 3 {
574                        // portal platform
575                        painter
576                            .cylinder(Aabb {
577                                min: (room_center - 3).with_z(room_base - (room_size / 4) - 5),
578                                max: (room_center + 3).with_z(room_base - (room_size / 4) - 4),
579                            })
580                            .fill(rock.clone());
581                    } else {
582                        painter
583                            .cylinder(Aabb {
584                                min: (room_center - room_size + 10)
585                                    .with_z(room_base - room_size - 3),
586                                max: (room_center + room_size - 10)
587                                    .with_z(room_base - room_size - 2),
588                            })
589                            .fill(rock.clone());
590                        painter
591                            .cylinder(Aabb {
592                                min: (room_center - room_size + 10)
593                                    .with_z(room_base - room_size - 2),
594                                max: (room_center + room_size - 10)
595                                    .with_z(room_base - room_size - 1),
596                            })
597                            .fill(water.clone());
598                        painter
599                            .aabb(Aabb {
600                                min: (room_center - room_size + 10)
601                                    .with_z(room_base - room_size - 1),
602                                max: (room_center + room_size - 10)
603                                    .with_z(room_base - (room_size / 4) - 3),
604                            })
605                            .clear();
606                    }
607                }
608            }
609            // room portals
610            let mob_portal = room_center.with_z(room_base - (room_size / 4));
611            let mob_portal_target = (room_center + 10).with_z(room_base - (room_size * 2));
612            let mini_boss_portal = room_center.with_z(room_base - (room_size * 2));
613            let exit_position = (center - 10).with_z(base - (6 * room_size));
614            let boss_position = (center - 10).with_z(base - (7 * room_size));
615            let boss_portal = center.with_z(base - (7 * room_size));
616            let mini_boss_portal_target = if portal_to_boss {
617                boss_position.as_::<f32>()
618            } else {
619                exit_position.as_::<f32>()
620            };
621            if mob_room {
622                painter.spawn(EntityInfo::at(mob_portal.as_::<f32>()).into_special(
623                    SpecialEntity::Teleporter(PortalData {
624                        target: mob_portal_target.as_::<f32>(),
625                        requires_no_aggro: true,
626                        buildup_time: Secs(5.),
627                    }),
628                ));
629                painter.spawn(EntityInfo::at(mini_boss_portal.as_::<f32>()).into_special(
630                    SpecialEntity::Teleporter(PortalData {
631                        target: mini_boss_portal_target,
632                        requires_no_aggro: true,
633                        buildup_time: Secs(5.),
634                    }),
635                ));
636            } else if boss_room {
637                painter.spawn(EntityInfo::at(boss_portal.as_::<f32>()).into_special(
638                    SpecialEntity::Teleporter(PortalData {
639                        target: exit_position.as_::<f32>(),
640                        requires_no_aggro: true,
641                        buildup_time: Secs(5.),
642                    }),
643                ));
644            }
645
646            if !mob_room {
647                if boss_room {
648                    let npc_pos = room_center.with_z(room_base - room_size);
649
650                    painter.spawn(EntityInfo::at(npc_pos.as_()).with_asset_expect(
651                        "common.entity.dungeon.cultist.mindflayer",
652                        &mut rng,
653                        None,
654                    ));
655                } else {
656                    let npc_pos = (room_center - 2).with_z(room_base - room_size);
657                    painter.spawn(EntityInfo::at(npc_pos.as_()).with_asset_expect(
658                        "common.entity.dungeon.cultist.warlock",
659                        &mut rng,
660                        None,
661                    ));
662
663                    painter.spawn(EntityInfo::at(npc_pos.as_()).with_asset_expect(
664                        "common.entity.dungeon.cultist.warlord",
665                        &mut rng,
666                        None,
667                    ));
668                    painter.spawn(
669                        EntityInfo::at(((room_center + 5).with_z(room_base - room_size)).as_())
670                            .with_asset_expect(
671                                "common.entity.dungeon.cultist.beastmaster",
672                                &mut rng,
673                                None,
674                            ),
675                    );
676                }
677                // gold chains
678                let chain_positions = place_circular(room_center, 15.0, 10);
679                for pos in chain_positions {
680                    painter
681                        .aabb(Aabb {
682                            min: pos.with_z(room_base - 12),
683                            max: (pos + 1).with_z(room_base - 4),
684                        })
685                        .fill(gold_chain.clone());
686                }
687            }
688            let down = if mob_room && decor_var < 3 {
689                0
690            } else if mob_room && decor_var > 2 {
691                room_size
692            } else {
693                10
694            };
695            let magic_circle_bb = painter.cylinder(Aabb {
696                min: (room_center - 15).with_z(room_base - 3 - down),
697                max: (room_center + 16).with_z(room_base - 2 - down),
698            });
699            star_positions.push((magic_circle_bb, room_center));
700        }
701        // candles & chests & npcs
702        for sprite_pos in sprite_positions {
703            // keep center pit clear
704            if !within_distance(sprite_pos.xy(), center, 40)
705                || sprite_pos.z < (base - (6 * room_size))
706            {
707                match (RandomField::new(0).get(sprite_pos + 1)) % 16 {
708                    0 => {
709                        if sprite_pos.z > (base - (6 * room_size)) {
710                            random_npcs.push(sprite_pos)
711                        }
712                    },
713                    1 => {
714                        // prisoners
715                        painter
716                            .aabb(Aabb {
717                                min: (sprite_pos - 1).with_z(sprite_pos.z),
718                                max: (sprite_pos + 2).with_z(sprite_pos.z + 3),
719                            })
720                            .fill(key_door.clone());
721                        painter
722                            .aabb(Aabb {
723                                min: sprite_pos.with_z(sprite_pos.z + 3),
724                                max: (sprite_pos + 1).with_z(sprite_pos.z + 4),
725                            })
726                            .fill(key_hole.clone());
727                        painter
728                            .aabb(Aabb {
729                                min: (sprite_pos).with_z(sprite_pos.z),
730                                max: (sprite_pos + 1).with_z(sprite_pos.z + 2),
731                            })
732                            .clear();
733                        painter.spawn(EntityInfo::at(sprite_pos.as_()).with_asset_expect(
734                            match (RandomField::new(0).get(sprite_pos)) % 10 {
735                                0 => "common.entity.village.farmer",
736                                1 => "common.entity.village.guard",
737                                2 => "common.entity.village.hunter",
738                                3 => "common.entity.village.skinner",
739                                _ => "common.entity.village.villager",
740                            },
741                            &mut rng,
742                            None,
743                        ));
744                    },
745                    _ => {
746                        painter.sprite(
747                            sprite_pos,
748                            match (RandomField::new(0).get(sprite_pos)) % 20 {
749                                0 => SpriteKind::DungeonChest5,
750                                _ => SpriteKind::Candle,
751                            },
752                        );
753                    },
754                }
755            }
756        }
757        // floor entrances to rooms
758        for s in 1..floors {
759            let room_base = base - (s * (2 * room_size));
760            let passage_height = room_base - room_size - 1;
761
762            painter
763                .cylinder(Aabb {
764                    min: (center - 22).with_z(passage_height - 4),
765                    max: (center + 23).with_z(passage_height - 1),
766                })
767                .fill(rock_broken.clone());
768
769            for dir in DIAGONALS {
770                let clear_pos = center + dir * 30;
771                painter
772                    .line(
773                        center.with_z(passage_height),
774                        clear_pos.with_z(passage_height),
775                        5.0,
776                    )
777                    .fill(rock_broken.clone());
778                painter
779                    .line(
780                        center.with_z(passage_height + 2),
781                        clear_pos.with_z(passage_height + 2),
782                        5.0,
783                    )
784                    .clear();
785            }
786
787            painter
788                .cylinder(Aabb {
789                    min: (center - 15).with_z(passage_height - 4),
790                    max: (center + 16).with_z(passage_height + 1),
791                })
792                .clear();
793
794            let magic_circle_passage = painter.cylinder(Aabb {
795                min: (center - 15).with_z(passage_height - 3),
796                max: (center + 16).with_z(passage_height - 2),
797            });
798            star_positions.push((magic_circle_passage, center));
799        }
800        // random_npcs around upper entrance and bottom portal
801        for s in 0..=1 {
802            let radius = 62.0 - (s * 50) as f32;
803            let npcs = place_circular(center, radius, 8 - (s * 4));
804            for npc_pos in npcs {
805                random_npcs.push(npc_pos.with_z(base + 8 - ((6 * room_size) * s) - (s * 8)));
806            }
807        }
808        for pos in random_npcs {
809            let entities = [
810                "common.entity.dungeon.cultist.cultist",
811                "common.entity.dungeon.cultist.turret",
812                "common.entity.dungeon.cultist.husk",
813                "common.entity.dungeon.cultist.husk_brute",
814                "common.entity.dungeon.cultist.hound",
815            ];
816            let npc = entities[(RandomField::new(0).get(pos) % entities.len() as u32) as usize];
817            painter.spawn(EntityInfo::at(pos.as_()).with_asset_expect(npc, &mut rng, None));
818        }
819
820        // outside portal
821        let top_position = (center - 20).with_z(base + 125);
822        let bottom_position = center.with_z(base - (6 * room_size));
823        let top_pos = Vec3::new(
824            top_position.x as f32,
825            top_position.y as f32,
826            top_position.z as f32,
827        );
828        let bottom_pos = Vec3::new(
829            bottom_position.x as f32,
830            bottom_position.y as f32,
831            bottom_position.z as f32,
832        );
833        painter.spawn(
834            EntityInfo::at(bottom_pos).into_special(SpecialEntity::Teleporter(PortalData {
835                target: top_pos,
836                requires_no_aggro: true,
837                buildup_time: Secs(5.),
838            })),
839        );
840        let stone_purple = Block::new(BlockKind::GlowingRock, Rgb::new(96, 0, 128));
841        let magic_circle_bb = painter.cylinder(Aabb {
842            min: (center - 15).with_z(base - (floors * (2 * room_size)) - 1),
843            max: (center + 16).with_z(base - (floors * (2 * room_size))),
844        });
845        let magic_circle_bb_boss = painter.cylinder(Aabb {
846            min: (center - 15).with_z(base - (7 * room_size) - 2),
847            max: (center + 16).with_z(base - (7 * room_size) - 1),
848        });
849        star_positions.push((magic_circle_bb, center));
850        star_positions.push((magic_circle_bb_boss, center));
851        for (magic_circle_bb, position) in star_positions {
852            let magic_circle = painter.prim(Primitive::sampling(
853                magic_circle_bb,
854                inscribed_polystar(position, 15.0, 7),
855            ));
856            painter.fill(magic_circle, Fill::Block(stone_purple));
857        }
858        // base floor
859        painter
860            .cylinder(Aabb {
861                min: (center - room_size - 15).with_z(base - (floors * (2 * room_size)) - 3),
862                max: (center + room_size + 15).with_z(base - (floors * (2 * room_size)) - 2),
863            })
864            .fill(rock.clone());
865    }
866}