A Small Update

Hello everyone! It's been quite a while since my last post and I just wanted to update everyone on what's been going on. I have been quite busy lately as I have moved and started a summer internship. I have not given up on the 'Building a Roguelike in Javascript' series however it will be a while before I can continue it.

Along with this announcement, I have added a personal disclaimer to this site. To repeat it: I am Dominic Charley-Roy. This is is my personal website. The views and opinions expressed on this site are mine alone and are not representative of my current employer. This also holds for any communication I may have with you, the reader, whether it be in person or through the internet.

Although I may not have time to write a new post for a while, please feel free to post any comments or questions you may have and I will gladly answer them. I also encourage you to fork the jsrogue source and to play around with it.

Thank you for reading,

Dominic

 
Read and Post Comments

Building a Roguelike in Javascript - Part 7

Deeper Into The Cave

This is the ninth post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the seventh part in Trystan's series. All the code for this part can be found at the part 7 tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

Our game currently has one simple level. In this post we are going to make our cave have a third dimension allowing our hero to venture deeper and deeper into the cave! I am going to take Trystan's approach of creating the entire world at once, allowing us to freely roam through the various levels without encountering loading screens / delays. This is going to be a long post as there is quite a lot to change, but hang in there - it will be worth it. I'd hate to finish a post without you have a working game, so I'm not going to split it into two parts.

Demo Link

The results after this post can be seen here

assets/tile.js

The first thing we are going to want to create is a tile which allows us to go up one level as well as a tile which allows us to go down a level. Traditionally, roguelikes use stairs for this, and represent stairs going up as < and similarly > for going down.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.Tile.stairsUpTile = new Game.Tile({
    character: '<',
    foreground: 'white',
    isWalkable: true
});
Game.Tile.stairsDownTile = new Game.Tile({
    character: '>',
    foreground: 'white',
    isWalkable: true
});

While we are here, we will also create a helper function which will return a list containing all 8 neighbors of a given tile. It will shuffle them using Array.prototype.randomize to prevent a bias, as or else we will always favor the top-left corner. Admittedly this may not be the best place for this code and if I find that there are too many coordinate helper functions I will move them to their own file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Game.getNeighborPositions = function(x, y) {
    var tiles = [];
    // Generate all possible offsets
    for (var dX = -1; dX < 2; dX ++) {
        for (var dY = -1; dY < 2; dY++) {
            // Make sure it isn't the same tile
            if (dX == 0 && dY == 0) {
                continue;
            }
            tiles.push({x: x + dX, y: y + dY});
        }
    }
    return tiles.randomize();
}

assets/builder.js

As our map generation code is starting to get complicated, we are going to move all the generation code to this file. We will have a Builder object which will be responsible for generating all the tiles for a world. We can then use these tiles to create a Map object. When we generate our map, we are going to have two key variables. The first will be a 3D array representing the tile at each cell in the world. The second will be a 3D array assigning a region number to each cell in a world. Note that for both arrays, the depth will be the first dimension rather than the 3rd one as this will allow us to manipulate an entire depth layer at once.

We are going to split each individual Z level into a bunch of regions. A region is essentialy a grouping of tiles such that any tile in the region can reach all other tiles in the region without having to dig! Every non-walkable tile will have a region value of 0. All walkable tiles will have a region value starting at 1 designating which region they belong to. Here is an example map with the region numbers shown in red:

Let's start with our basic constructor:

1
2
3
4
5
6
7
Game.Builder = function(width, height, depth) {
    this._width = width;
    this._height = height;
    this._depth = depth;
    this._tiles = new Array(depth);
    this._regions = new Array(depth);
};

In order to build the tiles, we're going to create a helper function _generateLevel which consists of our old map generation code and generates a single level using ROT.Map.Cellular.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Game.Builder.prototype._generateLevel = function() {
    // Create the empty map
    var map = new Array(this._width);
    for (var w = 0; w < this._width; w++) {
        map[w] = new Array(this._height);
    }
    // Setup the cave generator
    var generator = new ROT.Map.Cellular(this._width, this._height);
    generator.randomize(0.5);
    var totalIterations = 3;
    // Iteratively smoothen the map
    for (var i = 0; i < totalIterations - 1; i++) {
        generator.create();
    }
    // Smoothen it one last time and then update our map
    generator.create(function(x,y,v) {
        if (v === 1) {
            map[x][y] = Game.Tile.floorTile;
        } else {
            map[x][y] = Game.Tile.wallTile;
        }
    });
    return map;
};

Using this function, we can now build our 3D tiles array! We are also going to want to create our 3D array of region numbers, starting out each cell as 0.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Builder = function(width, height, depth) {
    // ...
    // Instantiate the arrays to be multi-dimension
    for (var z = 0; z < depth; z++) {
        // Create a new cave at each level
        this._tiles[z] = this._generateLevel();
        // Setup the regions array for each depth
        this._regions[z] = new Array(width);
        for (var x = 0; x < width; x++) {
            this._regions[z][x] = new Array(height);
            // Fill with zeroes
            for (var y = 0; y < height; y++) {
                this._regions[z][x][y] = 0;
            }
        }
    }
};

Now that we've got our arrays instantiated, we want to set up the regions for each depth level. In order to do this, we'll need a few helper methods. The first is going to be a method for filling the region that a certain tile belongs to using a flood fill technique. One way to visualize this is to suppose you had a water bucket and you dropped it on a particular tile - our approach then consists of simulating the water expanding out and covering all walkable tiles. Here is a great animation of this process to make things even more clear. Our function will accept a starting tile and a region number and spread out from there, changing the region of all tiles which should belong in the same region. It will return the number of tiles affected. Note that we will be flood filling in all eight directions, even though we only currently have 4-directional movement! We can easily extend our movement and I encourage you to do so as a challenge. We also create a helper function which makes sure a tile can actually be assigned a region.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Game.Builder.prototype._canFillRegion = function(x, y, z) {
    // Make sure the tile is within bounds
    if (x < 0 || y < 0 || z < 0 || x >= this._width ||
        y >= this._height || z >= this._depth) {
        return false;
    }
    // Make sure the tile does not already have a region
    if (this._regions[z][x][y] != 0) {
        return false;
    }
    // Make sure the tile is walkable
    return this._tiles[z][x][y].isWalkable();
}

Game.Builder.prototype._fillRegion = function(region, x, y, z) {
    var tilesFilled = 1;
    var tiles = [{x:x, y:y}];
    var tile;
    var neighbors;
    // Update the region of the original tile
    this._regions[z][x][y] = region;
    // Keep looping while we still have tiles to process
    while (tiles.length > 0) {
        tile = tiles.pop();
        // Get the neighbors of the tile
        neighbors = Game.getNeighborPositions(tile.x, tile.y);
        // Iterate through each neighbor, checking if we can use it to fill
        // and if so updating the region and adding it to our processing
        // list.
        while (neighbors.length > 0) {
            tile = neighbors.pop();
            if (this._canFillRegion(tile.x, tile.y, z)) {
                this._regions[z][tile.x][tile.y] = region;
                tiles.push(tile);
                tilesFilled++;
            }
        }

    }
    return tilesFilled;
}

In order to set up our map, we are going to iterate through each tile of each depth layer, and if it can be filled with a region, we try to fill it. If our region is too small (say 20 tiles or less were filled), then we are going to remove it by filling it with wall tiles. For the sake of simplicity when removing a region I will simply iterate through all tiles searching for tiles with a given region ID. While this may perform extra unnecessary lookups, it will keep the code much simpler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// This removes all tiles at a given depth level with a region number.
// It fills the tiles with a wall tile.
Game.Builder.prototype._removeRegion = function(region, z) {
    for (var x = 0; x < this._width; x++) {
        for (var y = 0; y < this._height; y++) {
            if (this._regions[z][x][y] == region) {
                // Clear the region and set the tile to a wall tile
                this._regions[z][x][y] = 0;
                this._tiles[z][x][y] = Game.Tile.wallTile;
            }
        }
    }
}

// This sets up the regions for a given depth level.
Game.Builder.prototype._setupRegions = function(z) {
    var region = 1;
    var tilesFilled;
    // Iterate through all tiles searching for a tile that
    // can be used as the starting point for a flood fill
    for (var x = 0; x < this._width; x++) {
        for (var y = 0; y < this._height; y++) {
            if (this._canFillRegion(x, y, z)) {
                // Try to fill
                tilesFilled = this._fillRegion(region, x, y, z);
                // If it was too small, simply remove it
                if (tilesFilled <= 20) {
                    this._removeRegion(region, z);
                } else {
                    region++;
                }
            }
        }
    }
}

We are now going to connect the depth levels using their regions! For example if we have a region at level 1, we want to try to connect it to all regions that it overlaps with on the level below it. This will help us minimize the number of times where a player is actualy stuck, and so we could make digging have an actual cost in the future. To do this we first need to be able to find where two regions overlap, and then we iterate through each tiles and try to connect each region at most once with all other regions it overlaps. Again this code may not be the best performance-wise but, like Trystan says, as world generation only happens once it is acceptable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// This fetches a list of points that overlap between one
// region at a given depth level and a region at a level beneath it.
Game.Builder.prototype._findRegionOverlaps = function(z, r1, r2) {
    var matches = [];
    // Iterate through all tiles, checking if they respect
    // the region constraints and are floor tiles. We check
    // that they are floor to make sure we don't try to
    // put two stairs on the same tile.
    for (var x = 0; x < this._width; x++) {
        for (var y = 0; y < this._height; y++) {
            if (this._tiles[z][x][y]  == Game.Tile.floorTile &&
                this._tiles[z+1][x][y] == Game.Tile.floorTile &&
                this._regions[z][x][y] == r1 &&
                this._regions[z+1][x][y] == r2) {
                matches.push({x: x, y: y});
            }
        }
    }
    // We shuffle the list of matches to prevent bias
    return matches.randomize();
}

// This tries to connect two regions by calculating 
// where they overlap and adding stairs
Game.Builder.prototype._connectRegions = function(z, r1, r2) {
    var overlap = this._findRegionOverlaps(z, r1, r2);
    // Make sure there was overlap
    if (overlap.length == 0) {
        return false;
    }
    // Select the first tile from the overlap and change it to stairs
    var point = overlap[0];
    this._tiles[z][point.x][point.y] = Game.Tile.stairsDownTile;
    this._tiles[z+1][point.x][point.y] = Game.Tile.stairsUpTile;
    return true;
}

// This tries to connect all regions for each depth level,
// starting from the top most depth level.
Game.Builder.prototype._connectAllRegions = function() {
    for (var z = 0; z < this._depth - 1; z++) {
        // Iterate through each tile, and if we haven't tried
        // to connect the region of that tile on both depth levels
        // then we try. We store connected properties as strings
        // for quick lookups.
        var connected = {};
        var key;
        for (var x = 0; x < this._width; x++) {
            for (var y = 0; y < this._height; y++) {
                key = this._regions[z][x][y] + ',' +
                      this._regions[z+1][x][y];
                if (this._tiles[z][x][y] == Game.Tile.floorTile &&
                    this._tiles[z+1][x][y] == Game.Tile.floorTile &&
                    !connected[key]) {
                    // Since both tiles are floors and we haven't 
                    // already connected the two regions, try now.
                    this._connectRegions(z, this._regions[z][x][y],
                        this._regions[z+1][x][y]);
                    connected[key] = true;
                }
            }
        }
    }
}

Now all we have to do is update the constructor!

1
2
3
4
5
6
7
Game.Builder = function(width, height, depth) {
    // ...
    for (var z = 0; z < this._depth; z++) {
        this._setupRegions(z);
    }
    this._connectAllRegions();
};

Finally we want to provide some simple getter methods to be able to access the generated data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.Builder.prototype.getTiles = function () {
    return this._tiles;
}
Game.Builder.prototype.getDepth = function () {
    return this._depth;
}
Game.Builder.prototype.getWidth = function () {
    return this._width;
}
Game.Builder.prototype.getHeight = function () {
    return this._height;
}

assets/entity.js

Now that we've added a notion of depth, we must add a Z coordinate to each entity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Entity = function(properties) {
    // ...
    // Instantiate any properties from the passed object
    this._name = properties['name'] || '';
    this._x = properties['x'] || 0;
    this._y = properties['y'] || 0;
    this._z = properties['z'] || 0;
    this._map = null;
}
// ...
Game.Entity.prototype.setZ = function(z) {
    this._z = z;
}
// ...
Game.Entity.prototype.getZ = function() {
    return this._z;
}

We are also going to add a function which will allow us to simply update all 3 coordinates at once:

1
2
3
4
5
Game.Entity.prototype.setPosition = function(x, y, z) {
    this._x = x;
    this._y = y;
    this._z = z;
}

assets/map.js

This file will require some pretty substantial changes to keep track of the third dimension. First we have to change the constructor to keep track of the depth (and change how we get the width and height). We also add a simple getter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.Map = function(tiles, player) {
    this._tiles = tiles;
    // cache dimensions
    this._depth = tiles.length
    this._width = tiles[0].length;
    this._height = tiles[0][0].length;
    // ...
}
Game.Map.prototype.getDepth = function() {
    return this._depth;
};

Now we need to update almost all our helper functions in order to have a third dimension. This is pretty tedious and so to save space I will simply give you the code for it, however feel free to ask for any clarifications in the comments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Gets the tile for a given coordinate set
Game.Map.prototype.getTile = function(x, y, z) {
    // Make sure we are inside the bounds. If we aren't, return
    // null tile.
    if (x < 0 || x >= this._width || y < 0 || y >= this._height ||
        z < 0 || z >= this._depth) {
        return Game.Tile.nullTile;
    } else {
        return this._tiles[z][x][y] || Game.Tile.nullTile;
    }
};

Game.Map.prototype.dig = function(x, y, z) {
    // If the tile is diggable, update it to a floor
    if (this.getTile(x, y, z).isDiggable()) {
        this._tiles[z][x][y] = Game.Tile.floorTile;
    }
}

Game.Map.prototype.isEmptyFloor = function(x, y, z) {
    // Check if the tile is floor and also has no entity
    return this.getTile(x, y, z) == Game.Tile.floorTile &&
           !this.getEntityAt(x, y, z);
}

// ...
Game.Map.prototype.getEntityAt = function(x, y, z){
    // Iterate through all entities searching for one with
    // matching position
    for (var i = 0; i < this._entities.length; i++) {
        if (this._entities[i].getX() == x && this._entities[i].getY() == y &&
            this._entities[i].getZ() == z) {
            return this._entities[i];
        }
    }
    return false;
}
Game.Map.prototype.getEntitiesWithinRadius = function(centerX, centerY,
                                                      centerZ, radius) {
    results = [];
    // Determine our bounds
    var leftX = centerX - radius;
    var rightX = centerX + radius;
    var topY = centerY - radius;
    var bottomY = centerY + radius;
    // Iterate through our entities, adding any which are within the bounds
    for (var i = 0; i < this._entities.length; i++) {
        if (this._entities[i].getX() >= leftX &&
            this._entities[i].getX() <= rightX && 
            this._entities[i].getY() >= topY &&
            this._entities[i].getY() <= bottomY &&
            this._entities[i].getZ() == centerZ) {
            results.push(this._entities[i]);
        }
    }
    return results;
}

Game.Map.prototype.addEntity = function(entity) {
    // Make sure the entity's position is within bounds
    if (entity.getX() < 0 || entity.getX() >= this._width ||
        entity.getY() < 0 || entity.getY() >= this._height ||
        entity.getZ() < 0 || entity.getZ() >= this._depth) {
        throw new Error('Adding entity out of bounds.');
    }
    // Update the entity's map
    entity.setMap(this);
    // Add the entity to the list of entities
    this._entities.push(entity);
    // Check if this entity is an actor, and if so add
    // them to the scheduler
    if (entity.hasMixin('Actor')) {
       this._scheduler.add(entity, true);
    }
}

We also have to modify our function which generates a random position. I am going to add an argument allowing us to specify what level we want to generate a random floor position on. Finally we can add an entity at a random position on a given level.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Map.prototype.getRandomFloorPosition = function(z) {
    // Randomly generate a tile which is a floor
    var x, y;
    do {
        x = Math.floor(Math.random() * this._width);
        y = Math.floor(Math.random() * this._height);
    } while(!this.isEmptyFloor(x, y, z));
    return {x: x, y: y, z: z};
}

Game.Map.prototype.addEntityAtRandomPosition = function(entity, z) {
    var position = this.getRandomFloorPosition(z);
    entity.setX(position.x);
    entity.setY(position.y);
    entity.setZ(position.z);
    this.addEntity(entity);
}

We now update our constructor to fix the player on level 1 as well as to generate more fungus so that it spreads out evenly throughout the dungeon.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.Map = function(tiles, player) {
    // ...
    // add the player
    this.addEntityAtRandomPosition(player, 0);
    // add random fungi
    for (var z = 0; z < this._depth; z++) {
        for (var i = 0; i < 25; i++) {
            this.addEntityAtRandomPosition(new Game.Entity(Game.FungusTemplate), z);
        }
    }
}

assets/entities.js

We have to update some of our mixins which take location into consideration. The first one we will update is the movement mixin. While we are here we will also add some code to handle changing depth levels.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Game.Mixins.Moveable = {
    name: 'Moveable',
    tryMove: function(x, y, z, map) {
        var map = this.getMap();
        // Must use starting z
        var tile = map.getTile(x, y, this.getZ());
        var target = map.getEntityAt(x, y, this.getZ());
        // If our z level changed, check if we are on stair
        if (z < this.getZ()) {
            if (tile != Game.Tile.stairsUpTile) {
                Game.sendMessage(this, "You can't go up here!");
            } else {
                Game.sendMessage(this, "You ascend to level %d!", [z + 1]);
                this.setPosition(x, y, z);
            }
        } else if (z > this.getZ()) {
            if (tile != Game.Tile.stairsDownTile) {
                Game.sendMessage(this, "You can't go down here!");
            } else {
                this.setPosition(x, y, z);
                Game.sendMessage(this, "You descend to level %d!", [z + 1]);
            }
        // If an entity was present at the tile
        } else if (target) {
            // If we are an attacker, try to attack
            // the target
            if (this.hasMixin('Attacker')) {
                this.attack(target);
                return true;
            } else {
                // If not nothing we can do, but we can't 
                // move to the tile
                return false;
            }
        // Check if we can walk on the tile
        // and if so simply walk onto it
        } else if (tile.isWalkable()) {        
            // Update the entity's position
            this.setPosition(x, y, z);
            return true;
        // Check if the tile is diggable, and
        // if so try to dig it
        } else if (tile.isDiggable()) {
            map.dig(x, y, z);
            return true;
        }
        return false;
    }
}

As we changed our getEntitiesWithinRadius function, we also have to change our sendMessageNearby function:

1
2
3
4
5
6
Game.sendMessageNearby = function(map, centerX, centerY, centerZ, message, args) {
    // ...
    // Get the nearby entities
    entities = map.getEntitiesWithinRadius(centerX, centerY, centerZ, 5);
    // ...
}

Finally we have to update our FungusActor to place newly grown fungus at the right depth level as well as use the updated sendMessageNearby.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Game.Mixins.FungusActor = {
    // ...
    act: function() { 
                // ...
                if (xOffset != 0 || yOffset != 0) {
                    // Check if we can actually spawn at that location, and if so
                    // then we grow!
                    if (this.getMap().isEmptyFloor(this.getX() + xOffset,
                                                   this.getY() + yOffset,
                                                   this.getZ())) {
                        var entity = new Game.Entity(Game.FungusTemplate);
                        entity.setPosition(this.getX() + xOffset, 
                            this.getY() + yOffset, this.getZ());
                        this.getMap().addEntity(entity);
                        this._growthsRemaining--;
                        // Send a message nearby!
                        Game.sendMessageNearby(this.getMap(),
                            entity.getX(), entity.getY(), entity.getZ(),
                            'The fungus is spreading!');
                    }
                }
                // ...   
    }
}

assets/game.js

Before we can change the screen input handling, there is one small chnage we must make. Because we will be using the greater than and less than keys as our stairs keys, we must enable the 'keypress' event handler, as these are special keys and the character code can only be extracted from the key press data.

1
2
3
4
// Bind keyboard input events
bindEventToScreen('keydown');
//bindEventToScreen('keyup');
bindEventToScreen('keypress');

assets/screens.js

Now that we've done all this work for generating our map usng the builder, let's update our PlayScreen's enter function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Game.Screen.playScreen = {
    _map: null,
    _player: null,
    enter: function() {
        // Create a map based on our size parameters
        var width = 100;
        var height = 48;
        var depth = 6;
        // Create our map from the tiles and player
        var tiles = new Game.Builder(width, height, depth).getTiles();
        this._player = new Game.Entity(Game.PlayerTemplate);
        this._map = new Game.Map(tiles, this._player);
        //this._map = new Game.Map(map, this._player);
        // Start the map's engine
        this._map.getEngine().start();
    },
    // ...
}

Now our rendering code also needs to be updated to make sure we only render at the player's Z level.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        // ...
        // Iterate through all visible map cells
        for (var x = topLeftX; x < topLeftX + screenWidth; x++) {
            for (var y = topLeftY; y < topLeftY + screenHeight; y++) {
                // Fetch the glyph for the tile and render it to the screen
                // at the offset position.
                var tile = this._map.getTile(x, y, this._player.getZ());
                // ...
            }
        }
        // Render the entities
        var entities = this._map.getEntities();
        for (var i = 0; i < entities.length; i++) {
            var entity = entities[i];
            // Only render the entitiy if they would show up on the screen
            if (entity.getX() >= topLeftX && entity.getY() >= topLeftY &&
                entity.getX() < topLeftX + screenWidth &&
                entity.getY() < topLeftY + screenHeight &&
                entity.getZ() == this._player.getZ()) {
                // ...
            }
        }
        // ...
    }
    // ...
};

Lastly we have to update our input handling! We have to add the cases for the stairs keys as update the movement to take the 3rd dimension into consideration. I also fixed a small bug which caused a turn to be executed when no valid key was pressed. Note that I had to use String.fromCharCode to check if it was a stairs key as they require SHIFT and the Javascript key code always returned period or commma.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Game.Screen.playScreen = {
    // ...
    handleInput: function(inputType, inputData) {
        if (inputType === 'keydown') {
            // If enter is pressed, go to the win screen
            // If escape is pressed, go to lose screen
            if (inputData.keyCode === ROT.VK_RETURN) {
                Game.switchScreen(Game.Screen.winScreen);
            } else if (inputData.keyCode === ROT.VK_ESCAPE) {
                Game.switchScreen(Game.Screen.loseScreen);
            } else {
                // Movement
                if (inputData.keyCode === ROT.VK_LEFT) {
                    this.move(-1, 0, 0);
                } else if (inputData.keyCode === ROT.VK_RIGHT) {
                    this.move(1, 0, 0);
                } else if (inputData.keyCode === ROT.VK_UP) {
                    this.move(0, -1, 0);
                } else if (inputData.keyCode === ROT.VK_DOWN) {
                    this.move(0, 1, 0);
                } else {
                    // Not a valid key
                    return;
                }
                // Unlock the engine
                this._map.getEngine().unlock();
            }
        } else if (inputType === 'keypress') {
            var keyChar = String.fromCharCode(inputData.charCode);
            if (keyChar === '>') {
                this.move(0, 0, 1);
            } else if (keyChar === '<') {
                this.move(0, 0, -1);
            } else {
                // Not a valid key
                return;
            }
            // Unlock the engine
            this._map.getEngine().unlock();
        } 
    },
    move: function(dX, dY, dZ) {
        var newX = this._player.getX() + dX;
        var newY = this._player.getY() + dY;
        var newZ = this._player.getZ() + dZ;
        // Try to move to the new cell
        this._player.tryMove(newX, newY, newZ, this._map);
    }
}

assets/index.html

We are finally done! All we have to do now is update our scripts:

1
2
3
<script src="assets/tile.js"></script>
<script src="assets/builder.js"></script>
<script src="assets/map.js"></script>

Conclusion

Our dungeon now spans across multiple levels! Our hero can now descend into the deepest and darkest parts of the cave and then come back up. I'm terribly sorry about the length of this post, but as I've mentioned before I'd hate to leave you with something that you couldn't play with. In the next post we will add a field of vision so that we only see our hero's immediate surroundings. This will be a great addition and will add a nice touch of atmosphere to our cave.

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 7 tag of the jsrogue repository. As always please feel free to post any comments whether questions, clarifications or criticism!

Thanks for reading,

Dominic

 
Read and Post Comments

Building a Roguelike in Javascript - Part 6

Combat and Messages

This is the eight post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the sixth part in Trystan's series. All the code for this part can be found at the part 6 tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

In the last post we introduced basic combat allowing our hero to whack down any pesky fungi by simply bumping into them. We're now going to make this combat system a bit more realistic, giving attack and defense stats to entities and adding a bit of a random factor to attack damage. As we play our game we're also going to want to know what's going on so we're going to add a simple way to show messages on the screen.

Demo Link

The results after this post can be seen here

Combat - assets/entities.js

Our combat system will work by giving a defense value to Destructible mixins as well as an attack value to Attacker mixins. We will calculate the difference between the two (attackValue - defenseValue) and if the result is greater than zero, randomly select a damage amount in the range [1, attackValue - defenseValue ]. If our defense value is greater than or equal to our attack value, than we only do a damage of 1.

We're going to update the Destructible mixin first. In the last post this mixin always had 1 HP. We will change this so that an entity starts out with a given level of health (maxHp) which can be passed in the template. We also add some getters for the health point stats.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Mixins.Destructible = {
    name: 'Destructible',
    init: function(template) {
        this._maxHp = template['maxHp'] || 10;
        // We allow taking in health from the template incase we want
        // the entity to start with a different amount of HP than the 
        // max specified.
        this._hp = template['hp'] || this._maxHp;
    },
    getHp: function() {
        return this._hp;
    },
    getMaxHp: function() {
        return this._maxHp;
    },
    // ...
};

Now we want to add the defense value. This is going to be a function which all Destructible mixins will implement and will simply return an int. By doing it this way we can have all sorts of neat effects, such as an entity who can be in a defensive mode to get a defense bonus or a chest which gets harder to break the further in the game we are. To make our base destructible mixin more flexible, let's update our constructor to allow specifying a defense value in template. For now we'll make the default defense value function return 0.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.Mixins.Destructible = {
    // ...
    init: function(template) {
        // ...
        this._defenseValue = template['defenseValue'] || 0;
    },
    getDefenseValue: function() {
        return this._defenseValue;
    },
    // ...
};

Now to take care of the attacking mixin. We have a bit of refactoring to do from last post. We are going to rename SimpleAttacker to Attacker.

1
2
3
4
5
Game.Mixins.Attacker = {
    name: 'Attacker',
    groupName: 'Attacker',
    // ...
}

Now we want to add an attack value function to the Attacker mixin similar to the defense value. As the default defense value was 0, our default attack value will be 1. We also want to add an option to read the attack value from the template used to create the mixin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.Mixins.Attacker = {
    // ...
    init: function(template) {
        this._attackValue = template['attackValue'] || 1;
    },
    getAttackValue: function() {
        return this._attackValue;
    },
    // ...
};

Finally we update the default attack method to calculate the damage using the formula we mentioned above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Mixins.Attacker = {
    // ...
    attack: function(target) {
        // If the target is destructible, calculate the damage
        // based on attack and defense value
        if (target.hasMixin('Destructible')) {
            var attack = this.getAttackValue();
            var defense = target.getDefenseValue();
            var max = Math.max(0, attack - defense);
            target.takeDamage(this, 1 + Math.floor(Math.random() * max));
        }
    }
}

Our combat system is now much more flexible and the aspect of randomness certainly makes it more interesting. Let's update our templates to give our player and fungus some stats. Our player will have a max HP of 40 for now and an attack value of 10. Our fungi will have the default defense value and 10 HP.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Player template
Game.PlayerTemplate = {
    character: '@',
    foreground: 'white',
    maxHp: 40,
    attackValue: 10,
    mixins: [Game.Mixins.Moveable, Game.Mixins.PlayerActor,
             Game.Mixins.Attacker, Game.Mixins.Destructible]
}
// Fungus template
Game.FungusTemplate = {
    character: 'F',
    foreground: 'green',
    maxHp: 10,
    mixins: [Game.Mixins.FungusActor, Game.Mixins.Destructible]
}

Messaging - assets/entities.js

Now that we can hit our fungi with varying damage, we're going to want to know how much damage we're inflicting as well as when we sucesfully kill an enemy. To do this, we're going to want to be able to display messages on the screen. To keep in line with our mixin system, we are going to have a mixin for entities which can receive messages. Our player will have this mixin. As the entities take their turn, they will fill up the player's message queue. When the player's turn finally comes, we will display these messages on the screen. We will need a way to clear this message queue as well in order to make sure we don't show the same messages every single turn.

Let's create the MessageRecipient mixin. This will add an internal array of messages to the entity, and provide a method for receiving a message, which will be a string for now, as well methdods for fetching and clearing the messages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Game.Mixins.MessageRecipient = {
    name: 'MessageRecipient',
    init: function(template) {
        this._messages = [];
    },
    receiveMessage: function(message) {
        this._messages.push(message);
    },
    getMessages: function() {
        return this._messages;
    },
    clearMessages: function() {
        this._messages = [];
    }
}

We are now going to want a function to send a message to an entity if it can receive them. As this will be primarily used in the entity code, we will place the function here for now, although I may move it in the future if I can think of a better place or if we start needing more advanced message processing. Our message sending function will specify the entity we wish to target as well as the message itself. To make message formatting simple, I will be using the fantastic sprintf.js library by Alexandru Marasteanu, which is currently at commit 2e852e4b7e. This will allow us to send messages like so:

Game.sendMessage(player, "You hit the %s for %d damage", [name, damage]);


To install this library you will want to download the sprintf.min.js file (this is at the right commit) and add it to the assets folder.

Let's define our sending function! This will take at least a recipient entity and a message as arguments, as well as an optional array of parameters to pass to vsprintf, which is a function similar to sprintf that accepts it's formatting arguments as an array. If our recipient entity has the MessageRecipient mixin, then we will send them the message!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.sendMessage = function(recipient, message, args) {
    // Make sure the recipient can receive the message 
    // before doing any work.
    if (recipient.hasMixin(Game.Mixins.MessageRecipient)) {
        // If args were passed, then we format the message, else
        // no formatting is necessary
        if (args) {
            message = vsprintf(message, args);
        }
        recipient.receiveMessage(message);
    }
}

Now we are all set up to send some messages! Before we do that, we have some small updates to do to our entity templates. We will start using the name property to give a textual name to our entities so that our message could say something along the lines of 'you hit the fungus' rather than 'you hit the F'. We will also make the player a message recipient!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Player template
Game.PlayerTemplate = {
    character: '@',
    foreground: 'white',
    maxHp: 40,
    attackValue: 10,
    mixins: [Game.Mixins.Moveable, Game.Mixins.PlayerActor,
             Game.Mixins.Attacker, Game.Mixins.Destructible,
             Game.Mixins.MessageRecipient]
}
// Fungus template
Game.FungusTemplate = {
    name: 'fungus',
    character: 'F',
    foreground: 'green',
    maxHp: 10,
    mixins: [Game.Mixins.FungusActor, Game.Mixins.Destructible]
}

Now we're ready to add some messages! We want a message to appear when our hero attacks another entity as well as when damage is received in the future! So let's modify the Attacker mixin's attack function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Game.Mixins.Attacker = {
    // ...
    attack: function(target) {
        // If the target is destructible, calculate the damage
        // based on attack and defense value
        if (target.hasMixin('Destructible')) {
            var attack = this.getAttackValue();
            var defense = target.getDefenseValue();
            var max = Math.max(0, attack - defense);
            var damage = 1 + Math.floor(Math.random() * max);

            Game.sendMessage(this, 'You strike the %s for %d damage!', 
                [target.getName(), damage]);
            Game.sendMessage(target, 'The %s strikes you for %d damage!', 
                [this.getName(), damage]);

            target.takeDamage(this, damage);
        }
    }
}

Another potential message we could want is when an entity is destroyed! Let's modify the Destructible mixin!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.Mixins.Destructible = {
    // ...
    takeDamage: function(attacker, damage) {
        this._hp -= damage;
        // If have 0 or less HP, then remove ourseles from the map
        if (this._hp <= 0) {
            Game.sendMessage(attacker, 'You kill the %s!', [this.getName()]);
            Game.sendMessage(this, 'You die!');
            this.getMap().removeEntity(this);
        }
    }
}

Our system seems pretty straightforward for direct entity to entity messaging! We may have cases where we wish to broadcast a message to all entities within a certain distance! In order to do this, we'll need to create some helper functions in our map class first!

assets/map.js

We currently only have methods for returning the entity at a given position! We are going to add a method which will return an array containing all entities which are within a given radial distance from the specified center X and Y position. An example of radial distance is:

3333333
3222223
3211123
3210123
3211123
3222223
3333333


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Game.Map.prototype.getEntitiesWithinRadius = function(centerX, centerY, radius) {
    results = [];
    // Determine our bounds
    var leftX = centerX - radius;
    var rightX = centerX + radius;
    var topY = centerY - radius;
    var bottomY = centerY + radius;
    // Iterate through our entities, adding any which are within the bounds
    for (var i = 0; i < this._entities.length; i++) {
        if (this._entities[i].getX() >= leftX &&
            this._entities[i].getX() <= rightX && 
            this._entities[i].getY() >= topY &&
            this._entities[i].getY() <= bottomY) {
            results.push(this._entities[i]);
        }
    }
    return results;
}

assets/entities.js

We are now ready to make a function for sending a message to all entities near a location. This function will be similar to the sendMessage function we previously defined, except it allows us to specify a location rather than a recipient entity. We will send the message to all entities within a given radius from the location (radius is fixed at 5 for now).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Game.sendMessageNearby = function(map, centerX, centerY, message, args) {
    // If args were passed, then we format the message, else
    // no formatting is necessary
    if (args) {
        message = vsprintf(message, args);
    }
    // Get the nearby entities
    entities = map.getEntitiesWithinRadius(centerX, centerY, 5);
    // Iterate through nearby entities, sending the message if
    // they can receive it.
    for (var i = 0; i < entities.length; i++) {
        if (entities[i].hasMixin(Game.Mixins.MessageRecipient)) {
            entities[i].receiveMessage(message);
        }
    }
}

To make sure that this works, lets make it so that when a new fungus grows it will send a message to the entities nearby! This may not be the most efficient code, but it will serve our purposes just fine for now! We will update the FungusActor mixin!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Game.Mixins.FungusActor = {
    // ...
    act: function() { 
        // ...
                    if (this.getMap().isEmptyFloor(this.getX() + xOffset,
                                                   this.getY() + yOffset)) {
                        var entity = new Game.Entity(Game.FungusTemplate);
                        entity.setX(this.getX() + xOffset);
                        entity.setY(this.getY() + yOffset);
                        this.getMap().addEntity(entity);
                        this._growthsRemaining--;

                        // Send a message nearby!
                        Game.sendMessageNearby(this.getMap(),
                            entity.getX(), entity.getY(),
                            'The fungus is spreading!');
                    }
        // ...
    }
}

The last thing we're going to want to do in this file is make it so that it clears the messages after they have been rendered to the screen. While we haven't done the actual rendering yet, we know that it will be done when we call Game.refresh in the PlayerActor mixin. So after we do this call, let's clear our messages!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Main player's actor mixin
Game.Mixins.PlayerActor = {
    name: 'PlayerActor',
    groupName: 'Actor',
    act: function() {
        // Re-render the screen
        Game.refresh();
        // Lock the engine and wait asynchronously
        // for the player to press a key.
        this.getMap().getEngine().lock();        
        // Clear the message queue
        this.clearMessages();
    }
}

assets/screens.js

Now that our messaging system is up and running, let's start rendering them to the screen! For now we will simply render them in the top left corner. We have to make sure to render them after rendering the map! This may not be the most elegant location for them, and we may move them to a centralized message box in the future (or as a challenge try and modify the code to do that now!) but for now this will do. Luckily we can use the drawText function to wrap text and determine how many lines the text spanned across. This will allow us to easily messages that are too long to fit on one line.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        // ...
        // Get the messages in the player's queue and render them
        var messages = this._player.getMessages();
        var messageY = 0;
        for (var i = 0; i < messages.length; i++) {
            // Draw each message, adding the number of lines
            messageY += display.drawText(
                0, 
                messageY,
                '%c{white}%b{black}' + messages[i]
            );
        }
    },
    // ...
};

Now your hero can go around attacking and killing fungus and seeing feedback right on the screen! One last change I want to make before I finish off this post is to show the player's stats on the last row of the screen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        // ...
        // Render player HP 
        var stats = '%c{white}%b{black}';
        stats += vsprintf('HP: %d/%d ', [this._player.getHp(), this._player.getMaxHp()]);
        display.drawText(0, screenHeight, stats);
    },
    // ...
};

assets/game.js

As we want to reserve the last line of the display for rendering the stats, we are going to increase the size of the display by 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var Game =  {
    // ...
    init: function() {
        // Any necessary initialization will go here.
        this._display = new ROT.Display({width: this._screenWidth,
                                         height: this._screenHeight + 1})
        // ...
    }
    // ...
}

index.html

We have to update our scripts to include the sprintf.js library.

1
2
3
<script src="assets/rot.min.js"></script>
<script src="assets/sprintf.min.js"></script>
<script src="assets/game.js"></script>

Conclusion

This post was pretty significant towards pushing our game closer to a real roguelike game! We now have a more robust combat system as well as a nice way of sending messages to the player! In the next post we will be making our cave have multiple levels, so stick around as it's going to be exciting!

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 6 tag of the jsrogue repository. As always please feel free to post any comments whether questions, clarifications or criticism!

Thanks for reading,

Dominic

Next Part

Part 7 - Deeper Into the Cave

 
Read and Post Comments

Building a Roguelike in Javascript - Part 5b

Attacking Spreading Fungi

This is the seventh post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the fifth part in Trystan's series. All the code for this part can be found at the part 5b tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

Today we're going to make it so that we can interact with our fungi! We're going to add a basic attack system which will simply remove an entity when our player attacks it. For those unfamiliar with roguelikes, attacking is generally done by bumping into or moving into the entity we wish to attack. We're also going to make the fungus entity spread randomly over time, eventually taking over our cave unless our hero chooses to rise to the task and attack every fungus!

Demo Link

The results after this post can be seen here

assets/map.js

Before we can get started with our attack system, we need a way to actually remove an entity from the map. One special consideration to make is that we must also remove the entity from the scheduler if they were an actor. So you're going to want to add the following function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Map.prototype.removeEntity = function(entity) {
    // Find the entity in the list of entities if it is present
    for (var i = 0; i < this._entities.length; i++) {
        if (this._entities[i] == entity) {
            this._entities.splice(i, 1);
            break;
        }
    }
    // If the entity is an actor, remove them from the scheduler
    if (entity.hasMixin('Actor')) {
        this._scheduler.remove(entity);
    }
}

When we implement the fungi spreading, we're going to make it spread to an adjacent tile. In order to do this, we'll need to check whether a tile is an empty floor tile. As this will be a pretty common thing to check for, let's create a helper function which will do just that so we have a central place to add in features:

1
2
3
4
5
Game.Map.prototype.isEmptyFloor = function(x, y) {
    // Check if the tile is floor and also has no entity
    return this.getTile(x, y) == Game.Tile.floorTile &&
           !this.getEntityAt(x, y);
}

While we're here, let's go ahead and update our getRandomFloorPosition to make use of this function:

1
2
3
4
5
6
7
8
9
Game.Map.prototype.getRandomFloorPosition = function() {
    // Randomly generate a tile which is a floor
    var x, y;
    do {
        x = Math.floor(Math.random() * this._width);
        y = Math.floor(Math.random() * this._width);
    } while(!this.isEmptyFloor(x, y));
    return {x: x, y: y};
}

The last change we're going to want to make is to decrease the number of initial fungi. Because they will be spreading throughout the dungeon it will be much more apparent if we start with a small number. Feel free to play around with the number! We have to change:

1
2
3
4
5
6
7
Game.Map = function(tiles, player) {
    // ...
    // add random fungi
    for (var i = 0; i < 50; i++) {
        this.addEntityAtRandomPosition(new Game.Entity(Game.FungusTemplate));
    }
}

We're now ready to make it so we can hack away at these pesky growing fungi!

assets/entities.js

In order to implement our basic attacking system, we're going to have two mixins. The first wil be a mixin denoting an entity that is destructible. For now the most basic mixin will have a certain number of hit points and a takeDamage function. The takeDamage function will subtract the damage caused by an attacker, and if the hit points are 0 or below, remove that entity from the map. For now all Destructible entities will have 1 hit point.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Mixins.Destructible = {
    name: 'Destructible',
    init: function() {
        this._hp = 1;
    },
    takeDamage: function(attacker, damage) {
        this._hp -= damage;
        // If have 0 or less HP, then remove ourseles from the map
        if (this._hp <= 0) {
            this.getMap().removeEntity(this);
        }
    }
}

The second will be some kind of Attacker mixin, which has a sole method attack. When an entity chooses to attack a target, we will call the attack method with the target. Similar to the Actor mixin we implemented last post, the Attacker mixin will also be a common group (using the groupName) and we'll create more specific ones when necessary, allowing us to come up with really cool ways of attacking. To keep it simple, if our target is destructible, we will damage the target by 1 HP, removing it from the map.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.Mixins.SimpleAttacker = {
    name: 'SimpleAttacker',
    groupName: 'Attacker',
    attack: function(target) {
        // Only remove the entity if they were attackable
        if (target.hasMixin('Destructible')) {
            target.takeDamage(this, 1);
        }
    }
}

Now we need to update our templates to include these mixins! We want to add a SimpleAttacker and Destructible to the player's template as in the future the player will be attackable. We also want to add Destructible to the fungi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Player template
Game.PlayerTemplate = {
    character: '@',
    foreground: 'white',
    mixins: [Game.Mixins.Moveable, Game.Mixins.PlayerActor,
             Game.Mixins.SimpleAttacker, Game.Mixins.Destructible]
}
// Fungus template
Game.FungusTemplate = {
    character: 'F',
    foreground: 'green',
    mixins: [Game.Mixins.FungusActor, Game.Mixins.Destructible]
}

So now we have our attacking mixins set up and put in place. However we have to change our Moveable as well in order to make it so that if there is an entity present at the cell we wish to move to, and we are an attacker, than try to attack the entity!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Define our Moveable mixin
Game.Mixins.Moveable = {
    name: 'Moveable',
    tryMove: function(x, y, map) {
        var tile = map.getTile(x, y);
        var target = map.getEntityAt(x, y);
        // If an entity was present at the tile
        if (target) {
            // If we are an attacker, try to attack
            // the target
            if (this.hasMixin('Attacker')) {
                this.attack(target);
                return true;
            } else {
                // If not nothing we can do, but we can't 
                // move to the tile
                return false;
            }
        // Check if we can walk on the tile
        // and if so simply walk onto it
        } else if (tile.isWalkable()) {        
            // Update the entity's position
            this._x = x;
            this._y = y;
            return true;
        // Check if the tile is diggable, and
        // if so try to dig it
        } else if (tile.isDiggable()) {
            map.dig(x, y);
            return true;
        }
        return false;
    }
}

Our attacking system is now put in place! At this point you can go ahead and try it out! If everything went well you should be able to go up to a fungus on the map, walk into it and it will dissapear! In a few posts we'll turn this into a real attacking system with stats and health and all sorts of other goodies, but for it's pretty pleasing to see what we've already accomplished!

The next step is to implement the fungus growth. Recall last post that the scheduling system worked by calling the act method of entities turn by turn. We had created an actor mixin for the fungus, but it didn't do anything yet. As this is called every time the fungus has a turn, this seems like it could be the perfect place to put our spreading logic!

We want each fungus to be able to spread in one of the eight adjacent squares, however to make it interesting we don't want to do it every turn. Instead we are going to make it so that every turn the fungus has a fixed chance of growing. We're also going to want to limit the number of times a fungus can grow to let's say 5. Remember that mixins have an optional init function which can be called when the mixin is created to add state to the fungus. For now let's make it set up our state with a counter keeping trakc of how many times we can grow.

1
2
3
4
5
6
7
8
9
Game.Mixins.FungusActor = {
    name: 'FungusActor',
    groupName: 'Actor',
    init: function() {
        this._growthsRemaining = 5;
    },
    act: function() { 
    }
}

At every turn, if a fungus can still grow we use Math.random, which returns a number between 0.0 and 1.0, to determine if it should grow this turn. If so, then we generate the coordinates of a random adjacent square and check if we can grow to it. If we can, we will spawn a new fungus at that position and update our counters!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Game.Mixins.FungusActor = {
    name: 'FungusActor',
    groupName: 'Actor',
    init: function() {
        this._growthsRemaining = 5;
    },
    act: function() { 
        // Check if we are going to try growing this turn
        if (this._growthsRemaining > 0) {
            if (Math.random() <= 0.02) {
                // Generate the coordinates of a random adjacent square by
                // generating an offset between [-1, 0, 1] for both the x and
                // y directions. To do this, we generate a number from 0-2 and then
                // subtract 1.
                var xOffset = Math.floor(Math.random() * 3) - 1;
                var yOffset = Math.floor(Math.random() * 3) - 1;
                // Make sure we aren't trying to spawn on the same tile as us
                if (xOffset != 0 || yOffset != 0) {
                    // Check if we can actually spawn at that location, and if so
                    // then we grow!
                    if (this.getMap().isEmptyFloor(this.getX() + xOffset,
                                                   this.getY() + yOffset)) {
                        var entity = new Game.Entity(Game.FungusTemplate);
                        entity.setX(this.getX() + xOffset);
                        entity.setY(this.getY() + yOffset);
                        this.getMap().addEntity(entity);
                        this._growthsRemaining--;
                    }
                }
            }
        }
    }
}

assets/screens.js

We are now just about ready to watch our fungus growth! We're going to reduce the map size as the fungus growing rapidly gets out of hand and depending on your computer may make movement very sluggish. So I've shrunk the map down to 100 by 48:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.Screen.playScreen = {
    // ...
    enter: function() {  
        var map = [];
        // Create a map based on our size parameters
        var mapWidth = 100;
        var mapHeight = 48;
        // ...
    }
    // ...
}

Conclusion

We've now got a cave which gets overrun by fungus which our hero can then go and hack away with our simple attacking system! In the next post we are going to start focusing on real combat as well as giving some feedbak to the player through messages.

I hope you enjoyed this post and that you'll stick around for the next part! Remember that all the code for this part can be found at the part 5b tag of the jsrogue repository. Also please feel free to post any comments whether questions, clarifications or criticism!

Thanks for reading,

Dominic

Next Part

Part 6 - Combat and Messages

 
Read and Post Comments

Building a Roguelike in Javascript - Part 5a

Populating the Cave

This is the sixth post in the Building a Roguelike in Javascript series. I recommend you start at the beginning unless you've been following along. This part corresponds to the fifth part in Trystan's series. All the code for this part can be found at the part 5a tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js however there seemed to be an error in the rot.min.js file included in the github repository (it was missing ROT.Scheduler.Simple) so I fetched it again from the same commit. You may need to do the same. It can be obtained here.

Today's going to be an exciting post! We are going to add more entities to our cave so that the player is not alone! This is going to be a pretty significant step towards populating our cave with a variety of terrifying monsters. To keep it simple our first monster will be a fungus. This fungus won't move for the moment and we can't currently walk through it, but in the next post it will spread over time and we'll be able to clear it by walking through it! I'm warning you ahead of time - this post is quite long, but builds some pretty fundamental stuff.

Demo Link

The results after this post can be seen here

assets/entity.js

Before we do anything, we're going to add an extra field to the Entity class. We want an entity to be associated with a map. Let's first update the constructor:

1
2
3
4
5
6
7
8
9
Game.Entity = function(properties) {
    // ...
    // Instantiate any properties from the passed object
    this._name = properties['name'] || '';
    this._x = properties['x'] || 0;
    this._y = properties['y'] || 0;
    this._map = null;
    // ...
}

Now we'll also want our standard getter and setter:

1
2
3
4
5
6
Game.Entity.prototype.setMap = function(map) {
    this._map = map;
}
Game.Entity.prototype.getMap = function() {
    return this._map;
}

We are also going to add a way to group together mixins which offer a common interface. Consider the Moveable mixin we added last post. Suppose you wanted to have a ghost entity which could move through any type of cell and a molerat entity which could dig and move at the same time. Rather than creating an extremely complex and general Moveable mixin, we'd like to create a bunch of simple ones and just note that they offer the same common functionality. Once this is done, we'd like to be able to check if a mixin implements a given interface without being specific (eg. is it moveable rather than is it a ghost's moveable mixin). To do this we will add an optional groupName to mixins which acts similar to the name property but mixins implementing the same interface should have the same groupName.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var PlayerMoveableMixin = {
    name: 'PlayerMoveable',
    groupName: 'Moveable',
    tryMove: function(x, y){...}
}
var MoleratMoveableMixin = {
    name: 'MoleratMoveable',
    groupName: 'Moveable',
    tryMove: function(x, y){...}
}
var GhostMoveableMixin = {
    name: 'GhostMoveable',
    groupName: 'Moveable',
    tryMove: function(x, y){...}
}

Ideally we'd like to be able to test both specifically for a given mixin by passing either the name or the mixin itself, as well as testing generally by passing the group name:

1
2
3
4
5
6
// Testing specifically
entity.hasMixin('GhostMoveable')
entity.hasMixin(GhostMoveableMixin)

// Testing generally 
entity.hasMixin('Moveable')

To implement this we must first update the constructor to keep track of the group names as well as the mixin names:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Game.Entity = function(properties) {
    // ...
    // Create an object which will keep track what mixins we have
    // attached to this entity based on the name property
    this._attachedMixins = {};
    // Create a similar object for groups
    this._attachedMixinGroups = {};
    // Setup the object's mixins
    var mixins = properties['mixins'] || [];
    for (var i = 0; i < mixins.length; i++) {
        // Copy over all properties from each mixin as long
        // as it's not the name or the init property. We
        // also make sure not to override a property that
        // already exists on the entity.
        for (var key in mixins[i]) {
            if (key != 'init' && key != 'name' && !this.hasOwnProperty(key)) {
                this[key] = mixins[i][key];
            }
        }
        // Add the name of this mixin to our attached mixins
        this._attachedMixins[mixins[i].name] = true;
        // If a group name is present, add it
        if (mixins[i].groupName) {
            this._attachedMixinGroups[mixins[i].groupName] = true;
        }
        // Finally call the init function if there is one
        if (mixins[i].init) {
            mixins[i].init.call(this, properties);
        }
    }
};

Finally we need to update the hasMixin function to check the group names as well if we are checking with a string:

1
2
3
4
5
6
7
8
Game.Entity.prototype.hasMixin = function(obj) {
    // Allow passing the mixin itself or the name / group name as a string
    if (typeof obj === 'object') {
        return this._attachedMixins[obj.name];
    } else {
        return this._attachedMixins[obj] || this._attachedMixinGroups[obj];
    }
}

assets/map.js

We now need a way to keep track of our entities on the map. All our entities presently on the map are going to be stored in a list. We are also going to be using the ROT.Engine and ROT.Scheduler to manage our entities and make them take turns in the appropriate order. This system will be very important later on when entities start having different speeds so we will put it in the base right away. Let's update the constructor to reflect this:

1
2
3
4
5
6
7
8
Game.Map = function(tiles) {
    // ...
    // create a list which will hold the entities
    this._entities = [];
    // create the engine and scheduler
    this._scheduler = new ROT.Scheduler.Simple();
    this._engine = new ROT.Engine(this._scheduler);
};

We now want to provide a way to get the engine as well as all the entities on the map. Finally we'll need to be able to check if there is an entity at a given position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Game.Map.prototype.getEngine = function() {
    return this._engine;
}
Game.Map.prototype.getEntities = function() {
    return this._entities;
}
Game.Map.prototype.getEntityAt = function(x, y){
    // Iterate through all entities searching for one with
    // matching position
    for (var i = 0; i < this._entities.length; i++) {
        if (this._entities[i].getX() == x && this._entities[i].getY() == y) {
            return this._entities[i];
        }
    }
    return false;
}

Before we can actually add entities to our maps, we need to look at how the scheduler actually works!

Scheduling and the Game Loop

The scheduling engine has a queue of all the entities and gives them a turn one by one. We're currently using the Simple scheduler which simply gives the entities turns in the order they were added to the queue, and allows us to mark some entities as being re-added into the queue when their turn is complete. In the future we will use a more complicated scheduler which will allow us for some entities to act faster than others.

The ROT.Engine object takes care of interacting with the scheduler and is started using start. It extracts the next entity from the scheduler and calls the act function on that entity, so we will need to define this function for all our entities that we want to be dynamic. We are going to do this with our mixin system we developed in the last post! Because of the nature of roguelikes, when it is the player's turn we want to wait until the player presses a key before we process the next turn. However the engine is continually running and so we won't be able to interact with our game if we simply start the engine! So when it is the player's turn we must lock the engine in order to wait for a key press, unlocking it when we are done.

Just to clarify, let's consider the game loop. Up until now, our game loop has consisted of rendering the screen once and then waiting for input, processing said input and then re-rendering the screen. Our new game loop will be structured like this:

  1. Process all turns until it is the player's turn
  2. Render the screen and lock the engine
  3. Wait for a player to press a key, process it and then unlock the engine
  4. Go back to step 1

assets/game.js

In order to get set up for our new game loop, there are some things we need to change. First of all, we will make it so that the screen no longer automatically renders after we call the screen's handleInput. This means screens will have to make a call to rerender the screen whenever they want the screen to be updated. Remember that switching screens will automatically render the screen, so we don't have to change anything in the menu screen.

To do this, we have to edit the code for the bindEventToScreen function in Game.init:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var Game = {
    // ...
    init: function() {
        // ...
        var bindEventToScreen = function(event) {
            window.addEventListener(event, function(e) {
                // When an event is received, send it to the
                // screen if there is one
                if (game._currentScreen !== null) {
                    // Send the event type and data to the screen
                    game._currentScreen.handleInput(event, e);
                }
            });
        }
        // ...
    },
    // ...
}

We are now going to add a helper function called refresh which will clear the screen and render the current screen to our Game namespace.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var Game = {
    // ...
    refresh: function() {
        // Clear the screen
        this._display.clear();
        // Render the screen
        this._currentScreen.render(this._display);
    },
    // ...
}

Finally we're going to update the code of our switchScreen function to use this refresh helper function to make sure we always go through the same route for re-rendering the screen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var Game = {
    // ...
    switchScreen: function(screen) {
        // If we had a screen before, notify it that we exited
        if (this._currentScreen !== null) {
            this._currentScreen.exit();
        }
        // Clear the display
        this.getDisplay().clear();
        // Update our current screen, notify it we entered
        // and then render it
        this._currentScreen = screen;
        if (!this._currentScreen !== null) {
            this._currentScreen.enter();
            this.refresh();
        }
    }
}

This now gives us more control over when we re-render the screen, and will allow us to same some rendering calls in the future! If we made it so that the screen would render after each time we handled input, than we would end up re-rendering screen twice - once after handling the input and another time after processing all the turns.

assets/entities.js

We will be creating Actor mixins which will have a sole method act. We will use the group name Actor for all our actor mixins to allow us to easily check if a given entity needs to be scheduled. Let's create our player's actor mixin first. As I mentioned before we need to refresh the screen as well as lock the engine when it is the player's turn.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Main player's actor mixin
Game.Mixins.PlayerActor = {
    name: 'PlayerActor',
    groupName: 'Actor',
    act: function() {
        // Re-render the screen
        Game.refresh();
        // Lock the engine and wait asynchronously
        // for the player to press a key.
        this.getMap().getEngine().lock();        
    }
}

Now we have to add this mixin to our player's template!

1
2
3
4
Game.PlayerTemplate = {
    // ...
    mixins: [Game.Mixins.Moveable, Game.Mixins.PlayerActor]
}

We will also be creating an Actor mixin for the fungus. Notice that both actor mixins have the same group name!

1
2
3
4
5
Game.Mixins.FungusActor = {
    name: 'FungusActor',
    groupName: 'Actor',
    act: function() { }
}

We're going to create a template for our fungus entity! They will show us a F on the screen, but feel free to play around with the template and make it look however you like! It's your game after all!

1
2
3
4
5
Game.FungusTemplate = {
    character: 'F',
    foreground: 'green',
    mixins: [Game.Mixins.FungusActor]
}

Finally we're going to change our Moveable mixin to make it so that we can't walk through a tile if an entity is present at that tile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Game.Mixins.Moveable = {
    name: 'Moveable',
    tryMove: function(x, y, map) {
        var tile = map.getTile(x, y);
        var target = map.getEntityAt(x, y);
        // If an entity was present at the tile, then we
        // can't move there
        if (target) {
            return false;
        // Check if we can walk on the tile
        // and if so simply walk onto it
        } else if (tile.isWalkable()) {        
            // Update the entity's position
            this._x = x;
            this._y = y;
            return true;
        // Check if the tile is diggable, and
        // if so try to dig it
        } else if (tile.isDiggable()) {
            map.dig(x, y);
            return true;
        }
        return false;
    }
}

assets/map.js

We haven't actually provided a way to add an entity to our map yet! We're going to create a base function which allows us to add an entity to our map. In our function, we check if the entity is an Actor (thanks to our improvement), and if so we add them to the scheduler as well! We'll also want a method which adds an entity at a random position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Game.Map.prototype.addEntity = function(entity) {
    // Make sure the entity's position is within bounds
    if (entity.getX() < 0 || entity.getX() >= this._width ||
        entity.getY() < 0 || entity.getY() >= this._height) {
        throw new Error('Adding entity out of bounds.');
    }
    // Update the entity's map
    entity.setMap(this);
    // Add the entity to the list of entities
    this._entities.push(entity);
    // Check if this entity is an actor, and if so add
    // them to the scheduler
    if (entity.hasMixin('Actor')) {
       this._scheduler.add(entity, true);
    }
}

Game.Map.prototype.addEntityAtRandomPosition = function(entity) {
    var position = this.getRandomFloorPosition();
    entity.setX(position.x);
    entity.setY(position.y);
    this.addEntity(entity);
}

We now have a fix to make to our function which generates a random floor position! We need to also check that there is no entity already present at the location we generated!:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.Map.prototype.getRandomFloorPosition = function() {
    // Randomly generate a tile which is a floor
    var x, y;
    do {
        x = Math.floor(Math.random() * this._width);
        y = Math.floor(Math.random() * this._width);
    } while(this.getTile(x, y) != Game.Tile.floorTile ||
            this.getEntityAt(x, y));
    return {x: x, y: y};
}

The last part, and arguably the most important part, is that we must add our player entity as well as some fungus to the map! We will do this in the constructor before starting the engine, and in fact will modify the constructor to accept the player entity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Game.Map = function(tiles, player) {
    this._tiles = tiles;
    // cache the width and height based
    // on the length of the dimensions of
    // the tiles array
    this._width = tiles.length;
    this._height = tiles[0].length;
    // create a list which will hold the entities
    this._entities = [];
    // create the engine and scheduler
    this._scheduler = new ROT.Scheduler.Simple();
    this._engine = new ROT.Engine(this._scheduler);
    // add the player
    this.addEntityAtRandomPosition(player);
    // add random fungi
    for (var i = 0; i < 1000; i++) {
        this.addEntityAtRandomPosition(new Game.Entity(Game.FungusTemplate));
    }
};

assets/screens.js

We now need to update the enter function of the playScreen to pass our player to the map, as well as to start the engine.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.Screen.playScreen = {
    // ...
    enter: function() {  
        // ...
        // Create our map from the tiles and player
        this._player = new Game.Entity(Game.PlayerTemplate);
        this._map = new Game.Map(map, this._player);
        // Start the map's engine
        this._map.getEngine().start();
    }
    // ...
}

We now need to change our rendering function to render all the map's entities as opposed to just the player. You're going to want to replace the player rendering with this snippet, which renders all visible entities:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Game.Screen.playScreen = {
    // ...
    render: function() {  
        // ...
        // Render the entities
        var entities = this._map.getEntities();
        for (var i = 0; i < entities.length; i++) {
            var entity = entities[i];
            // Only render the entitiy if they would show up on the screen
            if (entity.getX() >= topLeftX && entity.getY() >= topLeftY &&
                entity.getX() < topLeftX + screenWidth &&
                entity.getY() < topLeftY + screenHeight) {
                display.draw(
                    entity.getX() - topLeftX, 
                    entity.getY() - topLeftY,    
                    entity.getChar(), 
                    entity.getForeground(), 
                    entity.getBackground()
                );
            }
        }
    }
    // ...
}

And the last step is to make our handleInput function unlock the map engine!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Game.Screen.playScreen = {
    // ...
    handleInput: function(inputType, inputData) {
        if (inputType === 'keydown') {
            // If enter is pressed, go to the win screen
            // If escape is pressed, go to lose screen
            if (inputData.keyCode === ROT.VK_RETURN) {
                Game.switchScreen(Game.Screen.winScreen);
            } else if (inputData.keyCode === ROT.VK_ESCAPE) {
                Game.switchScreen(Game.Screen.loseScreen);
            } else {
                // Movement
                if (inputData.keyCode === ROT.VK_LEFT) {
                    this.move(-1, 0);
                } else if (inputData.keyCode === ROT.VK_RIGHT) {
                    this.move(1, 0);
                } else if (inputData.keyCode === ROT.VK_UP) {
                    this.move(0, -1);
                } else if (inputData.keyCode === ROT.VK_DOWN) {
                    this.move(0, 1);
                }
                // Unlock the engine
                this._map.getEngine().unlock();
            }
        }    
    },
    // ...
}

Conclusion

Our game is now actually starting to ressemble a real game! We can walk around a cave filled with fungi! We are also set up for having many entities on a given map, and our small adjustement to the mixin system has made it much easier to create simple but powerful mixins.

I hope you enjoyed this post and that you'll stick around for the next part! I'm sorry for the length of the last two posts, I will try to keep it down in the future. I'd just hate to finish off a post with something you couldn't use! Remember that all the code for this part can be found at the part 5a tag of the jsrogue repository. Also please feel free to post any comments whether questions, clarifications or criticism!

Thanks for reading,

Dominic

Next Part

Part 5b - Attacking Spreading Fungi

 
Read and Post Comments