We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
13. Inventory and Power-Ups
Here we go we are now going to add in some items to the dungeon. This will help the player, heal and so much more. Let’s first design and create them with components.
Designing Items
We want to first add in: Healing Items and a Dungeon Map. We have the graphics for the 2 items in the dungeonfont.png.
Describing the Items with Components
So we added in many components for the items that we need:
• Item: Potion or Scroll • Name: The name that appears in tooltips and the player’s inventory list • Point: The item’s position on the map • Render: The visual display component for the item
We need to add a new one that will ProvidesHealing component, so open up the components.rs and add the following.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ProvidesHealing{
pub amount: i32
}
The amount field will hold the amount that the item will heal. This will allow you to have potions of different strength. Now let’s add in the Map
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ProvidesDungeonMap;
This is just to denote that the user has access to the DungeonMap
Spawning Potions and Maps
We should now go into the spawner.rs in order to have the ability to spawn the map and potions.
pub fn spawn_healing_potion(ecs: &mut World, pos: Point) {
ecs.push((
Item,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('!'),
},
Name("Healing Potion".to_string()),
ProvidesHealing { amount: 6 },
));
}
pub fn spawn_magic_mapper(ecs: &mut World, pos: Point) {
ecs.push((
Item,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('{'),
},
Name("Dungeon Map".to_string()),
ProvidesDungeonMap,
));
}
We now have a way of providing a World and a point to the spawners and we will be able to place the items in the world. Now we can add a new spawner that will spawn some entities.
pub fn spawn_entity(
ecs: &mut World,
rng: &mut RandomNumberGenerator,
pos: Point
) {
let roll = rng.roll_dice(1, 6);
match roll {
1 => spawn_healing_potion(ecs, pos),
2 => spawn_magic_mapper(ecs, pos),
_ => spawn_monster(ecs, rng, pos),
}
}
So for this we are rolling a 6 sided dice and; spawning a potion, a Map, or spawning a monster depending on the rolls. You will now need to replace all instances of spawn_monster with spawn_entity use your editor to do some now. I recommend you committing the work before hand. Once that is done you can run your game and see the potions and the Maps throughout the map. You just can’t do anything with them yet.
Managing Inventory
Here is where we will be able to add the items to our inventory and then be able to use them. We don’t want to just move over an item and then pick it up we want a way to make it more dynamic, allowing a player to choose when to fill up their limited inventory space with items. The other thing is that we are using a point system in order to render an item. We can use this to our advantage by removing the Point from the item if we pick it up. We will then add a Carried component to the item so that we know that it’s being carried. Let’s head back to components.rs
#[derive(Clone, Copy, PartialEq)]
pub struct Carried(pub Entity);
We now have an other component to add to an entity. Let’s start to be able to pick up and item.
Picking Up Items
So let’s start by adding in the “G” key to the player input which will allow us to pick up and item that shares the same point as the player. Head to player_input.rs and add in some new code. We will need to add access to some other components to the read for the system.
#[system]
#[read_component(Point)]
#[read_component(Player)]
#[read_component(Enemy)]
#[write_component(Health)]
#[read_component(Item)]
#[read_component(Carried)]
...
VirtualKeyCode::G => {
let (player, player_pos) = players
.iter(ecs)
.find_map(|(entity, pos)| Some((*entity, *pos)))
.unwrap();
let mut items = <(Entity, &Item, &Point)>::query();
items
.iter(ecs)
.filter(|(_entity, _item, &item_pos)| item_pos == player_pos)
.for_each(|(entity, _item, _item_pos)| {
commands.remove_component::<Point>(*entity);
commands.add_component(*entity, Carried(player));
});
Point::new(0, 0)
},
We first added in the new components to read, added in a new key press to watch for, pulled the player information to get the position, grabbed all the items, then filtered so that we only pull the items that share the same Point as the player, then removed the Point from the item and then added in the Carried component to the item.
Okay so when you run the game now we can pick up items and they will vanish from the game. They are now Carried but we don’t have a way of knowing that and they are not able to be used.
Displaying Inventory
Now we can work on displaying the Players inventory. Let’s head to the hud.rs to add in the display for the inventory.
#[read_component(Item)]
#[read_component(Carried)]
#[read_component(Name)]
...
let player = <(Entity, &Player)>::query()
.iter(ecs)
.find_map(|(entity, _player)| Some(*entity))
.unwrap();
let mut item_query = <(&Item, &Name, &Carried)>::query();
let mut y = 3;
item_query
.iter(ecs)
.filter(|(_, _, carried)| carried.0 == player)
.for_each(|(_, name, _)| {
draw.batch
.print(Point::new(3, y), format!("{} : {}", y-2, &name.0));
y += 1;
});
if y > 3 {
draw_batch.print_color(
Point::new(3, 2),
"Items carried",
ColorPair::new(YELLOW, BLACK),
);
}
draw_batch.submit(10000).expect("Batch error");
Okay so here is what we did. Added in the new reads for the components, grabbed the player, queried for all items that have (Item, Name, Carried), queried the items for a Some within the vector for the carried items, for each of the items printed the item and a counter number, if we have an item then we draw the “Items Carried” above the list of items.
Run the game and pick up a few items.
Sending an Item Activation Message
We went over this before but when you want an action to take place you should not just run it in the component or the system we need to send a message that will be processed so we can test against other systems. Let’s open component.rs and add a new component ActivateItem
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ActivateItem {
pub used_by: Entity,
pub item: Entity,
}
This is the start of the code to use an item. It says the Entity that is using the item and the Entity (Item) that is being used. Now go back to player_input.rs and add in a new function.
// End of the File
fn use_item(n: usize, ecs: &mut SubWorld, commands: &mut CommandBuffer) -> Point {
let player_entity = <(Entity, &Player)>::query()
.iter(ecs)
.find_map(|(entity, _player)| Some(*entity))
.unwrap();
let item_entity = <(Entity, &Item, &Carried)>::query()
.iter(ecs)
.filter(|(_, _, carried)| carried.0 == player_entity)
.enumerate()
.filter(|(item_count, (_, _, _))| *item_count == n)
.find_map(|(_, (item_entity, _, _))| Some(*item_entity));
if let Some(item_entity) = item_entity {
commands.push((
(),
ActivateItem {
used_by: player_entity,
item: item_entity,
},
));
}
Point::zero()
}
Okay so this does the following: creates a new function, queries Entities for players, Queries the items for (Entity, Item, Carried), Filters for items that have the carried by player, filters for an item in the n position, maps the items into a variable, takes that item and sends an ActivateItem message with the player, and the item.
Now we need to add in all the VirtualKeyCodes for all the items that we might carry.
VirtualKeyCode::Key1 => use_item(0, ecs, commands),
VirtualKeyCode::Key2 => use_item(1, ecs, commands),
VirtualKeyCode::Key3 => use_item(2, ecs, commands),
VirtualKeyCode::Key4 => use_item(3, ecs, commands),
VirtualKeyCode::Key5 => use_item(4, ecs, commands),
VirtualKeyCode::Key6 => use_item(5, ecs, commands),
VirtualKeyCode::Key7 => use_item(6, ecs, commands),
VirtualKeyCode::Key8 => use_item(7, ecs, commands),
VirtualKeyCode::Key9 => use_item(8, ecs, commands),
These are all the new commands keeping in mind that the use_item will issue the message and will need to return a delta for the distance to travel.
Activating Items
We will start off with simply adding a new system for using items. It will listen for a ActivateItem message and then deal with all the backend. As with everything create a new file systems/use_item.rs and then add that to the mod.rs so it can be used.
use crate::prelude::*;
#[system]
#[read_component(ActivateItem)]
#[read_component(ProvidesHealing)]
#[write_component(Health)]
#[read_component(ProvidesDungeonMap)]
pub fn use_items(ecs: &mut SubWorld, commands: &mut CommandBuffer, #[resource] map: &mut Map) {}
// mod.rs
use crate::prelude::*;
mod chasing;
mod combat;
mod end_turn;
mod entity_render;
mod fov;
mod hud;
mod map_render;
mod movement;
mod player_input;
mod random_move;
mod tooltips;
mod use_items;
Applying Healing
Create a Vector to store the list of healing effects we want to apply.
let mut healing_to_apply = Vec::<(Entity, i32)>::new();
Quickly lets go over some borrowing rules for rust:
Rust has some hard-and-fast rules for borrowing:
• You may borrow something immutably as many times as you want.
• You may only borrow something mutably once.
• You can’t simultaneously borrow something mutably and immutably.
If you apply healing in situ, you will run into borrow checker errors:
1. You would borrow the SubWorld to execute the query.
2. You borrow from within the SubWorld to access the item’s entity and components.
3. You borrow again to find the healing properties of the potion.
4. You mutably borrow the Health component of the player. Unfortunately,
you’re also still borrowing the SubWorld. Rust will refuse to compile your
program in case your changes make the SubWorld invalid.
In this case, we still have an active immutable reference to the ECS (via item) when we’d want to apply healing, so we can’t also take a mutable reference at the same time — that’s why we collect the healing effects into a Vec first and apply them in a separate pass afterward.
Iterate Item Application
Now we need to deal with all the Entities with the vector we have created to store all the ActivateItem
<(Entity, &ActivateItem)>::query()
.iter(ecs)
.for_each(|(entity, activate)| {
let item = ecs.entry_ref(activate.item);
if let Ok(item) = item {
if let Ok(healing) = item.get_component::<ProvidesHealing>() {
healing_to_apply.push((activate.used_by, healing.amount));
}
if let Ok(_mapper) = item.get_component::<ProvidesDungeonMap>() {
map.revealed_tiles.iter_mut().for_each(|t| *t = true);
}
}
commands.remove(activate.item);
commands.remove(*entity);
});
So we get all the Entities that have the ActivateItem tag, get a single item that wasn’t returned in the initial query, if we don’t get a null for the item, if the item is a healing apply the healing from the vector you created, if its a map then reveal all the map tiles, delete the item so it can’t be used again, remove the ActivateItem command.
Apply Healing Events
Now we need to iterate over the healing events so that we can take care of the healing or potion effect. Add this to the clause for the healing item.
for heal in healing_to_apply.iter() {
if let Ok(mut target) = ecs.entry_mut(heal.0) {
if let Ok(health) = target.get_component_mut::<Health>() {
health.current = i32::min(health.max, health.current + heal.1);
}
}
}
take all the items in the Vector, grab the target from the heals target field, get the health component from the target, change the health to be the min of the max or current health + the heal.
Add to the Schedule
Now we add it to the schedule with the mod.rs
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(combat::combat_system())
.add_system(use_items::use_items_system())
.flush()
...
.build()
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
// .add_system(random_move::random_move_system())
.add_system(chasing::chasing_system())
.flush()
.add_system(use_items::use_items_system())
.add_system(combat::combat_system())
.flush()
...
.build()
}
Okay so now that we have that within the system we will want to remove the ability to wait to heal as we now have potions.
Remove Waiting to Heal
Remove the following from the player_input.rs
if !did_something {
if let Ok(mut health) = ecs
.entry_mut(player_entity)
.unwrap()
.get_component_mut::<Health>()
{
health.current = i32::min(health.max, health.current + 1);
}
}
Now the player will have to find potions to heal themselves.
Wrap-Up
Okay we have the items in the dungeon we can add in more items to the game but will need to be able to spawn them as well as add in the abilities for each of them. Go crazy if you want but try and balance the items. I would think that you would only want to add in a Map once. Think about adding in a check for that.