Complete Roguelike Tutorial, using python+libtcod, part 5

From RogueBasin
Revision as of 19:48, 17 August 2010 by Jotaf (talk | contribs) (page created)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

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

Preparing for Combat


Populating the Dungeon

Can you feel that? It's the sense of antecipation in the air! That's right, from now on we won't rest until our game lets us smite some pitiful minions of evil, for great justice. It'll be a long journey, and the code will become more complicated, but there's no point in tip-toeing around it any more; it's a game and we wanna play it. Some sections will be a bit of a drag, but if you survive that, the next part will be much more fun.

First, the monster placement. With our generic object system, it's pretty easy to create a new object and append it to the objects list. All we need to do is, for each room, create a few monsters in random positions. So we'll create a function to populate a room:


def place_objects(room):
    #choose random number of monsters
    num_monsters = libtcod.random_get_int(0, 0, MAX_ROOM_MONSTERS)
    
    for i in range(num_monsters):
        #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)
        
        if libtcod.random_get_int(0, 0, 100) < 80:  #80% chance of getting an orc
            #create an orc
            monster = Object(x, y, 'o', 'orc', libtcod.desaturated_green,
                blocks=True)
        else:
            #create a troll
            monster = Object(x, y, 'T', 'troll', libtcod.darker_green,
                blocks=True)
            
        objects.append(monster)


The constant MAX_ROOM_MONSTERS = 3 will be defined along with the other constants so it can be easily tweaked.

I decided to create orcs and trolls, but you can choose anything else. In fact, you should change this function as much as you want; this is probably the simplest method. As an alternative, you could define a number of pre-set squads and choose one of them randomly, each squad being a combination of some monsters (for example, one troll and a few orcs, or 50% orcs and 50% goblin archers). The sky is the limit! You can also place items in the same manner; we'll get there later. Remember that this tutorial is only a starting point!

Now, for the dungeon generator to place monsters in each room, call this function right after create_room, inside make_map:


            #add some contents to this room, such as monsters
            place_objects(new_room)


There! I also removed the dummy NPC from the initial objects list (before the main loop), it won't be needed anymore.


Blocking Objects

Here, we'll add a few bits that are necessary before we can move on. First, blocking objects. We don't want more than one monster standing in the same tile, because only one will show up and the rest will be hidden! Some objects, especially items, don't block (it would be silly if you couldn't stand right next to a healing potion!), so each object will have an extra "blocks" property. We'll take the opportunity to allow each object to have a name, which will be useful for game messages and the GUI. Just add those 2 properties to the beggining of the Object 's __init__ method:


    def __init__(self, x, y, char, name, color, blocks=False):
        self.name = name
        self.blocks = blocks


Now, we'll create a function that tests if a tile is blocked, whether due to a wall or an object blocking it. It's very simple, but it will be useful in a bunch of places.


def is_blocked(x, y):
    #first test the map tile
    if map[x][y].blocked:
        return True
    
    #now check for any blocking objects
    for object in objects:
        if object.blocks and object.x == x and object.y == y:
            return True
    
    return False


Ok, time to give it some use! First, the Object 's move method, change the if condition to:


        if not is_blocked(self.x + dx, self.y + dy):


The player (or anyone, for that matter) can't move over a blocking object now! Next, the place_objects function -- see if the tile is unblocked before placing a new monster:


        #only place it if the tile is not blocked
        if not is_blocked(x, y):


Don't forget to indent the lines after that. This guarantees that monsters don't overlap! And since objects have 2 more properties, we need to define them whenever we create one, such as the line that creates the player object:


#create object representing the player
player = Object(0, 0, '@', 'player', libtcod.white, blocks=True)


And the code that creates the monsters:


            if libtcod.random_get_int(0, 0, 100) < 80:  #80% chance of getting an orc
                #create an orc
                monster = Object(x, y, 'o', 'orc', libtcod.desaturated_green,
                    blocks=True)
            else:
                #create a troll
                monster = Object(x, y, 'T', 'troll', libtcod.darker_green,
                    blocks=True)


Game States

Last stop before we get to the actual combat! Our input system has a fatal flaw: player actions (movement, combat) and other keys (fullscreen, other options) are handled the same way. We need to separate them. This way, if the player pauses or dies he can't move or fight, but can press other keys. We also want to know if the player's input means he finished his turn or not; changing to fullscreen shouldn't count as a turn. I know, I know, they're just simple details -- but the game would be very annoying without them! We just need two global variables, the game state and the player's last action (set before the main loop).


game_state = 'playing'
player_action = None


Inside handle_keys, the movement/combat keys can only be used if the game state is "playing":


    if game_state == 'playing':
        #movement keys


We'll also change the same function so it returns a string with the type of player action. Instead of returning True to exit the game, return a special string:


        return 'exit'  #exit game


And testing for all the movement keys, if the player didn't press any, then he didn't take a turn, so return a special string in that case:


        else:
            return 'didnt-take-turn'


After the call to handle_keys in the main loop, we can check for the special string 'exit' to exit the game. Later we'll do other stuff according to the player_action string.


            player_action = handle_keys()
            if player_action == 'exit':
                break


Done. These details were lingering since the beginning and had to be taken care of. Now all that's left is the fun part!

The whole code is available here.