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

In this point we're going to make two major changes to our game. First we are going to make our entities be more event-driven. What this means is that mixins will be able to subscribe to certain events, say when an entity dies or gains a level, and react accordingly. The second change will be adding a new final level to the game. This will allow us to have different map objects and will add a nice bit of variety to our game. As I said, this post does not correspond to a part in Trystan's series, but I do think this is the right time for this post.

Demo Link

The results after this post can be seen here.

assets/dynamicglyph.js

Currently when we add a mixin to our entity the mixin's method simply get added to the entity's set of functions. While this has been working well so far, it is somewhat limited in the sense that we can't really have mixins with overlapping functions. Consider the CorpseDropper mixin we've previously created. Suppose we wanted to add a mixin that would allow entities to drop items. If we wanted an entity to drop both a corpse and an item, we'd have to name both dropping functions differently and check for the presence of both mixins. This causes our mixins to be quite coupled with each other. It would be much cleaner if we could simply raise an event onDeath which mixins could listen for and react to accordingly.

In order to do this, we're going to add a special optional field to mixins called listeners. In here, we will define all of our mixins event listeners. Rather than adding these directly to the mixin, we'll keep an object containing all of our event names and the associated listeners. We'll then add a function called raiseEvent which will take care of calling all listeners with the appropriate arguments.

While we're at it, there's no reason to limit ourselves to entities! We may want items to be event-driven at some point as well, so DynamicGlyph becomes the perfect class to add this functionality to! Let's modify the constructor to establish these listeners.

 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
Game.DynamicGlyph = function(properties) {
    // ...
    // Create a similar object for groups
    this._attachedMixinGroups = {};
    // Set up an object for listeners
    this._listeners = {};
    // 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, init, or listeners 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' && key != 'listeners' 
                && !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;
        }
        // Add all of our listeners
        if (mixins[i].listeners) {
            for (var key in mixins[i].listeners) {
                // If we don't already have a key for this event in our listeners
                // array, add it.
                if (!this._listeners[key]) {
                    this._listeners[key] = [];
                }
                // Add the listener.
                this._listeners[key].push(mixins[i].listeners[key]);
            }
        }
        // Finally call the init function if there is one
        if (mixins[i].init) {
            mixins[i].init.call(this, properties);
        }
    }
};

Now we need to make our function which will raise an event. The signature for this method will look like raiseEvent(eventName, arg1, arg2, ..., argn).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ...
Game.DynamicGlyph.prototype.raiseEvent = function(event) {
    // Make sure we have at least one listener, or else exit
    if (!this._listeners[event]) {
        return;
    }
    // Extract any arguments passed, removing the event name
    var args = Array.prototype.slice.call(arguments, 1)
    // Invoke each listener, with this entity as the context and the arguments
    for (var i = 0; i < this._listeners[event].length; i++) {
        this._listeners[event][i].apply(this, args);
    }
};

And that's it! Our event system is now ready to be used! Let's convert a few mixins to make use of this.

assets/entitymixins.js

The first mixin we will be converting is the CorpseDropper. As I mentioned in the introduction, it'd be nice to simply raise an event for the victim dying (say onDeath). In fact, we could also raise an event for the attacker notifying them that they've killed an entity (say onKill)! We could then convert our ExperienceGainer to use this as well! The code for gaining experience was initially in the Destructible mixin, but we will move it to ExperienceGainer.

 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.EntityMixins.CorpseDropper = {
    name: 'CorpseDropper',
    init: function(template) {
        // Chance of dropping a cropse (out of 100).
        this._corpseDropRate = template['corpseDropRate'] || 100;
    },
    listeners: {
        onDeath: function(attacker) {
            // Check if we should drop a corpse.
            if (Math.round(Math.random() * 100) <= this._corpseDropRate) {
                // Create a new corpse item and drop it.
                this._map.addItem(this.getX(), this.getY(), this.getZ(),
                    Game.ItemRepository.create('corpse', {
                        name: this._name + ' corpse',
                        foreground: this._foreground
                    }));
            }    
        }
    }
};

// ...

Game.EntityMixins.ExperienceGainer = {
    // ...
    listeners: {
        onKill: function(victim) {
            var exp = victim.getMaxHp() + victim.getDefenseValue();
            if (victim.hasMixin('Attacker')) {
                exp += victim.getAttackValue();
            }
            // Account for level differences
            if (victim.hasMixin('ExperienceGainer')) {
                exp -= (this.getLevel() - victim.getLevel()) * 3;
            }
            // Only give experience if more than 0.
            if (exp > 0) {
                this.giveExperience(exp);
            }
        }
    }
};

Now that we've defined our listeners, we can modify our Destructible mixin to raise these events instead of explicitly checking for the presence of mixins.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Game.EntityMixins.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()]);
            // Raise events
            this.raiseEvent('onDeath', attacker);
            attacker.raiseEvent('onKill', this);
            this.kill();
        }
    }
};

As you can see our mixin is now much cleaner! We no longer check for whether an entity can drop a corpse or gain experience, we simply tell them the event happened and let each mixin take care of it's own responsibility!

Another event we could define is when an entity gains a level. Currently we were checking if the entity was Destructible and healing if so as well as checking if the entity should gain a stat point. Instead let's create a onGainLevel event and all of these mixins can listen for this.

 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
// ...

Game.EntityMixins.Destructible = {
    // ...
    listeners: {
        onGainLevel: function() {
            // Heal the entity.
            this.setHp(this.getMaxHp());
        }
    }
};

// ...

Game.EntityMixins.RandomStatGainer = {
    name: 'RandomStatGainer',
    groupName: 'StatGainer',
    listeners: {
        onGainLevel: function() {
            var statOptions = this.getStatOptions();
            // Randomly select a stat option and execute the callback for each
            // stat point.
            while (this.getStatPoints() > 0) {
                // Call the stat increasing function with this as the context.
                statOptions.random()[1].call(this);
                this.setStatPoints(this.getStatPoints() - 1);
            }
        }
    }
};

Game.EntityMixins.PlayerStatGainer = {
    name: 'PlayerStatGainer',
    groupName: 'StatGainer',
    listeners: {
        onGainLevel: function() {
            // Setup the gain stat screen and show it.
            Game.Screen.gainStatScreen.setup(this);
            Game.Screen.playScreen.setSubScreen(Game.Screen.gainStatScreen);
        }
    }
};

In order to make use of these events, let's modify our ExperienceGainer to simply raise the event!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.EntityMixins.ExperienceGainer = {
    // ...
    giveExperience: function(points) {
        // ...
        // Check if we gained at least one level.
        if (levelsGained > 0) {
            Game.sendMessage(this, "You advance to level %d.", [this._level]);
            this.raiseEvent('onGainLevel');
        }
    },
    // ...
};

Our dynamic glyphs now have event functionality! This will make the system much more decoupled overall and will be particularly useful when we may want to attach more than one mixin that will react to a given event. Now let's get to creating our new map!

The final boss

Currently we can win the game by simply hitting [Enter]. Let's change that by adding a special level which can be reached from the bottom of the cave. I'd like to make it so that a hole appears somewhere in the final cave level. When our hero steps over it and descends, he will fall into a large cavern. In the darkness of this cavern, a special enemy awaits - the Giant Zombie (Z)! This zombie will be a very tough foe indeed, randomly leaving behind slime (s) and will grow an extra arm once it's health reaches low enough, increasing it's strength. The hero will only win the game once the zombie is killed! Ideally this cave would be a very big circle with some water patches randomly spread throughout.

Feel free to get creative with your final boss! I just tought it'd be a good test for our mixin framework! :-)

assets/map.js

Our map class so far is great, although it isn't very flexible. If we tried to make another map, it would also have random entities generated onto it. We're going to create seperate map classes which extend the Map class for each map we make, so let's generalize our constructor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Game.Map = function(tiles) {
    this._tiles = tiles;
    // Cache dimensions
    this._depth = tiles.length
    this._width = tiles[0].length;
    this._height = tiles[0][0].length;
    // Setup the field of visions
    this._fov = [];
    this.setupFov();
    // Create a table which will hold the entities
    this._entities = {};
    // Create a table which will hold the items
    this._items = {};
    // Create the engine and scheduler
    this._scheduler = new ROT.Scheduler.Speed();
    this._engine = new ROT.Engine(this._scheduler);
    // Setup the explored array
    this._explored = new Array(this._depth);
    this._setupExploredArray();
};

Since we're no longer passing in our player in the constructor, let's modify the functions for adding and removing an entity to update the player field.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Game.Map.prototype.addEntity = function(entity) {
    // ...
    // If the entity is the player, set the player.
    if (entity.hasMixin(Game.EntityMixins.PlayerActor)) {
        this._player = entity;
    }
};

Game.Map.prototype.removeEntity = function(entity) {
    // ...
    // If the entity is the player, update the player field.
    if (entity.hasMixin(Game.EntityMixins.PlayerActor)) {
        this._player = undefined;
    }
};

assets/tile.js

In order to go about creating our final cave, we're going to need a hole for the player to descend into. Let's make a tile type for this! We also want some water to be spread out in the final cavern, so we'll add this as well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ...
Game.Tile.holeToCavernTile = new Game.Tile({
    character: 'O',
    foreground: 'white',
    walkable: true,
    blocksLight: false
});
Game.Tile.waterTile = new Game.Tile({
    character: '~',
    foreground: 'blue',
    walkable: false,
    blocksLight: false
});
// ...

assets/maps/cave.js

Before we create our final level, let's create our class for the main Cave. This class will simply extend the Map class and will take care of randomly spawning entities and items. We'll also take care of generating a hole on the final 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
30
31
32
33
34
35
36
37
38
39
Game.Map.Cave = function(tiles, player) {
    // Call the Map constructor
    Game.Map.call(this, tiles);
    // Add the player
    this.addEntityAtRandomPosition(player, 0);
    // Add random entities and items to each floor.
    for (var z = 0; z < this._depth; z++) {
        // 15 entities per floor
        for (var i = 0; i < 15; i++) {
            var entity = Game.EntityRepository.createRandom();
            // Add a random entity
            this.addEntityAtRandomPosition(entity, z);
            // Level up the entity based on the floor
            if (entity.hasMixin('ExperienceGainer')) {
                for (var level = 0; level < z; level++) {
                    entity.giveExperience(entity.getNextLevelExperience() -
                        entity.getExperience());
                }
            }
        }
        // 15 items per floor
        for (var i = 0; i < 15; i++) {
            // Add a random entity
            this.addItemAtRandomPosition(Game.ItemRepository.createRandom(), z);
        }
    }
    // Add weapons and armor to the map in random positions and floors
    var templates = ['dagger', 'sword', 'staff', 
        'tunic', 'chainmail', 'platemail'];
    for (var i = 0; i < templates.length; i++) {
        this.addItemAtRandomPosition(Game.ItemRepository.create(templates[i]),
            Math.floor(this._depth * Math.random()));
    }
    // Add a hole to the final cavern on the last level.
    var holePosition = this.getRandomFloorPosition(this._depth - 1);
    this._tiles[this._depth - 1][holePosition.x][holePosition.y] = 
        Game.Tile.holeToCavernTile;
};
Game.Map.Cave.extend(Game.Map);

assets/maps/bosscavern.js

Now we're going to create our final cavern's map! This map will also extend Map. This will be the constructor.

1
2
3
4
5
Game.Map.BossCavern = function() {
    // Call the Map constructor
    Game.Map.call(this, this._generateTiles(80, 24));
};
Game.Map.BossCavern.extend(Game.Map);

As you may have noticed, we have a call to a function called _generateTiles which which should, as the name suggests, build the tiles for the map! As I mentioned, we'd like to have a map with a big open circle with some water spread out throughout the cave. In order to generate this map, we're going to use the Midpoint circle algorithm. We'll have an additional method fillCircle which will accept an array of tiles, a center position, a radius, and a tile to fill with which makes use of this algorithm.

 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
Game.Map.BossCavern.prototype._fillCircle = function(tiles, centerX, centerY, radius, tile) {
    // Copied from the DrawFilledCircle algorithm
    // http://stackoverflow.com/questions/1201200/fast-algorithm-for-drawing-filled-circles
    var x = radius;
    var y = 0;
    var xChange = 1 - (radius << 1);
    var yChange = 0;
    var radiusError = 0;

    while (x >= y) {    
        for (var i = centerX - x; i <= centerX + x; i++) {
            tiles[i][centerY + y] = tile;
            tiles[i][centerY - y] = tile;
        }
        for (var i = centerX - y; i <= centerX + y; i++) {
            tiles[i][centerY + x] = tile;
            tiles[i][centerY - x] = tile;   
        }

        y++;
        radiusError += yChange;
        yChange += 2;
        if (((radiusError << 1) + xChange) > 0) {
            x--;
            radiusError += xChange;
            xChange += 2;
        }
    }
};

Game.Map.BossCavern.prototype._generateTiles = function(width, height) {
    // First we create an array, filling it with empty tiles.
    var tiles = new Array(width);
    for (var x = 0; x < width; x++) {
        tiles[x] = new Array(height);
        for (var y = 0; y < height; y++) {
            tiles[x][y] = Game.Tile.wallTile;
        }
    }
    // Now we determine the radius of the cave to carve out.
    var radius = (Math.min(width, height) - 2) / 2;
    this._fillCircle(tiles, width / 2, height / 2, radius, Game.Tile.floorTile);

    // Now we randomly position lakes (3 - 6 lakes)
    var lakes = Math.round(Math.random() * 3) + 3;
    var maxRadius = 2;
    for (var i = 0; i < lakes; i++) {
        // Random position, taking into consideration the radius to make sure
        // we are within the bounds.
        var centerX = Math.floor(Math.random() * (width - (maxRadius * 2)));
        var centerY = Math.floor(Math.random() * (height - (maxRadius * 2)));
        centerX += maxRadius;
        centerY += maxRadius;
        // Random radius
        var radius = Math.floor(Math.random() * maxRadius) + 1;
        // Position the lake!
        this._fillCircle(tiles, centerX, centerY, radius, Game.Tile.waterTile);
    }

    // Return the tiles in an array as we only have 1 depth level.
    return [tiles];
};

We'd also like to make it so that when the player is added to the map, they are placed at a random walkable position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.Map.BossCavern.prototype.addEntity = function(entity) {
    // Call super method.
    Game.Map.prototype.addEntity.call(this, entity);
    // If it's a player, place at random position
    if (this.getPlayer() === entity) {
        var position = this.getRandomFloorPosition(0);
        entity.setPosition(position.x, position.y, 0);
        // Start the engine!
        this.getEngine().start();
    }
};

assets/entity.js

Now that we've got our final level, we'd like to be able to descend to it! The first thing we're going to do is add a function which simplifies switching an entities map. This will take care of removing the entity from the old map and adding it to the new map! We also change the playScreen's map to the new map if this is the player!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Entity.prototype.switchMap = function(newMap) {
    // If it's the same map, nothing to do!
    if (newMap === this.getMap()) {
        return;
    }
    this.getMap().removeEntity(this);
    // Clear the position
    this._x = 0;
    this._y = 0;
    this._z = 0;
    // Add to the new map
    newMap.addEntity(this);
};

Let's modify our tryMove function to allow the player to change maps to the final boss cavern.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Game.Entity.prototype.tryMove = function(x, y, z, map) {
    // ...
    // If our z level changed, check if we are on stair
    if (z < this.getZ()) {
        // ...
    } else if (z > this.getZ()) {
        if (tile === Game.Tile.holeToCavernTile &&
            this.hasMixin(Game.EntityMixins.PlayerActor)) {
            // Switch the entity to a boss cavern!
            this.switchMap(new Game.Map.BossCavern());
        } else 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]);
        }
    } // ...
};

assets/screens.js

Now we have to actually change the name of the map class we use in our playScreen's enter function! The code for the playScreen has a problem in that it all makes use of an internal map variable. This is kind of redundant as we will generally always want to render the player's map. We're going to remove this internal variable as it's not needed! While we're here, let's make it so that pressing Enter and Escape no longer shows the Win and Lose screen respectively by erasing their cases.

 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
// Define our playing screen
Game.Screen.playScreen = {
    _player: null,
    // ...
    enter: function() {
        // ... 
        // Create our map from the tiles and player
        this._player = new Game.Entity(Game.PlayerTemplate);
        var tiles = new Game.Builder(width, height, depth).getTiles();
        var map = new Game.Map.Cave(tiles, this._player);
        // Start the map's engine
        map.getEngine().start();
    },
    // ...
    render: function(display) {
        // ...
        // Make sure we still have enough space to fit an entire game screen
        topLeftX = Math.min(topLeftX, this._player.getMap().getWidth() - screenWidth);
        // ...
        topLeftY = Math.min(topLeftY, this._player.getMap().getHeight() - screenHeight);
        // ...
        // Store this._player.getMap() and player's z to prevent losing it in callbacks
        var map = this._player.getMap();
        // ...
        // Render the explored map cells
        for (var x = topLeftX; x < topLeftX + screenWidth; x++) {
            for (var y = topLeftY; y < topLeftY + screenHeight; y++) {
                if (map.isExplored(x, y, currentDepth)) {
                    // Fetch the glyph for the tile and render it to the screen
                    // at the offset position.
                    var glyph = map.getTile(x, y, currentDepth);
                    // ...

    },
    handleInput: function(inputType, inputData) {
        if (inputType === 'keydown') {
            // Movement
            // ...
            } else if (inputData.keyCode === ROT.VK_COMMA) {
                var items = this._player.getMap().getItemsAt(this._player.getX(), 
                    this._player.getY(), this._player.getZ());
            // ...
            // Unlock the engine
            this._player.getMap().getEngine().unlock();
        } else if (inputType === 'keypress') {
            // ...
            // Unlock the engine
            this._player.getMap().getEngine().unlock();
        }
    },
    move: function(dX, dY, dZ) {
        // ...
        // Try to move to the new cell
        this._player.tryMove(newX, newY, newZ, this._player.getMap());
    },
    // ...
}

assets/entities.js

We are finally ready to create our final boss and the slime that will be spawned! Our final boss will have a special actor that we haven't created yet, but the slime will just be a regular wander enemy. We also make it so that our giant zombie can't randomly spawn.

 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
Game.EntityRepository.define('giant zombie', {
    name: 'giant zombie', 
    character: 'Z',
    foreground: 'teal',
    maxHp: 30,
    attackValue: 8,
    defenseValue: 5,
    level: 5,
    sightRadius: 6,
    mixins: [Game.EntityMixins.GiantZombieActor, Game.EntityMixins.Sight,
             Game.EntityMixins.Attacker, Game.EntityMixins.Destructible,
             Game.EntityMixins.CorpseDropper,
             Game.EntityMixins.ExperienceGainer]
}, {
    disableRandomCreation: true
});

Game.EntityRepository.define('slime', {
    name: 'slime',
    character: 's',
    foreground: 'lightGreen',
    maxHp: 10,
    attackValue: 5,
    sightRadius: 3,
    tasks: ['hunt', 'wander'],
    mixins: [Game.EntityMixins.TaskActor, Game.EntityMixins.Sight,
             Game.EntityMixins.Attacker, Game.EntityMixins.Destructible,
             Game.EntityMixins.CorpseDropper,
             Game.EntityMixins.ExperienceGainer, Game.EntityMixins.RandomStatGainer]
});

assets/utilities.js

Our GiantZombieActor will operate very similarly to the TaskActor, however we'd like to define our own tasks. In order to do this, we're going to need to be able to extend the TaskActor mixin object. Let's create an extend function which will allow us to extend one object with another.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.extend = function(src, dest) {
    // Create a copy of the source.
    var result = {};
    for (var key in src) {
        result[key] = src[key];
    }
    // Copy over all keys from dest
    for (var key in dest) {
        result[key] = dest[key];
    }
    return result;
};

assets/entitymixins.js

We're now ready to create our GiantZombieActor! We'd like it to be like the TaskActor, however we want to add two tasks - the first will check if the zombie's health is low enough and, if so, "grow" another arm and increase the attack value, and the second will randomly spawn a slime nearby. This actor will also need a listener to make the player win when the zombie dies!

 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
Game.EntityMixins.GiantZombieActor = Game.extend(Game.EntityMixins.TaskActor, {
    init: function(template) {
        // Call the task actor init with the right tasks.
        Game.EntityMixins.TaskActor.init.call(this, Game.extend(template, {
            'tasks' : ['growArm', 'spawnSlime', 'hunt', 'wander']
        }));
        // We only want to grow the arm once.
        this._hasGrownArm = false;
    },
    canDoTask: function(task) {
        // If we haven't already grown arm and HP <= 20, then we can grow.
        if (task === 'growArm') {
            return this.getHp() <= 20 && !this._hasGrownArm;
        // Spawn a slime only a 10% of turns.
        } else if (task === 'spawnSlime') {
            return Math.round(Math.random() * 100) <= 10;
        // Call parent canDoTask
        } else {
            return Game.EntityMixins.TaskActor.canDoTask.call(this, task);
        }
    },
    growArm: function() {
        this._hasGrownArm = true;
        this.increaseAttackValue(5);
        // Send a message saying the zombie grew an arm.
        Game.sendMessageNearby(this.getMap(),
            this.getX(), this.getY(), this.getZ(),
            'An extra arm appears on the giant zombie!');
    },
    spawnSlime: function() {
        // Generate a random position nearby.
        var xOffset = Math.floor(Math.random() * 3) - 1;
        var yOffset = Math.floor(Math.random() * 3) - 1;

        // Check if we can spawn an entity at that position.
        if (!this.getMap().isEmptyFloor(this.getX() + xOffset, this.getY() + yOffset,
            this.getZ())) {
            // If we cant, do nothing
            return;
        }
        // Create the entity
        var slime = Game.EntityRepository.create('slime');
        slime.setX(this.getX() + xOffset);
        slime.setY(this.getY() + yOffset)
        slime.setZ(this.getZ());
        this.getMap().addEntity(slime);
    },
    listeners: {
        onDeath: function(attacker) {
            // Switch to win screen when killed!
            Game.switchScreen(Game.Screen.winScreen);
        }
    }
});

assets/maps/bosscavern.js

We're almost done! We have one last step - actually spawning the giant zombie!

1
2
3
4
5
Game.Map.BossCavern = function() {
    // ...
    // Create the giant zombie
    this.addEntityAtRandomPosition(Game.EntityRepository.create('giant zombie'), 0);
};

index.html

Let's update our scripts!

1
2
3
4
5
6
7
       <!-- ... -->
        <script src="assets/game.js"></script>
        <script src="assets/utilities.js"></script>
        <!-- ... -->
        <script src="assets/items.js"></script>
        <script src="assets/maps/cave.js"></script>
        <script src="assets/maps/bosscavern.js"></script>

Conclusion

We now have a much more robust event-based mixin system, as well as an actual final boss! Can you beat the game? The first time I beat it my hero was level 9, had an attack value of 20 (wielding a pumpkin), defense value of 8 (wearing platemail), and a max HP of 80. Sorry about the length post, but I wanted to try my best to make it so that we only diverted from Trystan's series for 1 post. Our game is now much more entertaining though as it has a real winning condition! In the next post we are back to regular scheduling with Trystan's series where we will be adding screens for help, examining and looking.

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 15 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 16 - Help, Examine and Look Screens