Added dungeon bosses, boss loot, boss arenas

This commit is contained in:
Joshua Barretto 2020-05-15 16:05:50 +01:00
parent 71dd520cd6
commit aac28d04d5
16 changed files with 270 additions and 42 deletions

View File

@ -73,6 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added tab completion in chat for player names and chat commands - Added tab completion in chat for player names and chat commands
- Added server persistence for character stats - Added server persistence for character stats
- Added a popup when setting your character's waypoint - Added a popup when setting your character's waypoint
- Added dungeon arenas
- Added dungeon bosses and rare boss loot
### Changed ### Changed

View File

@ -0,0 +1,12 @@
Item(
name: "Magic Lantern",
description: "Illuminates even the darkest dungeon\nA great monster was slain for this item",
kind: Lantern(
(
kind: Blue0,
color: (r: 220, g: 220, b: 255),
strength_thousandths: 6500,
flicker_thousandths: 300,
),
),
)

View File

@ -0,0 +1,12 @@
Item(
name: "Powerful Potion",
description: "Restores 100 Health\nA great monster was slain for this item\n\n<Right-Click to use>",
kind: Consumable(
kind: Potion,
effect: Health((
amount: 100,
cause: Item,
)),
amount: 15,
),
)

View File

@ -0,0 +1,8 @@
Item(
name: "Potion of Skill",
description: "Provides 250 XP to the drinker\n\n<Right-Click to use>",
kind: Consumable(
kind: Potion,
effect: Xp(250),
),
)

View File

@ -123,6 +123,8 @@ impl Item {
} }
} }
pub fn expect_from_asset(asset: &str) -> Self { (*assets::load_expect::<Self>(asset)).clone() }
pub fn set_amount(&mut self, give_amount: u32) -> Result<(), assets::Error> { pub fn set_amount(&mut self, give_amount: u32) -> Result<(), assets::Error> {
use ItemKind::*; use ItemKind::*;
match self.kind { match self.kind {
@ -235,3 +237,10 @@ impl Item {
impl Component for Item { impl Component for Item {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>; type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
} }
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ItemDrop(pub Item);
impl Component for ItemDrop {
type Storage = FlaggedStorage<Self, IDVStorage<Self>>;
}

View File

@ -31,7 +31,9 @@ pub use controller::{
pub use energy::{Energy, EnergySource}; pub use energy::{Energy, EnergySource};
pub use inputs::CanBuild; pub use inputs::CanBuild;
pub use inventory::{ pub use inventory::{
item, item::Item, slot, Inventory, InventoryUpdate, InventoryUpdateEvent, MAX_PICKUP_RANGE_SQR, item,
item::{Item, ItemDrop},
slot, Inventory, InventoryUpdate, InventoryUpdateEvent, MAX_PICKUP_RANGE_SQR,
}; };
pub use last::Last; pub use last::Last;
pub use location::{Waypoint, WaypointArea}; pub use location::{Waypoint, WaypointArea};

View File

@ -1,5 +1,5 @@
use crate::{comp, sync::Uid, util::Dir}; use crate::{comp, sync::Uid, util::Dir};
use comp::{item::ToolKind, InventoryUpdateEvent}; use comp::{item::ToolKind, InventoryUpdateEvent, Item};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Deserialize; use serde::Deserialize;
use specs::Entity as EcsEntity; use specs::Entity as EcsEntity;
@ -107,6 +107,7 @@ pub enum ServerEvent {
agent: comp::Agent, agent: comp::Agent,
alignment: comp::Alignment, alignment: comp::Alignment,
scale: comp::Scale, scale: comp::Scale,
drop_item: Option<Item>,
}, },
CreateWaypoint(Vec3<f32>), CreateWaypoint(Vec3<f32>),
ClientDisconnect(EcsEntity), ClientDisconnect(EcsEntity),

View File

@ -16,6 +16,9 @@ pub struct EntityInfo {
pub body: Body, pub body: Body,
pub name: Option<String>, pub name: Option<String>,
pub main_tool: Option<Item>, pub main_tool: Option<Item>,
pub scale: f32,
pub level: Option<u32>,
pub loot_drop: Option<Item>,
} }
impl EntityInfo { impl EntityInfo {
@ -28,6 +31,9 @@ impl EntityInfo {
body: Body::Humanoid(humanoid::Body::random()), body: Body::Humanoid(humanoid::Body::random()),
name: None, name: None,
main_tool: Some(Item::empty()), main_tool: Some(Item::empty()),
scale: 1.0,
level: None,
loot_drop: None,
} }
} }
@ -68,6 +74,21 @@ impl EntityInfo {
self self
} }
pub fn with_loot_drop(mut self, loot_drop: Item) -> Self {
self.loot_drop = Some(loot_drop);
self
}
pub fn with_scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
pub fn with_level(mut self, level: u32) -> Self {
self.level = Some(level);
self
}
pub fn with_automatic_name(mut self) -> Self { pub fn with_automatic_name(mut self) -> Self {
self.name = match &self.body { self.name = match &self.body {
Body::Humanoid(body) => Some(get_npc_name(&NPC_NAMES.humanoid, body.race)), Body::Humanoid(body) => Some(get_npc_name(&NPC_NAMES.humanoid, body.race)),

View File

@ -153,6 +153,7 @@ impl State {
ecs.register::<comp::Waypoint>(); ecs.register::<comp::Waypoint>();
ecs.register::<comp::Projectile>(); ecs.register::<comp::Projectile>();
ecs.register::<comp::Attacking>(); ecs.register::<comp::Attacking>();
ecs.register::<comp::ItemDrop>();
// Register synced resources used by the ECS. // Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0)); ecs.insert(TimeOfDay(0.0));

View File

@ -1,8 +1,8 @@
use crate::{sys, Server, StateExt}; use crate::{sys, Server, StateExt};
use common::{ use common::{
comp::{ comp::{
self, Agent, Alignment, Body, Gravity, LightEmitter, Loadout, Pos, Projectile, Scale, self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos,
Stats, Vel, WaypointArea, Projectile, Scale, Stats, Vel, WaypointArea,
}, },
util::Dir, util::Dir,
}; };
@ -32,14 +32,22 @@ pub fn handle_create_npc(
agent: Agent, agent: Agent,
alignment: Alignment, alignment: Alignment,
scale: Scale, scale: Scale,
drop_item: Option<Item>,
) { ) {
server let entity = server
.state .state
.create_npc(pos, stats, loadout, body) .create_npc(pos, stats, loadout, body)
.with(agent) .with(agent)
.with(scale) .with(scale)
.with(alignment) .with(alignment);
.build();
let entity = if let Some(drop_item) = drop_item {
entity.with(ItemDrop(drop_item))
} else {
entity
};
entity.build();
} }
pub fn handle_shoot( pub fn handle_shoot(

View File

@ -106,10 +106,17 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
.ecs() .ecs()
.write_storage() .write_storage()
.insert(entity, Body::Object(object::Body::Pouch)); .insert(entity, Body::Object(object::Body::Pouch));
let _ = state.ecs().write_storage().insert(
entity, let mut item_drops = state.ecs().write_storage::<comp::ItemDrop>();
assets::load_expect_cloned::<Item>("common.items.cheese"), let item = if let Some(item_drop) = item_drops.get(entity).cloned() {
); item_drops.remove(entity);
item_drop.0
} else {
assets::load_expect_cloned::<Item>("common.items.cheese")
};
let _ = state.ecs().write_storage().insert(entity, item);
state.ecs().write_storage::<comp::Stats>().remove(entity); state.ecs().write_storage::<comp::Stats>().remove(entity);
state.ecs().write_storage::<comp::Agent>().remove(entity); state.ecs().write_storage::<comp::Agent>().remove(entity);
state state

View File

@ -84,7 +84,10 @@ impl Server {
agent, agent,
alignment, alignment,
scale, scale,
} => handle_create_npc(self, pos, stats, loadout, body, agent, alignment, scale), drop_item,
} => handle_create_npc(
self, pos, stats, loadout, body, agent, alignment, scale, drop_item,
),
ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos), ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos),
ServerEvent::ClientDisconnect(entity) => { ServerEvent::ClientDisconnect(entity) => {
frontend_events.push(handle_client_disconnect(self, entity)) frontend_events.push(handle_client_disconnect(self, entity))

View File

@ -245,11 +245,15 @@ impl<'a> System<'a> for Sys {
}, },
}; };
let mut scale = 1.0; let mut scale = entity.scale;
// TODO: Remove this and implement scaling or level depending on stuff like // TODO: Remove this and implement scaling or level depending on stuff like
// species instead // species instead
stats.level.set_level(rand::thread_rng().gen_range(1, 9)); stats.level.set_level(
entity.level.unwrap_or_else(|| {
(rand::thread_rng().gen_range(1, 9) as f32 * scale) as u32
}),
);
// Replace stuff if it's a boss // Replace stuff if it's a boss
if entity.is_giant { if entity.is_giant {
@ -336,6 +340,7 @@ impl<'a> System<'a> for Sys {
alignment, alignment,
agent: comp::Agent::default().with_patrol_origin(entity.pos), agent: comp::Agent::default().with_patrol_origin(entity.pos),
scale: comp::Scale(scale), scale: comp::Scale(scale),
drop_item: entity.loot_drop,
}) })
} }
} }

37
world/examples/namegen.rs Normal file
View File

@ -0,0 +1,37 @@
use rand::prelude::*;
fn main() {
let cons = vec![
"d", "f", "ph", "r", "st", "t", "s", "p", "sh", "th", "br", "tr", "m", "k", "st", "w", "y",
];
let mut start = cons.clone();
start.extend(vec![
"cr", "thr", "str", "br", "ivy", "est", "ost", "ing", "kr", "in", "on", "tr", "tw", "wh",
"eld", "ar", "or", "ear", "ir",
]);
let mut middle = cons.clone();
middle.extend(vec!["tt"]);
let vowel = vec!["o", "e", "a", "i", "u", "au", "ee", "ow", "ay", "ey", "oe"];
let end = vec![
"et", "ige", "age", "ist", "en", "on", "og", "end", "ind", "ock", "een", "edge", "ist",
"ed", "est", "eed", "ast", "olt", "ey", "ean", "ead", "onk", "ink", "eon", "er", "ow",
"cot", "in", "on",
];
let gen_name = || {
let mut name = String::new();
name += start.choose(&mut thread_rng()).unwrap();
if thread_rng().gen() {
name += vowel.choose(&mut thread_rng()).unwrap();
name += middle.choose(&mut thread_rng()).unwrap();
}
name += end.choose(&mut thread_rng()).unwrap();
name
};
for _ in 0..20 {
println!("{}", gen_name());
}
}

View File

@ -11,6 +11,7 @@ use common::{
astar::Astar, astar::Astar,
comp, comp,
generation::{ChunkSupplement, EntityInfo}, generation::{ChunkSupplement, EntityInfo},
npc,
store::{Id, Store}, store::{Id, Store},
terrain::{Block, BlockKind, Structure, TerrainChunkSize}, terrain::{Block, BlockKind, Structure, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol}, vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
@ -49,6 +50,8 @@ pub struct GenCtx<'a, R: Rng> {
const ALT_OFFSET: i32 = -2; const ALT_OFFSET: i32 = -2;
const LEVELS: usize = 5;
impl Dungeon { impl Dungeon {
pub fn generate(wpos: Vec2<i32>, sim: Option<&WorldSim>, rng: &mut impl Rng) -> Self { pub fn generate(wpos: Vec2<i32>, sim: Option<&WorldSim>, rng: &mut impl Rng) -> Self {
let mut ctx = GenCtx { sim, rng }; let mut ctx = GenCtx { sim, rng };
@ -61,9 +64,9 @@ impl Dungeon {
+ 6, + 6,
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
noise: RandomField::new(ctx.rng.gen()), noise: RandomField::new(ctx.rng.gen()),
floors: (0..6) floors: (0..LEVELS)
.scan(Vec2::zero(), |stair_tile, level| { .scan(Vec2::zero(), |stair_tile, level| {
let (floor, st) = Floor::generate(&mut ctx, *stair_tile, level); let (floor, st) = Floor::generate(&mut ctx, *stair_tile, level as i32);
*stair_tile = st; *stair_tile = st;
Some(floor) Some(floor)
}) })
@ -184,7 +187,7 @@ const TILE_SIZE: i32 = 13;
#[derive(Clone)] #[derive(Clone)]
pub enum Tile { pub enum Tile {
UpStair, UpStair,
DownStair, DownStair(Id<Room>),
Room(Id<Room>), Room(Id<Room>),
Tunnel, Tunnel,
Solid, Solid,
@ -194,7 +197,7 @@ impl Tile {
fn is_passable(&self) -> bool { fn is_passable(&self) -> bool {
match self { match self {
Tile::UpStair => true, Tile::UpStair => true,
Tile::DownStair => true, Tile::DownStair(_) => true,
Tile::Room(_) => true, Tile::Room(_) => true,
Tile::Tunnel => true, Tile::Tunnel => true,
_ => false, _ => false,
@ -206,7 +209,10 @@ pub struct Room {
seed: u32, seed: u32,
loot_density: f32, loot_density: f32,
enemy_density: Option<f32>, enemy_density: Option<f32>,
boss: bool,
area: Rect<i32, i32>, area: Rect<i32, i32>,
height: i32,
pillars: Option<i32>, // Pillars with the given separation
} }
pub struct Floor { pub struct Floor {
@ -227,40 +233,69 @@ impl Floor {
stair_tile: Vec2<i32>, stair_tile: Vec2<i32>,
level: i32, level: i32,
) -> (Self, Vec2<i32>) { ) -> (Self, Vec2<i32>) {
let new_stair_tile = std::iter::from_fn(|| { let final_level = level == LEVELS as i32 - 1;
Some(FLOOR_SIZE.map(|sz| ctx.rng.gen_range(-sz / 2 + 2, sz / 2 - 1)))
}) let new_stair_tile = if final_level {
.filter(|pos| *pos != stair_tile) Vec2::zero()
.take(8) } else {
.max_by_key(|pos| (*pos - stair_tile).map(|e| e.abs()).sum()) std::iter::from_fn(|| {
.unwrap(); 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 tile_offset = -FLOOR_SIZE / 2;
let mut this = Floor { let mut this = Floor {
tile_offset, tile_offset,
tiles: Grid::new(FLOOR_SIZE, Tile::Solid), tiles: Grid::new(FLOOR_SIZE, Tile::Solid),
rooms: Store::default(), rooms: Store::default(),
solid_depth: if level == 0 { 80 } else { 13 * 2 }, solid_depth: if level == 0 { 80 } else { 32 },
hollow_depth: 13, hollow_depth: 30,
stair_tile: new_stair_tile - tile_offset, stair_tile: new_stair_tile - tile_offset,
}; };
const STAIR_ROOM_HEIGHT: i32 = 13;
// Create rooms for entrance and exit // Create rooms for entrance and exit
this.create_room(Room { this.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.0, loot_density: 0.0,
enemy_density: None, enemy_density: None,
boss: false,
area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))), area: Rect::from((stair_tile - tile_offset - 1, Extent2::broadcast(3))),
height: STAIR_ROOM_HEIGHT,
pillars: None,
}); });
this.tiles.set(stair_tile - tile_offset, Tile::UpStair); this.tiles.set(stair_tile - tile_offset, Tile::UpStair);
this.create_room(Room { if final_level {
seed: ctx.rng.gen(), // Boss room
loot_density: 0.0, this.create_room(Room {
enemy_density: None, seed: ctx.rng.gen(),
area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))), loot_density: 0.0,
}); enemy_density: Some(0.001), // Minions!
this.tiles boss: true,
.set(new_stair_tile - tile_offset, Tile::DownStair); area: Rect::from((new_stair_tile - tile_offset - 4, Extent2::broadcast(9))),
height: 30,
pillars: Some(2),
});
} else {
// Create downstairs room
let downstair_room = this.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: None,
boss: false,
area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))),
height: STAIR_ROOM_HEIGHT,
pillars: None,
});
this.tiles.set(
new_stair_tile - tile_offset,
Tile::DownStair(downstair_room),
);
}
this.create_rooms(ctx, level, 7); this.create_rooms(ctx, level, 7);
// Create routes between all rooms // Create routes between all rooms
@ -316,8 +351,11 @@ impl Floor {
self.create_room(Room { self.create_room(Room {
seed: ctx.rng.gen(), seed: ctx.rng.gen(),
loot_density: 0.000025 + level as f32 * 0.00015, loot_density: 0.000025 + level as f32 * 0.00015,
enemy_density: Some(0.001 + level as f32 * 0.00004), enemy_density: Some(0.001 + level as f32 * 0.00006),
boss: false,
area, area,
height: ctx.rng.gen_range(10, 15),
pillars: None,
}); });
} }
} }
@ -334,7 +372,7 @@ impl Floor {
let transition = |_a: &Vec2<i32>, b: &Vec2<i32>| match self.tiles.get(*b) { let transition = |_a: &Vec2<i32>, b: &Vec2<i32>| match self.tiles.get(*b) {
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0, Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
Some(Tile::Solid) => 25.0, Some(Tile::Solid) => 25.0,
Some(Tile::UpStair) | Some(Tile::DownStair) => 0.0, Some(Tile::UpStair) | Some(Tile::DownStair(_)) => 0.0,
_ => 100000.0, _ => 100000.0,
}; };
let satisfied = |l: &Vec2<i32>| *l == b; let satisfied = |l: &Vec2<i32>| *l == b;
@ -367,6 +405,7 @@ impl Floor {
for x in area.min.x..area.max.x { for x in area.min.x..area.max.x {
for y in area.min.y..area.max.y { 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 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) { if let Some(Tile::Room(room)) = self.tiles.get(tile_pos) {
let room = &self.rooms[*room]; let room = &self.rooms[*room];
@ -376,10 +415,20 @@ impl Floor {
.map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2), .map(|e| e.div_euclid(TILE_SIZE) * TILE_SIZE + TILE_SIZE / 2),
); );
let tile_is_pillar = room
.pillars
.map(|pillar_space| {
tile_pos
.map(|e| e.rem_euclid(pillar_space) == 0)
.reduce_and()
})
.unwrap_or(false);
if room if room
.enemy_density .enemy_density
.map(|density| rng.gen_range(0, density.recip() as usize) == 0) .map(|density| rng.gen_range(0, density.recip() as usize) == 0)
.unwrap_or(false) .unwrap_or(false)
&& !tile_is_pillar
{ {
// Bad // Bad
let entity = EntityInfo::at( let entity = EntityInfo::at(
@ -389,7 +438,7 @@ impl Floor {
.map(|e| (RandomField::new(room.seed.wrapping_add(10 + e)).get(Vec3::from(tile_pos)) % 32) as i32 - 16) .map(|e| (RandomField::new(room.seed.wrapping_add(10 + e)).get(Vec3::from(tile_pos)) % 32) as i32 - 16)
.map(|e| e as f32 / 16.0), .map(|e| e as f32 / 16.0),
) )
.do_if(RandomField::new(room.seed.wrapping_add(1)).chance(Vec3::from(tile_pos), 0.2), |e| e.into_giant()) .do_if(RandomField::new(room.seed.wrapping_add(1)).chance(Vec3::from(tile_pos), 0.2) && !room.boss, |e| e.into_giant())
.with_alignment(comp::Alignment::Enemy) .with_alignment(comp::Alignment::Enemy)
.with_body(comp::Body::Humanoid(comp::humanoid::Body::random())) .with_body(comp::Body::Humanoid(comp::humanoid::Body::random()))
.with_automatic_name() .with_automatic_name()
@ -404,6 +453,46 @@ impl Floor {
supplement.add_entity(entity); supplement.add_entity(entity);
} }
if room.boss {
let boss_spawn_tile = room.area.center();
// Don't spawn the boss in a pillar
let boss_spawn_tile = boss_spawn_tile + if tile_is_pillar { 1 } else { 0 };
if tile_pos == boss_spawn_tile && tile_wcenter.xy() == wpos2d {
let entity = EntityInfo::at(tile_wcenter.map(|e| e as f32))
.with_scale(4.0)
.with_level(rng.gen_range(50, 70))
.with_alignment(comp::Alignment::Enemy)
.with_body(comp::Body::Humanoid(comp::humanoid::Body::random()))
.with_name(format!(
"{}, Destroyer of Worlds",
npc::get_npc_name(npc::NpcKind::Humanoid)
))
.with_main_tool(assets::load_expect_cloned(
match rng.gen_range(0, 5) {
0 => "common.items.weapons.sword.starter_sword",
1 => "common.items.weapons.sword.short_sword_0",
2 => "common.items.weapons.sword.wood_sword",
3 => "common.items.weapons.sword.zweihander_sword_0",
_ => "common.items.weapons.hammer.hammer_1",
},
))
.with_loot_drop(match rng.gen_range(0, 3) {
0 => comp::Item::expect_from_asset(
"common.items.boss_drops.lantern",
),
1 => comp::Item::expect_from_asset(
"common.items.boss_drops.potions",
),
_ => comp::Item::expect_from_asset(
"common.items.boss_drops.xp_potion",
),
});
supplement.add_entity(entity);
}
}
} }
} }
} }
@ -477,9 +566,20 @@ impl Floor {
BlockMask::nothing() BlockMask::nothing()
} }
}, },
Some(Tile::Room(_)) | Some(Tile::DownStair) Some(Tile::Room(room)) | Some(Tile::DownStair(room))
if dist_to_wall < wall_thickness if dist_to_wall < wall_thickness
|| z as f32 >= self.hollow_depth as f32 - 13.0 * tunnel_dist.powf(4.0) => || z as f32
>= self.rooms[*room].height as f32 * (1.0 - tunnel_dist.powf(4.0))
|| 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()
< 3.5f32.powf(2.0)
})
.unwrap_or(false) =>
{ {
BlockMask::nothing() BlockMask::nothing()
}, },
@ -492,7 +592,7 @@ impl Floor {
empty empty
} }
}, },
Some(Tile::DownStair) => { Some(Tile::DownStair(_)) => {
make_staircase(Vec3::new(rtile_pos.x, rtile_pos.y, z), 0.0, 0.5, 9.0) make_staircase(Vec3::new(rtile_pos.x, rtile_pos.y, z), 0.0, 0.5, 9.0)
.resolve_with(empty) .resolve_with(empty)
}, },