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 server persistence for character stats
- Added a popup when setting your character's waypoint
- Added dungeon arenas
- Added dungeon bosses and rare boss loot
### 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

@ -3,7 +3,7 @@ Item(
description: "Two-Hand Staff\n\nPower: 2-10\n\nWalking stick with a sharpened end\n\n<Right-Click to use>",
kind: Tool(
(
kind: Staff(BasicStaff),
kind: Staff(BasicStaff),
equip_time_millis: 200,
)
),

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> {
use ItemKind::*;
match self.kind {
@ -235,3 +237,10 @@ impl Item {
impl Component for Item {
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 inputs::CanBuild;
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 location::{Waypoint, WaypointArea};

View File

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

View File

@ -16,6 +16,9 @@ pub struct EntityInfo {
pub body: Body,
pub name: Option<String>,
pub main_tool: Option<Item>,
pub scale: f32,
pub level: Option<u32>,
pub loot_drop: Option<Item>,
}
impl EntityInfo {
@ -28,6 +31,9 @@ impl EntityInfo {
body: Body::Humanoid(humanoid::Body::random()),
name: None,
main_tool: Some(Item::empty()),
scale: 1.0,
level: None,
loot_drop: None,
}
}
@ -68,6 +74,21 @@ impl EntityInfo {
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 {
self.name = match &self.body {
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::Projectile>();
ecs.register::<comp::Attacking>();
ecs.register::<comp::ItemDrop>();
// Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0));

View File

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

View File

@ -106,10 +106,17 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
.ecs()
.write_storage()
.insert(entity, Body::Object(object::Body::Pouch));
let _ = state.ecs().write_storage().insert(
entity,
assets::load_expect_cloned::<Item>("common.items.cheese"),
);
let mut item_drops = state.ecs().write_storage::<comp::ItemDrop>();
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::Agent>().remove(entity);
state

View File

@ -84,7 +84,10 @@ impl Server {
agent,
alignment,
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::ClientDisconnect(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
// 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
if entity.is_giant {
@ -336,6 +340,7 @@ impl<'a> System<'a> for Sys {
alignment,
agent: comp::Agent::default().with_patrol_origin(entity.pos),
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,
comp,
generation::{ChunkSupplement, EntityInfo},
npc,
store::{Id, Store},
terrain::{Block, BlockKind, Structure, TerrainChunkSize},
vol::{BaseVol, ReadVol, RectSizedVol, RectVolSize, Vox, WriteVol},
@ -49,6 +50,8 @@ pub struct GenCtx<'a, R: Rng> {
const ALT_OFFSET: i32 = -2;
const LEVELS: usize = 5;
impl Dungeon {
pub fn generate(wpos: Vec2<i32>, sim: Option<&WorldSim>, rng: &mut impl Rng) -> Self {
let mut ctx = GenCtx { sim, rng };
@ -61,9 +64,9 @@ impl Dungeon {
+ 6,
seed: ctx.rng.gen(),
noise: RandomField::new(ctx.rng.gen()),
floors: (0..6)
floors: (0..LEVELS)
.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;
Some(floor)
})
@ -184,7 +187,7 @@ const TILE_SIZE: i32 = 13;
#[derive(Clone)]
pub enum Tile {
UpStair,
DownStair,
DownStair(Id<Room>),
Room(Id<Room>),
Tunnel,
Solid,
@ -194,7 +197,7 @@ impl Tile {
fn is_passable(&self) -> bool {
match self {
Tile::UpStair => true,
Tile::DownStair => true,
Tile::DownStair(_) => true,
Tile::Room(_) => true,
Tile::Tunnel => true,
_ => false,
@ -206,7 +209,10 @@ pub struct Room {
seed: u32,
loot_density: f32,
enemy_density: Option<f32>,
boss: bool,
area: Rect<i32, i32>,
height: i32,
pillars: Option<i32>, // Pillars with the given separation
}
pub struct Floor {
@ -227,40 +233,69 @@ impl Floor {
stair_tile: Vec2<i32>,
level: i32,
) -> (Self, Vec2<i32>) {
let new_stair_tile = 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 final_level = level == LEVELS as i32 - 1;
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 { 13 * 2 },
hollow_depth: 13,
solid_depth: if level == 0 { 80 } else { 32 },
hollow_depth: 30,
stair_tile: new_stair_tile - tile_offset,
};
const STAIR_ROOM_HEIGHT: i32 = 13;
// Create rooms for entrance and exit
this.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: None,
boss: false,
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.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: None,
area: Rect::from((new_stair_tile - tile_offset - 1, Extent2::broadcast(3))),
});
this.tiles
.set(new_stair_tile - tile_offset, Tile::DownStair);
if final_level {
// Boss room
this.create_room(Room {
seed: ctx.rng.gen(),
loot_density: 0.0,
enemy_density: Some(0.001), // Minions!
boss: true,
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);
// Create routes between all rooms
@ -316,8 +351,11 @@ impl Floor {
self.create_room(Room {
seed: ctx.rng.gen(),
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,
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) {
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
Some(Tile::Solid) => 25.0,
Some(Tile::UpStair) | Some(Tile::DownStair) => 0.0,
Some(Tile::UpStair) | Some(Tile::DownStair(_)) => 0.0,
_ => 100000.0,
};
let satisfied = |l: &Vec2<i32>| *l == b;
@ -367,6 +405,7 @@ impl Floor {
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];
@ -376,10 +415,20 @@ impl Floor {
.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
.enemy_density
.map(|density| rng.gen_range(0, density.recip() as usize) == 0)
.unwrap_or(false)
&& !tile_is_pillar
{
// Bad
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| 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_body(comp::Body::Humanoid(comp::humanoid::Body::random()))
.with_automatic_name()
@ -404,6 +453,46 @@ impl Floor {
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()
}
},
Some(Tile::Room(_)) | Some(Tile::DownStair)
Some(Tile::Room(room)) | Some(Tile::DownStair(room))
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()
},
@ -492,7 +592,7 @@ impl Floor {
empty
}
},
Some(Tile::DownStair) => {
Some(Tile::DownStair(_)) => {
make_staircase(Vec3::new(rtile_pos.x, rtile_pos.y, z), 0.0, 0.5, 9.0)
.resolve_with(empty)
},