Complete Roguelike Tutorial, using python+libtcod, part 13
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 you can explore a large dungeon, I'm sure you can't help but notice a few things missing. Where are all the swords, armor, enchanted boots and other assorted junk? Sure, we have some cool items, but they can only be used once. We can't really handle weapons and armor in the current system. How do we solve this?
First, we can add a new component to take care of the new functionality. An item with the Equipment component can be equipped or taken off, and while equipped will give the player some bonuses (more power, defense, etc). Sounds good!
Now we must plan ahead how this data will be stored in our game. It's time for a small detour into game architecture!
You see, the way you store your data can have a big impact on how easy it will be to handle and debug. There are two types. A brittle data structure can be easily put in an inconsistent state. A strong data structure cannot; it always makes sense, no matter how you change it. For example, you can keep a list of equipped items. To equip, you move an item to the "equipped" list. There are several inconsistent states: what does it mean for an item to be on both lists? What does it mean to be on no list? (Python will just delete an object if it's not referenced anywhere.) This is brittle.
A stronger data structure is to have a "is_equipped" property on each item, and if it's True the item is equipped. This is much harder to break, because in any case the item is either equipped or unequipped; there are no weird states.
We will use the same idea for bonuses, which you'll see later on. In a nutshell, try to store data in a way that allows a minimum of inconsistent states. Duplicated data or data that requires perfect coordination to make sense is usually a bad sign. This is more of an art than a science, though, and there is no absolute answer. So after the tutorial it will all be up to you!
Ok, it's time to code that up. We'll have an Equipment component that knows whether it's equipped. It will also have an associated slot, like 'hand' for weapons or 'head' for helmets. The slot will simply be indicated by a string.
Nothing fancy there! To allow objects to have this component, add equipment=None to the parameters of the Object class's __init__ function, and the usual component initialization code:
At that point, we can also create an Item component automatically, because a piece of Equipment is always an Item (can be picked up and used).
When the player goes to the inventory screen and tries to use a piece of equipment, it will be equipped or dequipped. So, in the use function of the Item class, add to the beginning:
That's the basic functionality! To test it quickly, we can let a sword appear in the dungeon, by adding a new item choice in place_objects:
And item_chances['sword'] = 25 after the other item's chances, at the top of that function.
Ready to test! Equipping the sword doesn't do much though. You'll also notice you can equip 2 swords at once (how cool is that?). But 3 swords or more is a bit unrealistic, so we'll take care of that.
We don't want to let the player equip more than one item in the same slot. Fair enough! Let's make a function to check if any item occupies a slot, and return it while we're at it:
We can use it to prevent a second item in the same slot, or better yet: dequip the old item to make room for the new one. In the equip function:
Another nice behavior is to automatically equip picked up items, if their slots are available. In the pick_up function, at the end:
It is necessary, though, that dropped items be dequipped; simply add to the drop function:
Finally, another bit of polishing. We'd like to see in the inventory which items are equipped! So in inventory_menu, this information should be shown next to the item names. Replace the line options = [item.name for item in inventory] with:
That's it. You can check the equipment's state in the inventory screen, and it changes correctly as you pick up, drop, equip and dequip various items!
The last bit is to make equipment useful, by letting it change the player's stats when equipped. We can do this in different ways, but as I mentioned in the beginning, it's better to avoid brittle data structures. For example, you could simply add the bonus value to a stat (say, attack power) when the item is equipped, and subtract it when dequipped. This is brittle because any tiny mistake will permanently change the player's stats!
A more reliable approach is to calculate on-the-fly the player's stats when they are needed, based on the original stat and any bonuses. This way there's no room for inconsistencies -- the stat is truly based on whatever bonuses apply at the moment.
But how can we change a stored variable to a dynamic value? Won't this mean we have to change all of the code that uses those stats? Not really, because of a neat Python feature! You can define a read-only property that is calculated on-the-fly very easily:
The bonus will be defined later. So now accessing player.power will call this function instead of getting the value of a power variable. We still need a variable to hold the player's power not counting any bonuses, though, and that's called base_power. This means that, in the Fighter class's __init__ function, we don't initialize power directly, but rather base_power:
More generally, you can get the value of power normally, but you only change it through base_power. So, you must also make this change in check_level_up.
All that's left is to calculate the bonus! An Equipment component will remember what's its power bonus, by passing it as a new argument at initialization. I will also go ahead and define the bonuses for all the other stats:
The power property can now just iterate through all equipped items, and sum up their bonuses to get the needed total:
Finally, we need a helper function that returns the list of equipped items. For the player, we just go through the inventory. For monsters, we just return an empty list since they don't really have any. Feel free to change this if you want to let monsters equip items as well!
That's it! Attack power is now a dynamic property. That wasn't so hard! Remember that you can change the bonus calculation easily, so if there are other modifiers, permanent spells and other conditions, it's only a small change away.
For the sake of completeness, here are the properties for the other stats:
Don't forget to make the appropriate changes in check_level_up as well. Now we can define some items with hefty bonuses! In place_objects, I changed the sword to have power_bonus=3, and added a shield for good measure:
You can get really creative with equipment, of course. I'll just modify the chances to make them appear at level 4 and level 8, respectively:
Now, since we don't want the player to enter the dungeon unprepared, you can give him or her some starting equipment at the end of new_game:
Not bad! I also decreased the player's starting power to 2; we don't want to be too generous. It's a dungeon of doom after all!
I showed you read-only properties, which are a breeze to use. If you're wondering about writable properties, check out the Python docs on the subject.
We managed to create a neat bonus system, and it's generic enough that you can add new stats and ways to change them very easily. There's also equipment and slots that you can choose at will. Now you can create all sorts of useful plunder for the player to discover!
The whole code is available here.