This is the tenth 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 eight part in Trystan's series. All the code for this part can be found at the part 8a tag of the jsrogue repository. At the time of writing, I am still using the d4ea2ab commit for rot.js.

The cave in our game currently isn't all that much fun to explore as we can see the entire cave right away. In this post we will be adding a 'field of vision' to the cave, meaning our hero will only be able to see what is in his direct surroundings. In part 8b our hero will also remember what's been seen before however any monsters or items that are in those areas will remain hidden in the shadows until we re-visit them. This is going to give the cave more atmosphere and will make it much more fun to explore.

Demo Link

The results after this post can be seen here.

assets/tile.js

To add lighting to our game, we will need to define whether light can pass through a certain location or not. We will add a property to our tiles called blocksLight which, if set to true, will mean tiles of that type block light. We can then combine this with our other properties for really interesting effects, eg. a wall of ice which the player cannot walk through but light can pass through or a fake wall which blocks light but the player can walk through.

Let's add our property to the constructor, which will be true by default. Note that for consistency, I have renamed the internal isWalkable and isDiggable properties to walkable and diggable respectively.

1
2
3
4
5
6
7
8
Game.Tile = function(properties) {
    // ...
    // Set up the properties.
    this._walkable = properties['walkable'] || false;
    this._diggable = properties['diggable'] || false;
    this._blocksLight = (properties['blocksLight'] !== undefined) ?
        properties['blocksLight'] : true;
};

Next we add a standard getter for this new property (as well as update the old ones).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Standard getters
Game.Tile.prototype.isWalkable = function() {
    return this._walkable;
}
Game.Tile.prototype.isDiggable = function() {
    return this._diggable;
}
Game.Tile.prototype.isBlockingLight = function() {
    return this._blocksLight;
}

Finally we have to update all our existing tile types to include this new property as well as to rename our old properties.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Game.Tile.nullTile = new Game.Tile();
Game.Tile.floorTile = new Game.Tile({
    character: '.',
    walkable: true,
    blocksLight: false
});
Game.Tile.wallTile = new Game.Tile({
    character: '#',
    foreground: 'goldenrod',
    diggable: true
});
Game.Tile.stairsUpTile = new Game.Tile({
    character: '<',
    foreground: 'white',
    walkable: true,
    blocksLight: false
});
Game.Tile.stairsDownTile = new Game.Tile({
    character: '>',
    foreground: 'white',
    walkable: true,
    blocksLight: false
});

assets/map.js

In order to perform our FOV (field of vision) calculations, we will be using the rot.js FOV.DiscreteShadowcasting class. We will initialize this class with a predicate function that returns whether light can pass through a given tile. Because this class operates on a 2D grid, we will have to create a field of vision for each depth level. There is also a PreciseShadowcasting object provided by rot.js. The Discrete method simply states whether a cell is visible (1) or not (0) while the Precise method allows for more interesting lighting effects by returning partially visible cells (a decimal value between 0 and 1).

Our map class will have an array of fields of visions, with each index corresponding to a given depth level. We will then have a function setupFov which will setup each level's field of vision as well as a getter which returns the field of vision for a given depth level. Note that we set up the field of vision with the topology option set to 4, making our character have a diamond as vision instead of a square.

 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.Map = function(tiles, player) {
    // ...
    // setup the field of visions
    this._fov = [];
    this.setupFov();
    // create a list which will hold the entities
    // ...
}

// ...

Game.Map.prototype.setupFov = function() {
    // Keep this in 'map' variable so that we don't lose it.
    var map = this;
    // Iterate through each depth level, setting up the field of vision
    for (var z = 0; z < this._depth; z++) {
        // We have to put the following code in it's own scope to prevent the
        // depth variable from being hoisted out of the loop.
        (function() {
            // For each depth, we need to create a callback which figures out
            // if light can pass through a given tile.
            var depth = z;
            map._fov.push(
                new ROT.FOV.DiscreteShadowcasting(function(x, y) {
                    return !map.getTile(x, y, depth).isBlockingLight();
                }, {topology: 4}));
        })();
    }
}

Game.Map.prototype.getFov = function(depth) {
    return this._fov[depth];
}

We are now ready to play around with the field of vision!

assets/entities.js

A field of vision generally has a radius of sight, showing all other entities within said radius. Rather than hardcoding this radius, we will create a mixin which will hold the sight radius as a stat, similar to the attack value and health points. This will allow us to create entities which can see different amounts, and will allow us to make more interesting NPC AI in the future.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// This signifies our entity posseses a field of vision of a given radius.
Game.Mixins.Sight = {
    name: 'Sight',
    groupName: 'Sight',
    init: function(template) {
        this._sightRadius = template['sightRadius'] || 5;
    },
    getSightRadius: function() {
        return this._sightRadius;
    }
}

We will now update the Player's template to give them an initial sight radius of 6, signifying the radius of our field of vision is 6 tiles.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Game.PlayerTemplate = {
    character: '@',
    foreground: 'white',
    maxHp: 40,
    attackValue: 10,
    sightRadius: 6,
    mixins: [Game.Mixins.Moveable, Game.Mixins.PlayerActor,
             Game.Mixins.Attacker, Game.Mixins.Destructible,
             Game.Mixins.Sight, Game.Mixins.MessageRecipient]
};

assets/screens.js

We're now ready to use our field of vision objects that we created before to actually determine what can be seen and what to render. Rather than rendering all tiles, we will use the FOV object's compute method to determine which cells can be seen from a given position. This method accepts a callback which will be run for each visible cell.

We will keep track of the visible cells in a hashmap (a javascript object with keys of the form "x,y") and use this to check whether a given tile/entity should be rendered. There are more efficient (both space-wise and performance-wise) ways that this could be done, including some neat bit-twiddling tricks, but to keep it simple we will use string keys. I encourage you to try out other techniques and feel free to ask any questions in the comments! This method will also allow us to easily keep track of cells that have been previously explored in part 8b.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        // ...
        // Make sure we still have enough space to fit an entire game screen
        topLeftY = Math.min(topLeftY, this._map.getHeight() - screenHeight);
        // This object will keep track of all visible map cells
        var visibleCells = {};
        // Find all visible cells and update the object
        this._map.getFov(this._player.getZ()).compute(
            this._player.getX(), this._player.getY(), 
            this._player.getSightRadius(), 
            function(x, y, radius, visibility) {
                visibleCells[x + "," + y] = true;
            });
        // Iterate through all visible map cells
        // ...
    },
    // ...
};

Now that we've got the visible cells, we need to modify our tile rendering and entity rendering to make sure they are in a visible cell.

 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
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++) {
                if (visibleCells[x + ',' + 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());
                    display.draw(
                        x - topLeftX,
                        y - topLeftY,
                        tile.getChar(), 
                        tile.getForeground(), 
                        tile.getBackground());
                }
            }
        }
        // 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()) {
                if (visibleCells[entity.getX() + ',' + entity.getY()]) {
                    display.draw(
                        entity.getX() - topLeftX, 
                        entity.getY() - topLeftY,    
                        entity.getChar(), 
                        entity.getForeground(), 
                        entity.getBackground()
                    );
                }
            }
        }
        // ...
    },
    // ...
};

Conclusion

Our cave is now becoming more interesting! We can no longer see the entire map at once giving the game more atmosphere as well as a more traditional roguelike feel. The next post will modify this field of vision to also render places we've previously explored, so keep an eye out for that!

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 8a 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 8b - Remembering the Cave Layout