Roguelike Tutorial, using python3+tdl, part 8
This is part of a series of tutorials; the main page can be found here.
The tutorial uses tdl version 3.1.0 and Python 3.5
Items and inventory
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:
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:
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:
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.
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 GameObject 's __init__ method, like all other components. Just add another parameter to it item=None, and an initialization similar to the other components:
Now that we have an Item component, you can add it to the healing potion in place_objects:
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:
Note: In the older versions of the tutorial which used tdl version 1.x.x, "user_input.text" was "user_input.char". This old code will no longer work on version 3.x.x, and vice-versa.
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:
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.
Now we calculate the height of the window; as I said, it's implicit. The header will be shown using the draw_rect function. We'll also need to word-wrap long sentences so they fit a given width. We'll calculate the number of lins of wrapped text; the total height is that plus the number of options.
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.
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.
Ok, all of the window's contents are stored in the off-screen console! It's a simple matter of calling root.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.
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:
The arrow keys return an empty string as a character, which won't work well later, so we replace the empty string with a space character as a placeholder in the last two lines.
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:
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.
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.
Then, a generic method can call the Item's use_function:
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).
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.
To make the healing potion object call it on use, pass it as a parameter to the Item component, in place_objects.
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 tdl.event.key_wait, 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.
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:
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:
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.