From b8f8691e900d247c93cce421d6c05b20f4571b2b Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 10 Jul 2023 03:35:09 +0100 Subject: [PATCH] depth, waiting (with hp recovery), hit die for monsters, ui tweak --- src/gui.rs | 11 ++++-- src/main.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++--- src/map.rs | 13 ++++++- src/player.rs | 70 ++++++++++++++++++++++++++++++++++-- src/spawner.rs | 26 ++++++++++---- 5 files changed, 200 insertions(+), 18 deletions(-) diff --git a/src/gui.rs b/src/gui.rs index 2c5f1f8..9b41dfa 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -12,9 +12,9 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { let combat_stats = ecs.read_storage::(); let players = ecs.read_storage::(); for (_player, stats) in (&players, &combat_stats).join() { - let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp); - ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health); - ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK)); + let health = format!(" HP {}/{} ", stats.hp, stats.max_hp); + ctx.print_color_right(36, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health); + ctx.draw_bar_horizontal(38, 43, 34, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK)); } // Render message log @@ -27,6 +27,11 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { y += 1; } + // Render depth + let map = ecs.fetch::(); + let depth = format!(" D{} ", map.depth); + ctx.print_color(74, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &depth); + // Render mouse cursor let mouse_pos = ctx.mouse_pos(); ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::MAGENTA)); diff --git a/src/main.rs b/src/main.rs index 8c538d0..3a03e0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ pub enum RunState { ShowTargeting { range: i32, item: Entity, aoe: i32 }, MainMenu { menu_selection: gui::MainMenuSelection }, SaveGame, + NextLevel, } pub struct State { @@ -62,20 +63,102 @@ impl State { mob.run_now(&self.ecs); let mut mapindex = MapIndexingSystem {}; mapindex.run_now(&self.ecs); - let mut melee_system = MeleeCombatSystem {}; - melee_system.run_now(&self.ecs); - let mut damage_system = DamageSystem {}; - damage_system.run_now(&self.ecs); let mut inventory_system = ItemCollectionSystem {}; inventory_system.run_now(&self.ecs); let mut item_use_system = ItemUseSystem {}; item_use_system.run_now(&self.ecs); let mut drop_system = ItemDropSystem {}; drop_system.run_now(&self.ecs); + let mut melee_system = MeleeCombatSystem {}; + melee_system.run_now(&self.ecs); + let mut damage_system = DamageSystem {}; + damage_system.run_now(&self.ecs); let mut particle_system = particle_system::ParticleSpawnSystem {}; particle_system.run_now(&self.ecs); self.ecs.maintain(); } + + fn entities_to_remove_on_level_change(&mut self) -> Vec { + let entities = self.ecs.entities(); + let player = self.ecs.read_storage::(); + let backpack = self.ecs.read_storage::(); + let player_entity = self.ecs.fetch::(); + + let mut to_delete: Vec = Vec::new(); + for entity in entities.join() { + let mut should_delete = true; + + // Don't delete player + let p = player.get(entity); + if let Some(_p) = p { + should_delete = false; + } + + // Don't delete player's equipment + let bp = backpack.get(entity); + if let Some(bp) = bp { + if bp.owner == *player_entity { + should_delete = false; + } + } + + if should_delete { + to_delete.push(entity); + } + } + + return to_delete; + } + + fn goto_next_level(&mut self) { + // Delete entities that aren't player/player's equipment + let to_delete = self.entities_to_remove_on_level_change(); + for target in to_delete { + self.ecs.delete_entity(target).expect("Unable to delete entity"); + } + + // Build new map + let worldmap; + { + let mut worldmap_resource = self.ecs.write_resource::(); + let current_depth = worldmap_resource.depth; + *worldmap_resource = Map::new_map_rooms_and_corridors(current_depth + 1); + worldmap = worldmap_resource.clone(); + } + + // Spawn things in rooms + for room in worldmap.rooms.iter().skip(1) { + spawner::spawn_room(&mut self.ecs, room); + } + + // Place the player and update resources + let (player_x, player_y) = worldmap.rooms[0].centre(); + let mut player_position = self.ecs.write_resource::(); + *player_position = Point::new(player_x, player_y); + let mut position_components = self.ecs.write_storage::(); + let player_entity = self.ecs.fetch::(); + let player_pos_comp = position_components.get_mut(*player_entity); + if let Some(player_pos_comp) = player_pos_comp { + player_pos_comp.x = player_x; + player_pos_comp.y = player_y; + } + + // Dirtify viewshed + let mut viewshed_components = self.ecs.write_storage::(); + let viewshed = viewshed_components.get_mut(*player_entity); + if let Some(viewshed) = viewshed { + viewshed.dirty = true; + } + + // Notify player, restore health up to a point. + let mut gamelog = self.ecs.fetch_mut::(); + gamelog.entries.push("You descend the stairwell, and take a moment to recover your strength.".to_string()); + let mut player_health_store = self.ecs.write_storage::(); + let player_health = player_health_store.get_mut(*player_entity); + if let Some(player_health) = player_health { + player_health.hp = i32::max(player_health.hp, player_health.max_hp / 2); + } + } } impl GameState for State { @@ -221,6 +304,10 @@ impl GameState for State { saveload_system::save_game(&mut self.ecs); new_runstate = RunState::MainMenu { menu_selection: gui::MainMenuSelection::LoadGame }; } + RunState::NextLevel => { + self.goto_next_level(); + new_runstate = RunState::PreRun; + } } { @@ -240,6 +327,7 @@ fn main() -> rltk::BError { let mut context = RltkBuilder::new() .with_title("rust-rl") .with_dimensions(DISPLAYWIDTH, DISPLAYHEIGHT) + .with_fullscreen(true) .with_tile_dimensions(16, 16) .with_resource_path("resources/") .with_font("terminal8x8.jpg", 8, 8) @@ -277,7 +365,7 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.insert(SimpleMarkerAllocator::::new()); - let map = Map::new_map_rooms_and_corridors(); + let map = Map::new_map_rooms_and_corridors(1); let (player_x, player_y) = map.rooms[0].centre(); let player_name = "wanderer".to_string(); let player_entity = spawner::player(&mut gs.ecs, player_x, player_y, player_name); diff --git a/src/map.rs b/src/map.rs index 679bf1d..1ef2404 100644 --- a/src/map.rs +++ b/src/map.rs @@ -10,6 +10,7 @@ use std::ops::{Add, Mul}; pub enum TileType { Wall, Floor, + DownStair, } pub const MAPWIDTH: usize = 80; @@ -29,6 +30,7 @@ pub struct Map { pub green_offset: Vec, pub blue_offset: Vec, pub blocked: Vec, + pub depth: i32, pub bloodstains: HashSet, #[serde(skip_serializing)] @@ -90,7 +92,7 @@ impl Map { } /// Makes a procgen map out of rooms and corridors, and returns the rooms and the map. - pub fn new_map_rooms_and_corridors() -> Map { + pub fn new_map_rooms_and_corridors(new_depth: i32) -> Map { let mut map = Map { tiles: vec![TileType::Wall; MAPCOUNT], rooms: Vec::new(), @@ -102,6 +104,7 @@ impl Map { green_offset: vec![0; MAPCOUNT], blue_offset: vec![0; MAPCOUNT], blocked: vec![false; MAPCOUNT], + depth: new_depth, bloodstains: HashSet::new(), tile_content: vec![Vec::new(); MAPCOUNT], }; @@ -156,6 +159,10 @@ impl Map { } } + let stairs_position = map.rooms[map.rooms.len() - 1].centre(); + let stairs_idx = map.xy_idx(stairs_position.0, stairs_position.1); + map.tiles[stairs_idx] = TileType::DownStair; + map } } @@ -241,6 +248,10 @@ pub fn draw_map(ecs: &World, ctx: &mut Rltk) { glyph = wall_glyph(&*map, x, y); fg = fg.add(RGB::from_f32(0.6, 0.5, 0.25)); } + TileType::DownStair => { + glyph = rltk::to_cp437('>'); + fg = RGB::from_f32(0., 1., 1.); + } } if map.bloodstains.contains(&idx) { bg = bg.add(RGB::from_f32(0.6, 0., 0.)); diff --git a/src/player.rs b/src/player.rs index e1d98ec..bf8a378 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,8 +1,8 @@ use super::{ - gamelog::GameLog, CombatStats, Item, Map, Player, Position, RunState, State, Viewshed, WantsToMelee, - WantsToPickupItem, MAPHEIGHT, MAPWIDTH, + gamelog::GameLog, CombatStats, Item, Map, Monster, Player, Position, RunState, State, TileType, Viewshed, + WantsToMelee, WantsToPickupItem, MAPHEIGHT, MAPWIDTH, }; -use rltk::{Point, Rltk, VirtualKeyCode}; +use rltk::{Point, RandomNumberGenerator, Rltk, VirtualKeyCode}; use specs::prelude::*; use std::cmp::{max, min}; @@ -94,6 +94,18 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { VirtualKeyCode::Numpad7 | VirtualKeyCode::U => try_move_player(-1, -1, &mut gs.ecs), VirtualKeyCode::Numpad3 | VirtualKeyCode::N => try_move_player(1, 1, &mut gs.ecs), VirtualKeyCode::Numpad1 | VirtualKeyCode::B => try_move_player(-1, 1, &mut gs.ecs), + // Depth + VirtualKeyCode::Period => { + if ctx.shift { + if !try_next_level(&mut gs.ecs) { + return RunState::AwaitingInput; + } + return RunState::NextLevel; // > to descend + } else { + return skip_turn(&mut gs.ecs); // (Wait a turn) + } + } + // Items VirtualKeyCode::G => get_item(&mut gs.ecs), VirtualKeyCode::I => return RunState::ShowInventory, @@ -107,6 +119,58 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { RunState::PlayerTurn } +pub fn try_next_level(ecs: &mut World) -> bool { + let player_pos = ecs.fetch::(); + let map = ecs.fetch::(); + let player_idx = map.xy_idx(player_pos.x, player_pos.y); + if map.tiles[player_idx] == TileType::DownStair { + return true; + } else { + let mut gamelog = ecs.fetch_mut::(); + gamelog.entries.push("You don't see a way down.".to_string()); + return false; + } +} + +fn skip_turn(ecs: &mut World) -> RunState { + let player_entity = ecs.fetch::(); + let viewshed_components = ecs.read_storage::(); + let monsters = ecs.read_storage::(); + let mut wait_message = "You wait a turn."; + + let worldmap_resource = ecs.fetch::(); + + let mut can_heal = true; + let viewshed = viewshed_components.get(*player_entity).unwrap(); + for tile in viewshed.visible_tiles.iter() { + let idx = worldmap_resource.xy_idx(tile.x, tile.y); + for entity_id in worldmap_resource.tile_content[idx].iter() { + let mob = monsters.get(*entity_id); + match mob { + None => {} + Some(_) => { + can_heal = false; + } + } + } + } + + if can_heal { + let mut health_components = ecs.write_storage::(); + let player_hp = health_components.get_mut(*player_entity).unwrap(); + let mut rng = ecs.write_resource::(); + let roll = rng.roll_dice(1, 6); + if (roll == 6) && player_hp.hp < player_hp.max_hp { + player_hp.hp += 1; + wait_message = "You wait a turn, and recover a hit point."; + } + } + + let mut gamelog = ecs.fetch_mut::(); + gamelog.entries.push(wait_message.to_string()); + return RunState::PlayerTurn; +} + /* Playing around with autoexplore, without having read how to do it. pub fn auto_explore(ecs: &mut World) { let player_pos = ecs.fetch::(); diff --git a/src/spawner.rs b/src/spawner.rs index fd44949..9e3111e 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -8,6 +8,7 @@ use specs::saveload::{MarkedBuilder, SimpleMarker}; /// Spawns the player and returns his/her entity object. pub fn player(ecs: &mut World, player_x: i32, player_y: i32, player_name: String) -> Entity { + // d8 hit die - but always maxxed at level 1, so player doesn't have to roll. ecs.create_entity() .with(Position { x: player_x, y: player_y }) .with(Renderable { @@ -19,7 +20,7 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32, player_name: String .with(Player {}) .with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true }) .with(Name { name: player_name }) - .with(CombatStats { max_hp: 30, hp: 30, defence: 2, power: 5 }) + .with(CombatStats { max_hp: 8, hp: 8, defence: 0, power: 4 }) .marked::>() .build() } @@ -57,7 +58,9 @@ pub fn random_item(ecs: &mut World, x: i32, y: i32) { const MAX_MONSTERS: i32 = 4; const MAX_ITEMS: i32 = 3; -fn monster(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharType, name: S) { +fn monster(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharType, name: S, hit_die: i32) { + let rolled_hp = roll_hit_dice(ecs, 1, hit_die); + ecs.create_entity() .with(Position { x, y }) .with(Renderable { glyph: glyph, fg: RGB::named(rltk::GREEN), bg: RGB::named(rltk::BLACK), render_order: 1 }) @@ -65,21 +68,32 @@ fn monster(ecs: &mut World, x: i32, y: i32, glyph: rltk::FontCharTy .with(Monster {}) .with(Name { name: name.to_string() }) .with(BlocksTile {}) - .with(CombatStats { max_hp: 16, hp: 16, defence: 1, power: 4 }) + .with(CombatStats { max_hp: rolled_hp, hp: rolled_hp, defence: 0, power: 2 }) .marked::>() .build(); } fn orc(ecs: &mut World, x: i32, y: i32) { - monster(ecs, x, y, rltk::to_cp437('o'), "orc"); + monster(ecs, x, y, rltk::to_cp437('o'), "orc", 8); } fn goblin(ecs: &mut World, x: i32, y: i32) { - monster(ecs, x, y, rltk::to_cp437('g'), "goblin"); + monster(ecs, x, y, rltk::to_cp437('g'), "goblin", 6); } fn goblin_chieftain(ecs: &mut World, x: i32, y: i32) { - monster(ecs, x, y, rltk::to_cp437('G'), "goblin chieftain"); + monster(ecs, x, y, rltk::to_cp437('G'), "goblin chieftain", 8); +} + +pub fn roll_hit_dice(ecs: &mut World, n: i32, d: i32) -> i32 { + let mut rng = ecs.write_resource::(); + let mut rolled_hp: i32 = 0; + + for _i in 0..n { + rolled_hp += rng.roll_dice(1, d); + } + + return rolled_hp; } pub fn spawn_room(ecs: &mut World, room: &Rect) {