Use new dungeons in dungeon_voxel_export, remove old dungeon sampling function, and add more of world/examples to CI.

This commit is contained in:
Avi Weinstock 2021-06-27 15:35:43 -04:00
parent f6782e21b8
commit 957ba5d218
9 changed files with 176 additions and 376 deletions

View File

@ -6,7 +6,7 @@ code-quality:
script:
- ln -s /dockercache/target target
- rm -r target/debug/incremental/* || echo "all good" # TMP FIX FOR 2021-03-22-nightly
- cargo clippy --all-targets --locked --features="bin_csv,bin_graphviz,bin_bot,asset_tweak" -- -D warnings
- cargo clippy --all-targets --locked --features="bin_compression,bin_csv,bin_graphviz,bin_bot,asset_tweak" -- -D warnings
# Ensure that the veloren-voxygen default-publish feature builds as it excludes some default features
- cargo clippy -p veloren-voxygen --locked --no-default-features --features="default-publish" -- -D warnings
- cargo fmt --all -- --check

View File

@ -1,3 +1,4 @@
#![allow(clippy::type_complexity)]
use common::{
spiral::Spiral2d,
terrain::{chonk::Chonk, Block, BlockKind, SpriteKind},
@ -15,6 +16,7 @@ use common_net::msg::compression::{
use hashbrown::HashMap;
use image::ImageBuffer;
use num_traits::cast::FromPrimitive;
use rayon::ThreadPoolBuilder;
use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
@ -56,8 +58,7 @@ fn do_deflate_rle(data: &[u8]) -> Vec<u8> {
let mut encoder = DeflateEncoder::new(Vec::new(), CompressionOptions::rle());
encoder.write_all(data).expect("Write error!");
let compressed_data = encoder.finish().expect("Failed to finish compression!");
compressed_data
encoder.finish().expect("Failed to finish compression!")
}
// Separate function so that it shows up differently on the flamegraph
@ -66,8 +67,7 @@ fn do_deflate_flate2_zero(data: &[u8]) -> Vec<u8> {
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(0));
encoder.write_all(data).expect("Write error!");
let compressed_data = encoder.finish().expect("Failed to finish compression!");
compressed_data
encoder.finish().expect("Failed to finish compression!")
}
fn do_deflate_flate2<const LEVEL: u32>(data: &[u8]) -> Vec<u8> {
@ -75,8 +75,7 @@ fn do_deflate_flate2<const LEVEL: u32>(data: &[u8]) -> Vec<u8> {
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::new(LEVEL));
encoder.write_all(data).expect("Write error!");
let compressed_data = encoder.finish().expect("Failed to finish compression!");
compressed_data
encoder.finish().expect("Failed to finish compression!")
}
fn chonk_to_dyna<V: Clone, S: RectVolSize, M: Clone, A: Access>(
@ -489,6 +488,7 @@ impl VoxelImageEncoding for MixedEncodingDenseSprites {
}
}
#[allow(clippy::many_single_char_names)]
fn histogram_to_dictionary(histogram: &HashMap<Vec<u8>, usize>, dictionary: &mut Vec<u8>) {
let mut tmp: Vec<(Vec<u8>, usize)> = histogram.iter().map(|(k, v)| (k.clone(), *v)).collect();
tmp.sort_by_key(|(_, count)| *count);
@ -507,13 +507,17 @@ fn histogram_to_dictionary(histogram: &HashMap<Vec<u8>, usize>, dictionary: &mut
}
fn main() {
let pool = ThreadPoolBuilder::new().build().unwrap();
common_frontend::init_stdout(None);
println!("Loading world");
let (world, index) = World::generate(59686, WorldOpts {
seed_elements: true,
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
..WorldOpts::default()
});
let (world, index) = World::generate(
59686,
WorldOpts {
seed_elements: true,
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
},
&pool,
);
println!("Loaded world");
const HISTOGRAMS: bool = false;
let mut histogram: HashMap<Vec<u8>, usize> = HashMap::new();
@ -523,45 +527,45 @@ fn main() {
let k = 32;
let sz = world.sim().get_size();
let mut sites = Vec::new();
sites.push(("center", sz / 2));
sites.push((
"dungeon",
world
.civs()
.sites()
.find(|s| s.is_dungeon())
.map(|s| s.center.as_())
.unwrap(),
));
sites.push((
"town",
world
.civs()
.sites()
.find(|s| s.is_settlement())
.map(|s| s.center.as_())
.unwrap(),
));
sites.push((
"castle",
world
.civs()
.sites()
.find(|s| s.is_castle())
.map(|s| s.center.as_())
.unwrap(),
));
sites.push((
"tree",
world
.civs()
.sites()
.find(|s| matches!(s.kind, SiteKind::Tree))
.map(|s| s.center.as_())
.unwrap(),
));
let sites = vec![
("center", sz / 2),
(
"dungeon",
world
.civs()
.sites()
.find(|s| s.is_dungeon())
.map(|s| s.center.as_())
.unwrap(),
),
(
"town",
world
.civs()
.sites()
.find(|s| s.is_settlement())
.map(|s| s.center.as_())
.unwrap(),
),
(
"castle",
world
.civs()
.sites()
.find(|s| s.is_castle())
.map(|s| s.center.as_())
.unwrap(),
),
(
"tree",
world
.civs()
.sites()
.find(|s| matches!(s.kind, SiteKind::Tree))
.map(|s| s.center.as_())
.unwrap(),
),
];
const SKIP_DEFLATE_2_5: bool = false;
const SKIP_DYNA: bool = true;
@ -600,6 +604,7 @@ fn main() {
let lz4chonk_pre = Instant::now();
let lz4_chonk = lz4_with_dictionary(&bincode::serialize(&chunk).unwrap(), &[]);
let lz4chonk_post = Instant::now();
#[allow(clippy::reversed_empty_ranges)]
for _ in 0..ITERS {
let _deflate0_chonk =
do_deflate_flate2_zero(&bincode::serialize(&chunk).unwrap());
@ -1024,7 +1029,7 @@ fn main() {
for (name, value) in totals.iter() {
println!("Average {}: {}", name, *value / count as f32);
}
println!("");
println!();
for (name, time) in total_timings.iter() {
println!("Average {} nanos: {:02}", name, *time / count as f32);
}

View File

@ -6,28 +6,65 @@ use std::{
type Result = std::io::Result<()>;
use common::{
terrain::{Block, BlockKind},
terrain::{Block, BlockKind, SpriteKind},
vol::{BaseVol, ReadVol, RectSizedVol, WriteVol},
};
use rayon::ThreadPoolBuilder;
use vek::{Vec2, Vec3};
use veloren_world::{index::Index, IndexOwned, Land};
use veloren_world::{
sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP},
site2::{plot::PlotKind, Structure},
CanvasInfo, Land, World,
};
/// This exports a dungeon (structure only, no entities or sprites) to a
/// MagicaVoxel .vox file
fn main() -> Result {
common_frontend::init_stdout(None);
let pool = ThreadPoolBuilder::new().build().unwrap();
println!("Loading world");
let (world, index) = World::generate(
59686,
WorldOpts {
seed_elements: true,
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
},
&pool,
);
println!("Loaded world");
let export_path = "dungeon.vox";
let seed = 0;
println!("Saving into {}", export_path);
let mut volume = ExportVol::new();
let index = IndexOwned::new(Index::new(seed));
let dungeon = veloren_world::site2::plot::Dungeon::generate(
volume.size_xy().map(|p| p as i32 / 2),
&Land::empty(),
&mut rand::thread_rng(),
);
dungeon.apply_to(index.as_index_ref(), Vec2::new(0, 0), |_| None, &mut volume);
let wpos = volume.size_xy().map(|p| p as i32 / 2);
let site =
veloren_world::site2::Site::generate_dungeon(&Land::empty(), &mut rand::thread_rng(), wpos);
CanvasInfo::with_mock_canvas_info(index.as_index_ref(), world.sim(), |canvas| {
for plot in site.plots() {
if let PlotKind::Dungeon(dungeon) = plot.kind() {
let (prim_tree, fills) = dungeon.render_collect(&site);
for (prim, fill) in fills {
let aabb = fill.get_bounds(&prim_tree, prim);
for x in aabb.min.x..aabb.max.x {
for y in aabb.min.y..aabb.max.y {
for z in aabb.min.z..aabb.max.z {
let pos = Vec3::new(x, y, z);
if let Some(block) = fill.sample_at(&prim_tree, prim, pos, &canvas)
{
let _ = volume.set(pos, block);
}
}
}
}
}
}
}
});
volume.write(&mut File::create(export_path)?)
}
@ -157,7 +194,9 @@ impl ExportVol {
write_chunk(file, "RGBA", &|file| {
file.write_all(&[220, 220, 255, 0])?; // Air
file.write_all(&[100, 100, 100, 0])?; // Rock
file.write_all(&[0; 4 * (256 - 2)])
file.write_all(&[255, 0, 0, 0])?; // Sprite
file.write_all(&[255, 0, 255, 0])?; // GlowingRock
file.write_all(&[0; 4 * (256 - 4)])
})?;
let chunks_end = file.stream_position()?;
@ -200,9 +239,16 @@ impl WriteVol for ExportVol {
.entry(model_pos)
.or_default()
.extend_from_slice(&[rel_pos.x, rel_pos.y, rel_pos.z, match vox.kind() {
BlockKind::Air => 1,
BlockKind::Air => {
if !matches!(vox.get_sprite(), Some(SpriteKind::Empty)) {
3
} else {
1
}
},
BlockKind::Rock => 2,
_ => 3,
BlockKind::GlowingRock => 4,
_ => 5,
}]);
Ok(vox)
}

View File

@ -2,6 +2,7 @@ use image::{
codecs::png::{CompressionType, FilterType, PngEncoder},
ImageBuffer,
};
use rayon::ThreadPoolBuilder;
use std::{fs::File, io::Write};
use vek::*;
use veloren_world::{
@ -116,12 +117,16 @@ fn image_with_autorange<F: Fn(f32, f32, f32) -> [u8; 3], G: FnMut(u32, u32) -> f
fn main() {
common_frontend::init_stdout(None);
let pool = ThreadPoolBuilder::new().build().unwrap();
println!("Loading world");
let (world, _index) = World::generate(59686, WorldOpts {
seed_elements: true,
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
..WorldOpts::default()
});
let (world, _index) = World::generate(
59686,
WorldOpts {
seed_elements: true,
world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()),
},
&pool,
);
println!("Loaded world");
let land = Land::from_sim(world.sim());

View File

@ -49,6 +49,44 @@ impl<'a> CanvasInfo<'a> {
pub fn chunks(&self) -> &'a WorldSim { self.chunks }
pub fn land(&self) -> Land<'_> { Land::from_sim(self.chunks) }
pub fn with_mock_canvas_info<A, F: for<'b> FnOnce(&CanvasInfo<'b>) -> A>(
index: IndexRef<'a>,
sim: &'a WorldSim,
f: F,
) -> A {
let zcache_grid = Grid::populate_from(Vec2::broadcast(1), |_| None);
let sim_chunk = SimChunk {
chaos: 0.0,
alt: 0.0,
basement: 0.0,
water_alt: 0.0,
downhill: None,
flux: 0.0,
temp: 0.0,
humidity: 0.0,
rockiness: 0.0,
tree_density: 0.0,
forest_kind: crate::all::ForestKind::Palm,
spawn_rate: 0.0,
river: Default::default(),
surface_veg: 0.0,
sites: Vec::new(),
place: None,
path: Default::default(),
cave: Default::default(),
cliff_height: 0.0,
contains_waypoint: false,
};
f(&CanvasInfo {
wpos: Vec2::broadcast(0),
column_grid: &zcache_grid,
column_grid_border: 0,
chunks: &sim,
index,
chunk: &sim_chunk,
})
}
}
pub struct Canvas<'a> {

View File

@ -157,7 +157,7 @@ impl Fill {
tree: &Store<Primitive>,
prim: Id<Primitive>,
pos: Vec3<i32>,
canvas: &Canvas,
canvas_info: &crate::CanvasInfo,
) -> Option<Block> {
if self.contains_at(tree, prim, pos) {
match self {
@ -169,10 +169,9 @@ impl Fill {
% *range as u32) as u8,
)),
Fill::Prefab(p, tr, seed) => p.get(pos - tr).ok().and_then(|sb| {
let info = canvas.info;
let col_sample = info.col(info.wpos)?;
let col_sample = canvas_info.col(canvas_info.wpos)?;
block_from_structure(
canvas.index,
canvas_info.index,
*sb,
pos - tr,
p.get_bounds().center().xy(),

View File

@ -2,10 +2,10 @@ mod gen;
pub mod plot;
mod tile;
use self::{
use self::tile::{HazardKind, KeepKind, Ori, RoofKind, Tile, TileGrid, TileKind, TILE_SIZE};
pub use self::{
gen::{aabr_with_z, Fill, Primitive, Structure},
plot::{Plot, PlotKind},
tile::{HazardKind, KeepKind, Ori, RoofKind, Tile, TileGrid, TileKind, TILE_SIZE},
};
use crate::{
site::SpawnRules,
@ -772,7 +772,8 @@ impl Site {
for z in aabb.min.z..aabb.max.z {
let pos = Vec3::new(x, y, z);
if let Some(block) = fill.sample_at(&prim_tree, prim, pos, &canvas) {
if let Some(block) = fill.sample_at(&prim_tree, prim, pos, &canvas.info)
{
canvas.set(pos, block);
}
}

View File

@ -24,6 +24,8 @@ impl Plot {
b.expanded_to_contain_point(*t)
})
}
pub fn kind(&self) -> &PlotKind { &self.kind }
}
pub enum PlotKind {

View File

@ -1,11 +1,9 @@
use super::SpawnRules;
use crate::{
block::block_from_structure,
column::ColumnSample,
site::{namegen::NameGen, BlockMask},
site::namegen::NameGen,
site2::{self, aabr_with_z, Fill, Primitive, Structure as SiteStructure},
util::{attempt, Grid, RandomField, Sampler, CARDINALS, DIRS},
IndexRef, Land,
Land,
};
use common::{
@ -15,7 +13,7 @@ use common::{
generation::{ChunkSupplement, EntityInfo},
store::{Id, Store},
terrain::{Block, BlockKind, SpriteKind, Structure, StructuresGroup, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, WriteVol},
vol::RectVolSize,
};
use core::{f32, hash::BuildHasherDefault};
use fxhash::FxHasher64;
@ -128,79 +126,6 @@ impl Dungeon {
pub fn difficulty(&self) -> u32 { self.difficulty }
pub fn apply_to<'a>(
&'a self,
index: IndexRef,
wpos2d: Vec2<i32>,
mut get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
) {
lazy_static! {
pub static ref ENTRANCES: AssetHandle<StructuresGroup> =
Structure::load_group("dungeon_entrances");
}
let entrances = ENTRANCES.read();
let entrance = &entrances[self.seed as usize % entrances.len()];
for y in 0..vol.size_xy().y as i32 {
for x in 0..vol.size_xy().x as i32 {
let offs = Vec2::new(x, y);
let wpos2d = wpos2d + offs;
let rpos = wpos2d - self.origin;
// Apply the dungeon entrance
if let Some(col_sample) = get_column(offs) {
for z in entrance.get_bounds().min.z..entrance.get_bounds().max.z {
let wpos = Vec3::new(offs.x, offs.y, self.alt + z + ALT_OFFSET);
let spos = Vec3::new(rpos.x - TILE_SIZE / 2, rpos.y - TILE_SIZE / 2, z);
if let Some(block) = entrance
.get(spos)
.ok()
.copied()
.map(|sb| {
block_from_structure(
index,
sb,
spos,
self.origin,
self.seed,
col_sample,
// TODO: Take environment into account.
Block::air,
)
})
.unwrap_or(None)
{
let _ = vol.set(wpos, block);
}
}
};
// Apply the dungeon internals
let mut z = self.alt + ALT_OFFSET;
for floor in &self.floors {
z -= floor.total_depth();
let mut sampler = floor.col_sampler(
index,
rpos,
z,
// TODO: Take environment into account.
Block::air,
);
for rz in 0..floor.total_depth() {
if let Some(block) = sampler(rz).finish() {
let _ = vol.set(Vec3::new(offs.x, offs.y, z + rz), block);
}
}
}
}
}
}
#[allow(clippy::or_fun_call)] // TODO: Pending review in #587
pub fn apply_supplement<'a>(
&'a self,
@ -695,10 +620,6 @@ impl Floor {
fn total_depth(&self) -> i32 { self.solid_depth + self.hollow_depth }
fn nearest_wall(&self, rpos: Vec2<i32>) -> Option<Vec2<i32>> {
tilegrid_nearest_wall(&self.tiles, rpos)
}
// Find orientation of a position relative to another position
#[allow(clippy::collapsible_else_if)]
fn relative_ori(pos1: Vec2<i32>, pos2: Vec2<i32>) -> u8 {
@ -708,223 +629,6 @@ impl Floor {
if pos1.x > pos2.x { 2 } else { 6 }
}
}
#[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587
fn col_sampler<'a>(
&'a self,
index: IndexRef<'a>,
pos: Vec2<i32>,
_floor_z: i32,
mut with_sprite: impl FnMut(SpriteKind) -> Block,
) -> impl FnMut(i32) -> BlockMask + 'a {
let rpos = pos - self.tile_offset * TILE_SIZE;
let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE));
let tile_center = tile_pos * TILE_SIZE + TILE_SIZE / 2;
let rtile_pos = rpos - tile_center;
let colors = &index.colors.site.dungeon;
let vacant = BlockMask::new(with_sprite(SpriteKind::Empty), 1);
let stone = BlockMask::new(Block::new(BlockKind::Rock, colors.stone.into()), 5);
let make_spiral_staircase =
move |pos: Vec3<i32>, radius: f32, inner_radius: f32, stretch: f32| {
if (pos.xy().magnitude_squared() as f32) < inner_radius.powi(2) {
stone
} else if (pos.xy().magnitude_squared() as f32) < radius.powi(2) {
if ((pos.x as f32).atan2(pos.y as f32) / (f32::consts::PI * 2.0) * stretch
+ pos.z as f32)
.rem_euclid(stretch)
< 1.5
{
stone
} else {
vacant
}
} else {
BlockMask::nothing()
}
};
let make_wall_staircase =
move |pos: Vec3<i32>, radius: f32, stretch: f32, height_limit: i32| {
if (pos.x.abs().max(pos.y.abs())) as f32 > 0.6 * radius && pos.z <= height_limit {
if ((pos.x as f32).atan2(pos.y as f32) / (f32::consts::PI * 2.0) * stretch
+ pos.z as f32)
.rem_euclid(stretch)
< 1.0
{
stone
} else {
vacant
}
} else {
vacant
}
};
let make_staircase = move |kind: &StairsKind,
pos: Vec3<i32>,
radius: f32,
inner_radius: f32,
stretch: f32,
height_limit: i32| {
match kind {
StairsKind::Spiral => make_spiral_staircase(pos, radius, inner_radius, stretch),
StairsKind::WallSpiral => {
make_wall_staircase(pos, radius, stretch * 3.0, height_limit)
},
}
};
let wall_thickness = 3.0;
let dist_to_wall = self
.nearest_wall(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);
let floor_sprite = if RandomField::new(7331).chance(Vec3::from(pos), 0.001) {
BlockMask::new(
with_sprite(
match (RandomField::new(1337).get(Vec3::from(pos)) / 2) % 30 {
0 => SpriteKind::Apple,
1 => SpriteKind::VeloriteFrag,
2 => SpriteKind::Velorite,
3..=8 => SpriteKind::Mushroom,
9..=15 => SpriteKind::FireBowlGround,
_ => SpriteKind::ShortGrass,
},
),
1,
)
} else if let Some(Tile::Room(room)) | Some(Tile::DownStair(room)) =
self.tiles.get(tile_pos)
{
let room = &self.rooms[*room];
if RandomField::new(room.seed).chance(Vec3::from(pos), room.loot_density * 0.5) {
match room.difficulty {
0 => BlockMask::new(with_sprite(SpriteKind::DungeonChest0), 1),
1 => BlockMask::new(with_sprite(SpriteKind::DungeonChest1), 1),
2 => BlockMask::new(with_sprite(SpriteKind::DungeonChest2), 1),
3 => BlockMask::new(with_sprite(SpriteKind::DungeonChest3), 1),
4 => BlockMask::new(with_sprite(SpriteKind::DungeonChest4), 1),
5 => BlockMask::new(with_sprite(SpriteKind::DungeonChest5), 1),
_ => BlockMask::new(with_sprite(SpriteKind::Chest), 1),
}
} else {
vacant
}
} else {
vacant
};
let tunnel_height = if self.final_level { 16.0 } else { 8.0 };
let pillar_thickness: i32 = 4;
move |z| match self.tiles.get(tile_pos) {
Some(Tile::Solid) => BlockMask::nothing(),
Some(Tile::Tunnel) => {
let light_offset: i32 = 7;
if (dist_to_wall - wall_thickness) as i32 == 1
&& rtile_pos.map(|e| e % light_offset == 0).reduce_bitxor()
&& z == 1
{
let ori =
Floor::relative_ori(rpos, self.nearest_wall(rpos).unwrap_or_default());
let furniture = SpriteKind::WallSconce;
BlockMask::new(Block::air(furniture).with_ori(ori).unwrap(), 1)
} else if dist_to_wall >= wall_thickness
&& (z as f32) < tunnel_height * (1.0 - tunnel_dist.powi(4))
{
if z == 0 { floor_sprite } else { vacant }
} else {
BlockMask::nothing()
}
},
Some(Tile::Room(room)) | Some(Tile::DownStair(room))
if dist_to_wall < wall_thickness
|| z as f32
>= self.rooms[*room].height as f32 * (1.0 - tunnel_dist.powi(4)) =>
{
BlockMask::nothing()
},
Some(Tile::Room(room)) | Some(Tile::DownStair(room))
if self.rooms[*room]
.pillars
.map(|pillar_space| {
tile_pos
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
&& rtile_pos.map(|e| e as f32).magnitude_squared()
< (pillar_thickness as f32 + 0.5).powi(2)
})
.unwrap_or(false) =>
{
if z == 1 && rtile_pos.product() == 0 && rtile_pos.sum().abs() == pillar_thickness {
let ori = Floor::relative_ori(rtile_pos, Vec2::zero());
let furniture = SpriteKind::WallSconce;
BlockMask::new(Block::air(furniture).with_ori(ori).unwrap(), 1)
} else if z < self.rooms[*room].height
&& rtile_pos.map(|e| e as f32).magnitude_squared()
> (pillar_thickness as f32 - 0.5).powi(2)
{
vacant
} else {
BlockMask::nothing()
}
}
Some(Tile::Room(_)) => {
let light_offset = 7;
if z == 0 {
floor_sprite
} else if dist_to_wall as i32 == 4
&& rtile_pos.map(|e| e % light_offset == 0).reduce_bitxor()
&& z == 1
{
let ori = Floor::relative_ori(
rpos,
self.nearest_wall(rpos).unwrap_or_else(Vec2::zero),
);
let furniture = SpriteKind::WallSconce;
BlockMask::new(Block::air(furniture).with_ori(ori).unwrap(), 1)
} else {
vacant
}
},
Some(Tile::DownStair(_)) => vacant,
Some(Tile::UpStair(room, kind)) => {
let inner_radius: f32 = 0.5;
let stretch = 9;
let block = make_staircase(
kind,
Vec3::new(rtile_pos.x, rtile_pos.y, z),
TILE_SIZE as f32 / 2.0,
inner_radius,
stretch as f32,
self.total_depth(),
);
let furniture = SpriteKind::WallSconce;
let ori = Floor::relative_ori(Vec2::zero(), rtile_pos);
if z < self.rooms[*room].height {
block.resolve_with(vacant)
} else if z % stretch == 0 && rtile_pos.x == 0 && rtile_pos.y == -TILE_SIZE / 2 {
BlockMask::new(Block::air(furniture).with_ori(ori).unwrap(), 1)
} else {
make_staircase(
kind,
Vec3::new(rtile_pos.x, rtile_pos.y, z),
TILE_SIZE as f32 / 2.0,
inner_radius,
stretch as f32,
self.total_depth(),
)
}
},
None => BlockMask::nothing(),
}
}
}
fn enemy_0(dynamic_rng: &mut impl Rng, entity: EntityInfo) -> EntityInfo {