veloren_world/site2/plot/
cultist.rs

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