Placing items
Now that our GUI is all spiffed up, let’s put in some more core Roguelike functionality: the inventory! This has been a staple of Roguelikes and RPGs for literally decades. It’s a way of gating the player’s access to some abilities, and presents an incentive for exploration. Also, why else would you explore a dungeon if not to haul out as much precious items as you can?
We can place some items in each room in pretty much the same way we
place monsters, at the end of the place_objects
function:
// choose random number of items
let num_items = rand::thread_rng().gen_range(0, MAX_ROOM_ITEMS + 1);
for _ in 0..num_items {
// choose random spot for this item
let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2);
let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2);
// only place it if the tile is not blocked
if !is_blocked(x, y, map, objects) {
// create a healing potion
let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false);
objects.push(object);
}
}
For this to work, we must define the new constant MAX_ROOM_ITEMS
.
Later we’ll expand this with a few magic scrolls in addition to the
healing potions; this is the spot to add any items you want in your
game. The healing potions don’t have any special components for now;
we’ll get to that in a second.
const MAX_ROOM_ITEMS: i32 = 2;
Next, let’s define the inventory! We’ll add it to our Game
struct:
struct Game {
map: Map,
messages: Messages,
inventory: Vec<Object>, (1)
}
1 | New field: inventory |
And we need to add it to our main
function where we initialise the game
variable:
let mut game = Game {
// generate map (at this point it's not drawn to the screen)
map: make_map(&mut objects),
messages: Messages::new(),
inventory: vec![], (1)
};
1 | Initialise the new inventory field |
Simple enough: the inventory is a list of items, and it starts empty.
Now the Item
component — it will hold all data that makes an object
behave like an item. For now that will be just the item type.
#[derive(Clone, Copy, Debug, PartialEq)]
enum Item {
Heal,
}
To have the item find its way to player’s inventory, we’ll add a
pick_item_up
function.
/// add to the player's inventory and remove from the map
fn pick_item_up(object_id: usize, game: &mut Game, objects: &mut Vec<Object>) {
if game.inventory.len() >= 26 {
game.messages.add(
format!(
"Your inventory is full, cannot pick up {}.",
objects[object_id].name
),
RED,
);
} else {
let item = objects.swap_remove(object_id);
game.messages
.add(format!("You picked up a {}!", item.name), GREEN);
game.inventory.push(item);
}
}
We limit the inventory to 26 items because later, in the inventory screen, items will be selected by pressing a key from A to Z, and there are 26 letters. You could overcome this restriction by implementing "pages" in the inventory, or a fancier interface with scrollbars. That would be a bit harder, so we’ll stick to this for now. You could also assign weights to the items and limit the total weight here, as some games do.
We’re using the swap_remove method to take an item out of the vector. We could use the remove method instead, but it usually copies more memory and can be a bit slower.
Until now, the ID (index) of each Object remained the
same throughout the game. This is no longer true — when we move an
item into the inventory, it’s removed from the objects vector and
some indices (IDs) will change. As a consequence, we cannot store an
ID somewhere and expect it to not change. We need to keep that in mind
as we write our game.
|
This may not be a trade-off you’re willing to make. Having unique unchanging IDs to objects is useful: you can store the ID as a monster’s target and it will keep following it. If the ID persists even in death, you can have things like spells or missiles homing in on a corpse. There are several ways of dealing with this. |
Your inventory may store an objects index/ID instead of the object
itself (but you’ll have to make sure picked up items don’t show up on
the map — perhaps by having a Renderable component or by making the
position optional).
|
You can also use a hash table of ID → Object. That way even when you do remove objects, you can still look them up by their IDs. |
We now add Item
as another component the Object
can have:
struct Object {
...
fighter: Option<Fighter>,
ai: Option<Ai>,
item: Option<Item>, (1)
}
1 | Added the Item component |
And add item: None,
to Object’s `new
method:
fighter: None,
ai: None,
item: None,
Now that we have an Item
component, you can add it to the healing
potion in place_objects
:
// create a healing potion
let mut object = Object::new(x, y, '!', "healing potion", VIOLET, false);
object.item = Some(Item::Heal);
objects.push(object);
How does the player pick up an item? It’s very easy: just test for
another key in the handle_keys function. If it’s pressed, look for an
item under the player and pick it up. The new code goes below the
arrow-handling and the final return DidntTakeTurn
line:
(Key { code: Text, .. }, "g", true) => {
// pick up an item
let item_id = objects
.iter()
.position(|object| object.pos() == objects[PLAYER].pos() && object.item.is_some());
if let Some(item_id) = item_id {
pick_item_up(item_id, game, objects);
}
DidntTakeTurn
}
You can test it out now! There will be a few potions scattered around,
and you’ll get a message when you pick them up by pressing G
. The
inventory is still invisible though.
The inventory screen
We now get to what’s probably the trickiest part: showing the inventory screen. Since the functionality is tightly bound to the user interface, it’s hard to do it without super-messy code.
To minimize the amount of hacks, we’ll define a single function to present a list of options to the player, and reuse the hell out of it! We’ll start by defining its parameters so we can decide exactly what it’s supposed to do:
fn menu<T: AsRef<str>>(header: &str, options: &[T], width: i32, root: &mut Root) -> Option<usize> {
// body goes here ...
}
This function should show a window with a string at the top, the header, which can be the title of the window and/or an explanatory text (say, "Choose an item to use" or "Choose an item to drop"). Following are the options, which are nothing more than a list of strings (for instance, the names of the items). We also need to define the window’s width; the height is implicit, since it depends on the header height and number of options.
A letter will be shown next to each option (A
, B
, …) so you can
select it by pressing that key. Finally, the function returns the
index of the selected option (starting with 0), or None
if the user
pressed some other key. We’ll start by just displaying the menu and
worry about choosing an option later.
First, check if there are more options than allowed. Since the menu
function is supposed to be reused, it’s possible that in the future
you’ll get too carried away and try to give it more options than the
letters from A to Z! It’s better to get an early error and fix it than
let it slide and get harder-to-track errors down the line.
assert!(
options.len() <= 26,
"Cannot have a menu with more than 26 options."
);
Now we calculate the height of the window — it’s implicit. The header
will be shown using the print_rect_ex
function, which can word-wrap
long sentences so it fits a given width. The number of lines after
word-wrapping can be calculated with get_height_rect
; so the
total height is that plus the number of options.
// calculate total height for the header (after auto-wrap) and one line per option
let header_height = root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header);
let height = options.len() as i32 + header_height;
Given the window’s size, we can create an off-screen console where the window’s contents will be drawn first. The header is printed at the top, using the auto-wrap functionality.
// create an off-screen console that represents the menu's window
let mut window = Offscreen::new(width, height);
// print the header, with auto-wrap
window.set_default_foreground(WHITE);
window.print_rect_ex(
0,
0,
width,
height,
BackgroundFlag::None,
TextAlignment::Left,
header,
);
Now to the actual options, printed in a loop. We use the
enumerate method on Iterator method to get the index for
each time we loop through (0, 1, 2, …) and then use it to calculate the y
coordinate and the option letter to display next to it.
// print all the options
for (index, option_text) in options.iter().enumerate() {
let menu_letter = (b'a' + index as u8) as char;
let text = format!("({}) {}", menu_letter, option_text.as_ref());
window.print_ex(
0,
header_height + index as i32,
BackgroundFlag::None,
TextAlignment::Left,
text,
);
}
We need to do a bit of type casting here. Rust does not convert
numeric types silently, so when we need to work with different types
(in our case adding b’a'
which is u8
and index which is usize
),
we have to convert the type explicitly. You can try to remove the
casts (the as type
code) and see what happens.
Ok, all of the window’s contents are stored in the off-screen console!
It’s a simple matter of calling blit
function to display them on the
screen. These little formulae calculate what the position of the
top-left corner of the window should be, so that it’s centered on the
screen.
// blit the contents of "window" to the root console
let x = SCREEN_WIDTH / 2 - width / 2;
let y = SCREEN_HEIGHT / 2 - height / 2;
blit(&window, (0, 0), (width, height), root, (x, y), 1.0, 0.7);
The last 2 parameters to blit
hadn’t been used in our game before:
according to the libtcod docs, they define the foreground and
background transparency, respectively. The first is 1.0
so the
foreground (the text) is printed fully opaque, as usual. But since the
second one is a smaller value, what happens is that the off-screen
console’s background (which is black by default) does not entirely
replace the background colors that were previously on the screen. So
what you see is a semi-transparent window, overlaying the game! As you
can see, these neat effects are very easy to do with libtcod.
It’s not complete though; this screen will be shown for a single frame
and then vanish immediately, replaced by the new frame. We need to
stop time until the player makes a choice, and only then can the game
carry on. This is easy to do with wait_for_keypress
. There’s
also the need to flush the screen to present the changes before
waiting for input:
// present the root console to the player and wait for a key-press
root.flush();
let key = root.wait_for_keypress(true);
// convert the ASCII code to an index; if it corresponds to an option, return it
if key.printable.is_alphabetic() {
let index = key.printable.to_ascii_lowercase() as usize - 'a' as usize;
if index < options.len() {
Some(index)
} else {
None
}
} else {
None
}
That was one really long function! But if you base most of your
interfaces on this function, you won’t need to create any more
like it. As an example, here’s how you show an inventory — just build
a list of the items' names, and call the menu
function:
fn inventory_menu(inventory: &[Object], header: &str, root: &mut Root) -> Option<usize> {
// how a menu with each item of the inventory as an option
let options = if inventory.len() == 0 {
vec!["Inventory is empty.".into()]
} else {
inventory.iter().map(|item| item.name.clone()).collect()
};
let inventory_index = menu(header, &options, INVENTORY_WIDTH, root);
// if an item was chosen, return it
if inventory.len() > 0 {
inventory_index
} else {
None
}
}
It also tells the player if the inventory is empty; simply displaying
an empty list would be rude! The constant INVENTORY_WIDTH
is defined
at the top, as usual:
const INVENTORY_WIDTH: i32 = 50;
The header text is a parameter because we want to call this both for using and dropping items (and maybe other actions).
Speaking of which, we can define the inventory key
right now, in handle_keys
(after the code to pick up items). The line
break \n
after the header gives one line of separation between it and
the options.
(Key { code: Text, .. }, "i", true) => {
// show the inventory
inventory_menu(
inventory,
"Press the key next to an item to use it, or any other to cancel.\n",
root);
TookTurn
}
Finally, the inventory is visible! You can list the items you pick up
by pressing I
. Selecting them does nothing though; that is handled
in the next section.
Using items
What happens when you use an item? Well, it depends on which item you’re talking about. They’re all different, so the "use" behavior of each item must be defined as a different function.
First, a function that tries to use an item from the inventory and handles things like removing it after use and printing a message when the player changes their mind.
fn use_item(inventory_id: usize, tcod: &mut Tcod, game: &mut Game, objects: &mut [Object]) {
use Item::*;
// just call the "use_function" if it is defined
if let Some(item) = game.inventory[inventory_id].item {
let on_use = match item {
Heal => cast_heal,
};
match on_use(inventory_id, tcod, game, objects) {
UseResult::UsedUp => {
// destroy after use, unless it was cancelled for some reason
game.inventory.remove(inventory_id);
}
UseResult::Cancelled => {
game.messages.add("Cancelled", WHITE);
}
}
} else {
game.messages.add(
format!("The {} cannot be used.", game.inventory[inventory_id].name),
WHITE,
);
}
}
If we do actually have an item, we match on its type (that’s just
Heal
for now but we’ll soon have more), find the right function to
call for the specific on_use effect and call it.
Then based on the result we either remove the item (if it was used up) or print a message if it were cancelled.
So all our on_use
functions will return UseResult
. Let’s define it:
enum UseResult {
UsedUp,
Cancelled,
}
An item can either be used up (so we delete it) or the action can be canceled. We’ll add a third variant, soon.
Now let’s add cast_heal
for our potions to have effect!
fn cast_heal(
_inventory_id: usize,
_tcod: &mut Tcod,
game: &mut Game,
objects: &mut [Object],
) -> UseResult {
// heal the player
if let Some(fighter) = objects[PLAYER].fighter {
if fighter.hp == fighter.max_hp {
game.messages.add("You are already at full health.", RED);
return UseResult::Cancelled;
}
game.messages
.add("Your wounds start to feel better!", LIGHT_VIOLET);
objects[PLAYER].heal(HEAL_AMOUNT);
return UseResult::UsedUp;
}
UseResult::Cancelled
}
The heal
method is very simple too; still, it’s handy to keep it
since it will probably be used multiple times. In impl Object
:
/// heal by the given amount, without going over the maximum
pub fn heal(&mut self, amount: i32) {
if let Some(ref mut fighter) = self.fighter {
fighter.hp += amount;
if fighter.hp > fighter.max_hp {
fighter.hp = fighter.max_hp;
}
}
}
The constant HEAL_AMOUNT = 4
is defined at the top:
const HEAL_AMOUNT: i32 = 4;
That’s it for creating usable items! You can make other items easily by just defining their use function. This could also work for wielding weapons or wearing armor, zapping wands, rubbing a magic lamp and all that stuff we know and love.
Finally, we can now change the code in handle_keys to use the selected item:
(Key { code: Text, .. }, "i", true) => {
// show the inventory: if an item is selected, use it
let inventory_index = inventory_menu(
&game.inventory,
"Press the key next to an item to use it, or any other to cancel.\n",
&mut tcod.root,
);
if let Some(inventory_index) = inventory_index {
use_item(inventory_index, tcod, game, objects);
}
DidntTakeTurn
}
There you go, the inventory code is complete! Well, minus dropping
items. That’s fairly easy with the inventory_menu
, but to keep this
from getting long we’ll leave it to the next part: magic scrolls! That
will really make the most of this inventory system.
Here’s the complete code so far.
Continue to the next part.