Part 13 - Gearing up
For the final part of our tutorial series, we’ll take a look at implementing some equipment. Equipment is a subtype of items that the player can equip for some stat boosts. Obviously it can get more complicated than that, depending on the game, but I’ll leave it up to you to implement that depending on your needs. For this tutorial, equipping a weapon will increase attack power, and equipping a shield will increase defense.
You might’ve already guessed at this point, but we’ll need a new
component that tells us which items are equippable, and what the effects
of equipping them are. This component we’ll call Equippable
, and we’ll
put it in a file called equippable.py
, which, of course, lives in the
components
directory.
class Equippable:
def __init__(self, slot, power_bonus=0, defense_bonus=0, max_hp_bonus=0):
self.slot = slot
self.power_bonus = power_bonus
self.defense_bonus = defense_bonus
self.max_hp_bonus = max_hp_bonus
power_bonus
, defense_bonus
, and max_hp_bonus
will be the bonuses
that the player gets from equipping a certain item. A weapon will give a
power bonus, and a shield will give a defense bonus. We won’t add
anything with an HP bonus in this tutorial, but you could use this for
something like armor or a ring that increases health.
But what about slot
? That describes what the equipment piece gets
equipped to. The player will have two different equipment slots
available: the main hand (for weapons) and off hand (for shields). We’ll
implement that as an enum
. Create a file called equipment_slots.py
in the base directory, and add the following to it:
from enum import Enum
class EquipmentSlots(Enum):
MAIN_HAND = 1
OFF_HAND = 2
You can extend this as much as you want, to give the player slots for things like head, body, legs, or fingers for rings.
Now we have what we need in place for items to become “equippable”, but
what do they become equipped to? For that, we’ll need another component,
which we’ll call Equipment
. Put the following in a new file, in the
components
folder, called equipment.py
:
from equipment_slots import EquipmentSlots
class Equipment:
def __init__(self, main_hand=None, off_hand=None):
self.main_hand = main_hand
self.off_hand = off_hand
@property
def max_hp_bonus(self):
bonus = 0
if self.main_hand and self.main_hand.equippable:
bonus += self.main_hand.equippable.max_hp_bonus
if self.off_hand and self.off_hand.equippable:
bonus += self.off_hand.equippable.max_hp_bonus
return bonus
@property
def power_bonus(self):
bonus = 0
if self.main_hand and self.main_hand.equippable:
bonus += self.main_hand.equippable.power_bonus
if self.off_hand and self.off_hand.equippable:
bonus += self.off_hand.equippable.power_bonus
return bonus
@property
def defense_bonus(self):
bonus = 0
if self.main_hand and self.main_hand.equippable:
bonus += self.main_hand.equippable.defense_bonus
if self.off_hand and self.off_hand.equippable:
bonus += self.off_hand.equippable.defense_bonus
return bonus
def toggle_equip(self, equippable_entity):
results = []
slot = equippable_entity.equippable.slot
if slot == EquipmentSlots.MAIN_HAND:
if self.main_hand == equippable_entity:
self.main_hand = None
results.append({'dequipped': equippable_entity})
else:
if self.main_hand:
results.append({'dequipped': self.main_hand})
self.main_hand = equippable_entity
results.append({'equipped': equippable_entity})
elif slot == EquipmentSlots.OFF_HAND:
if self.off_hand == equippable_entity:
self.off_hand = None
results.append({'dequipped': equippable_entity})
else:
if self.off_hand:
results.append({'dequipped': self.off_hand})
self.off_hand = equippable_entity
results.append({'equipped': equippable_entity})
return results
That’s a lot of code all at once, so let’s break things down a bit.
The two variables main_hand
and off_hand
will hold the entities that
we’re equipping. If they are set to None
, then that means nothing is
equipped to that slot.
The three properties all do essentially the same thing: they sum up the “bonuses” from both the main hand and off hand equipment, and return the value. Since we’re using properties, these values can be accessed like a regular variable, which will come in handy soon enough. If the player has equipment in both the main hand and off hand that increases attack, for instance, then we’ll get the bonus the same either way.
toggle_equip
is what we’ll call when we’re either equipping or
dequipping an item. If the item was not previously equipped, we equip
it, removing any previously equipped item. If it’s equipped already,
we’ll assume the player meant to remove it, and just dequip it. We
return the results of this operation similarly to how we’ve done with
other functions, which the engine
will process.
Like the other components we’ve created, we’ll need to add these new
ones to the Entity
class.
import tcod as libtcod
import math
+from components.item import Item
from render_functions import RenderOrder
class Entity:
def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE, fighter=None, ai=None,
- item=None, inventory=None, stairs=None, level=None):
+ item=None, inventory=None, stairs=None, level=None, equipment=None, equippable=None):
self.x = x
self.y = y
self.char = char
self.color = color
self.name = name
self.blocks = blocks
self.render_order = render_order
self.fighter = fighter
self.ai = ai
self.item = item
self.inventory = inventory
self.stairs = stairs
self.level = level
+ self.equipment = equipment
+ self.equippable = equippable
if self.fighter:
self.fighter.owner = self
if self.ai:
self.ai.owner = self
if self.item:
self.item.owner = self
if self.inventory:
self.inventory.owner = self
if self.stairs:
self.stairs.owner = self
if self.level:
self.level.owner = self
+ if self.equipment:
+ self.equipment.owner = self
+
+ if self.equippable:
+ self.equippable.owner = self
+
+ if not self.item:
+ item = Item()
+ self.item = item
+ self.item.owner = self
def move(self, dx, dy):
...
import tcod as libtcod import math from components.item import Item from render_functions import RenderOrder class Entity: def __init__(self, x, y, char, color, name, blocks=False, render_order=RenderOrder.CORPSE, fighter=None, ai=None, item=None, inventory=None, stairs=None, level=None, equipment=None, equippable=None): self.x = x self.y = y self.char = char self.color = color self.name = name self.blocks = blocks self.render_order = render_order self.fighter = fighter self.ai = ai self.item = item self.inventory = inventory self.stairs = stairs self.level = level self.equipment = equipment self.equippable = equippable if self.fighter: self.fighter.owner = self if self.ai: self.ai.owner = self if self.item: self.item.owner = self if self.inventory: self.inventory.owner = self if self.stairs: self.stairs.owner = self if self.level: self.level.owner = self if self.equipment: self.equipment.owner = self if self.equippable: self.equippable.owner = self if not self.item: item = Item() self.item = item self.item.owner = self def move(self, dx, dy): ...
Notice that if the entity does not have an Item
component, then we add
one. This is because every piece of equipment is also an item by
definition, because it gets added to the inventory, picked up, and
dropped.
Let’s add the new Equipment
component to the player, in
initialize_new_game.py
:
...
level_component = Level()
+ equipment_component = Equipment()
- player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR,
- fighter=fighter_component, inventory=inventory_component, level=level_component)
+ player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR,
+ fighter=fighter_component, inventory=inventory_component, level=level_component,
+ equipment=equipment_component)
entities = [player]
...
... level_component = Level() equipment_component = Equipment() player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR, fighter=fighter_component, inventory=inventory_component, level=level_component) player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR, fighter=fighter_component, inventory=inventory_component, level=level_component, equipment=equipment_component) entities = [player] ...
Be sure to import the component in this file as well.
import tcod as libtcod
+from components.equipment import Equipment
from components.fighter import Fighter
from components.inventory import Inventory
from components.level import Level
...
import tcod as libtcod
from components.equipment import Equipment
from components.fighter import Fighter
from components.inventory import Inventory
from components.level import Level
...
So how does the player actually go about equipping a piece of equipment?
Well, the equipment will be viewable from the inventory screen like any
usable item, so why not just extend that? We can modify the use
method
in Inventory
to equip an item if its equippable, like this:
...
if item_component.use_function is None:
- results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)})
+ equippable_component = item_entity.equippable
+
+ if equippable_component:
+ results.append({'equip': item_entity})
+ else:
+ results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)})
else:
...
... if item_component.use_function is None: results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)}) equippable_component = item_entity.equippable if equippable_component: results.append({'equip': item_entity}) else: results.append({'message': Message('The {0} cannot be used'.format(item_entity.name), libtcod.yellow)}) else: ...
Now the method checks if the item is equippable, and if so, we return the equip result. If not, we display the warning message about it not being usable, as usual.
Let’s up the toggle_equip
method into action, in engine.py
:
...
item_dropped = player_turn_result.get('item_dropped')
+ equip = player_turn_result.get('equip')
targeting = player_turn_result.get('targeting')
...
...
item_dropped = player_turn_result.get('item_dropped')
equip = player_turn_result.get('equip')
targeting = player_turn_result.get('targeting')
...
...
if item_dropped:
entities.append(item_dropped)
game_state = GameStates.ENEMY_TURN
+ if equip:
+ equip_results = player.equipment.toggle_equip(equip)
+
+ for equip_result in equip_results:
+ equipped = equip_result.get('equipped')
+ dequipped = equip_result.get('dequipped')
+
+ if equipped:
+ message_log.add_message(Message('You equipped the {0}'.format(equipped.name)))
+
+ if dequipped:
+ message_log.add_message(Message('You dequipped the {0}'.format(dequipped.name)))
+
+ game_state = GameStates.ENEMY_TURN
if targeting:
...
...
if item_dropped:
entities.append(item_dropped)
game_state = GameStates.ENEMY_TURN
if equip:
equip_results = player.equipment.toggle_equip(equip)
for equip_result in equip_results:
equipped = equip_result.get('equipped')
dequipped = equip_result.get('dequipped')
if equipped:
message_log.add_message(Message('You equipped the {0}'.format(equipped.name)))
if dequipped:
message_log.add_message(Message('You dequipped the {0}'.format(dequipped.name)))
game_state = GameStates.ENEMY_TURN
if targeting:
...
There’s a little bug with our current implementation. The player can
drop an item from the inventory, yet still have it “equipped”! That’s
obviously not right, so let’s fix that in the Inventory
method
drop_item
:
...
def drop_item(self, item):
results = []
+ if self.owner.equipment.main_hand == item or self.owner.equipment.off_hand == item:
+ self.owner.equipment.toggle_equip(item)
item.x = self.owner.x
...
...
def drop_item(self, item):
results = []
if self.owner.equipment.main_hand == item or self.owner.equipment.off_hand == item:
self.owner.equipment.toggle_equip(item)
item.x = self.owner.x
...
So what does equipping something actually do? It should give bonuses
to the player’s fighting ability, but it’s not actually doing that right
now. Why? Because our Fighter
component doesn’t take equipment bonuses
into account! Let’s fix that now.
We need do adjust the way we get the values from Fighter
. It’d be
better if the max_hp
, power
, and defense
were properties, so we
could calculate them as their base plus the bonus at any given time.
Let’s change the initialization function to set the bases of each of
these values, and we’ll add properties for each to take the place of our
old variables.
class Fighter:
def __init__(self, hp, defense, power, xp=0):
- self.max_hp = hp
+ self.base_max_hp = hp
self.hp = hp
- self.defense = defense
+ self.base_defense = defense
- self.power = power
+ self.base_power = power
self.xp = xp
+ @property
+ def max_hp(self):
+ if self.owner and self.owner.equipment:
+ bonus = self.owner.equipment.max_hp_bonus
+ else:
+ bonus = 0
+
+ return self.base_max_hp + bonus
+
+ @property
+ def power(self):
+ if self.owner and self.owner.equipment:
+ bonus = self.owner.equipment.power_bonus
+ else:
+ bonus = 0
+
+ return self.base_power + bonus
+
+ @property
+ def defense(self):
+ if self.owner and self.owner.equipment:
+ bonus = self.owner.equipment.defense_bonus
+ else:
+ bonus = 0
+
+ return self.base_defense + bonus
def take_damage(self, amount):
...
class Fighter: def __init__(self, hp, defense, power, xp=0): self.max_hp = hp self.base_max_hp = hp self.hp = hp self.defense = defense self.base_defense = defense self.power = power self.base_power = power self.xp = xp @property def max_hp(self): if self.owner and self.owner.equipment: bonus = self.owner.equipment.max_hp_bonus else: bonus = 0 return self.base_max_hp + bonus @property def power(self): if self.owner and self.owner.equipment: bonus = self.owner.equipment.power_bonus else: bonus = 0 return self.base_power + bonus @property def defense(self): if self.owner and self.owner.equipment: bonus = self.owner.equipment.defense_bonus else: bonus = 0 return self.base_defense + bonus def take_damage(self, amount): ...
So now when we query for the player’s power
, for example, we’ll be
taking into account what equipment is equipped.
For the most part, this just works. The only thing that doesn’t is our
previous level up code, because we were increasing the max_hp
,
power
, and defense
values directly, whereas now we need to increase
their bases. It’s a pretty easy fix though, just open engine.py
and
make the following adjustment.
...
if level_up:
if level_up == 'hp':
- player.fighter.max_hp += 20
+ player.fighter.base_max_hp += 20
player.fighter.hp += 20
elif level_up == 'str':
- player.fighter.power += 1
+ player.fighter.base_power += 1
elif level_up == 'def':
- player.fighter.defense += 1
+ player.fighter.base_defense += 1
...
... if level_up: if level_up == 'hp': player.fighter.max_hp += 20 player.fighter.base_max_hp += 20 player.fighter.hp += 20 elif level_up == 'str': player.fighter.power += 1 player.fighter.base_power += 1 elif level_up == 'def': player.fighter.defense += 1 player.fighter.base_defense += 1 ...
With all that in place, let’s actually put some equipment on the map!
Open up game_map.py
and modify the place_entities
function to place
some equipment in the dungeon. Remember to import the needed components
at the top.
...
from components.ai import BasicMonster
+from components.equipment import EquipmentSlots
+from components.equippable import Equippable
from components.fighter import Fighter
...
...
from components.ai import BasicMonster
from components.equipment import EquipmentSlots
from components.equippable import Equippable
from components.fighter import Fighter
...
item_chances = {
'healing_potion': 35,
+ 'sword': from_dungeon_level([[5, 4]], self.dungeon_level),
+ 'shield': from_dungeon_level([[15, 8]], self.dungeon_level),
'lightning_scroll': from_dungeon_level([[25, 4]], self.dungeon_level),
'fireball_scroll': from_dungeon_level([[25, 6]], self.dungeon_level),
'confusion_scroll': from_dungeon_level([[10, 2]], self.dungeon_level)
}
item_chances = {
'healing_potion': 35,
'sword': from_dungeon_level([[5, 4]], self.dungeon_level),
'shield': from_dungeon_level([[15, 8]], self.dungeon_level),
'lightning_scroll': from_dungeon_level([[25, 4]], self.dungeon_level),
'fireball_scroll': from_dungeon_level([[25, 6]], self.dungeon_level),
'confusion_scroll': from_dungeon_level([[10, 2]], self.dungeon_level)
}
...
if item_choice == 'healing_potion':
item_component = Item(use_function=heal, amount=40)
item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
item=item_component)
+ elif item_choice == 'sword':
+ equippable_component = Equippable(EquipmentSlots.MAIN_HAND, power_bonus=3)
+ item = Entity(x, y, '/', libtcod.sky, 'Sword', equippable=equippable_component)
+ elif item_choice == 'shield':
+ equippable_component = Equippable(EquipmentSlots.OFF_HAND, defense_bonus=1)
+ item = Entity(x, y, '[', libtcod.darker_orange, 'Shield', equippable=equippable_component)
elif item_choice == 'fireball_scroll':
...
...
if item_choice == 'healing_potion':
item_component = Item(use_function=heal, amount=40)
item = Entity(x, y, '!', libtcod.violet, 'Healing Potion', render_order=RenderOrder.ITEM,
item=item_component)
elif item_choice == 'sword':
equippable_component = Equippable(EquipmentSlots.MAIN_HAND, power_bonus=3)
item = Entity(x, y, '/', libtcod.sky, 'Sword', equippable=equippable_component)
elif item_choice == 'shield':
equippable_component = Equippable(EquipmentSlots.OFF_HAND, defense_bonus=1)
item = Entity(x, y, '[', libtcod.darker_orange, 'Shield', equippable=equippable_component)
elif item_choice == 'fireball_scroll':
...
One thing we can do to make the game a bit more interesting is give the
player a default weapon to start with. Nothing too powerful of course;
this is a roguelike after all. Let’s modify the get_game_variables
function in initialize_new_game.py
to give the player a dagger at the
start.
import tcod as libtcod
from components.equipment import Equipment
+from components.equippable import Equippable
from components.fighter import Fighter
from components.inventory import Inventory
from components.level import Level
from entity import Entity
+from equipment_slots import EquipmentSlots
from game_messages import MessageLog
...
def get_game_variables(constants):
- fighter_component = Fighter(hp=100, defense=1, power=4)
+ fighter_component = Fighter(hp=100, defense=1, power=2)
inventory_component = Inventory(26)
level_component = Level()
equipment_component = Equipment()
player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR,
fighter=fighter_component, inventory=inventory_component, level=level_component,
equipment=equipment_component)
entities = [player]
+ equippable_component = Equippable(EquipmentSlots.MAIN_HAND, power_bonus=2)
+ dagger = Entity(0, 0, '-', libtcod.sky, 'Dagger', equippable=equippable_component)
+ player.inventory.add_item(dagger)
+ player.equipment.toggle_equip(dagger)
game_map = GameMap(constants['map_width'], constants['map_height'])
...
import tcod as libtcod from components.equipment import Equipment from components.equippable import Equippable from components.fighter import Fighter from components.inventory import Inventory from components.level import Level from entity import Entity from equipment_slots import EquipmentSlots from game_messages import MessageLog ... def get_game_variables(constants): fighter_component = Fighter(hp=100, defense=1, power=4) fighter_component = Fighter(hp=100, defense=1, power=2) inventory_component = Inventory(26) level_component = Level() equipment_component = Equipment() player = Entity(0, 0, '@', libtcod.white, 'Player', blocks=True, render_order=RenderOrder.ACTOR, fighter=fighter_component, inventory=inventory_component, level=level_component, equipment=equipment_component) entities = [player] equippable_component = Equippable(EquipmentSlots.MAIN_HAND, power_bonus=2) dagger = Entity(0, 0, '-', libtcod.sky, 'Dagger', equippable=equippable_component) player.inventory.add_item(dagger) player.equipment.toggle_equip(dagger) game_map = GameMap(constants['map_width'], constants['map_height']) ...
Note that we also modified the player’s starting power. We don’t want the player to start off too strong!
One last bit of polish to add: let’s show in the inventory screen which
items are equipped. We can do this by modifying the inventory_menu
function in menus.py
to check if each item is equipped or not. We’ll
have to make a change in the function’s arguments though; we need to
pass the player
instead of just the inventory. Modify the function
like
so:
-def inventory_menu(con, header, inventory, inventory_width, screen_width, screen_height):
+def inventory_menu(con, header, player, inventory_width, screen_width, screen_height):
- if len(inventory.items) == 0:
+ if len(player.inventory.items) == 0:
options = ['Inventory is empty.']
else:
- options = [item.name for item in inventory.items]
+ options = []
+
+ for item in player.inventory.items:
+ if player.equipment.main_hand == item:
+ options.append('{0} (on main hand)'.format(item.name))
+ elif player.equipment.off_hand == item:
+ options.append('{0} (on off hand)'.format(item.name))
+ else:
+ options.append(item.name)
menu(con, header, options, inventory_width, screen_width, screen_height)
def inventory_menu(con, header, inventory, inventory_width, screen_width, screen_height): def inventory_menu(con, header, player, inventory_width, screen_width, screen_height): if len(inventory.items) == 0: if len(player.inventory.items) == 0: options = ['Inventory is empty.'] else: options = [item.name for item in inventory.items] options = [] for item in player.inventory.items: if player.equipment.main_hand == item: options.append('{0} (on main hand)'.format(item.name)) elif player.equipment.off_hand == item: options.append('{0} (on off hand)'.format(item.name)) else: options.append(item.name) menu(con, header, options, inventory_width, screen_width, screen_height)
Because we changed the arguments of this function, we’ll need to adjust
the call we make to it in render_all
.
...
inventory_title = 'Press the key next to an item to drop it, or Esc to cancel.\n'
- inventory_menu(con, inventory_title, player.inventory, 50, screen_width, screen_height)
+ inventory_menu(con, inventory_title, player, 50, screen_width, screen_height)
elif game_state == GameStates.LEVEL_UP:
...
... inventory_title = 'Press the key next to an item to drop it, or Esc to cancel.\n' inventory_menu(con, inventory_title, player.inventory, 50, screen_width, screen_height) inventory_menu(con, inventory_title, player, 50, screen_width, screen_height) elif game_state == GameStates.LEVEL_UP: ...
With that, we have a functional equipment system! This concludes the main tutorial. If you’re wanting more, feel free to check out the extras section, which I’ll try to update now and then with some new content. Now go forth, and create the roguelike of your dreams!
If you want to see the code in its entirety click here.