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

Next Part

Part 8a - Shadows in the Cave