diff --git a/src/components.rs b/src/components.rs index 6f29d63..d4f9eee 100644 --- a/src/components.rs +++ b/src/components.rs @@ -84,6 +84,33 @@ impl SufferDamage { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Item {} +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum EquipmentSlot { + Melee, + Shield, +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct MeleePowerBonus { + pub amount: i32, +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct DefenceBonus { + pub amount: i32, +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct Equippable { + pub slot: EquipmentSlot, +} + +#[derive(Component, ConvertSaveload, Clone)] +pub struct Equipped { + pub owner: Entity, + pub slot: EquipmentSlot, +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Cursed {} @@ -131,6 +158,11 @@ pub struct WantsToDropItem { pub item: Entity, } +#[derive(Component, Debug, ConvertSaveload)] +pub struct WantsToRemoveItem { + pub item: Entity, +} + #[derive(Component, Debug, ConvertSaveload)] pub struct WantsToUseItem { pub item: Entity, diff --git a/src/gui.rs b/src/gui.rs index bfce50d..460c641 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,6 +1,6 @@ use super::{ - gamelog, rex_assets::RexAssets, CombatStats, InBackpack, Map, Name, Player, Point, Position, RunState, State, - Viewshed, + gamelog, rex_assets::RexAssets, CombatStats, Equipped, InBackpack, Map, Name, Player, Point, Position, RunState, + State, Viewshed, }; use rltk::{Rltk, VirtualKeyCode, RGB}; use specs::prelude::*; @@ -202,6 +202,48 @@ pub fn drop_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option } } +pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Option) { + let player_entity = gs.ecs.fetch::(); + let names = gs.ecs.read_storage::(); + let backpack = gs.ecs.read_storage::(); + 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 what?"); + ctx.print_color(18, y + count as i32 + 1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESC to cancel"); + + let mut equippable: Vec = 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 ranged_target(gs: &mut State, ctx: &mut Rltk, range: i32, aoe: i32) -> (ItemMenuResult, Option) { let player_entity = gs.ecs.fetch::(); let player_pos = gs.ecs.fetch::(); diff --git a/src/inventory_system.rs b/src/inventory_system.rs index e4ba2e2..d0d8ed6 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,7 +1,8 @@ use super::{ - gamelog, CombatStats, Confusion, Consumable, Cursed, Destructible, InBackpack, InflictsDamage, MagicMapper, Map, - Name, ParticleBuilder, Point, Position, ProvidesHealing, RunState, SufferDamage, WantsToDropItem, - WantsToPickupItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME, + gamelog, CombatStats, Confusion, Consumable, Cursed, Destructible, Equippable, Equipped, InBackpack, + InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Position, ProvidesHealing, RunState, SufferDamage, + WantsToDropItem, WantsToPickupItem, WantsToRemoveItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, + LONG_PARTICLE_LIFETIME, }; use specs::prelude::*; @@ -59,6 +60,9 @@ impl<'a> System<'a> for ItemUseSystem { WriteStorage<'a, Confusion>, ReadStorage<'a, MagicMapper>, WriteExpect<'a, RunState>, + ReadStorage<'a, Equippable>, + WriteStorage<'a, Equipped>, + WriteStorage<'a, InBackpack>, ); fn run(&mut self, data: Self::SystemData) { @@ -81,6 +85,9 @@ impl<'a> System<'a> for ItemUseSystem { mut confused, magic_mapper, mut runstate, + equippable, + mut equipped, + mut backpack, ) = data; for (entity, wants_to_use) in (&entities, &wants_to_use).join() { @@ -153,6 +160,48 @@ impl<'a> System<'a> for ItemUseSystem { } } + // EQUIPMENT + let item_equippable = equippable.get(wants_to_use.item); + match item_equippable { + None => {} + Some(can_equip) => { + let target_slot = can_equip.slot; + let target = targets[0]; + + // Room any items target has in item's slot + let mut to_unequip: Vec = 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::Logger::new() + .append("You unequip the") + .item_name_n(&item_being_used.name) + .period() + .log(); + } + } + } + for item in to_unequip.iter() { + equipped.remove(*item); + backpack.insert(*item, InBackpack { owner: target }).expect("Unable to insert backpack"); + } + + // Wield the item + equipped + .insert(wants_to_use.item, Equipped { owner: target, slot: target_slot }) + .expect("Unable to insert equipped component"); + backpack.remove(wants_to_use.item); + if target == *player_entity { + gamelog::Logger::new() + .append("You equip the") + .item_name_n(&item_being_used.name) + .period() + .log(); + } + } + } + // HEALING ITEM let item_heals = provides_healing.get(wants_to_use.item); match item_heals { @@ -339,3 +388,22 @@ impl<'a> System<'a> for ItemDropSystem { wants_drop.clear(); } } + +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(); + } +} diff --git a/src/main.rs b/src/main.rs index 4cde3f9..63783b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ pub enum RunState { MonsterTurn, ShowInventory, ShowDropItem, + ShowRemoveItem, ShowTargeting { range: i32, item: Entity, aoe: i32 }, MainMenu { menu_selection: gui::MainMenuSelection }, SaveGame, @@ -71,8 +72,10 @@ impl State { 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 item_drop_system = ItemDropSystem {}; + item_drop_system.run_now(&self.ecs); + let mut item_remove_system = ItemRemoveSystem {}; + item_remove_system.run_now(&self.ecs); let mut melee_system = MeleeCombatSystem {}; melee_system.run_now(&self.ecs); let mut damage_system = DamageSystem {}; @@ -87,6 +90,7 @@ impl State { let player = self.ecs.read_storage::(); let backpack = self.ecs.read_storage::(); let player_entity = self.ecs.fetch::(); + let equipped = self.ecs.read_storage::(); let mut to_delete: Vec = Vec::new(); for entity in entities.join() { @@ -105,6 +109,12 @@ impl State { should_delete = false; } } + 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); @@ -280,6 +290,21 @@ impl GameState for State { } } } + RunState::ShowRemoveItem => { + let result = gui::remove_item_menu(self, ctx); + match result.0 { + gui::ItemMenuResult::Cancel => new_runstate = RunState::AwaitingInput, + gui::ItemMenuResult::NoResponse => {} + gui::ItemMenuResult::Selected => { + let item_entity = result.1.unwrap(); + let mut intent = self.ecs.write_storage::(); + intent + .insert(*self.ecs.fetch::(), WantsToRemoveItem { item: item_entity }) + .expect("Unable to insert intent"); + new_runstate = RunState::PlayerTurn; + } + } + } RunState::ShowTargeting { range, item, aoe } => { let result = gui::ranged_target(self, ctx, range, aoe); match result.0 { @@ -397,6 +422,10 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); @@ -407,6 +436,7 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index e1e741b..4a35275 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,4 +1,7 @@ -use super::{gamelog, CombatStats, Name, ParticleBuilder, Position, SufferDamage, WantsToMelee}; +use super::{ + gamelog, CombatStats, DefenceBonus, Equipped, MeleePowerBonus, Name, ParticleBuilder, Position, SufferDamage, + WantsToMelee, +}; use specs::prelude::*; pub struct MeleeCombatSystem {} @@ -13,6 +16,9 @@ impl<'a> System<'a> for MeleeCombatSystem { WriteStorage<'a, SufferDamage>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, + ReadStorage<'a, Equipped>, + ReadStorage<'a, DefenceBonus>, + ReadStorage<'a, MeleePowerBonus>, ); fn run(&mut self, data: Self::SystemData) { @@ -25,6 +31,9 @@ impl<'a> System<'a> for MeleeCombatSystem { mut inflict_damage, mut particle_builder, positions, + equipped, + defence_bonuses, + melee_power_bonuses, ) = data; for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { @@ -37,18 +46,20 @@ impl<'a> System<'a> for MeleeCombatSystem { } let target_name = names.get(wants_melee.target).unwrap(); - let pos = positions.get(wants_melee.target); - if let Some(pos) = pos { - particle_builder.request( - pos.x, - pos.y, - rltk::RGB::named(rltk::ORANGE), - rltk::RGB::named(rltk::BLACK), - rltk::to_cp437('‼'), - 150.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.amount; + } } - let damage = i32::max(0, stats.power - target_stats.defence); + let mut defensive_bonus = 0; + for (_item_entity, defence_bonus, equipped_by) in (&entities, &defence_bonuses, &equipped).join() { + if equipped_by.owner == wants_melee.target { + defensive_bonus += defence_bonus.amount; + } + } + let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defence + defensive_bonus)); if damage == 0 { if entity == *player_entity { @@ -96,6 +107,17 @@ impl<'a> System<'a> for MeleeCombatSystem { .period() .log(); } + let pos = positions.get(wants_melee.target); + if let Some(pos) = pos { + particle_builder.request( + pos.x, + pos.y, + rltk::RGB::named(rltk::ORANGE), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('‼'), + 150.0, + ); + } SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); } } diff --git a/src/player.rs b/src/player.rs index 5e5d296..69e7cea 100644 --- a/src/player.rs +++ b/src/player.rs @@ -35,20 +35,30 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { } if !map.blocked[destination_idx] { - // TODO: Refactor - let mut tile_content = "You see ".to_string(); let names = ecs.read_storage::(); + // Push every entity name in the pile to a vector of strings + let mut item_names: Vec = Vec::new(); + let mut some = false; for entity in map.tile_content[destination_idx].iter() { if let Some(name) = names.get(*entity) { - if tile_content != "You see " { - tile_content.push_str(", "); - } - tile_content.push_str(&name.name); + let item_name = &name.name; + item_names.push(item_name.to_string()); + some = true; } } - if tile_content != "You see " { - tile_content.push_str("."); - gamelog::Logger::new().append(tile_content).log() + // If some names were found, append. Logger = logger is necessary + // makes logger called a mutable self. It's not the most efficient + // but it happens infrequently enough (once per player turn at most) + // that it shouldn't matter. + if some { + let mut logger = gamelog::Logger::new().append("You see a"); + for i in 0..item_names.len() { + if i > 0 && i < item_names.len() { + logger = logger.append(", a"); + } + logger = logger.item_name_n(&item_names[i]); + } + logger.period().log(); } pos.x = min((MAPWIDTH as i32) - 1, max(0, pos.x + delta_x)); pos.y = min((MAPHEIGHT as i32) - 1, max(0, pos.y + delta_y)); @@ -127,6 +137,7 @@ pub fn player_input(gs: &mut State, ctx: &mut Rltk) -> RunState { VirtualKeyCode::G => get_item(&mut gs.ecs), VirtualKeyCode::I => return RunState::ShowInventory, VirtualKeyCode::D => return RunState::ShowDropItem, + VirtualKeyCode::R => return RunState::ShowRemoveItem, VirtualKeyCode::Escape => return RunState::SaveGame, _ => { return RunState::AwaitingInput; diff --git a/src/saveload_system.rs b/src/saveload_system.rs index c53e899..0058e9b 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -58,6 +58,10 @@ pub fn save_game(ecs: &mut World) { SufferDamage, WantsToMelee, Item, + Equippable, + Equipped, + MeleePowerBonus, + DefenceBonus, Cursed, Consumable, Destructible, @@ -71,6 +75,7 @@ pub fn save_game(ecs: &mut World) { WantsToPickupItem, WantsToUseItem, WantsToDropItem, + WantsToRemoveItem, SerializationHelper ); } @@ -135,6 +140,10 @@ pub fn load_game(ecs: &mut World) { SufferDamage, WantsToMelee, Item, + Equippable, + Equipped, + MeleePowerBonus, + DefenceBonus, Cursed, Consumable, Destructible, @@ -148,6 +157,7 @@ pub fn load_game(ecs: &mut World) { WantsToPickupItem, WantsToUseItem, WantsToDropItem, + WantsToRemoveItem, SerializationHelper ); } diff --git a/src/spawner.rs b/src/spawner.rs index b4828de..e9d41db 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,7 +1,7 @@ use super::{ - random_table::RandomTable, BlocksTile, CombatStats, Confusion, Consumable, Cursed, Destructible, InflictsDamage, - Item, MagicMapper, Monster, Name, Player, Position, ProvidesHealing, Ranged, Rect, Renderable, SerializeMe, - Viewshed, AOE, MAPWIDTH, + random_table::RandomTable, BlocksTile, CombatStats, Confusion, Consumable, Cursed, DefenceBonus, Destructible, + EquipmentSlot, Equippable, InflictsDamage, Item, MagicMapper, MeleePowerBonus, Monster, Name, Player, Position, + ProvidesHealing, Ranged, Rect, Renderable, SerializeMe, Viewshed, AOE, MAPWIDTH, }; use rltk::{console, RandomNumberGenerator, RGB}; use specs::prelude::*; @@ -116,6 +116,11 @@ pub fn spawn_room(ecs: &mut World, room: &Rect, map_depth: i32) { "goblin" => goblin(ecs, x, y), "goblin chieftain" => goblin_chieftain(ecs, x, y), "orc" => orc(ecs, x, y), + // Equipment + "dagger" => dagger(ecs, x, y), + "shortsword" => shortsword(ecs, x, y), + "buckler" => buckler(ecs, x, y), + "shield" => shield(ecs, x, y), // Potions "weak health potion" => weak_health_potion(ecs, x, y), "health potion" => health_potion(ecs, x, y), @@ -149,14 +154,19 @@ fn mob_table(map_depth: i32) -> RandomTable { .add("orc", 2 + map_depth); } -// 25 potions : 10 scrolls : 2 cursed scrolls -fn item_table(_map_depth: i32) -> RandomTable { +// 6 equipment : 10 potions : 10 scrolls : 2 cursed scrolls +fn item_table(map_depth: i32) -> RandomTable { return RandomTable::new() + // Equipment + .add("dagger", 2) + .add("shortsword", map_depth - 1) + .add("buckler", 2) + .add("shield", 1) // Potions - .add("weak health potion", 20) - .add("health potion", 5) + .add("weak health potion", 7) + .add("health potion", 3) // Scrolls - .add("fireball scroll", 1) + .add("fireball scroll", map_depth - 1) .add("cursed fireball scroll", 1) .add("confusion scroll", 2) .add("magic missile scroll", 5) @@ -337,3 +347,72 @@ fn cursed_magic_map_scroll(ecs: &mut World, x: i32, y: i32) { .marked::>() .build(); } + +// EQUIPMENT +fn dagger(ecs: &mut World, x: i32, y: i32) { + ecs.create_entity() + .with(Position { x, y }) + .with(Renderable { + glyph: rltk::to_cp437('/'), + fg: RGB::named(rltk::GREY), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { name: "dagger".to_string() }) + .with(Item {}) + .with(Equippable { slot: EquipmentSlot::Melee }) + .with(MeleePowerBonus { amount: 1 }) + .marked::>() + .build(); +} +fn shortsword(ecs: &mut World, x: i32, y: i32) { + ecs.create_entity() + .with(Position { x, y }) + .with(Renderable { + glyph: rltk::to_cp437('/'), + fg: RGB::named(rltk::GREY), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { name: "shortsword".to_string() }) + .with(Item {}) + .with(Equippable { slot: EquipmentSlot::Melee }) + .with(MeleePowerBonus { amount: 2 }) + .marked::>() + .build(); +} + +fn buckler(ecs: &mut World, x: i32, y: i32) { + ecs.create_entity() + .with(Position { x, y }) + .with(Renderable { + glyph: rltk::to_cp437('('), + fg: RGB::named(rltk::GREY), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { name: "buckler".to_string() }) + .with(Item {}) + .with(DefenceBonus { amount: 1 }) + .with(Equippable { slot: EquipmentSlot::Shield }) + .marked::>() + .build(); +} + +fn 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::GREY), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { name: "shield".to_string() }) + .with(Item {}) + .with(DefenceBonus { amount: 2 }) + .with(MeleePowerBonus { amount: -1 }) + .with(Equippable { slot: EquipmentSlot::Shield }) + .marked::>() + .build(); +}