This is part of a series of tutorials; the main page can be found here.
The tutorial uses libtcod version 1.6.0 and above.
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 many 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
num_items = libtcod.random_get_int(0, 0, MAX_ROOM_ITEMS)
for i in range(num_items):
#choose random spot for this item
x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
#only place it if the tile is not blocked
if not is_blocked(x, y):
#create a healing potion
item = Object(x, y, '!', 'healing potion', libtcod.violet)
objects.append(item)
item.send_to_back() #items appear below other objects
For this to work, we must define the new constant MAX_ROOM_ITEMS = 2 at the top. 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.
The limits of the random position of the items (passed to random_get_int) are a bit different than for the monsters. In fact, the monstersâ coordinates have been slightly off the whole time! They should be changed from this:
#choose random spot for this monster
x = libtcod.random_get_int(0, room.x1, room.x2)
y = libtcod.random_get_int(0, room.y1, room.y2)
To this:
#choose random spot for this monster
x = libtcod.random_get_int(0, room.x1+1, room.x2-1)
y = libtcod.random_get_int(0, room.y1+1, room.y2-1)
Which is one tile tighter than the roomâs walls in every direction. Thatâs because the roomâs rectangle, as we defined it earlier, includes its walls too (oops!). Anyway, when the old code decided to place a monster on the walls, it wouldnât get created due to the is_blocked check, so there were less monsters on average â now the game got a little harder! The healing potions should balance this effect, but of course you can always tweak the values to your liking.
After that embarrassing revelation, letâs define the inventory! This goes before the main loop:
inventory = []
Simple enough: the inventory is a list of items, and it starts empty. Now the Item component â it will hold all data and functionality that makes an object behave like an item. For it to make its way into the playerâs inventory, weâll start by giving it a pick_up method.
class Item:
#an item that can be picked up and used.
def pick_up(self):
#add to the player's inventory and remove from the map
if len(inventory) >= 26:
message('Your inventory is full, cannot pick up ' + self.owner.name + '.', libtcod.red)
else:
inventory.append(self.owner)
objects.remove(self.owner)
message('You picked up a ' + self.owner.name + '!', libtcod.green)
The limit of 26 is 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.
This component must be accepted by the _Objectâs __init__
method, like all other components. Just add another parameter to it _item=None_
, and an initialization similar to the other components:
self.item = item
if self.item: #let the Item component know who owns it
self.item.owner = self
Now that we have an Item component, you can add it to the healing potion in place_objects:
item_component = Item()
item = Object(x, y, '!', 'healing potion', libtcod.violet, item=item_component)
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 between the else and the return âdidnt-take-turnâ line:
else:
#test for other keys
key_char = chr(key.c)
if key_char == 'g':
#pick up an item
for object in objects: #look for an item in the player's tile
if object.x == player.x and object.y == player.y and object.item:
object.item.pick_up()
break
return 'didnt-take-turn'
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.
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:
def menu(header, options, width):
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.
if len(options) > 26: raise ValueError('Cannot have a menu with more than 26 options.')
Now we calculate the height of the window; as I said, itâs implicit. The header will be shown using the console_print_left_rect function, which can word-wrap long sentences so it fits a given width. The number of lines after word-wrapping can be calculated with console_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
header_height = libtcod.console_get_height_rect(con, 0, 0, width, SCREEN_HEIGHT, header)
height = len(options) + 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 function.
#create an off-screen console that represents the menu's window
window = libtcod.console_new(width, height)
#print the header, with auto-wrap
libtcod.console_set_default_foreground(window, libtcod.white)
libtcod.console_print_rect_ex(window, 0, 0, width, height, libtcod.BKGND_NONE, libtcod.LEFT, header)
Now to the actual options, printed in a loop. The Y coordinate of the first option is right below the header; we print the optionâs text and increment it. We also want to start with the letter A and increment it each time, to show it next to the optionâs text. The ord function returns the ASCII code of the letter A; we can then increment it to get the codes of the remaining letters. To convert an ASCII code back to a character (string), we use the chr function. These are built-in functions; you can read up on them (and others) in the Python docs.
#print all the options
y = header_height
letter_index = ord('a')
for option_text in options:
text = '(' + chr(letter_index) + ') ' + option_text
libtcod.console_print_ex(window, 0, y, libtcod.BKGND_NONE, libtcod.LEFT, text)
y += 1
letter_index += 1
Ok, all of the windowâs contents are stored in the off-screen console! Itâs a simple matter of calling console_blit 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
x = SCREEN_WIDTH/2 - width/2
y = SCREEN_HEIGHT/2 - height/2
libtcod.console_blit(window, 0, 0, width, height, 0, x, y, 1.0, 0.7)
The last 2 parameters to console_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 console_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
libtcod.console_flush()
key = libtcod.console_wait_for_keypress(True)
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:
def inventory_menu(header):
#show a menu with each item of the inventory as an option
if len(inventory) == 0:
options = ['Inventory is empty.']
else:
options = [item.name for item in inventory]
index = menu(header, options, INVENTORY_WIDTH)
It also tells the player if the inventory is empty; simply displaying an empty list would be rude! The constant INVENTORY_WIDTH = 50 is defined at the top, as usual. 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.
if key_char == 'i':
#show the inventory
inventory_menu('Press the key next to an item to use it, or any other to cancel.\n')
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.
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. For the Item component to know which one is it, you need to pass it at initialization, just like the Fighter with its death_function.
def __init__(self, use_function=None):
self.use_function = use_function
Then, a generic method can call the Itemâs use_function:
def use(self):
#just call the "use_function" if it is defined
if self.use_function is None:
message('The ' + self.owner.name + ' cannot be used.')
else:
if self.use_function() != 'cancelled':
inventory.remove(self.owner) #destroy after use, unless it was cancelled for some reason
If itâs undefined, the item canât be used. Otherwise the function is called, and the item is destroyed (since most items are single-use). Weâll also allow the function to return a special string in case it found that it canât be used after all; for instance, the player is already at full health, so the potion shouldnât be destroyed.
Now letâs make the function for the healing potion! Itâs quite straightforward, as it simply calls a heal method of the Fighter component (which manages health).
def cast_heal():
#heal the player
if player.fighter.hp == player.fighter.max_hp:
message('You are already at full health.', libtcod.red)
return 'cancelled'
message('Your wounds start to feel better!', libtcod.light_violet)
player.fighter.heal(HEAL_AMOUNT)
The heal method is very simple too; still, itâs handy to keep it since it will probably be used multiple times. The constant HEAL_AMOUNT = 4 is defined at the top.
def heal(self, amount):
#heal by the given amount, without going over the maximum
self.hp += amount
if self.hp > self.max_hp:
self.hp = self.max_hp
To make the healing potion object call it on use, pass it as a parameter to the Item component, in place_objects.
item_component = Item(use_function=cast_heal)
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.
We now need to get back to the menu function, to finish it so it can actually select an item. Itâs only a few lines of code though; we already have the key from console_wait_for_keypress, and itâs just a matter of converting it to the selected optionâs index. We do that by subtracting the ASCII code of the letter A, so we get key codes from 0 to 25 corresponding to letters A to Z. Anything out of that range means itâs not a valid key. Actually, any index over the number of options is invalid too, since the number of options is usually smaller than 26.
#convert the ASCII code to an index; if it corresponds to an option, return it
index = key.c - ord('a')
if index >= 0 and index < len(options): return index
return None
This returns a valid index, or None if something else was pressed. The inventory_menu function can make use of that index, and return the corresponding item, so this goes at the end:
#if an item was chosen, return it
if index is None or len(inventory) == 0: return None
return inventory[index].item
The inventory_menu function is very handy; it returns the selected item directly, or None if cancelled. We can now change the code in handle_keys to use the selected item:
if key_char == 'i':
#show the inventory; if an item is selected, use it
chosen_item = inventory_menu('Press the key next to an item to use it, or any other to cancel.\n')
if chosen_item is not None:
chosen_item.use()
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.
The whole code is available here.