commit 2ac7670b86807ae74f2ae4e49f25ba6bf12c65aa
Author: Simon Watson <spw01@protonmail.com>
Date: Tue Oct 4 21:26:55 2022 -0400
Completed Section 1.
diff --git a/src/components.rs b/src/components.rs
index d917b81..9b37d57 100644
--- a/src/components.rs
+++ b/src/components.rs
@@ -6,6 +6,35 @@ use specs::saveload::{Marker, ConvertSaveload};
use specs::error::NoError;
// COMPONENTS
+#[derive(Component, Debug, ConvertSaveload, Clone)]
+pub struct WantsToRemoveItem {
+ pub item : Entity
+}
+
+#[derive(Component, ConvertSaveload, Clone)]
+pub struct MeleePowerBonus {
+ pub power : i32
+}
+
+#[derive(Component, ConvertSaveload, Clone)]
+pub struct DefenseBonus {
+ pub defense : i32
+}
+
+#[derive(Component, ConvertSaveload, Clone)]
+pub struct Equipped {
+ pub owner : Entity,
+ pub slot : EquipmentSlot
+}
+
+#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
+pub enum EquipmentSlot { Melee, Shield }
+
+#[derive(Component, Serialize, Deserialize, Clone)]
+pub struct Equippable {
+ pub slot : EquipmentSlot
+}
+
// Special component that exists to help serialize the game data
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct SerializationHelper {
diff --git a/src/damage_system.rs b/src/damage_system.rs
index b716d49..93a617d 100644
--- a/src/damage_system.rs
+++ b/src/damage_system.rs
@@ -1,5 +1,5 @@
use specs::prelude::*;
-use super::{CombatStats, SufferDamage, Player, GameLog, Name};
+use super::{CombatStats, SufferDamage, Player, GameLog, Name, RunState};
use rltk::{console};
pub struct DamageSystem {}
@@ -25,7 +25,10 @@ pub fn delete_the_dead(ecs : &mut World) {
}
dead.push(entity);
},
- Some(_) => console::log("You are dead")
+ Some(_) => {
+ let mut runstate = ecs.write_resource::<RunState>();
+ *runstate = RunState::GameOver;
+ }
}
}
}
diff --git a/src/gui.rs b/src/gui.rs
index 9a6921c..d437b36 100644
--- a/src/gui.rs
+++ b/src/gui.rs
@@ -3,7 +3,8 @@ use specs::prelude::*;
use super::{CombatStats, Player, GameLog,
MAPHEIGHT, Map, Name,
Position, Point, InBackpack,
- State, Viewshed, RunState};
+ State, Viewshed, RunState,
+ Equipped};
const GUI_HEIGHT: usize = 50 - MAPHEIGHT - 1;
@@ -89,6 +90,23 @@ pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }
}
+
+#[derive(PartialEq, Copy, Clone)]
+pub enum GameOverResult { NoSelection, QuitToMenu }
+
+pub fn game_over(ctx : &mut Rltk) -> GameOverResult {
+ ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Your journey has ended!");
+ ctx.print_color_centered(17, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "One day, we'll tell you all about how you did.");
+ ctx.print_color_centered(18, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "That day, sadly, is not in this chapter..");
+
+ ctx.print_color_centered(20, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Press any key to return to the menu.");
+
+ match ctx.key {
+ None => GameOverResult::NoSelection,
+ Some(_) => GameOverResult::QuitToMenu
+ }
+}
+
pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenuResult, Option<Point>) {
let player_entity = gs.ecs.fetch::<Entity>();
let player_pos = gs.ecs.fetch::<Point>();
@@ -134,6 +152,10 @@ pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenu
pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
ctx.draw_box(0, 38, 79, GUI_HEIGHT, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+ let map = ecs.fetch::<Map>();
+ let depth = format!("Depth: {}", map.depth);
+ ctx.print_color(2, 38, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &depth);
+
let combat_stats = ecs.read_storage::<CombatStats>();
let players = ecs.read_storage::<Player>();
for (_player, stats) in (&players, &combat_stats).join() {
@@ -160,7 +182,55 @@ pub fn draw_ui(ecs: &World, ctx : &mut Rltk) {
}
#[derive(PartialEq, Copy, Clone)]
-pub enum ItemMenuResult { Cancel, NoResponse, Selected }
+pub enum ItemMenuResult {
+ Cancel,
+ NoResponse,
+ Selected
+}
+
+pub fn remove_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
+ let player_entity = gs.ecs.fetch::<Entity>();
+ let names = gs.ecs.read_storage::<Name>();
+ let backpack = gs.ecs.read_storage::<Equipped>();
+ let entities = gs.ecs.entities();
+
+ let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
+ let count = inventory.count();
+
+ let mut y = (25 - (count / 2)) as i32;
+ ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
+ ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Remove Which Item?");
+ ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");
+
+ let mut equippable : Vec<Entity> = Vec::new();
+ let mut j = 0;
+ for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
+ ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
+ ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), 97+j as rltk::FontCharType);
+ ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
+
+ ctx.print(21, y, &name.name.to_string());
+ equippable.push(entity);
+ y += 1;
+ j += 1;
+ }
+
+ match ctx.key {
+ None => (ItemMenuResult::NoResponse, None),
+ Some(key) => {
+ match key {
+ VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
+ _ => {
+ let selection = rltk::letter_to_option(key);
+ if selection > -1 && selection < count as i32 {
+ return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
+ }
+ (ItemMenuResult::NoResponse, None)
+ }
+ }
+ }
+ }
+}
pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
let player_entity = gs.ecs.fetch::<Entity>();
diff --git a/src/inventory_system.rs b/src/inventory_system.rs
index cfff4db..ab08aad 100644
--- a/src/inventory_system.rs
+++ b/src/inventory_system.rs
@@ -4,7 +4,8 @@ use crate::{Consumable, ProvidesHealing, InflictsDamage};
use super::{WantsToPickupItem, Name, InBackpack,
Position, GameLog, WantsToUseItem,
CombatStats, Item, WantsToDropItem,
- Map, SufferDamage, AreaOfEffect};
+ Map, SufferDamage, AreaOfEffect,
+ Equippable,Equipped,WantsToRemoveItem};
pub struct ItemCollectionSystem {}
@@ -34,6 +35,29 @@ impl<'a> System<'a> for ItemCollectionSystem {
}
}
+pub struct ItemRemoveSystem {}
+
+impl<'a> System<'a> for ItemRemoveSystem {
+ #[allow(clippy::type_complexity)]
+ type SystemData = (
+ Entities<'a>,
+ WriteStorage<'a, WantsToRemoveItem>,
+ WriteStorage<'a, Equipped>,
+ WriteStorage<'a, InBackpack>
+ );
+
+ fn run(&mut self, data : Self::SystemData) {
+ let (entities, mut wants_remove, mut equipped, mut backpack) = data;
+
+ for (entity, to_remove) in (&entities, &wants_remove).join() {
+ equipped.remove(to_remove.item);
+ backpack.insert(to_remove.item, InBackpack{ owner: entity }).expect("Unable to insert backpack");
+ }
+
+ wants_remove.clear();
+ }
+}
+
pub struct ItemUseSystem {}
impl<'a> System<'a> for ItemUseSystem {
@@ -51,6 +75,9 @@ impl<'a> System<'a> for ItemUseSystem {
ReadStorage<'a, InflictsDamage>,
WriteStorage<'a, SufferDamage>,
ReadStorage<'a, AreaOfEffect>,
+ ReadStorage<'a, Equippable>,
+ WriteStorage<'a, Equipped>,
+ WriteStorage<'a, InBackpack>
);
@@ -67,7 +94,10 @@ impl<'a> System<'a> for ItemUseSystem {
_item,
inflict_damage,
mut suffer_damage,
- aoe) = data;
+ aoe,
+ equippable,
+ mut equipped,
+ mut backpack) = data;
for (entity, useitem) in (&entities, &wants_use).join() {
@@ -100,6 +130,37 @@ impl<'a> System<'a> for ItemUseSystem {
}
}
+ let item_equippable = equippable.get(useitem.item);
+ match item_equippable {
+ None => {}
+ Some(can_equip) => {
+ let target_slot = can_equip.slot;
+ let target = targets[0];
+
+ // Remove any items the target has in the item's slot
+ let mut to_unequip : Vec<Entity> = Vec::new();
+ for (item_entity, already_equipped, name) in (&entities, &equipped, &names).join() {
+ if already_equipped.owner == target && already_equipped.slot == target_slot {
+ to_unequip.push(item_entity);
+ if target == *player_entity {
+ gamelog.entries.push(format!("You unequip {}.", name.name));
+ }
+ }
+ }
+ for item in to_unequip.iter() {
+ equipped.remove(*item);
+ backpack.insert(*item, InBackpack{ owner: target }).expect("Unable to insert backpack entry");
+ }
+
+ // Wield the item
+ equipped.insert(useitem.item, Equipped{ owner: target, slot: target_slot }).expect("Unable to insert equipped component");
+ backpack.remove(useitem.item);
+ if target == *player_entity {
+ gamelog.entries.push(format!("You equip {}.", names.get(useitem.item).unwrap().name));
+ }
+ }
+ }
+
// If it inflicts damage, apply it to the target cell
let item_damages = inflict_damage.get(useitem.item);
match item_damages {
diff --git a/src/main.rs b/src/main.rs
index 425ddd2..78f873a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -29,6 +29,8 @@ mod spawner;
mod inventory_system;
use inventory_system::*;
mod saveload_system;
+mod random_table;
+use random_table::*;
// ***** //
// STATE //
@@ -49,6 +51,9 @@ pub enum RunState {
menu_selection: gui::MainMenuSelection
},
SaveGame,
+ NextLevel,
+ ShowRemoveItem,
+ GameOver,
}
pub struct State {
@@ -73,8 +78,147 @@ impl State {
items.run_now(&self.ecs);
let mut drop_items = ItemDropSystem{};
drop_items.run_now(&self.ecs);
+ let mut item_remove = ItemRemoveSystem{};
+ item_remove.run_now(&self.ecs);
self.ecs.maintain();
}
+
+ fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> {
+ let entities = self.ecs.entities();
+ let player = self.ecs.read_storage::<Player>();
+ let backpack = self.ecs.read_storage::<InBackpack>();
+ let player_entity = self.ecs.fetch::<Entity>();
+ let equipped = self.ecs.read_storage::<Equipped>();
+
+ let mut to_delete : Vec<Entity> = Vec::new();
+ for entity in entities.join() {
+ let mut should_delete = true;
+
+ // Don't delete the player
+ let p = player.get(entity);
+ if let Some(_p) = p {
+ should_delete = false;
+ }
+
+ // Don't delete the player's equipment
+ let bp = backpack.get(entity);
+ if let Some(bp) = bp {
+ if bp.owner == *player_entity {
+ should_delete = false;
+ }
+ }
+
+ // Don't delete items currently equipped by the player
+ let eq = equipped.get(entity);
+ if let Some(eq) = eq {
+ if eq.owner == *player_entity {
+ should_delete = false;
+ }
+ }
+
+ if should_delete {
+ to_delete.push(entity);
+ }
+ }
+
+ to_delete
+ }
+
+ fn goto_next_level(&mut self) {
+ // Delete entities that aren't the player or his/her 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 a new map and place the player
+ let worldmap;
+ let current_depth;
+ {
+ let mut worldmap_resource = self.ecs.write_resource::<Map>();
+ current_depth = worldmap_resource.depth;
+ *worldmap_resource = Map::new_map_rooms_and_corridors(current_depth + 1);
+ worldmap = worldmap_resource.clone();
+ }
+
+ // Spawn bad guys
+ for room in worldmap.rooms.iter().skip(1) {
+ spawner::spawn_room(&mut self.ecs, room, current_depth);
+ }
+
+ // Place the player and update resources
+ let (player_x, player_y) = worldmap.rooms[0].center();
+ let mut player_position = self.ecs.write_resource::<Point>();
+ *player_position = Point::new(player_x, player_y);
+ let mut position_components = self.ecs.write_storage::<Position>();
+ let player_entity = self.ecs.fetch::<Entity>();
+ 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;
+ }
+
+ // Mark the player's visibility as dirty
+ let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
+ let vs = viewshed_components.get_mut(*player_entity);
+ if let Some(vs) = vs {
+ vs.dirty = true;
+ }
+
+ // Notify the player and give them some health
+ let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
+ gamelog.entries.push("You descend to the next level, and take a moment to heal.".to_string());
+ let mut player_health_store = self.ecs.write_storage::<CombatStats>();
+ 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);
+ }
+ }
+
+ fn game_over_cleanup(&mut self) {
+ // Delete everything
+ let mut to_delete = Vec::new();
+ for e in self.ecs.entities().join() {
+ to_delete.push(e);
+ }
+ for del in to_delete.iter() {
+ self.ecs.delete_entity(*del).expect("Deletion failed");
+ }
+
+ // Build a new map and place the player
+ let worldmap;
+ {
+ let mut worldmap_resource = self.ecs.write_resource::<Map>();
+ *worldmap_resource = Map::new_map_rooms_and_corridors(1);
+ worldmap = worldmap_resource.clone();
+ }
+
+ // Spawn bad guys
+ for room in worldmap.rooms.iter().skip(1) {
+ spawner::spawn_room(&mut self.ecs, room, 1);
+ }
+
+ // Place the player and update resources
+ let (player_x, player_y) = worldmap.rooms[0].center();
+ let player_entity = spawner::player(&mut self.ecs, player_x, player_y);
+ let mut player_position = self.ecs.write_resource::<Point>();
+ *player_position = Point::new(player_x, player_y);
+ let mut position_components = self.ecs.write_storage::<Position>();
+ let mut player_entity_writer = self.ecs.write_resource::<Entity>();
+ *player_entity_writer = player_entity;
+ 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;
+ }
+
+ // Mark the player's visibility as dirty
+ let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
+ let vs = viewshed_components.get_mut(player_entity);
+ if let Some(vs) = vs {
+ vs.dirty = true;
+ }
+ }
}
impl GameState for State {
@@ -193,6 +337,33 @@ impl GameState for State {
saveload_system::save_game(&mut self.ecs);
newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame };
}
+ RunState::NextLevel => {
+ self.goto_next_level();
+ newrunstate = RunState::PreRun;
+ }
+ RunState::ShowRemoveItem => {
+ let result = gui::remove_item_menu(self, ctx);
+ match result.0 {
+ gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
+ gui::ItemMenuResult::NoResponse => {}
+ gui::ItemMenuResult::Selected => {
+ let item_entity = result.1.unwrap();
+ let mut intent = self.ecs.write_storage::<WantsToRemoveItem>();
+ intent.insert(*self.ecs.fetch::<Entity>(), WantsToRemoveItem{ item: item_entity }).expect("Unable to insert intent");
+ newrunstate = RunState::PlayerTurn;
+ }
+ }
+ }
+ RunState::GameOver => {
+ let result = gui::game_over(ctx);
+ match result {
+ gui::GameOverResult::NoSelection => {}
+ gui::GameOverResult::QuitToMenu => {
+ self.game_over_cleanup();
+ newrunstate = RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame };
+ }
+ }
+ }
}
{
@@ -242,10 +413,15 @@ fn main() -> rltk::BError {
gs.ecs.register::<AreaOfEffect>();
gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>();
+ gs.ecs.register::<Equippable>();
+ gs.ecs.register::<Equipped>();
+ gs.ecs.register::<MeleePowerBonus>();
+ gs.ecs.register::<DefenseBonus>();
+ gs.ecs.register::<WantsToRemoveItem>();
gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::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].center();
let player_entity = spawner::player(&mut gs.ecs, player_x, player_y);
@@ -256,7 +432,7 @@ fn main() -> rltk::BError {
gs.ecs.insert(rng);
for room in map.rooms.iter().skip(1) {
- spawner::spawn_room(&mut gs.ecs, room);
+ spawner::spawn_room(&mut gs.ecs, room, 1);
}
gs.ecs.insert(map);
diff --git a/src/map.rs b/src/map.rs
index 26f6ae5..69df5f7 100644
--- a/src/map.rs
+++ b/src/map.rs
@@ -11,7 +11,8 @@ pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum TileType {
Wall,
- Floor
+ Floor,
+ DownStairs,
}
#[derive(Default, Serialize, Deserialize, Clone)]
@@ -23,6 +24,8 @@ pub struct Map {
pub revealed_tiles : Vec<bool>,
pub visible_tiles: Vec<bool>,
pub blocked : Vec<bool>,
+ pub depth: i32,
+
#[serde(skip_serializing)]
#[serde(skip_deserializing)]
pub tile_content : Vec<Vec<Entity>>
@@ -77,7 +80,7 @@ impl 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(),
@@ -86,6 +89,7 @@ impl Map {
revealed_tiles : vec![false; MAPCOUNT],
visible_tiles : vec![false; MAPCOUNT],
blocked : vec![false; MAPCOUNT],
+ depth: new_depth,
tile_content : vec![Vec::new(); MAPCOUNT]
};
@@ -119,10 +123,14 @@ impl Map {
map.apply_horizontal_tunnel(prev_x, new_x, new_y);
}
}
-
+
map.rooms.push(new_room);
}
}
+
+ let stairs_position = map.rooms[map.rooms.len()-1].center();
+ let stairs_idx = map.xy_idx(stairs_position.0, stairs_position.1);
+ map.tiles[stairs_idx] = TileType::DownStairs;
map
}
@@ -219,6 +227,10 @@ pub fn draw_map(ecs: &World, ctx : &mut Rltk) {
glyph = rltk::to_cp437('#');
fg = RGB::from_f32(1.0, 0.6, 0.);
}
+ TileType::DownStairs => {
+ glyph = rltk::to_cp437('>');
+ fg = RGB::from_f32(0.0,1.0,1.0);
+ }
}
if !map.visible_tiles[idx] { fg = fg.to_greyscale() }
ctx.set(x, y, fg, RGB::from_f32(0., 0., 0.), glyph);
diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs
index 7d8a090..9f87a27 100644
--- a/src/melee_combat_system.rs
+++ b/src/melee_combat_system.rs
@@ -1,5 +1,7 @@
use specs::prelude::*;
-use super::{CombatStats, WantsToMelee, Name, SufferDamage, GameLog};
+use super::{CombatStats, WantsToMelee, Name,
+ SufferDamage, GameLog, MeleePowerBonus,
+ DefenseBonus, Equipped};
pub struct MeleeCombatSystem {}
@@ -9,19 +11,36 @@ impl<'a> System<'a> for MeleeCombatSystem {
WriteStorage<'a, WantsToMelee>,
ReadStorage<'a, Name>,
ReadStorage<'a, CombatStats>,
- WriteStorage<'a, SufferDamage>
+ WriteStorage<'a, SufferDamage>,
+ ReadStorage<'a, MeleePowerBonus>,
+ ReadStorage<'a, DefenseBonus>,
+ ReadStorage<'a, Equipped>
);
fn run(&mut self, data : Self::SystemData) {
- let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage) = data;
+ let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage, melee_power_bonuses, defense_bonuses, equipped) = data;
- for (_entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
+ for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
if stats.hp > 0 {
+ let mut offensive_bonus = 0;
+ for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() {
+ if equipped_by.owner == entity {
+ offensive_bonus += power_bonus.power;
+ }
+ }
+
let target_stats = combat_stats.get(wants_melee.target).unwrap();
if target_stats.hp > 0 {
let target_name = names.get(wants_melee.target).unwrap();
- let damage = i32::max(0, stats.power - target_stats.defense);
+ let mut defensive_bonus = 0;
+ for (_item_entity, defense_bonus, equipped_by) in (&entities, &defense_bonuses, &equipped).join() {
+ if equipped_by.owner == wants_melee.target {
+ defensive_bonus += defense_bonus.defense;
+ }
+ }
+
+ let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus));
if damage == 0 {
log.entries.push(format!("{} is unable to hurt {}", &name.name, &target_name.name));
diff --git a/src/player.rs b/src/player.rs
index 6f352e5..655e8ee 100644
--- a/src/player.rs
+++ b/src/player.rs
@@ -3,7 +3,8 @@ use specs::prelude::*;
use super::{Position, Player, WantsToMelee,
Map, State, Viewshed,
RunState, CombatStats, WantsToPickupItem,
- GameLog, Item};
+ GameLog, Item, TileType,
+ Monster};
use std::cmp::{min, max};
fn get_item(ecs: &mut World) {
@@ -110,6 +111,18 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
VirtualKeyCode::I => return RunState::ShowInventory,
VirtualKeyCode::X => return RunState::ShowDropItem,
+ VirtualKeyCode::Period => {
+ if try_next_level(&mut gs.ecs) {
+ return RunState::NextLevel;
+ }
+ }
+
+ VirtualKeyCode::R => return RunState::ShowRemoveItem,
+
+ // Skip Turn
+ VirtualKeyCode::Numpad5 => return skip_turn(&mut gs.ecs),
+ VirtualKeyCode::Space => return skip_turn(&mut gs.ecs),
+
// Save and Quit
VirtualKeyCode::Escape => return RunState::SaveGame,
@@ -117,4 +130,46 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState {
},
}
RunState::PlayerTurn
+}
+
+fn skip_turn(ecs: &mut World) -> RunState {
+ let player_entity = ecs.fetch::<Entity>();
+ let viewshed_components = ecs.read_storage::<Viewshed>();
+ let monsters = ecs.read_storage::<Monster>();
+
+ let worldmap_resource = ecs.fetch::<Map>();
+
+ 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::<CombatStats>();
+ let player_hp = health_components.get_mut(*player_entity).unwrap();
+ player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp);
+ }
+
+ RunState::PlayerTurn
+}
+
+pub fn try_next_level(ecs: &mut World) -> bool {
+ let player_pos = ecs.fetch::<Point>();
+ let map = ecs.fetch::<Map>();
+ let player_idx = map.xy_idx(player_pos.x, player_pos.y);
+ if map.tiles[player_idx] == TileType::DownStairs {
+ true
+ } else {
+ let mut gamelog = ecs.fetch_mut::<GameLog>();
+ gamelog.entries.push("There is no way down from here.".to_string());
+ false
+ }
}
\ No newline at end of file
diff --git a/src/random_table.rs b/src/random_table.rs
new file mode 100644
index 0000000..6406316
--- /dev/null
+++ b/src/random_table.rs
@@ -0,0 +1,49 @@
+use rltk::RandomNumberGenerator;
+
+pub struct RandomEntry {
+ name : String,
+ weight : i32
+}
+
+impl RandomEntry {
+ pub fn new<S:ToString>(name: S, weight: i32) -> RandomEntry {
+ RandomEntry{ name: name.to_string(), weight }
+ }
+}
+
+#[derive(Default)]
+pub struct RandomTable {
+ entries : Vec<RandomEntry>,
+ total_weight : i32
+}
+
+impl RandomTable {
+ pub fn new() -> RandomTable {
+ RandomTable{ entries: Vec::new(), total_weight: 0 }
+ }
+
+ pub fn add<S:ToString>(mut self, name : S, weight: i32) -> RandomTable {
+ if weight > 0 {
+ self.total_weight += weight;
+ self.entries.push(RandomEntry::new(name.to_string(), weight));
+ }
+ self
+ }
+
+ pub fn roll(&self, rng : &mut RandomNumberGenerator) -> String {
+ if self.total_weight == 0 { return "None".to_string(); }
+ let mut roll = rng.roll_dice(1, self.total_weight)-1;
+ let mut index : usize = 0;
+
+ while roll > 0 {
+ if roll < self.entries[index].weight {
+ return self.entries[index].name.clone();
+ }
+
+ roll -= self.entries[index].weight;
+ index += 1;
+ }
+
+ "None".to_string()
+ }
+}
\ No newline at end of file
diff --git a/src/saveload_system.rs b/src/saveload_system.rs
index 5394dbc..a8c3c27 100644
--- a/src/saveload_system.rs
+++ b/src/saveload_system.rs
@@ -58,7 +58,7 @@ pub fn save_game(ecs : &mut World) {
serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster,
Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage,
AreaOfEffect, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
- WantsToDropItem, SerializationHelper
+ WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem
);
}
@@ -97,7 +97,7 @@ pub fn load_game(ecs: &mut World) {
deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster,
Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage,
AreaOfEffect, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
- WantsToDropItem, SerializationHelper
+ WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleePowerBonus, DefenseBonus, WantsToRemoveItem
);
}
diff --git a/src/spawner.rs b/src/spawner.rs
index 09a955d..3682048 100644
--- a/src/spawner.rs
+++ b/src/spawner.rs
@@ -5,25 +5,142 @@ use super::{CombatStats, Player, Renderable,
Monster, BlocksTile, Rect,
MAPWIDTH, Item, ProvidesHealing,
Consumable, InflictsDamage, Ranged,
- AreaOfEffect,SerializeMe};
+ AreaOfEffect,SerializeMe,RandomTable,
+ EquipmentSlot,Equippable, MeleePowerBonus,
+ DefenseBonus};
use specs::saveload::{MarkedBuilder, SimpleMarker};
+use std::collections::HashMap;
const MAX_MONSTERS : i32 = 4;
-const MAX_ITEMS : i32 = 2; // PER MAP
-fn random_item(ecs: &mut World, x: i32, y: i32) {
- let roll :i32;
+fn room_table(map_depth: i32) -> RandomTable {
+ RandomTable::new()
+ .add("Goblin", 10)
+ .add("Orc", 1 + map_depth)
+ .add("Health Potion", 7)
+ .add("Fireball Scroll", 2 + map_depth)
+ .add("Magic Missile Scroll", 4 + map_depth)
+ .add("Staff", 3)
+ .add("Holy Cross", 3)
+ .add("Shepards Staff", map_depth - 1)
+ .add("Tower Shield", map_depth - 1)
+}
+
+#[allow(clippy::map_entry)]
+pub fn spawn_room(ecs: &mut World, room : &Rect, map_depth: i32) {
+ let spawn_table = room_table(map_depth);
+ let mut spawn_points : HashMap<usize, String> = HashMap::new();
+
+ // Scope to keep the borrow checker happy
{
let mut rng = ecs.write_resource::<RandomNumberGenerator>();
- roll = rng.roll_dice(1, 3);
+ let num_spawns = rng.roll_dice(1, MAX_MONSTERS + 3) + (map_depth - 1) - 3;
+
+ for _i in 0 .. num_spawns {
+ let mut added = false;
+ let mut tries = 0;
+ while !added && tries < 20 {
+ let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
+ let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
+ let idx = (y * MAPWIDTH) + x;
+ if !spawn_points.contains_key(&idx) {
+ spawn_points.insert(idx, spawn_table.roll(&mut rng));
+ added = true;
+ } else {
+ tries += 1;
+ }
+ }
+ }
}
- match roll {
- 1 => { health_potion(ecs, x, y) }
- 2 => { fireball_scroll(ecs, x, y) }
- _ => { magic_missile_scroll(ecs, x, y) }
+
+ // Actually spawn the monsters
+ for spawn in spawn_points.iter() {
+ let x = (*spawn.0 % MAPWIDTH) as i32;
+ let y = (*spawn.0 / MAPWIDTH) as i32;
+
+ match spawn.1.as_ref() {
+ "Goblin" => goblin(ecs, x, y),
+ "Orc" => orc(ecs, x, y),
+ "Health Potion" => health_potion(ecs, x, y),
+ "Fireball Scroll" => fireball_scroll(ecs, x, y),
+ "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y),
+ "Staff" => staff(ecs, x, y),
+ "Holy Cross" => holy_cross(ecs, x, y),
+ "Shepards Staff" => shepards_staff(ecs, x, y),
+ "Tower Sheild" => tower_shield(ecs, x, y),
+ _ => {}
+ }
}
}
+fn shepards_staff(ecs: &mut World, x: i32, y: i32) {
+ ecs.create_entity()
+ .with(Position{ x, y })
+ .with(Renderable{
+ glyph: rltk::to_cp437('/'),
+ fg: RGB::named(rltk::YELLOW),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 2
+ })
+ .with(Name{ name : "Shepards Staff".to_string() })
+ .with(Item{})
+ .with(Equippable{ slot: EquipmentSlot::Melee })
+ .with(MeleePowerBonus{ power: 4 })
+ .marked::<SimpleMarker<SerializeMe>>()
+ .build();
+}
+
+fn tower_shield(ecs: &mut World, x: i32, y: i32) {
+ ecs.create_entity()
+ .with(Position{ x, y })
+ .with(Renderable{
+ glyph: rltk::to_cp437('('),
+ fg: RGB::named(rltk::YELLOW),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 2
+ })
+ .with(Name{ name : "Tower Shield".to_string() })
+ .with(Item{})
+ .with(Equippable{ slot: EquipmentSlot::Shield })
+ .with(DefenseBonus{ defense: 3 })
+ .marked::<SimpleMarker<SerializeMe>>()
+ .build();
+}
+
+fn staff(ecs: &mut World, x: i32, y: i32) {
+ ecs.create_entity()
+ .with(Position{ x, y })
+ .with(Renderable{
+ glyph: rltk::to_cp437('|'),
+ fg: RGB::named(rltk::CYAN),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 2
+ })
+ .with(Name{ name : "Staff".to_string() })
+ .with(Item{})
+ .marked::<SimpleMarker<SerializeMe>>()
+ .with(Equippable{ slot: EquipmentSlot::Melee })
+ .with(MeleePowerBonus{power: 2})
+ .build();
+}
+
+fn holy_cross(ecs: &mut World, x: i32, y: i32) {
+ ecs.create_entity()
+ .with(Position{ x, y })
+ .with(Renderable{
+ glyph: rltk::to_cp437('+'),
+ fg: RGB::named(rltk::CYAN),
+ bg: RGB::named(rltk::BLACK),
+ render_order: 2
+ })
+ .with(Name{ name : "Holy Cross".to_string() })
+ .with(Item{})
+ .marked::<SimpleMarker<SerializeMe>>()
+ .with(Equippable{ slot: EquipmentSlot::Shield })
+ .with(DefenseBonus{ defense: 1})
+ .build();
+}
+
fn fireball_scroll(ecs: &mut World, x: i32, y: i32) {
ecs.create_entity()
.with(Position{ x, y })
@@ -78,60 +195,6 @@ fn health_potion(ecs: &mut World, x: i32, y: i32) {
.build();
}
-/// Fills a room with stuff!
-pub fn spawn_room(ecs: &mut World, room : &Rect) {
- let mut monster_spawn_points : Vec<usize> = Vec::new();
- let mut item_spawn_points : Vec<usize> = Vec::new();
-
- // Scope to keep the borrow checker happy
- {
- let mut rng = ecs.write_resource::<RandomNumberGenerator>();
- let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;
- let num_items = rng.roll_dice(1, MAX_ITEMS + 2) - 3;
-
-
- for _i in 0 .. num_monsters {
- let mut added = false;
- while !added {
- let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
- let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
- let idx = (y * MAPWIDTH) + x;
- if !monster_spawn_points.contains(&idx) {
- monster_spawn_points.push(idx);
- added = true;
- }
- }
- }
-
- for _i in 0 .. num_items {
- let mut added = false;
- while !added {
- let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
- let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
- let idx = (y * MAPWIDTH) + x;
- if !item_spawn_points.contains(&idx) {
- item_spawn_points.push(idx);
- added = true;
- }
- }
- }
- }
-
- // Actually spawn the monsters
- for idx in monster_spawn_points.iter() {
- let x = *idx % MAPWIDTH;
- let y = *idx / MAPWIDTH;
- random_monster(ecs, x as i32, y as i32);
- }
-
- // Actually spawn the items
- for idx in item_spawn_points.iter() {
- let x = *idx % MAPWIDTH;
- let y = *idx / MAPWIDTH;
- random_item(ecs, x as i32, y as i32);
- }
-}
-
/// Spawns the player and returns his/her entity object.
pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
ecs
@@ -151,19 +214,6 @@ pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
.build()
}
-/// Spawns a random monster at a given location
-pub fn random_monster(ecs: &mut World, x: i32, y: i32) {
- let roll :i32;
- {
- let mut rng = ecs.write_resource::<RandomNumberGenerator>();
- roll = rng.roll_dice(1, 2);
- }
- match roll {
- 1 => { orc(ecs, x, y) }
- _ => { goblin(ecs, x, y) }
- }
-}
-
fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('o'), "Orc"); }
fn goblin(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('g'), "Goblin"); }