Complete Roguelike Tutorial, using python+libtcod, part 8

From RogueBasin
Revision as of 19:08, 5 December 2010 by Jotaf (talk | contribs) (tabs for spaces and minor wording)
Jump to navigation Jump to search

This is part of a series of tutorials; the main page can be found here.

Items and inventory


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
    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. Actually, 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 you can always tweak the values to your liking of course.

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.

...