mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
1503 lines
56 KiB
Rust
1503 lines
56 KiB
Rust
use super::*;
|
|
use crate::{
|
|
site::namegen::NameGen,
|
|
site2::{self, aabr_with_z, Fill, Primitive, Structure as SiteStructure},
|
|
util::{attempt, Grid, RandomField, Sampler, CARDINALS, DIRS},
|
|
Land,
|
|
};
|
|
|
|
use common::{
|
|
assets::{self, AssetExt, AssetHandle},
|
|
astar::Astar,
|
|
generation::{ChunkSupplement, EntityInfo},
|
|
store::{Id, Store},
|
|
terrain::{
|
|
BiomeKind, Block, BlockKind, SpriteKind, Structure, StructuresGroup, TerrainChunkSize,
|
|
},
|
|
vol::RectVolSize,
|
|
};
|
|
use core::{f32, hash::BuildHasherDefault};
|
|
use fxhash::FxHasher64;
|
|
use lazy_static::lazy_static;
|
|
use rand::{prelude::*, seq::SliceRandom};
|
|
use serde::Deserialize;
|
|
use std::sync::Arc;
|
|
use vek::*;
|
|
|
|
pub struct Dungeon {
|
|
name: String,
|
|
origin: Vec2<i32>,
|
|
alt: i32,
|
|
seed: u32,
|
|
#[allow(dead_code)]
|
|
noise: RandomField,
|
|
floors: Vec<Floor>,
|
|
difficulty: u32,
|
|
}
|
|
|
|
pub struct GenCtx<'a, R: Rng> {
|
|
land: &'a Land<'a>,
|
|
rng: &'a mut R,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct Colors {
|
|
pub stone: (u8, u8, u8),
|
|
}
|
|
|
|
const ALT_OFFSET: i32 = -2;
|
|
|
|
#[derive(Deserialize)]
|
|
struct DungeonDistribution(Vec<(u32, f32)>);
|
|
impl assets::Asset for DungeonDistribution {
|
|
type Loader = assets::RonLoader;
|
|
|
|
const EXTENSION: &'static str = "ron";
|
|
}
|
|
|
|
lazy_static! {
|
|
static ref DUNGEON_DISTRIBUTION: Vec<(u32, f32)> =
|
|
DungeonDistribution::load_expect("world.dungeon.difficulty_distribution")
|
|
.read()
|
|
.0
|
|
.clone();
|
|
}
|
|
|
|
fn floor_amount(difficulty: u32) -> u32 { 3 + difficulty / 2 }
|
|
|
|
impl Dungeon {
|
|
pub fn generate(wpos: Vec2<i32>, land: &Land, rng: &mut impl Rng) -> Self {
|
|
let mut ctx = GenCtx { land, rng };
|
|
let difficulty = DUNGEON_DISTRIBUTION
|
|
.choose_weighted(&mut ctx.rng, |pair| pair.1)
|
|
.map(|(difficulty, _)| *difficulty)
|
|
.unwrap_or_else(|err| {
|
|
panic!(
|
|
"Failed to choose difficulty (check instruction in config). Error: {}",
|
|
err
|
|
)
|
|
});
|
|
let floors = floor_amount(difficulty);
|
|
|
|
Self {
|
|
name: {
|
|
let name = NameGen::location(ctx.rng).generate();
|
|
match ctx.rng.gen_range(0..5) {
|
|
0 => format!("{} Dungeon", name),
|
|
1 => format!("{} Lair", name),
|
|
2 => format!("{} Crib", name),
|
|
3 => format!("{} Catacombs", name),
|
|
_ => format!("{} Pit", name),
|
|
}
|
|
},
|
|
origin: wpos - TILE_SIZE / 2,
|
|
alt: ctx.land.get_alt_approx(wpos) as i32 + 6,
|
|
seed: ctx.rng.gen(),
|
|
noise: RandomField::new(ctx.rng.gen()),
|
|
floors: (0..floors)
|
|
.scan(Vec2::zero(), |stair_tile, level| {
|
|
let (floor, st) =
|
|
Floor::generate(&mut ctx, *stair_tile, level as i32, difficulty);
|
|
*stair_tile = st;
|
|
Some(floor)
|
|
})
|
|
.collect(),
|
|
difficulty,
|
|
}
|
|
}
|
|
|
|
pub fn name(&self) -> &str { &self.name }
|
|
|
|
pub fn get_origin(&self) -> Vec2<i32> { self.origin }
|
|
|
|
pub fn radius(&self) -> f32 {
|
|
self.floors
|
|
.iter()
|
|
.map(|floor| (TILE_SIZE * floor.tiles.size()).magnitude_squared())
|
|
.max()
|
|
.map(|d| (d as f32).sqrt() / 2.0)
|
|
.unwrap_or(200.0)
|
|
}
|
|
|
|
#[allow(clippy::needless_update)] // TODO: Pending review in #587
|
|
pub fn spawn_rules(&self, wpos: Vec2<i32>) -> SpawnRules {
|
|
SpawnRules {
|
|
trees: wpos.distance_squared(self.origin) > 64i32.pow(2),
|
|
..SpawnRules::default()
|
|
}
|
|
}
|
|
|
|
pub fn difficulty(&self) -> u32 { self.difficulty }
|
|
|
|
pub fn apply_supplement<'a>(
|
|
&'a self,
|
|
// NOTE: Used only for dynamic elements like chests and entities!
|
|
dynamic_rng: &mut impl Rng,
|
|
wpos2d: Vec2<i32>,
|
|
supplement: &mut ChunkSupplement,
|
|
) {
|
|
let rpos = wpos2d - self.origin;
|
|
let area = Aabr {
|
|
min: rpos,
|
|
max: rpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
|
|
};
|
|
|
|
// Add waypoint
|
|
let pos = self.origin.map2(FLOOR_SIZE, |e, sz| e + sz as i32 / 2);
|
|
if area.contains_point(pos - self.origin) {
|
|
supplement.add_entity(
|
|
EntityInfo::at(Vec3::new(pos.x as f32, pos.y as f32, self.alt as f32) + 5.0)
|
|
.into_waypoint(),
|
|
);
|
|
}
|
|
|
|
let mut z = self.alt + ALT_OFFSET;
|
|
for floor in &self.floors {
|
|
z -= floor.total_depth();
|
|
let origin = Vec3::new(self.origin.x, self.origin.y, z);
|
|
floor.apply_supplement(dynamic_rng, area, origin, supplement);
|
|
}
|
|
}
|
|
}
|
|
|
|
const TILE_SIZE: i32 = 13;
|
|
|
|
#[derive(Clone)]
|
|
pub enum StairsKind {
|
|
Spiral,
|
|
WallSpiral,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub enum Tile {
|
|
UpStair(Id<Room>, StairsKind),
|
|
DownStair(Id<Room>),
|
|
Room(Id<Room>),
|
|
Tunnel,
|
|
Solid,
|
|
}
|
|
|
|
impl Tile {
|
|
fn is_passable(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
Tile::UpStair(_, _) | Tile::DownStair(_) | Tile::Room(_) | Tile::Tunnel
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
|
enum RoomKind {
|
|
Peaceful,
|
|
Fight,
|
|
Boss,
|
|
Miniboss,
|
|
#[allow(dead_code)]
|
|
LavaPlatforming,
|
|
}
|
|
|
|
pub struct Room {
|
|
seed: u32,
|
|
loot_density: f32,
|
|
kind: RoomKind,
|
|
area: Rect<i32, i32>,
|
|
height: i32,
|
|
pillars: Option<i32>, // Pillars with the given separation
|
|
pits: Option<i32>, // Pits filled with lava
|
|
difficulty: u32,
|
|
}
|
|
|
|
impl Room {
|
|
fn fill_fight_cell(
|
|
&self,
|
|
supplement: &mut ChunkSupplement,
|
|
dynamic_rng: &mut impl Rng,
|
|
tile_wcenter: Vec3<i32>,
|
|
wpos2d: Vec2<i32>,
|
|
tile_pos: Vec2<i32>,
|
|
) {
|
|
let enemy_spawn_tile = self.area.center();
|
|
// Don't spawn enemies in a pillar
|
|
let enemy_tile_is_pillar = self.pillars.map_or(false, |pillar_space| {
|
|
enemy_spawn_tile
|
|
.map(|e| e.rem_euclid(pillar_space) == 0)
|
|
.reduce_and()
|
|
});
|
|
let enemy_spawn_tile = enemy_spawn_tile + if enemy_tile_is_pillar { 1 } else { 0 };
|
|
|
|
// Toss mobs in the center of the room
|
|
if tile_pos == enemy_spawn_tile && wpos2d == tile_wcenter.xy() {
|
|
let entities = match self.difficulty {
|
|
0 => enemy_0(dynamic_rng, tile_wcenter),
|
|
1 => enemy_1(dynamic_rng, tile_wcenter),
|
|
2 => enemy_2(dynamic_rng, tile_wcenter),
|
|
3 => enemy_3(dynamic_rng, tile_wcenter),
|
|
4 => enemy_4(dynamic_rng, tile_wcenter),
|
|
5 => enemy_5(dynamic_rng, tile_wcenter),
|
|
_ => enemy_fallback(dynamic_rng, tile_wcenter),
|
|
};
|
|
|
|
for entity in entities {
|
|
supplement.add_entity(entity);
|
|
}
|
|
} else {
|
|
// Turrets
|
|
// Turret has 1/5000 chance to spawn per voxel in fight room
|
|
if dynamic_rng.gen_range(0..5000) == 0 {
|
|
let pos = tile_wcenter.map(|e| e as f32)
|
|
+ Vec3::<u32>::iota()
|
|
.map(|e| {
|
|
(RandomField::new(self.seed.wrapping_add(10 + e))
|
|
.get(Vec3::from(tile_pos))
|
|
% 32) as i32
|
|
- 16
|
|
})
|
|
.map(|e| e as f32 / 16.0);
|
|
match self.difficulty {
|
|
3 => {
|
|
let turret = turret_3(pos);
|
|
supplement.add_entity(turret);
|
|
},
|
|
5 => {
|
|
let turret = turret_5(pos);
|
|
supplement.add_entity(turret);
|
|
},
|
|
_ => {},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fill_miniboss_cell(
|
|
&self,
|
|
supplement: &mut ChunkSupplement,
|
|
dynamic_rng: &mut impl Rng,
|
|
tile_wcenter: Vec3<i32>,
|
|
wpos2d: Vec2<i32>,
|
|
tile_pos: Vec2<i32>,
|
|
) {
|
|
let miniboss_spawn_tile = self.area.center();
|
|
// Don't spawn the miniboss in a pillar
|
|
let miniboss_tile_is_pillar = self.pillars.map_or(false, |pillar_space| {
|
|
miniboss_spawn_tile
|
|
.map(|e| e.rem_euclid(pillar_space) == 0)
|
|
.reduce_and()
|
|
});
|
|
let miniboss_spawn_tile = miniboss_spawn_tile + if miniboss_tile_is_pillar { 1 } else { 0 };
|
|
|
|
if tile_pos == miniboss_spawn_tile && tile_wcenter.xy() == wpos2d {
|
|
let entities = match self.difficulty {
|
|
0 => mini_boss_0(tile_wcenter),
|
|
1 => mini_boss_1(tile_wcenter),
|
|
2 => mini_boss_2(tile_wcenter),
|
|
3 => mini_boss_3(tile_wcenter),
|
|
4 => mini_boss_4(tile_wcenter),
|
|
5 => mini_boss_5(dynamic_rng, tile_wcenter),
|
|
_ => mini_boss_fallback(tile_wcenter),
|
|
};
|
|
|
|
for entity in entities {
|
|
supplement.add_entity(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn fill_boss_cell(
|
|
&self,
|
|
supplement: &mut ChunkSupplement,
|
|
tile_wcenter: Vec3<i32>,
|
|
wpos2d: Vec2<i32>,
|
|
tile_pos: Vec2<i32>,
|
|
) {
|
|
let boss_spawn_tile = self.area.center();
|
|
// Don't spawn the boss in a pillar
|
|
let boss_tile_is_pillar = self.pillars.map_or(false, |pillar_space| {
|
|
boss_spawn_tile
|
|
.map(|e| e.rem_euclid(pillar_space) == 0)
|
|
.reduce_and()
|
|
});
|
|
let boss_spawn_tile = boss_spawn_tile + if boss_tile_is_pillar { 1 } else { 0 };
|
|
|
|
if tile_pos == boss_spawn_tile && wpos2d == tile_wcenter.xy() {
|
|
let entities = match self.difficulty {
|
|
0 => boss_0(tile_wcenter),
|
|
1 => boss_1(tile_wcenter),
|
|
2 => boss_2(tile_wcenter),
|
|
3 => boss_3(tile_wcenter),
|
|
4 => boss_4(tile_wcenter),
|
|
5 => boss_5(tile_wcenter),
|
|
_ => boss_fallback(tile_wcenter),
|
|
};
|
|
|
|
for entity in entities {
|
|
supplement.add_entity(entity);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Floor {
|
|
tile_offset: Vec2<i32>,
|
|
tiles: Grid<Tile>,
|
|
rooms: Store<Room>,
|
|
solid_depth: i32,
|
|
hollow_depth: i32,
|
|
#[allow(dead_code)]
|
|
stair_tile: Vec2<i32>,
|
|
final_level: bool,
|
|
difficulty: u32,
|
|
}
|
|
|
|
const FLOOR_SIZE: Vec2<i32> = Vec2::new(18, 18);
|
|
|
|
impl Floor {
|
|
fn generate(
|
|
ctx: &mut GenCtx<impl Rng>,
|
|
stair_tile: Vec2<i32>,
|
|
level: i32,
|
|
difficulty: u32,
|
|
) -> (Self, Vec2<i32>) {
|
|
const MAX_WIDTH: u32 = 4;
|
|
let floors = floor_amount(difficulty);
|
|
let final_level = level == floors as i32 - 1;
|
|
let width = (2 + difficulty / 2).min(MAX_WIDTH);
|
|
let height = (15 + difficulty * 3).min(30);
|
|
|
|
let new_stair_tile = if final_level {
|
|
Vec2::zero()
|
|
} else {
|
|
std::iter::from_fn(|| {
|
|
Some(FLOOR_SIZE.map(|sz| ctx.rng.gen_range(-sz / 2 + 2..sz / 2 - 1)))
|
|
})
|
|
.filter(|pos| *pos != stair_tile)
|
|
.take(8)
|
|
.max_by_key(|pos| (*pos - stair_tile).map(|e| e.abs()).sum())
|
|
.unwrap()
|
|
};
|
|
|
|
let tile_offset = -FLOOR_SIZE / 2;
|
|
let mut this = Floor {
|
|
tile_offset,
|
|
tiles: Grid::new(FLOOR_SIZE, Tile::Solid),
|
|
rooms: Store::default(),
|
|
solid_depth: if level == 0 { 80 } else { 32 },
|
|
hollow_depth: 30,
|
|
stair_tile: new_stair_tile - tile_offset,
|
|
final_level,
|
|
difficulty,
|
|
};
|
|
|
|
const STAIR_ROOM_HEIGHT: i32 = 13;
|
|
// Create rooms for entrance and exit
|
|
let upstair_room = this.create_room(Room {
|
|
seed: ctx.rng.gen(),
|
|
loot_density: 0.0,
|
|
kind: RoomKind::Peaceful,
|
|
area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))),
|
|
height: STAIR_ROOM_HEIGHT,
|
|
pillars: None,
|
|
pits: None,
|
|
difficulty,
|
|
});
|
|
if final_level {
|
|
// Boss room
|
|
this.create_room(Room {
|
|
seed: ctx.rng.gen(),
|
|
loot_density: 0.0,
|
|
kind: RoomKind::Boss,
|
|
area: Rect::from((
|
|
new_stair_tile - tile_offset - MAX_WIDTH as i32 - 1,
|
|
Extent2::broadcast(width as i32 * 2 + 1),
|
|
)),
|
|
height: height as i32,
|
|
pillars: Some(2),
|
|
pits: None,
|
|
difficulty,
|
|
});
|
|
} else {
|
|
// Create downstairs room
|
|
let downstair_room = this.create_room(Room {
|
|
seed: ctx.rng.gen(),
|
|
loot_density: 0.0,
|
|
kind: RoomKind::Peaceful,
|
|
area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))),
|
|
height: STAIR_ROOM_HEIGHT,
|
|
pillars: None,
|
|
pits: None,
|
|
difficulty,
|
|
});
|
|
this.tiles.set(
|
|
new_stair_tile - tile_offset,
|
|
Tile::DownStair(downstair_room),
|
|
);
|
|
}
|
|
let stair_kind = if ctx.rng.gen::<f32>() < 0.3 {
|
|
StairsKind::Spiral
|
|
} else {
|
|
StairsKind::WallSpiral
|
|
};
|
|
|
|
this.tiles.set(
|
|
stair_tile - tile_offset,
|
|
Tile::UpStair(upstair_room, stair_kind),
|
|
);
|
|
|
|
this.create_rooms(ctx, level, 7);
|
|
// Create routes between all rooms
|
|
let room_areas = this.rooms.values().map(|r| r.area).collect::<Vec<_>>();
|
|
for a in room_areas.iter() {
|
|
for b in room_areas.iter() {
|
|
this.create_route(ctx, a.center(), b.center());
|
|
}
|
|
}
|
|
|
|
(this, new_stair_tile)
|
|
}
|
|
|
|
fn create_room(&mut self, room: Room) -> Id<Room> {
|
|
let area = room.area;
|
|
let id = self.rooms.insert(room);
|
|
for x in 0..area.extent().w {
|
|
for y in 0..area.extent().h {
|
|
self.tiles
|
|
.set(area.position() + Vec2::new(x, y), Tile::Room(id));
|
|
}
|
|
}
|
|
id
|
|
}
|
|
|
|
fn create_rooms(&mut self, ctx: &mut GenCtx<impl Rng>, level: i32, n: usize) {
|
|
let dim_limits = (3, 6);
|
|
|
|
for _ in 0..n {
|
|
let area = match attempt(64, || {
|
|
let sz = Vec2::<i32>::zero().map(|_| ctx.rng.gen_range(dim_limits.0..dim_limits.1));
|
|
let pos = FLOOR_SIZE.map2(sz, |floor_sz, room_sz| {
|
|
ctx.rng.gen_range(0..floor_sz + 1 - room_sz)
|
|
});
|
|
let area = Rect::from((pos, Extent2::from(sz)));
|
|
// The room, but with some personal space
|
|
let area_border = Rect::from((pos - 1, Extent2::from(sz) + 2));
|
|
|
|
// Ensure no overlap
|
|
if self
|
|
.rooms
|
|
.values()
|
|
.any(|r| r.area.collides_with_rect(area_border))
|
|
{
|
|
return None;
|
|
}
|
|
|
|
Some(area)
|
|
}) {
|
|
Some(area) => area,
|
|
None => return,
|
|
};
|
|
|
|
let loot_density = |difficulty, level| {
|
|
let max_floor = floor_amount(difficulty);
|
|
// We count floors from 0, don't divide by zero
|
|
let current_floor = level + 1;
|
|
let ratio = f64::from(current_floor) / f64::from(max_floor);
|
|
// filter starting floors
|
|
let ratio = 0.0_f64.max(ratio - 0.55);
|
|
0.00175 * ratio as f32
|
|
};
|
|
match ctx.rng.gen_range(0..5) {
|
|
// Miniboss room
|
|
0 => self.create_room(Room {
|
|
seed: ctx.rng.gen(),
|
|
loot_density: loot_density(self.difficulty, level),
|
|
kind: RoomKind::Miniboss,
|
|
area,
|
|
height: ctx.rng.gen_range(15..20),
|
|
pillars: Some(ctx.rng.gen_range(2..=4)),
|
|
pits: None,
|
|
difficulty: self.difficulty,
|
|
}),
|
|
//// Lava platforming room
|
|
//1 => self.create_room(Room {
|
|
// seed: ctx.rng.gen(),
|
|
// loot_density: 0.0,
|
|
// kind: RoomKind::LavaPlatforming,
|
|
// area,
|
|
// height: ctx.rng.gen_range(10..15),
|
|
// pillars: None,
|
|
// pits: Some(1),
|
|
// difficulty: self.difficulty,
|
|
//}),
|
|
// Fight room with enemies in it
|
|
_ => self.create_room(Room {
|
|
seed: ctx.rng.gen(),
|
|
loot_density: loot_density(self.difficulty, level),
|
|
kind: RoomKind::Fight,
|
|
area,
|
|
height: ctx.rng.gen_range(10..15),
|
|
pillars: if ctx.rng.gen_range(0..4) == 0 {
|
|
Some(ctx.rng.gen_range(2..=4))
|
|
} else {
|
|
None
|
|
},
|
|
pits: None,
|
|
difficulty: self.difficulty,
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
fn create_route(&mut self, _ctx: &mut GenCtx<impl Rng>, a: Vec2<i32>, b: Vec2<i32>) {
|
|
let heuristic = move |l: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32;
|
|
let neighbors = |l: &Vec2<i32>| {
|
|
let l = *l;
|
|
CARDINALS
|
|
.iter()
|
|
.map(move |dir| l + dir)
|
|
.filter(|pos| self.tiles.get(*pos).is_some())
|
|
};
|
|
let transition = |_a: &Vec2<i32>, b: &Vec2<i32>| match self.tiles.get(*b) {
|
|
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
|
|
Some(Tile::Solid) => 25.0,
|
|
Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0,
|
|
_ => 100000.0,
|
|
};
|
|
let satisfied = |l: &Vec2<i32>| *l == b;
|
|
// We use this hasher (FxHasher64) because
|
|
// (1) we don't care about DDOS attacks (ruling out SipHash);
|
|
// (2) we don't care about determinism across computers (we could use AAHash);
|
|
// (3) we have 8-byte keys (for which FxHash is fastest).
|
|
let mut astar = Astar::new(
|
|
20000,
|
|
a,
|
|
heuristic,
|
|
BuildHasherDefault::<FxHasher64>::default(),
|
|
);
|
|
let path = astar
|
|
.poll(
|
|
FLOOR_SIZE.product() as usize + 1,
|
|
heuristic,
|
|
neighbors,
|
|
transition,
|
|
satisfied,
|
|
)
|
|
.into_path()
|
|
.expect("No route between locations - this shouldn't be able to happen");
|
|
|
|
for pos in path.iter() {
|
|
if let Some(tile @ Tile::Solid) = self.tiles.get_mut(*pos) {
|
|
*tile = Tile::Tunnel;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_supplement(
|
|
&self,
|
|
// NOTE: Used only for dynamic elements like chests and entities!
|
|
dynamic_rng: &mut impl Rng,
|
|
area: Aabr<i32>,
|
|
origin: Vec3<i32>,
|
|
supplement: &mut ChunkSupplement,
|
|
) {
|
|
/*
|
|
// Add stair waypoint
|
|
let stair_rcenter =
|
|
Vec3::from((self.stair_tile + self.tile_offset).map(|e| e * TILE_SIZE + TILE_SIZE / 2));
|
|
|
|
if area.contains_point(stair_rcenter.xy()) {
|
|
let offs = Vec2::new(
|
|
dynamic_rng.gen_range(-1.0..1.0),
|
|
dynamic_rng.gen_range(-1.0..1.0),
|
|
)
|
|
.try_normalized()
|
|
.unwrap_or_else(Vec2::unit_y)
|
|
* (TILE_SIZE as f32 / 2.0 - 4.0);
|
|
if !self.final_level {
|
|
supplement.add_entity(
|
|
EntityInfo::at((origin + stair_rcenter).map(|e| e as f32)
|
|
+ Vec3::from(offs)) .into_waypoint(),
|
|
);
|
|
}
|
|
}
|
|
*/
|
|
|
|
for x in area.min.x..area.max.x {
|
|
for y in area.min.y..area.max.y {
|
|
let tile_pos = Vec2::new(x, y).map(|e| e.div_euclid(TILE_SIZE)) - self.tile_offset;
|
|
let wpos2d = origin.xy() + Vec2::new(x, y);
|
|
if let Some(Tile::Room(room)) = self.tiles.get(tile_pos) {
|
|
let room = &self.rooms[*room];
|
|
|
|
let tile_wcenter = origin
|
|
+ Vec3::from(
|
|
Vec2::new(x, y)
|
|
.map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2),
|
|
);
|
|
|
|
match room.kind {
|
|
RoomKind::Fight => room.fill_fight_cell(
|
|
supplement,
|
|
dynamic_rng,
|
|
tile_wcenter,
|
|
wpos2d,
|
|
tile_pos,
|
|
),
|
|
RoomKind::Miniboss => room.fill_miniboss_cell(
|
|
supplement,
|
|
dynamic_rng,
|
|
tile_wcenter,
|
|
wpos2d,
|
|
tile_pos,
|
|
),
|
|
RoomKind::Boss => {
|
|
room.fill_boss_cell(supplement, tile_wcenter, wpos2d, tile_pos)
|
|
},
|
|
RoomKind::Peaceful | RoomKind::LavaPlatforming => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn total_depth(&self) -> i32 { self.solid_depth + self.hollow_depth }
|
|
|
|
// Find orientation of a position relative to another position
|
|
#[allow(clippy::collapsible_else_if)]
|
|
fn relative_ori(pos1: Vec2<i32>, pos2: Vec2<i32>) -> u8 {
|
|
if (pos1.x - pos2.x).abs() < (pos1.y - pos2.y).abs() {
|
|
if pos1.y > pos2.y { 4 } else { 8 }
|
|
} else {
|
|
if pos1.x > pos2.x { 2 } else { 6 }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn enemy_0(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(2..=4);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
// TODO: give enemies health skills?
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_health_scaling(dynamic_rng.gen_range(3..4));
|
|
match dynamic_rng.gen_range(0..=4) {
|
|
0 => entity.with_asset_expect("common.entity.dungeon.tier-0.bow"),
|
|
1 => entity.with_asset_expect("common.entity.dungeon.tier-0.staff"),
|
|
_ => entity.with_asset_expect("common.entity.dungeon.tier-0.spear"),
|
|
}
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn enemy_1(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(2..=4);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
// TODO: give enemies health skills?
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_health_scaling(dynamic_rng.gen_range(4..5));
|
|
match dynamic_rng.gen_range(0..=4) {
|
|
0 => entity.with_asset_expect("common.entity.dungeon.tier-1.bow"),
|
|
1 => entity.with_asset_expect("common.entity.dungeon.tier-1.staff"),
|
|
_ => entity.with_asset_expect("common.entity.dungeon.tier-1.spear"),
|
|
}
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn enemy_2(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(2..=4);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
// TODO: give enemies health skills?
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_health_scaling(dynamic_rng.gen_range(5..7));
|
|
match dynamic_rng.gen_range(0..=4) {
|
|
0 => entity.with_asset_expect("common.entity.dungeon.tier-2.bow"),
|
|
1 => entity.with_asset_expect("common.entity.dungeon.tier-2.staff"),
|
|
_ => entity.with_asset_expect("common.entity.dungeon.tier-2.spear"),
|
|
}
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn enemy_3(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(2..=4);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
// TODO: give enemies health skills?
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_health_scaling(dynamic_rng.gen_range(7..9));
|
|
match dynamic_rng.gen_range(0..=4) {
|
|
0 => entity.with_asset_expect("common.entity.dungeon.tier-3.bow"),
|
|
1 => entity.with_asset_expect("common.entity.dungeon.tier-3.staff"),
|
|
_ => entity.with_asset_expect("common.entity.dungeon.tier-3.spear"),
|
|
}
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn enemy_4(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(2..=4);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
// TODO: give enemies health skills?
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_health_scaling(dynamic_rng.gen_range(9..12));
|
|
match dynamic_rng.gen_range(0..=4) {
|
|
0 => entity.with_asset_expect("common.entity.dungeon.tier-4.bow"),
|
|
1 => entity.with_asset_expect("common.entity.dungeon.tier-4.staff"),
|
|
_ => entity.with_asset_expect("common.entity.dungeon.tier-4.spear"),
|
|
}
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn enemy_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(1..=3);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
// TODO: give enemies health skills?
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_health_scaling(dynamic_rng.gen_range(10..15));
|
|
match dynamic_rng.gen_range(0..=4) {
|
|
0 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlock"),
|
|
1 => entity.with_asset_expect("common.entity.dungeon.tier-5.warlord"),
|
|
_ => entity.with_asset_expect("common.entity.dungeon.tier-5.cultist"),
|
|
}
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn enemy_fallback(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let number = dynamic_rng.gen_range(2..=4);
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(number, || {
|
|
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32));
|
|
entity.with_asset_expect("common.entity.dungeon.fallback.enemy")
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn turret_3(pos: Vec3<f32>) -> EntityInfo {
|
|
EntityInfo::at(pos).with_asset_expect("common.entity.dungeon.tier-3.sentry")
|
|
}
|
|
|
|
fn turret_5(pos: Vec3<f32>) -> EntityInfo {
|
|
EntityInfo::at(pos).with_asset_expect("common.entity.dungeon.tier-5.turret")
|
|
}
|
|
|
|
fn boss_0(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-0.boss"),
|
|
]
|
|
}
|
|
|
|
fn boss_1(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-1.boss"),
|
|
]
|
|
}
|
|
|
|
fn boss_2(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-2.boss"),
|
|
]
|
|
}
|
|
fn boss_3(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(2, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-3.boss")
|
|
});
|
|
|
|
entities
|
|
}
|
|
|
|
fn boss_4(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-4.boss"),
|
|
]
|
|
}
|
|
|
|
fn boss_5(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-5.boss"),
|
|
]
|
|
}
|
|
|
|
fn boss_fallback(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.fallback.boss"),
|
|
]
|
|
}
|
|
|
|
fn mini_boss_0(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-0.miniboss"),
|
|
]
|
|
}
|
|
|
|
fn mini_boss_1(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(8, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-1.rat")
|
|
});
|
|
entities
|
|
}
|
|
|
|
fn mini_boss_2(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(6, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-2.hakulaq")
|
|
});
|
|
entities
|
|
}
|
|
|
|
fn mini_boss_3(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let mut entities = Vec::new();
|
|
entities.resize_with(3, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-3.bonerattler")
|
|
});
|
|
entities
|
|
}
|
|
|
|
fn mini_boss_4(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-4.miniboss"),
|
|
]
|
|
}
|
|
|
|
fn mini_boss_5(dynamic_rng: &mut impl Rng, tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
let mut entities = Vec::new();
|
|
match dynamic_rng.gen_range(0..=2) {
|
|
0 => {
|
|
entities.push(
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-5.beastmaster")
|
|
.with_health_scaling(dynamic_rng.gen_range(50..75)),
|
|
);
|
|
entities.resize_with(entities.len() + 2, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-5.hound")
|
|
});
|
|
},
|
|
1 => {
|
|
entities.resize_with(2, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-5.husk_brute")
|
|
});
|
|
},
|
|
_ => {
|
|
entities.resize_with(10, || {
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.tier-5.husk")
|
|
});
|
|
},
|
|
}
|
|
entities
|
|
}
|
|
|
|
fn mini_boss_fallback(tile_wcenter: Vec3<i32>) -> Vec<EntityInfo> {
|
|
vec![
|
|
EntityInfo::at(tile_wcenter.map(|e| e as f32))
|
|
.with_asset_expect("common.entity.dungeon.fallback.miniboss"),
|
|
]
|
|
}
|
|
|
|
pub fn tilegrid_nearest_wall(tiles: &Grid<Tile>, rpos: Vec2<i32>) -> Option<Vec2<i32>> {
|
|
let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE));
|
|
|
|
DIRS.iter()
|
|
.map(|dir| tile_pos + *dir)
|
|
.filter(|other_tile_pos| {
|
|
tiles
|
|
.get(*other_tile_pos)
|
|
.filter(|tile| tile.is_passable())
|
|
.is_none()
|
|
})
|
|
.map(|other_tile_pos| {
|
|
rpos.clamped(
|
|
other_tile_pos * TILE_SIZE,
|
|
(other_tile_pos + 1) * TILE_SIZE - 1,
|
|
)
|
|
})
|
|
.min_by_key(|nearest| rpos.distance_squared(*nearest))
|
|
}
|
|
|
|
pub fn spiral_staircase(
|
|
origin: Vec3<i32>,
|
|
radius: f32,
|
|
inner_radius: f32,
|
|
stretch: f32,
|
|
) -> Box<dyn Fn(Vec3<i32>) -> bool> {
|
|
Box::new(move |pos: Vec3<i32>| {
|
|
let pos = pos - origin;
|
|
if (pos.xy().magnitude_squared() as f32) < inner_radius.powi(2) {
|
|
true
|
|
} else if (pos.xy().magnitude_squared() as f32) < radius.powi(2) {
|
|
((pos.x as f32).atan2(pos.y as f32) / (f32::consts::PI * 2.0) * stretch + pos.z as f32)
|
|
.rem_euclid(stretch)
|
|
< 1.5
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn wall_staircase(
|
|
origin: Vec3<i32>,
|
|
radius: f32,
|
|
stretch: f32,
|
|
) -> Box<dyn Fn(Vec3<i32>) -> bool> {
|
|
Box::new(move |pos: Vec3<i32>| {
|
|
let pos = pos - origin;
|
|
if (pos.x.abs().max(pos.y.abs())) as f32 > 0.6 * radius {
|
|
((pos.x as f32).atan2(pos.y as f32) / (f32::consts::PI * 2.0) * stretch + pos.z as f32)
|
|
.rem_euclid(stretch)
|
|
< 1.0
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn inscribed_polystar(
|
|
origin: Vec2<i32>,
|
|
radius: f32,
|
|
sides: usize,
|
|
) -> Box<dyn Fn(Vec3<i32>) -> bool> {
|
|
Box::new(move |pos| {
|
|
use std::f32::consts::TAU;
|
|
let rpos: Vec2<f32> = pos.xy().as_() - origin.as_();
|
|
let is_border = rpos.magnitude_squared() > (radius - 2.0).powi(2);
|
|
let is_line = (0..sides).into_iter().any(|i| {
|
|
let f = |j: f32| {
|
|
let t = j * TAU / sides as f32;
|
|
radius * Vec2::new(t.cos(), t.sin())
|
|
};
|
|
let line = LineSegment2 {
|
|
start: f(i as f32),
|
|
end: f((i + 2) as f32),
|
|
};
|
|
line.distance_to_point(rpos) <= 1.0
|
|
});
|
|
is_border || is_line
|
|
})
|
|
}
|
|
|
|
pub fn make_wall_contours(
|
|
tiles: Arc<Grid<Tile>>,
|
|
floor_corner: Vec2<i32>,
|
|
floor_z: i32,
|
|
wall_thickness: f32,
|
|
tunnel_height: f32,
|
|
) -> Box<dyn Fn(Vec3<i32>) -> bool> {
|
|
Box::new(move |pos| {
|
|
let rpos = pos.xy() - floor_corner;
|
|
let dist_to_wall = tilegrid_nearest_wall(&tiles, rpos)
|
|
.map(|nearest| (nearest.distance_squared(rpos) as f32).sqrt())
|
|
.unwrap_or(TILE_SIZE as f32);
|
|
let tunnel_dist =
|
|
1.0 - (dist_to_wall - wall_thickness).max(0.0) / (TILE_SIZE as f32 - wall_thickness);
|
|
dist_to_wall < wall_thickness
|
|
|| ((pos.z - floor_z) as f32) >= tunnel_height * (1.0 - tunnel_dist.powi(4))
|
|
})
|
|
}
|
|
|
|
impl Floor {
|
|
fn render(&self, painter: &Painter, dungeon: &Dungeon, floor_z: i32) {
|
|
// Calculate an AABB and corner for the AABB that covers the current floor.
|
|
let floor_corner = dungeon.origin + TILE_SIZE * self.tile_offset;
|
|
let floor_aabb = Aabb {
|
|
min: floor_corner.with_z(floor_z),
|
|
max: (floor_corner + TILE_SIZE * self.tiles.size())
|
|
.with_z(floor_z + self.total_depth()),
|
|
};
|
|
let floor_prim = painter.prim(Primitive::Aabb(floor_aabb));
|
|
|
|
// This is copied from `src/layer/mod.rs`. It should be moved into
|
|
// a util file somewhere
|
|
let noisy_color = |color: Rgb<u8>, factor: u32| {
|
|
let nz = RandomField::new(0).get(Vec3::new(floor_corner.x, floor_corner.y, floor_z));
|
|
color.map(|e| {
|
|
(e as u32 + nz % (factor * 2))
|
|
.saturating_sub(factor)
|
|
.min(255) as u8
|
|
})
|
|
};
|
|
|
|
// Declare the various kinds of blocks that will be used as fills
|
|
let vacant = Block::air(SpriteKind::Empty);
|
|
// FIXME: Lava and stone color hardcoded here, it is available in colors.ron
|
|
// but that file is not accessed from site2 yet
|
|
let lava = Block::new(BlockKind::Lava, noisy_color(Rgb::new(184, 39, 0), 8));
|
|
let stone = Block::new(BlockKind::Rock, Rgb::new(150, 150, 175));
|
|
let stone_purple = Block::new(BlockKind::GlowingRock, Rgb::new(96, 0, 128));
|
|
|
|
// Sprites are randomly positioned and have random kinds, this primitive
|
|
// produces a box of dots that will later get truncated to just the
|
|
// floor, and the corresponding fill places the random kinds where the
|
|
// mask says to
|
|
let floor_sprite = painter.prim(Primitive::sampling(
|
|
floor_prim,
|
|
Box::new(|pos| RandomField::new(7331).chance(pos, 0.001)),
|
|
));
|
|
|
|
let floor_sprite_fill = Fill::Sampling(Arc::new(|pos| {
|
|
Some(Block::air(
|
|
match (RandomField::new(1337).get(pos) / 2) % 30 {
|
|
0 => SpriteKind::Apple,
|
|
1 => SpriteKind::VeloriteFrag,
|
|
2 => SpriteKind::Velorite,
|
|
3..=8 => SpriteKind::Mushroom,
|
|
9..=15 => SpriteKind::FireBowlGround,
|
|
_ => SpriteKind::ShortGrass,
|
|
},
|
|
))
|
|
}));
|
|
|
|
let wall_thickness = 3.0;
|
|
let tunnel_height = if self.final_level { 16.0 } else { 8.0 };
|
|
let pillar_thickness: i32 = 4;
|
|
|
|
// Several primitives and fills use the tile information for finding the nearest
|
|
// wall, a copy of the tilegrid for the floor is stored in an Arc to
|
|
// avoid making a copy for each primitive
|
|
let tiles = Arc::new(self.tiles.clone());
|
|
|
|
// The way the ceiling is curved around corners and near hallways is intricate
|
|
// enough that it's easiest to do with a sampling primitive, this gets
|
|
// masked per room so that it's more efficient to query
|
|
let wall_contours = painter.prim(Primitive::sampling(floor_prim, {
|
|
let tiles = Arc::clone(&tiles);
|
|
make_wall_contours(tiles, floor_corner, floor_z, wall_thickness, tunnel_height)
|
|
}));
|
|
|
|
// The surface 1 unit thicker than the walls is used to place the torches onto
|
|
let wall_contour_surface = painter.prim(Primitive::sampling(floor_prim, {
|
|
let tiles = Arc::clone(&tiles);
|
|
make_wall_contours(
|
|
tiles,
|
|
floor_corner,
|
|
floor_z,
|
|
wall_thickness + 1.0,
|
|
tunnel_height - 1.0,
|
|
)
|
|
}));
|
|
|
|
// The sconces use a sampling-based fill to orient them properly relative to the
|
|
// walls/staircases/pillars
|
|
let light_offset: i32 = 7;
|
|
let sconces_wall = Fill::Sampling(Arc::new(move |pos| {
|
|
let rpos = pos.xy() - floor_corner;
|
|
let nearest = tilegrid_nearest_wall(&tiles, rpos);
|
|
let ori = Floor::relative_ori(rpos, nearest.unwrap_or_default());
|
|
Block::air(SpriteKind::WallSconce).with_ori(ori)
|
|
}));
|
|
let sconces_inward = Fill::Sampling(Arc::new(move |pos| {
|
|
let rpos = pos.xy() - floor_corner;
|
|
let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE));
|
|
let tile_center = tile_pos * TILE_SIZE + TILE_SIZE / 2;
|
|
let ori = Floor::relative_ori(rpos, tile_center);
|
|
Block::air(SpriteKind::WallSconce).with_ori(ori)
|
|
}));
|
|
let sconces_outward = Fill::Sampling(Arc::new(move |pos| {
|
|
let rpos = pos.xy() - floor_corner;
|
|
let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE));
|
|
let tile_center = tile_pos * TILE_SIZE + TILE_SIZE / 2;
|
|
let ori = Floor::relative_ori(tile_center, rpos);
|
|
Block::air(SpriteKind::WallSconce).with_ori(ori)
|
|
}));
|
|
|
|
// The lighting mask is a grid of thin AABB planes with the same period as the
|
|
// tile grid, but offset by `lighting_offset`, used to space the torches
|
|
// on the walls/pillars/staircases
|
|
let lighting_mask = {
|
|
let mut lighting_mask_x = painter.prim(Primitive::Empty);
|
|
let floor_w = floor_aabb.max.x - floor_aabb.min.x;
|
|
for i in 0..floor_w / light_offset {
|
|
let j = floor_corner.x + i * TILE_SIZE + light_offset;
|
|
let plane = painter.prim(Primitive::Aabb(Aabb {
|
|
min: floor_aabb.min.with_x(j - 1),
|
|
max: floor_aabb.max.with_x(j),
|
|
}));
|
|
lighting_mask_x = painter.prim(Primitive::or(plane, lighting_mask_x));
|
|
}
|
|
let mut lighting_mask_y = painter.prim(Primitive::Empty);
|
|
let floor_h = floor_aabb.max.y - floor_aabb.min.y;
|
|
for i in 0..floor_h / light_offset {
|
|
let j = floor_corner.y + i * TILE_SIZE + light_offset;
|
|
let plane = painter.prim(Primitive::Aabb(Aabb {
|
|
min: floor_aabb.min.with_y(j - 1),
|
|
max: floor_aabb.max.with_y(j),
|
|
}));
|
|
lighting_mask_y = painter.prim(Primitive::or(plane, lighting_mask_y));
|
|
}
|
|
painter.prim(Primitive::xor(lighting_mask_x, lighting_mask_y))
|
|
};
|
|
|
|
// Declare collections of various disjoint primitives that need postprocessing
|
|
// after handling all the local information per-tile
|
|
let mut stairs_bb = Vec::new();
|
|
let mut stairs = Vec::new();
|
|
let mut pillars = Vec::new();
|
|
let mut boss_room_center = None;
|
|
let mut sprites = Vec::new();
|
|
|
|
// This loop processes the tile grid, carving out rooms and tunnels and
|
|
// collecting stair/pillar/sprite info to place afterwards
|
|
for (tile_pos, tile) in self.tiles.iter() {
|
|
let tile_corner = dungeon.origin + TILE_SIZE * (self.tile_offset + tile_pos);
|
|
let tile_aabr = Aabr {
|
|
min: tile_corner,
|
|
max: tile_corner + Vec2::broadcast(TILE_SIZE),
|
|
};
|
|
let tile_center = tile_corner + Vec2::broadcast(TILE_SIZE / 2);
|
|
let (mut height, room) = match tile {
|
|
Tile::UpStair(room, _) => (self.hollow_depth, Some(room)),
|
|
Tile::DownStair(room) => (self.hollow_depth, Some(room)),
|
|
Tile::Room(room) => (self.hollow_depth, Some(room)),
|
|
Tile::Tunnel => (tunnel_height as i32, None),
|
|
Tile::Solid => continue,
|
|
};
|
|
|
|
// Sprites are contained to the level above the floor, and not within walls
|
|
let sprite_layer = painter.prim(Primitive::Aabb(aabr_with_z(
|
|
tile_aabr,
|
|
floor_z..floor_z + 1,
|
|
)));
|
|
let sprite_layer = painter.prim(Primitive::diff(sprite_layer, wall_contours));
|
|
|
|
// Lights are 2 units above the floor, and aligned with the `lighting_mask` grid
|
|
let lighting_plane = painter.prim(Primitive::Aabb(aabr_with_z(
|
|
tile_aabr,
|
|
floor_z + 1..floor_z + 2,
|
|
)));
|
|
let lighting_plane = painter.prim(Primitive::and(lighting_plane, lighting_mask));
|
|
|
|
let mut chests = None;
|
|
|
|
if let Some(room) = room.map(|i| self.rooms.get(*i)) {
|
|
height = height.min(room.height);
|
|
if let Tile::UpStair(_, kind) = tile {
|
|
// Construct the staircase that connects this tile to the matching DownStair
|
|
// tile on the floor above (or to the surface if this is the top floor), and a
|
|
// hollow bounding box to place air in
|
|
let center = tile_center.with_z(floor_z);
|
|
let radius = TILE_SIZE as f32 / 2.0;
|
|
let aabb = aabr_with_z(tile_aabr, floor_z..floor_z + self.total_depth());
|
|
let bb = painter.prim(match kind {
|
|
StairsKind::Spiral => Primitive::Cylinder(aabb),
|
|
StairsKind::WallSpiral => Primitive::Aabb(aabb),
|
|
});
|
|
let stair = painter.prim(Primitive::sampling(bb, match kind {
|
|
StairsKind::Spiral => spiral_staircase(center, radius, 0.5, 9.0),
|
|
StairsKind::WallSpiral => wall_staircase(center, radius, 27.0),
|
|
}));
|
|
// Construct the lights that go inside the staircase, starting above the
|
|
// ceiling to avoid placing them floating in mid-air
|
|
let mut lights = painter.prim(Primitive::Empty);
|
|
for i in height..self.total_depth() {
|
|
if i % 9 == 0 {
|
|
let mut light = painter.prim(Primitive::Aabb(Aabb {
|
|
min: aabb.min.with_z(floor_z + i),
|
|
max: aabb.max.with_z(floor_z + i + 1),
|
|
}));
|
|
let inner = painter.prim(Primitive::Aabb(Aabb {
|
|
min: (aabb.min + Vec3::new(1, 1, 0)).with_z(floor_z + i),
|
|
max: (aabb.max - Vec3::new(1, 1, 0)).with_z(floor_z + i + 1),
|
|
}));
|
|
|
|
light = painter.prim(Primitive::diff(light, inner));
|
|
lights = painter.prim(Primitive::or(light, lights));
|
|
}
|
|
}
|
|
lights = painter.prim(Primitive::and(lights, lighting_mask));
|
|
stairs_bb.push(bb);
|
|
stairs.push((stair, lights));
|
|
}
|
|
if matches!(tile, Tile::Room(_) | Tile::DownStair(_)) {
|
|
let seed = room.seed;
|
|
let loot_density = room.loot_density;
|
|
let difficulty = room.difficulty;
|
|
// Place chests with a random distribution based on the
|
|
// room's loot density in valid sprite locations,
|
|
// filled based on the room's difficulty
|
|
let chest_sprite = painter.prim(Primitive::sampling(
|
|
sprite_layer,
|
|
Box::new(move |pos| RandomField::new(seed).chance(pos, loot_density * 0.5)),
|
|
));
|
|
let chest_sprite_fill = Fill::Block(Block::air(match difficulty {
|
|
0 => SpriteKind::DungeonChest0,
|
|
1 => SpriteKind::DungeonChest1,
|
|
2 => SpriteKind::DungeonChest2,
|
|
3 => SpriteKind::DungeonChest3,
|
|
4 => SpriteKind::DungeonChest4,
|
|
5 => SpriteKind::DungeonChest5,
|
|
_ => SpriteKind::Chest,
|
|
}));
|
|
chests = Some((chest_sprite, chest_sprite_fill));
|
|
|
|
// If a room has pits, place them
|
|
if room.pits.is_some() {
|
|
// Make an air pit
|
|
let tile_pit = painter.prim(Primitive::Aabb(aabr_with_z(
|
|
tile_aabr,
|
|
floor_z - 7..floor_z,
|
|
)));
|
|
let tile_pit = painter.prim(Primitive::diff(tile_pit, wall_contours));
|
|
painter.fill(tile_pit, Fill::Block(vacant));
|
|
|
|
// Fill with lava
|
|
let tile_lava = painter.prim(Primitive::Aabb(aabr_with_z(
|
|
tile_aabr,
|
|
floor_z - 7..floor_z - 5,
|
|
)));
|
|
let tile_lava = painter.prim(Primitive::diff(tile_lava, wall_contours));
|
|
//pits.push(tile_pit);
|
|
//pits.push(tile_lava);
|
|
painter.fill(tile_lava, Fill::Block(lava));
|
|
}
|
|
if room
|
|
.pits
|
|
.map(|pit_space| {
|
|
tile_pos.map(|e| e.rem_euclid(pit_space) == 0).reduce_and()
|
|
})
|
|
.unwrap_or(false)
|
|
{
|
|
let platform = painter.prim(Primitive::Aabb(Aabb {
|
|
min: (tile_center - Vec2::broadcast(pillar_thickness - 1))
|
|
.with_z(floor_z - 7),
|
|
max: (tile_center + Vec2::broadcast(pillar_thickness)).with_z(floor_z),
|
|
}));
|
|
painter.fill(platform, Fill::Block(stone));
|
|
}
|
|
|
|
// If a room has pillars, the current tile aligns with the pillar spacing, and
|
|
// we're not too close to a wall (i.e. the adjacent tiles are rooms and not
|
|
// hallways/solid), place a pillar
|
|
if room
|
|
.pillars
|
|
.map(|pillar_space| {
|
|
tile_pos
|
|
.map(|e| e.rem_euclid(pillar_space) == 0)
|
|
.reduce_and()
|
|
})
|
|
.unwrap_or(false)
|
|
&& DIRS
|
|
.iter()
|
|
.map(|dir| tile_pos + *dir)
|
|
.all(|other_tile_pos| {
|
|
matches!(self.tiles.get(other_tile_pos), Some(Tile::Room(_)))
|
|
})
|
|
{
|
|
let mut pillar = painter.prim(Primitive::Cylinder(Aabb {
|
|
min: (tile_center - Vec2::broadcast(pillar_thickness - 1))
|
|
.with_z(floor_z),
|
|
max: (tile_center + Vec2::broadcast(pillar_thickness))
|
|
.with_z(floor_z + height),
|
|
}));
|
|
let base = painter.prim(Primitive::Cylinder(Aabb {
|
|
min: (tile_center - Vec2::broadcast(1 + pillar_thickness - 1))
|
|
.with_z(floor_z),
|
|
max: (tile_center + Vec2::broadcast(1 + pillar_thickness))
|
|
.with_z(floor_z + 1),
|
|
}));
|
|
|
|
let scale = (pillar_thickness + 2) as f32 / pillar_thickness as f32;
|
|
let mut lights = painter
|
|
.prim(Primitive::scale(pillar, Vec2::broadcast(scale).with_z(1.0)));
|
|
lights = painter.prim(Primitive::and(lighting_plane, lights));
|
|
// Only add the base (and shift the lights up)
|
|
// for boss-rooms pillars
|
|
if room.kind == RoomKind::Boss {
|
|
lights = painter.prim(Primitive::translate(lights, 3 * Vec3::unit_z()));
|
|
pillar = painter.prim(Primitive::or(pillar, base));
|
|
}
|
|
pillars.push((tile_center, pillar, lights));
|
|
}
|
|
}
|
|
|
|
// Keep track of the boss room to be able to add decorations later
|
|
if room.kind == RoomKind::Boss {
|
|
boss_room_center =
|
|
Some(floor_corner + TILE_SIZE * room.area.center() + TILE_SIZE / 2);
|
|
}
|
|
}
|
|
|
|
// Carve out the room's air inside the walls
|
|
let tile_air = painter.prim(Primitive::Aabb(aabr_with_z(
|
|
tile_aabr,
|
|
floor_z..floor_z + height,
|
|
)));
|
|
let tile_air = painter.prim(Primitive::diff(tile_air, wall_contours));
|
|
painter.fill(tile_air, Fill::Block(vacant));
|
|
|
|
// Place torches on the walls with the aforementioned spacing
|
|
let sconces_layer = painter.prim(Primitive::and(tile_air, lighting_plane));
|
|
let sconces_layer = painter.prim(Primitive::and(sconces_layer, wall_contour_surface));
|
|
painter.fill(sconces_layer, sconces_wall.clone());
|
|
|
|
// Defer chest/floor sprite placement
|
|
if let Some((chest_sprite, chest_sprite_fill)) = chests {
|
|
let chest_sprite = painter.prim(Primitive::diff(chest_sprite, wall_contours));
|
|
sprites.push((chest_sprite, chest_sprite_fill));
|
|
}
|
|
|
|
let floor_sprite = painter.prim(Primitive::and(sprite_layer, floor_sprite));
|
|
sprites.push((floor_sprite, floor_sprite_fill.clone()));
|
|
}
|
|
|
|
// Place a glowing purple septagonal star inscribed in a circle in the boss room
|
|
if let Some(boss_room_center) = boss_room_center {
|
|
let magic_circle_bb = painter.prim(Primitive::Cylinder(Aabb {
|
|
min: (boss_room_center - 3 * Vec2::broadcast(TILE_SIZE) / 2).with_z(floor_z - 1),
|
|
max: (boss_room_center + 3 * Vec2::broadcast(TILE_SIZE) / 2).with_z(floor_z),
|
|
}));
|
|
let magic_circle = painter.prim(Primitive::sampling(
|
|
magic_circle_bb,
|
|
inscribed_polystar(boss_room_center, 1.4 * TILE_SIZE as f32, 7),
|
|
));
|
|
painter.fill(magic_circle, Fill::Block(stone_purple));
|
|
}
|
|
|
|
// Place pillars and pillar lights facing the pillars
|
|
for (pos, pillar, lights) in pillars.iter() {
|
|
// Avoid placing pillars that would cover the septagonal star
|
|
if let Some(boss_room_center) = boss_room_center {
|
|
if pos.distance_squared(boss_room_center) < (3 * TILE_SIZE / 2).pow(2) {
|
|
continue;
|
|
}
|
|
}
|
|
painter.fill(*lights, sconces_inward.clone());
|
|
painter.fill(*pillar, Fill::Block(stone));
|
|
}
|
|
// Carve out space for the stairs
|
|
for stair_bb in stairs_bb.iter() {
|
|
painter.fill(*stair_bb, Fill::Block(vacant));
|
|
// Prevent sprites from floating above the stairs
|
|
let stair_bb_up = painter.prim(Primitive::translate(*stair_bb, Vec3::unit_z()));
|
|
for (sprite, _) in sprites.iter_mut() {
|
|
*sprite = painter.prim(Primitive::diff(*sprite, stair_bb_up));
|
|
}
|
|
}
|
|
// Place the stairs themselves, and lights within the stairwells
|
|
for (stair, lights) in stairs.iter() {
|
|
painter.fill(*lights, sconces_outward.clone());
|
|
painter.fill(*stair, Fill::Block(stone));
|
|
}
|
|
// Place the sprites
|
|
for (sprite, sprite_fill) in sprites.into_iter() {
|
|
painter.fill(sprite, sprite_fill);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SiteStructure for Dungeon {
|
|
fn render(&self, _site: &site2::Site, land: &Land, painter: &Painter) {
|
|
let origin = (self.origin + Vec2::broadcast(TILE_SIZE / 2)).with_z(self.alt + ALT_OFFSET);
|
|
|
|
lazy_static! {
|
|
pub static ref JUNGLE: AssetHandle<StructuresGroup> =
|
|
Structure::load_group("dungeon_entrances.jungle");
|
|
pub static ref GRASSLAND: AssetHandle<StructuresGroup> =
|
|
Structure::load_group("dungeon_entrances.grassland");
|
|
pub static ref DESERT: AssetHandle<StructuresGroup> =
|
|
Structure::load_group("dungeon_entrances.desert");
|
|
}
|
|
|
|
let biome = land
|
|
.get_chunk_at(self.origin)
|
|
.map_or(BiomeKind::Void, |c| c.get_biome());
|
|
let entrances = match biome {
|
|
BiomeKind::Jungle => *JUNGLE,
|
|
BiomeKind::Desert => *DESERT,
|
|
_ => *GRASSLAND,
|
|
};
|
|
let entrances = entrances.read();
|
|
let entrance = entrances[self.seed as usize % entrances.len()].clone();
|
|
|
|
let entrance_prim = painter.prim(Primitive::Prefab(Box::new(entrance.clone())));
|
|
let entrance_prim = painter.prim(Primitive::translate(entrance_prim, origin));
|
|
painter.fill(
|
|
entrance_prim,
|
|
Fill::Prefab(Box::new(entrance), origin, self.seed),
|
|
);
|
|
|
|
let mut z = self.alt + ALT_OFFSET;
|
|
for floor in &self.floors {
|
|
z -= floor.total_depth();
|
|
|
|
floor.render(painter, self, z);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_creating_bosses() {
|
|
let tile_wcenter = Vec3::new(0, 0, 0);
|
|
boss_0(tile_wcenter);
|
|
boss_1(tile_wcenter);
|
|
boss_2(tile_wcenter);
|
|
boss_3(tile_wcenter);
|
|
boss_4(tile_wcenter);
|
|
boss_5(tile_wcenter);
|
|
boss_fallback(tile_wcenter);
|
|
}
|
|
|
|
#[test]
|
|
// FIXME: Uses random, test may be not great
|
|
fn test_creating_enemies() {
|
|
let mut dynamic_rng = rand::thread_rng();
|
|
let random_position = Vec3::new(0, 0, 0);
|
|
enemy_0(&mut dynamic_rng, random_position);
|
|
enemy_1(&mut dynamic_rng, random_position);
|
|
enemy_2(&mut dynamic_rng, random_position);
|
|
enemy_3(&mut dynamic_rng, random_position);
|
|
enemy_4(&mut dynamic_rng, random_position);
|
|
enemy_5(&mut dynamic_rng, random_position);
|
|
enemy_fallback(&mut dynamic_rng, random_position);
|
|
}
|
|
|
|
#[test]
|
|
// FIXME: Uses random, test may be not great
|
|
fn test_creating_minibosses() {
|
|
let mut dynamic_rng = rand::thread_rng();
|
|
let tile_wcenter = Vec3::new(0, 0, 0);
|
|
mini_boss_0(tile_wcenter);
|
|
mini_boss_1(tile_wcenter);
|
|
mini_boss_2(tile_wcenter);
|
|
mini_boss_3(tile_wcenter);
|
|
mini_boss_4(tile_wcenter);
|
|
mini_boss_5(&mut dynamic_rng, tile_wcenter);
|
|
mini_boss_fallback(tile_wcenter);
|
|
}
|
|
|
|
#[test]
|
|
fn test_creating_turrets() {
|
|
let pos = Vec3::new(0.0, 0.0, 0.0);
|
|
turret_3(pos);
|
|
turret_5(pos);
|
|
}
|
|
}
|