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.

Continue to the next part.