From aac28d04d5013180d2d91a574c5fb07f09b16dda Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 15 May 2020 16:05:50 +0100 Subject: [PATCH] Added dungeon bosses, boss loot, boss arenas --- CHANGELOG.md | 2 + assets/common/items/boss_drops/lantern.ron | 12 ++ assets/common/items/boss_drops/potions.ron | 12 ++ assets/common/items/boss_drops/xp_potion.ron | 8 + assets/common/items/weapons/staff/staff_1.ron | 2 +- common/src/comp/inventory/item/mod.rs | 9 + common/src/comp/mod.rs | 4 +- common/src/event.rs | 3 +- common/src/generation.rs | 21 +++ common/src/state.rs | 1 + server/src/events/entity_creation.rs | 18 +- server/src/events/entity_manipulation.rs | 15 +- server/src/events/mod.rs | 5 +- server/src/sys/terrain.rs | 9 +- world/examples/namegen.rs | 37 +++++ world/src/site/dungeon/mod.rs | 154 +++++++++++++++--- 16 files changed, 270 insertions(+), 42 deletions(-) create mode 100644 assets/common/items/boss_drops/lantern.ron create mode 100644 assets/common/items/boss_drops/potions.ron create mode 100644 assets/common/items/boss_drops/xp_potion.ron create mode 100644 world/examples/namegen.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 25dd9ec1cc..99a2bedbe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/assets/common/items/boss_drops/lantern.ron b/assets/common/items/boss_drops/lantern.ron new file mode 100644 index 0000000000..3cdaa8ecca --- /dev/null +++ b/assets/common/items/boss_drops/lantern.ron @@ -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, + ), + ), +) diff --git a/assets/common/items/boss_drops/potions.ron b/assets/common/items/boss_drops/potions.ron new file mode 100644 index 0000000000..d73ddf4948 --- /dev/null +++ b/assets/common/items/boss_drops/potions.ron @@ -0,0 +1,12 @@ +Item( + name: "Powerful Potion", + description: "Restores 100 Health\nA great monster was slain for this item\n\n", + kind: Consumable( + kind: Potion, + effect: Health(( + amount: 100, + cause: Item, + )), + amount: 15, + ), +) diff --git a/assets/common/items/boss_drops/xp_potion.ron b/assets/common/items/boss_drops/xp_potion.ron new file mode 100644 index 0000000000..84cdcb223c --- /dev/null +++ b/assets/common/items/boss_drops/xp_potion.ron @@ -0,0 +1,8 @@ +Item( + name: "Potion of Skill", + description: "Provides 250 XP to the drinker\n\n", + kind: Consumable( + kind: Potion, + effect: Xp(250), + ), +) diff --git a/assets/common/items/weapons/staff/staff_1.ron b/assets/common/items/weapons/staff/staff_1.ron index 5fb37f87b6..0654dc2b8b 100644 --- a/assets/common/items/weapons/staff/staff_1.ron +++ b/assets/common/items/weapons/staff/staff_1.ron @@ -3,7 +3,7 @@ Item( description: "Two-Hand Staff\n\nPower: 2-10\n\nWalking stick with a sharpened end\n\n", kind: Tool( ( - kind: Staff(BasicStaff), + kind: Staff(BasicStaff), equip_time_millis: 200, ) ), diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 6d743f90ae..f95eb2b2b7 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -123,6 +123,8 @@ impl Item { } } + pub fn expect_from_asset(asset: &str) -> Self { (*assets::load_expect::(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>; } + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ItemDrop(pub Item); + +impl Component for ItemDrop { + type Storage = FlaggedStorage>; +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 2659d08650..5bf573e9f0 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -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}; diff --git a/common/src/event.rs b/common/src/event.rs index 54452a9e12..762703337d 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -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, }, CreateWaypoint(Vec3), ClientDisconnect(EcsEntity), diff --git a/common/src/generation.rs b/common/src/generation.rs index 65fc409020..b7058cd8c4 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -16,6 +16,9 @@ pub struct EntityInfo { pub body: Body, pub name: Option, pub main_tool: Option, + pub scale: f32, + pub level: Option, + pub loot_drop: Option, } 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)), diff --git a/common/src/state.rs b/common/src/state.rs index 3e1fb9d8c6..9d47b9bbb6 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -153,6 +153,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); // Register synced resources used by the ECS. ecs.insert(TimeOfDay(0.0)); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 9529f87ad5..9cbd194165 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -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, ) { - 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( diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 4a8bb74206..266145cbe3 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -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::("common.items.cheese"), - ); + + let mut item_drops = state.ecs().write_storage::(); + let item = if let Some(item_drop) = item_drops.get(entity).cloned() { + item_drops.remove(entity); + item_drop.0 + } else { + assets::load_expect_cloned::("common.items.cheese") + }; + + let _ = state.ecs().write_storage().insert(entity, item); + state.ecs().write_storage::().remove(entity); state.ecs().write_storage::().remove(entity); state diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index cf1a632dde..d36f86e9d4 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -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)) diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 3ff9f6e74f..d60b52ea25 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -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, }) } } diff --git a/world/examples/namegen.rs b/world/examples/namegen.rs new file mode 100644 index 0000000000..be0a8bc3b8 --- /dev/null +++ b/world/examples/namegen.rs @@ -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()); + } +} diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 1554d7c59e..b2342f1772 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -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, 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(Id), 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, + boss: bool, area: Rect, + height: i32, + pillars: Option, // Pillars with the given separation } pub struct Floor { @@ -227,40 +233,69 @@ impl Floor { stair_tile: Vec2, level: i32, ) -> (Self, Vec2) { - 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, b: &Vec2| 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| *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) },