basic equippables

This commit is contained in:
Llywelwyn 2023-07-11 07:43:35 +01:00
parent 595ec61332
commit 8d04c40389
8 changed files with 330 additions and 36 deletions

View file

@ -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,

View file

@ -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<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 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<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 ranged_target(gs: &mut State, ctx: &mut Rltk, range: i32, aoe: i32) -> (ItemMenuResult, Option<Point>) {
let player_entity = gs.ecs.fetch::<Entity>();
let player_pos = gs.ecs.fetch::<Point>();

View file

@ -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<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::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();
}
}

View file

@ -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::<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() {
@ -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::<WantsToRemoveItem>();
intent
.insert(*self.ecs.fetch::<Entity>(), 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::<WantsToMelee>();
gs.ecs.register::<SufferDamage>();
gs.ecs.register::<Item>();
gs.ecs.register::<Equippable>();
gs.ecs.register::<Equipped>();
gs.ecs.register::<MeleePowerBonus>();
gs.ecs.register::<DefenceBonus>();
gs.ecs.register::<Cursed>();
gs.ecs.register::<ProvidesHealing>();
gs.ecs.register::<InflictsDamage>();
@ -407,6 +436,7 @@ fn main() -> rltk::BError {
gs.ecs.register::<InBackpack>();
gs.ecs.register::<WantsToPickupItem>();
gs.ecs.register::<WantsToDropItem>();
gs.ecs.register::<WantsToRemoveItem>();
gs.ecs.register::<WantsToUseItem>();
gs.ecs.register::<Consumable>();
gs.ecs.register::<Destructible>();

View file

@ -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);
}
}

View file

@ -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::<Name>();
// Push every entity name in the pile to a vector of strings
let mut item_names: Vec<String> = 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;

View file

@ -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
);
}

View file

@ -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::<SimpleMarker<SerializeMe>>()
.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::<SimpleMarker<SerializeMe>>()
.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::<SimpleMarker<SerializeMe>>()
.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::<SimpleMarker<SerializeMe>>()
.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::<SimpleMarker<SerializeMe>>()
.build();
}