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

In this post, we are going to make our monsters more aggressive by making some chase down the hero! We're also going to do something a little bit different from Trystan's tutorial and add speed to the entities, so that some entities act faster than others!

Demo Link

The results after this post can be seen here.

assets/entity.js

Before we make our monsters more aggressive, I think now would be a good time to introduce speed to the game. This will allow us to have some monsters act faster than others, eg. a bat could do 2 moves for every one of the player's turns. All entities will have a speed of 1000 by default so the bat could have a speed of 2000 and move twice as fast.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Entity = function(properties) {
    // ...
    // Acting speed
    this._speed = properties['speed'] || 1000;
};
// ...
Game.Entity.prototype.setSpeed = function(speed) {
    this._speed = speed;
};
// ...
Game.Entity.prototype.getSpeed = function() {
    return this._speed;
};

assets/map.js

In order to take this speed into consideration, we're going to use the ROT.Scheduler.Speed object. This scheduler takes care of using the entity's speed (via the getSpeed method) to determine which entity will act next, as opposed to the ROT.Scheduler.Simple which we were using before that simply ran the entities turns in order.

While we're here, we'll also add a getPlayer getter to the map so that aggressive entities will have reference to the player.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Game.Map = function(tiles, player) {
    // ...
    // Create the engine and scheduler
    this._scheduler = new ROT.Scheduler.Speed();
    this._engine = new ROT.Engine(this._scheduler);
    // Add the player
    this._player = player;
    this.addEntityAtRandomPosition(player, 0);
    // ...
};

// ...

Game.Map.prototype.getPlayer = function() {
    return this._player;
};

assets/entities.js

Finally let's make our bat be twice as fast an our fungus only act every 4 player turns.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.EntityRepository.define('fungus', {
    // ...
    speed: 250,
    // ...
});

Game.EntityRepository.define('bat', {
    // ...
    speed: 2000,
    // ...
});

assets/entitymixins.js

Now that we've got fast monsters, let's make them chase the hero! The first thing I want to do is add a function to the Sight mixin to allow an entity to check and see if it can see another entity. This is not the most efficient code as it recomputes the entire field of vision, but it should do the trick for now.

 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.EntityMixins.Sight = {
    // ...
    },
    canSee: function(entity) {
        // If not on the same map or on different floors, then exit early
        if (!entity || this._map !== entity.getMap() || this._z !== entity.getZ()) {
            return false;
        }

        var otherX = entity.getX();
        var otherY = entity.getY();

        // If we're not in a square field of view, then we won't be in a real
        // field of view either.
        if ((otherX - this._x) * (otherX - this._x) +
            (otherY - this._y) * (otherY - this._y) >
            this._sightRadius * this._sightRadius) {
            return false;
        }

        // Compute the FOV and check if the coordinates are in there.
        var found = false;
        this.getMap().getFov(this.getZ()).compute(
            this.getX(), this.getY(), 
            this.getSightRadius(), 
            function(x, y, radius, visibility) {
                if (x === otherX && y === otherY) {
                    found = true;
                }
            });
        return found;
    }
};

In order to create more aggressive monster AI, we're going to create a task-based Actor mixin to replace the WanderActor. This mixin will allow entities to define a set of tasks in order of priority (eg. hunt, then wander). The mixin will then go through each task, find the first task that can be done that turn (eg. hunt cannot be done if the player is not in sight) and execute it. Tasks are passed in the entity template as an array of strings, and by default all entities simply wander.

In order to implement the hunt task, we want to find the fastest path from the enemy to the hero. In order to do this, we'll use the ROT.Path.AStar class to generate this path.

 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
Game.EntityMixins.TaskActor = {
    name: 'TaskActor',
    groupName: 'Actor',
    init: function(template) {
        // Load tasks
        this._tasks = template['tasks'] || ['wander']; 
    },
    act: function() {
        // Iterate through all our tasks
        for (var i = 0; i < this._tasks.length; i++) {
            if (this.canDoTask(this._tasks[i])) {
                // If we can perform the task, execute the function for it.
                this[this._tasks[i]]();
                return;
            }
        }
    },
    canDoTask: function(task) {
        if (task === 'hunt') {
            return this.hasMixin('Sight') && this.canSee(this.getMap().getPlayer());
        } else if (task === 'wander') {
            return true;
        } else {
            throw new Error('Tried to perform undefined task ' + task);
        }
    },
    hunt: function() {
        var player = this.getMap().getPlayer();

        // If we are adjacent to the player, then attack instead of hunting.
        var offsets = Math.abs(player.getX() - this.getX()) + 
            Math.abs(player.getY() - this.getY());
        if (offsets === 1) {
            if (this.hasMixin('Attacker')) {
                this.attack(player);
                return;
            }
        }

        // Generate the path and move to the first tile.
        var source = this;
        var z = source.getZ();
        var path = new ROT.Path.AStar(player.getX(), player.getY(), function(x, y) {
            // If an entity is present at the tile, can't move there.
            var entity = source.getMap().getEntityAt(x, y, z);
            if (entity && entity !== player && entity !== source) {
                return false;
            }
            return source.getMap().getTile(x, y, z).isWalkable();
        }, {topology: 4});
        // Once we've gotten the path, we want to move to the second cell that is
        // passed in the callback (the first is the entity's strting point)
        var count = 0;
        path.compute(source.getX(), source.getY(), function(x, y) {
            if (count == 1) {
                source.tryMove(x, y, z);
            }
            count++;
        });
    },
    wander: function() {
        // Flip coin to determine if moving by 1 in the positive or negative direction
        var moveOffset = (Math.round(Math.random()) === 1) ? 1 : -1;
        // Flip coin to determine if moving in x direction or y direction
        if (Math.round(Math.random()) === 1) {
            this.tryMove(this.getX() + moveOffset, this.getY(), this.getZ());
        } else {
            this.tryMove(this.getX(), this.getY() + moveOffset, this.getZ());
        }
    }
};

We no longer need WanderActor, so make sure to delete it!

assets/entities.js

We can now convert our entities to this new task-based-actor. Let's make the bat and newt wander. We will also create a kobold (k) that will chase down our hero!

 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
// ...
Game.EntityRepository.define('bat', {
    name: 'bat',
    character: 'B',
    foreground: 'white',
    maxHp: 5,
    attackValue: 4,
    speed: 2000,
    mixins: [Game.EntityMixins.TaskActor, 
             Game.EntityMixins.Attacker, Game.EntityMixins.Destructible,
             Game.EntityMixins.CorpseDropper]
});

Game.EntityRepository.define('newt', {
    name: 'newt',
    character: ':',
    foreground: 'yellow',
    maxHp: 3,
    attackValue: 2,
    mixins: [Game.EntityMixins.TaskActor,
             Game.EntityMixins.Attacker, Game.EntityMixins.Destructible,
             Game.EntityMixins.CorpseDropper]
});

Game.EntityRepository.define('kobold', {
    name: 'kobold',
    character: 'k',
    foreground: 'white',
    maxHp: 6,
    attackValue: 4,
    sightRadius: 5,
    tasks: ['hunt', 'wander'],
    mixins: [Game.EntityMixins.TaskActor, Game.EntityMixins.Sight,
             Game.EntityMixins.Attacker, Game.EntityMixins.Destructible,
             Game.EntityMixins.CorpseDropper]
});

Conclusion

In this post we introduced varying entity speed to the game as well as entities that chase the hero! The game is now becoming trickier and trickier! In the next post, we are going to introduce an experience point and leveling system to the game. Our hero will then be able to become stronger as more and more enemies are killed. I'm hoping to sidetrack from Trystan's series after that post to introduce a "final" level with a tough boss. This will allow us to define an actual winning condition and will also add some variety to the game.

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 13 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 14 - Experience Points and Levels