This is the sixteenth 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 the twelfth part in Trystan's series. All the code for this part can be found at the part 12 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 add weapons that an entity can wield to increase attack value as well as wearable items that will increase an entity's defense value.

Demo Link

The results after this post can be seen here.

assets/map.js (bug fix)

I spotted a strange bug after part 11 which was caused when an entity would go anywhere with position x=0. I simply updated the condition in the updateEntityPosition function to check that a position was given and that it was a number, rather than simply checking that it wasn't falsy since 0 is falsy in Javascript!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Map.prototype.updateEntityPosition = function(
    entity, oldX, oldY, oldZ) {
    // Delete the old key if it is the same entity
    // and we have old positions.
    if (typeof oldX === 'number') {
        var oldKey = oldX + ',' + oldY + ',' + oldZ;
        if (this._entities[oldKey] == entity) {
            delete this._entities[oldKey];
        }
    }
    // Make sure the entity's position is within bounds
    // ...
}

assets/itemmixins.js

We first need to create a mixin for equippable items. To keep things simple, entities will only be able to equip 1 weapon and 1 armor at a time. This mixin will contain both an attack value as well as a defence value (say you have spiked armor which increases your attack value). It will also denote whether an item can be used as a weapon (via the wieldable property) or as an armor (via the wearable property).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Game.ItemMixins.Equippable = {
    name: 'Equippable',
    init: function(template) {
        this._attackValue = template['attackValue'] || 0;
        this._defenseValue = template['defenseValue'] || 0;
        this._wieldable = template['wieldable'] || false;
        this._wearable = template['wearable'] || false;
    },
    getAttackValue: function() {
        return this._attackValue;
    },
    getDefenseValue: function() {
        return this._defenseValue;
    },
    isWieldable: function() {
        return this._wieldable;
    },
    isWearable: function() {
        return this._wearable;
    }
};

assets/items.js

Now that we've got these mixins, let's create a few standard weapons and armors. For now we'll make it so that these items do not get randomly selected by the repository.

 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
// ...
// Weapons
Game.ItemRepository.define('dagger', {
    name: 'dagger',
    character: ')',
    foreground: 'gray',
    attackValue: 5,
    wieldable: true,
    mixins: [Game.ItemMixins.Equippable]
}, {
    disableRandomCreation: true
});

Game.ItemRepository.define('sword', {
    name: 'sword',
    character: ')',
    foreground: 'white',
    attackValue: 10,
    wieldable: true,
    mixins: [Game.ItemMixins.Equippable]
}, {
    disableRandomCreation: true
});

Game.ItemRepository.define('staff', {
    name: 'staff',
    character: ')',
    foreground: 'yellow',
    attackValue: 5,
    defenseValue: 3,
    wieldable: true,
    mixins: [Game.ItemMixins.Equippable]
}, {
    disableRandomCreation: true
});

// Wearables
Game.ItemRepository.define('tunic', {
    name: 'tunic',
    character: '[',
    foreground: 'green',
    defenseValue: 2,
    wearable: true,
    mixins: [Game.ItemMixins.Equippable]
}, {
    disableRandomCreation: true
});

Game.ItemRepository.define('chainmail', {
    name: 'chainmail',
    character: '[',
    foreground: 'white',
    defenseValue: 4,
    wearable: true,
    mixins: [Game.ItemMixins.Equippable]
}, {
    disableRandomCreation: true
});

Game.ItemRepository.define('platemail', {
    name: 'platemail',
    character: '[',
    foreground: 'aliceblue',
    defenseValue: 6,
    wearable: true,
    mixins: [Game.ItemMixins.Equippable]
}, {
    disableRandomCreation: true
});

While we're here, let's fix a bug that I introduced last post when my code and blog post got out of sync. The color for a melon should be lightGreen not brightGreen!

1
2
3
4
5
Game.ItemRepository.define('melon', {
    // ...
    foreground: 'lightGreen',
    // ...
});

assets/entitymixins.js

Now that we've got some items created, we want our entities to be able to equip them! Let's create a mixin 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
Game.EntityMixins.Equipper = {
    name: 'Equipper',
    init: function(template) {
        this._weapon = null;
        this._armor = null;
    },
    wield: function(item) {
        this._weapon = item;
    },
    unwield: function() {
        this._weapon = null;
    },
    wear: function(item) {
        this._armor = item;
    },
    takeOff: function() {
        this._armor = null;
    },
    getWeapon: function() {
        return this._weapon;
    },
    getArmor: function() {
        return this._armor;
    },
    unequip: function(item) {
        // Helper function to be called before getting rid of an item.
        if (this._weapon === item) {
            this.unwield();
        }
        if (this._armor === item) {
            this.takeOff();
        }
    }
};

Now that we've got this mixin, we want the worn items to actually affect our stats. Let's update the getters for both our Attacker and Destructible mixins to reflect 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
Game.EntityMixins.Attacker = {
    // ...
    getAttackValue: function() {
        var modifier = 0;
        // If we can equip items, then have to take into 
        // consideration weapon and armor
        if (this.hasMixin(Game.EntityMixins.Equipper)) {
            if (this.getWeapon()) {
                modifier += this.getWeapon().getAttackValue();
            }
            if (this.getArmor()) {
                modifier += this.getArmor().getAttackValue();
            }
        }
        return this._attackValue + modifier;
    },
    // ...
};
// ...
Game.EntityMixins.Destructible = {
    // ...
    getDefenseValue: function() {
        var modifier = 0;
        // If we can equip items, then have to take into 
        // consideration weapon and armor
        if (this.hasMixin(Game.EntityMixins.Equipper)) {
            if (this.getWeapon()) {
                modifier += this.getWeapon().getDefenseValue();
            }
            if (this.getArmor()) {
                modifier += this.getArmor().getDefenseValue();
            }
        }
        return this._defenseValue + modifier;
    },
    // ...
};

Finally we'll want to make sure that removing an item from the inventory will actually unwield/take off the item!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Game.EntityMixins.InventoryHolder = {
    // ...
    removeItem: function(i) {
        // If we can equip items, then make sure we unequip the item we are removing.
        if (this._items[i] && this.hasMixin(Game.EntityMixins.Equipper)) {
            this.unequip(this._items[i]);
        }
        // Simply clear the inventory slot.
        this._items[i] = null;
    },
    // ...
};

assets/entities.js

Before we go on, let's make our player be able to equip items!

1
2
3
4
5
6
7
8
Game.PlayerTemplate = {
    // ...
    mixins: [Game.EntityMixins.PlayerActor,
             Game.EntityMixins.Attacker, Game.EntityMixins.Destructible,
             Game.EntityMixins.InventoryHolder, Game.EntityMixins.FoodConsumer,
             Game.EntityMixins.Sight, Game.EntityMixins.MessageRecipient,
             Game.EntityMixins.Equipper]
};

assets/screens.js

We're going to create two screens - one for wielding items and another for wearing items. The command for accessing the wield screen is usually w while for accessing the wear screen it is Shift+w . It would be nice if we could use these screens for unwielding a weapon or taking off armor as well. I think a nice approach would be to add an extra option to these item lists which will call the ok function with no item. This item will appear at the top of our list and will be selectable by the 0 (zero) key. Since not all screens will want to use this, we'll make it a configurable option. We will also want to write '(wearing)' or '(wielding)' beside the items that the player has equipped.

 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
Game.Screen.ItemListScreen = function(template) {
    // ...
    // Whether a 'no item' option should appear.
    this._hasNoItemOption = template['hasNoItemOption'];
};

// ...

Game.Screen.ItemListScreen.prototype.render = function(display) {
    var letters = 'abcdefghijklmnopqrstuvwxyz';
    // Render the caption in the top row
    display.drawText(0, 0, this._caption);
    // Render the no item row if enabled
    if (this._hasNoItemOption) {
        display.drawText(0, 1, '0 - no item');
    }
    var row = 0;
    for (var i = 0; i < this._items.length; i++) {
        // If we have an item, we want to render it.
        if (this._items[i]) {
            // Get the letter matching the item's index
            var letter = letters.substring(i, i + 1);
            // If we have selected an item, show a +, else show a dash between
            // the letter and the item's name.
            var selectionState = (this._canSelectItem && this._canSelectMultipleItems &&
                this._selectedIndices[i]) ? '+' : '-';
            // Check if the item is worn or wielded
            var suffix = '';
            if (this._items[i] === this._player.getArmor()) {
                suffix = ' (wearing)';
            } else if (this._items[i] === this._player.getWeapon()) {
                suffix = ' (wielding)';
            }
            // Render at the correct row and add 2.
            display.drawText(0, 2 + row,  letter + ' ' + selectionState + ' ' +
                this._items[i].describe() + suffix);
            row++;
        }
    }
};

// ...

Game.Screen.ItemListScreen.prototype.handleInput = function(inputType, inputData) {
    if (inputType === 'keydown') {
        // ...
        // Handle pressing return when items are selected
        } else if (inputData.keyCode === ROT.VK_RETURN) {
            this.executeOkFunction();
        // Handle pressing zero when 'no item' selection is enabled
        } else if (this._canSelectItem && this._hasNoItemOption && inputData.keyCode === ROT.VK_0) {
            this._selectedIndices = {};
            this.executeOkFunction();
        // Handle pressing a letter if we can select
        } else if (this._canSelectItem && inputData.keyCode >= ROT.VK_A &&
            inputData.keyCode <= ROT.VK_Z) {
        // ...
        }
    }
};

Now we want to build screens to let the user wield and wear items! In order to do this we'll use our trusty ItemListScreen.

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

Game.Screen.wieldScreen = new Game.Screen.ItemListScreen({
    caption: 'Choose the item you wish to wield',
    canSelect: true,
    canSelectMultipleItems: false,
    hasNoItemOption: true,
    isAcceptable: function(item) {
        return item && item.hasMixin('Equippable') && item.isWieldable();
    },
    ok: function(selectedItems) {
        // Check if we selected 'no item'
        var keys = Object.keys(selectedItems);
        if (keys.length === 0) {
            this._player.unwield();
            Game.sendMessage(this._player, "You are empty handed.")
        } else {
            // Make sure to unequip the item first in case it is the armor.
            var item = selectedItems[keys[0]];
            this._player.unequip(item);
            this._player.wield(item);
            Game.sendMessage(this._player, "You are wielding %s.", [item.describeA()]);
        }
        return true;
    }
});

Game.Screen.wearScreen = new Game.Screen.ItemListScreen({
    caption: 'Choose the item you wish to wear',
    canSelect: true,
    canSelectMultipleItems: false,
    hasNoItemOption: true,
    isAcceptable: function(item) {
        return item && item.hasMixin('Equippable') && item.isWearable();
    },
    ok: function(selectedItems) {
        // Check if we selected 'no item'
        var keys = Object.keys(selectedItems);
        if (keys.length === 0) {
            this._player.unwield();
            Game.sendMessage(this._player, "You are not wearing anthing.")
        } else {
            // Make sure to unequip the item first in case it is the weapon.
            var item = selectedItems[keys[0]];
            this._player.unequip(item);
            this._player.wear(item);
            Game.sendMessage(this._player, "You are wearing %s.", [item.describeA()]);
        }
        return true;
    }
});

Finally let's add the input shortcuts for these two new screens! Remember it is w for the wield screen and Shift+w for the wear screen. Another thing I've noticed is that the pattern of setting up a subscreen, checking if there are no available items, and showing a message to the user in the case is repeated for nearly evey subscreen. We're going to put this as a helper method in the playScreen, allowing us to have cleaner input handling code.

 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
Game.Screen.playScreen = {
    // ...
    handleInput: function(inputType, inputData) {
        // ...
                } else if (inputData.keyCode === ROT.VK_I) {
                    // Show the inventory screen
                    this.showItemsSubScreen(Game.Screen.inventoryScreen, this._player.getItems(),
                        'You are not carrying anything.');
                    return;
                } else if (inputData.keyCode === ROT.VK_D) {
                    // Show the drop screen
                    this.showItemsSubScreen(Game.Screen.dropScreen, this._player.getItems(),
                        'You have nothing to drop.');
                    return;
                } else if (inputData.keyCode === ROT.VK_E) {
                    // Show the drop screen
                    this.showItemsSubScreen(Game.Screen.eatScreen, this._player.getItems(),
                       'You have nothing to eat.');
                    return;
                } else if (inputData.keyCode === ROT.VK_W) {
                    if (inputData.shiftKey) {
                        // Show the wear screen
                        this.showItemsSubScreen(Game.Screen.wearScreen, this._player.getItems(),
                            'You have nothing to wear.');
                    } else {
                        // Show the wield screen
                        this.showItemsSubScreen(Game.Screen.wieldScreen, this._player.getItems(),
                            'You have nothing to wield.');
                    }
                    return;
                } else if (inputData.keyCode === ROT.VK_COMMA) {
                    var items = this._map.getItemsAt(this._player.getX(), this._player.getY(), this._player.getZ());
                    // If there is only one item, directly pick it up
                    if (items && items.length === 1) {
                        var item = items[0];
                        if (this._player.pickupItems([0])) {
                            Game.sendMessage(this._player, "You pick up %s.", [item.describeA()]);
                        } else {
                            Game.sendMessage(this._player, "Your inventory is full! Nothing was picked up.");
                        }
                    } else {
                        this.showItemsSubScreen(Game.Screen.pickupScreen, items,
                            'There is nothing here to pick up.');
                    } 
                } else {
                // ...
    },
    // ...
    },
    showItemsSubScreen: function(subScreen, items, emptyMessage) {
        if (items && subScreen.setup(this._player, items) > 0) {
            this.setSubScreen(subScreen);
        } else {
            Game.sendMessage(this._player, emptyMessage);
            Game.refresh();
        }
    }
};

assets/map.js

Now that we've got our equippable item set up, we want to spawn some on the map! For now we're just going to randomly generate one of each of the items we've created in a random position on the map.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Map = function(tiles, player) {
    // ...
    // Add weapons and armor to the map in random positions
    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()));
    }
    // Setup the explored array
    this._explored = new Array(this._depth);
    this._setupExploredArray();
};

assets/items.js

As a neat display of the power of our item mixin, we're going to create an item that we can wield, wear, and eat - a pumpkin! Try to find this item in game and put it on!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Game.ItemRepository.define('pumpkin', {
    name: 'pumpkin',
    character: '%',
    foreground: 'orange',
    foodValue: 50,
    attackValue: 2,
    defenseValue: 2,
    wearable: true,
    wieldable: true,
    mixins: [Game.ItemMixins.Edible, Game.ItemMixins.Equippable]
});

Conclusion

This post introduced equippable items which adds an extremely interesting twist to the game! In the next post we'll be creating aggressive monsters which will actively hunt down our hero!

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 12 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 13 - Aggressive Monsters