This is the fourteenth 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 second half of the tenth part in Trystan's series. All the code for this part can be found at the part 10b 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 give our hero an inventory! That means the hero will be able to pick up items from the cave floor as well as drop them back onto the floor. We're also going to create a screen to view the items the player currently has!

Demo Link

The results after this post can be seen here.

assets/entities.js (bug fix)

Before we begin, I found a bug that I introduced last post when I switched the entities over to using the repository system. In the FungusActor mixin, I forgot to change the way new fungi were grown and thus it was introducing blank entities into the game. You'll want to change that actor to look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Game.Mixins.FungusActor = {
    // ...
    act: function() { 
        // ...
        // 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 = Game.EntityRepository.create('fungus');
            entity.setPosition(this.getX() + xOffset, this.getY() + yOffset,
                this.getZ());
            this.getMap().addEntity(entity);
            // ...
    },
    // ...
};

assets/item.js

We're going to add some helper functions to our Item class in order to simplify generating strings with item names. This includes a function which will generate the name prefixe by 'a/an' using a rudimentary technique. This could be generalized to apply to entities as well, but for the moment it will be fine in the Item class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Item.prototype.describe = function() {
    return this._name;
};
Game.Item.prototype.describeA = function(capitalize) {
    // Optional parameter to capitalize the a/an.
    var prefixes = capitalize ? ['A', 'An'] : ['a', 'an'];
    var string = this.describe();
    var firstLetter = string.charAt(0).toLowerCase();
    // If word starts by a vowel, use an, else use a. Note that this is not perfect.
    var prefix = 'aeiou'.indexOf(firstLetter) >= 0 ? 1 : 0;

    return prefixes[prefix] + ' ' + string;
};

assets/entities.js

Rather than making it so that only our hero has an inventory, we're going to create a mixin which will give an entity all of the necessary inventory functionality (pick up and drop items). We're going to keep track of an inventory as an array. In traditional roguelike fashion, inventory items are mapped to by letters like so:

We are also going to have one extra complication. For convenience sake, if a player has 3 items (hence item a, b, and c) and drops the second one, rather then renaming item c to b we will simply have items a and c and the next item the player picks up will be labelled b. This is so that the player can get used to the labels on commonly used items and not have to constantly worry that an item might have a different name. We will also want to limit the number of inventory slots a given entity has, defaulting to 10. This number will be customizable in the entity template under the inventorySlots property. This mixin will provide functions for adding and removing items as well as picking up and dropping items from the map.

 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
Game.Mixins.InventoryHolder = {
    name: 'InventoryHolder',
    init: function(template) {
        // Default to 10 inventory slots.
        var inventorySlots = template['inventorySlots'] || 10;
        // Set up an empty inventory.
        this._items = new Array(inventorySlots);
    },
    getItems: function() {
        return this._items;
    },
    getItem: function(i) {
        return this._items[i];
    },
    addItem: function(item) {
        // Try to find a slot, returning true only if we could add the item.
        for (var i = 0; i < this._items.length; i++) {
            if (!this._items[i]) {
                this._items[i] = item;
                return true;
            }
        }
        return false;
    },
    removeItem: function(i) {
        // Simply clear the inventory slot.
        this._items[i] = null;
    },
    canAddItem: function() {
        // Check if we have an empty slot.
        for (var i = 0; i < this._items.length; i++) {
            if (!this._items[i]) {
                return true;
            }
        }
        return false;
    },
    pickupItems: function(indices) {
        // Allows the user to pick up items from the map, where indices is
        // the indices for the array returned by map.getItemsAt
        var mapItems = this._map.getItemsAt(this.getX(), this.getY(), this.getZ());
        var added = 0;
        // Iterate through all indices.
        for (var i = 0; i < indices.length; i++) {
            // Try to add the item. If our inventory is not full, then splice the 
            // item out of the list of items. In order to fetch the right item, we
            // have to offset the number of items already added.
            if (this.addItem(mapItems[indices[i]  - added])) {
                mapItems.splice(indices[i] - added, 1);
                added++;
            } else {
                // Inventory is full
                break;
            }
        }
        // Update the map items
        this._map.setItemsAt(this.getX(), this.getY(), this.getZ(), mapItems);
        // Return true only if we added all items
        return added === indices.length;
    },
    dropItem: function(i) {
        // Drops an item to the current map tile
        if (this._items[i]) {
            if (this._map) {
                this._map.addItem(this.getX(), this.getY(), this.getZ(), this._items[i]);
            }
            this.removeItem(i);      
        }
    }
};

Now let's update our player template to have this mixin and 22 inventory slots.

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

assets/screens.js

We now want a way to show the user's inventory. We'll also want a way to allow the user to drop certain items or to view all the items currently available for pickup at the player's location. In the future, you can imagine that we'll want a similar item selection screen when picking an item to eat, equip or quaff. In order to do this we're going to generalize the idea of a screen that presents a set of items and allows the user to select one or many depending on the scenario. This generalized screen will also take care of mapping input to the correct items. In order to create these item selection screens, we'll simply have to provide a caption (eg. "Your Inventory" or "What would you like to eat?"), whether the screen should allow any item selection at all (eg. inventory wouldn't while picking up item would), and an action to execute if the player succesfully selects items. Note that for screens that don't allow selection, hitting enter or escape will simply cancel and return to the game screen, while for screens that do allow selection hitting enter will execute the okFunction and then cost a turn if okFunction returns true.

We will create an abstract call called ItemListScreen which will accept a template similar to the entities as a constructor. We will render the list of items in 1 column. We won't implement pagination for now giving the limitation that we can only ever show 22 items at once. In the screens where the user can pick multiple items, we'll want to show a + when an item is selected.

First let's create our constuctor which will accept a template.

1
2
3
4
5
6
7
8
9
Game.Screen.ItemListScreen = function(template) {
    // Set up based on the template
    this._caption = template['caption'];
    this._okFunction = template['ok'];
    // Whether the user can select items at all.
    this._canSelectItem = template['canSelect'];
    // Whether the user can select multiple items.
    this._canSelectMultipleItems = template['canSelectMultipleItems'];
};

Now we'll need an initialization method that we can call with a player and a set of items.

1
2
3
4
5
6
7
Game.Screen.ItemListScreen.prototype.setup = function(player, items) {
    this._player = player;
    // Should be called before switching to the screen.
    this._items = items;
    // Clean set of selected indices
    this._selectedIndices = {};
};

We now need to make our rendering function, which will render our list of items as well as the selection states and the caption.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Game.Screen.ItemListScreen.prototype.render = function(display) {
    var letters = 'abcdefghijklmnopqrstuvwxyz';
    // Render the caption in the top row
    display.drawText(0, 0, this._caption);
    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]) ? '+' : '-';
            // Render at the correct row and add 2.
            display.drawText(0, 2 + row, letter + ' ' + selectionState + ' ' + this._items[i].describe());
            row++;
        }
    }
};

Finally we need to make a function for handling the input. Before we can do that, we'll need to make a simple way to show these item screens. Rather than using the Game.switchScreen function, the play screen will have a subscreen. We will provide a function for setting the current subscreen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Game.Screen.playScreen = {
    _map: null,
    _player: null,
    _gameEnded: false,
    _subScreen: null,
    // ...
    },
    setSubScreen: function(subScreen) {
        this._subScreen = subScreen;
        // Refresh screen on changing the subscreen
        Game.refresh();
    }
};

We'll now want to modify our rendering and input handling function such that if a subscreen is present all of these calls will be forwarded to the subscreen.

 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
Game.Screen.playScreen = {
    // ...
    render: function(display) {
        // Render subscreen if there is one
        if (this._subScreen) {
            this._subScreen.render(display);
            return;
        }
        // ...
    },
    handleInput: function(inputType, inputData) {
        // If the game is over, enter will bring the user to the losing screen.
        if (this._gameEnded) {
            if (inputType === 'keydown' && inputData.keyCode === ROT.VK_RETURN) {
                Game.switchScreen(Game.Screen.loseScreen);
            }
            // Return to make sure the user can't still play
            return;
        }
        // Handle subscreen input if there is one
        if (this._subScreen) {
            this._subScreen.handleInput(inputType, inputData);
            return;
        }
        // ...
    },
    // ...
};

Now that we've implemented this subscreen feature, we can finally add our input handling code to the ItemListScreen class. I created a helper function which takes care of gathering the selected items, calls the ok function with a hashtable mapping indexes to items, and ends the turn if necessary.

 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.ItemListScreen.prototype.executeOkFunction = function() {
    // Gather the selected items.
    var selectedItems = {};
    for (var key in this._selectedIndices) {
        selectedItems[key] = this._items[key];
    }
    // Switch back to the play screen.
    Game.Screen.playScreen.setSubScreen(undefined);
    // Call the OK function and end the player's turn if it return true.
    if (this._okFunction(selectedItems)) {
        this._player.getMap().getEngine().unlock();
    }
};
Game.Screen.ItemListScreen.prototype.handleInput = function(inputType, inputData) {
    if (inputType === 'keydown') {
        // If the user hit escape, hit enter and can't select an item, or hit
        // enter without any items selected, simply cancel out
        if (inputData.keyCode === ROT.VK_ESCAPE || 
            (inputData.keyCode === ROT.VK_RETURN && 
                (!this._canSelectItem || Object.keys(this._selectedIndices).length === 0))) {
            Game.Screen.playScreen.setSubScreen(undefined);
        // Handle pressing return when items are selected
        } else if (inputData.keyCode === ROT.VK_RETURN) {
            this.executeOkFunction();
        // Handle pressing a letter if we can select
        } else if (this._canSelectItem && inputData.keyCode >= ROT.VK_A &&
            inputData.keyCode <= ROT.VK_Z) {
            // Check if it maps to a valid item by subtracting 'a' from the character
            // to know what letter of the alphabet we used.
            var index = inputData.keyCode - ROT.VK_A;
            if (this._items[index]) {
                // If multiple selection is allowed, toggle the selection status, else
                // select the item and exit the screen
                if (this._canSelectMultipleItems) {
                    if (this._selectedIndices[index]) {
                        delete this._selectedIndices[index];
                    } else {
                        this._selectedIndices[index] = true;
                    }
                    // Redraw screen
                    Game.refresh();
                } else {
                    this._selectedIndices[index] = true;
                    this.executeOkFunction();
                }
            }
        }
    }
};

Finally we're going to create three screens. The first will be the basic inventory screen which simply lists the items that the user is carrying. The second is the pickup screen, showing the user all the items available for pickup at the current tile and allowing the user to pick up a set of items. The last screen is the drop screen, which shows the user's inventory and allows the user to select one item to drop.

 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.inventoryScreen = new Game.Screen.ItemListScreen({
    caption: 'Inventory',
    canSelect: false
});

Game.Screen.pickupScreen = new Game.Screen.ItemListScreen({
    caption: 'Choose the items you wish to pickup',
    canSelect: true,
    canSelectMultipleItems: true,
    ok: function(selectedItems) {
        // Try to pick up all items, messaging the player if they couldn't all be
        // picked up.
        if (!this._player.pickupItems(Object.keys(selectedItems))) {
            Game.sendMessage(this._player, "Your inventory is full! Not all items were picked up.");
        }
        return true;
    }
});

Game.Screen.dropScreen = new Game.Screen.ItemListScreen({
    caption: 'Choose the item you wish to drop',
    canSelect: true,
    canSelectMultipleItems: false,
    ok: function(selectedItems) {
        // Drop the selected item
        this._player.dropItem(Object.keys(selectedItems)[0]);
        return true;
    }
});

Now that we've got our screens all set up, let's modify our game's input handling so that we can actually use these screens! We will show the inventory with the i key, pick up items with the , key and drop items with the d key. There are some special cases to take care of here. If the user's inventory is empty and the user tries to show it, we'll simply send a message stating that it is empty. A similar message will appear if the user tries to drop an item and has an empty inventory. Finally, we only show the pick up screen if there is more than one item at the cell. If there is only one item we just try to pick it up and if there are none we show a message.

 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.Screen.playScreen = {
    // ...
    handleInput: function(inputType, inputData) {
        // ...
                } else if (inputData.keyCode === ROT.VK_DOWN) {
                    this.move(0, 1, 0);
                } else if (inputData.keyCode === ROT.VK_I) {
                    if (this._player.getItems().filter(function(x){return x;}).length === 0) {
                        // If the player has no items, send a message and don't take a turn
                        Game.sendMessage(this._player, "You are not carrying anything!");
                        Game.refresh();
                    } else {
                        // Show the inventory
                        Game.Screen.inventoryScreen.setup(this._player, this._player.getItems());
                        this.setSubScreen(Game.Screen.inventoryScreen);
                    }
                    return;
                } else if (inputData.keyCode === ROT.VK_D) {
                    if (this._player.getItems().filter(function(x){return x;}).length === 0) {
                        // If the player has no items, send a message and don't take a turn
                        Game.sendMessage(this._player, "You have nothing to drop!");
                        Game.refresh();
                    } else {
                        // Show the drop screen
                        Game.Screen.dropScreen.setup(this._player, this._player.getItems());
                        this.setSubScreen(Game.Screen.dropScreen);
                    }
                    return;
                } else if (inputData.keyCode === ROT.VK_COMMA) {
                    var items = this._map.getItemsAt(this._player.getX(), this._player.getY(), this._player.getZ());
                    // If there are no items, show a message
                    if (!items) {
                        Game.sendMessage(this._player, "There is nothing here to pick up.");
                    } else if (items.length === 1) {
                        // If only one item, try to pick it up
                        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 {
                        // Show the pickup screen if there are any items
                        Game.Screen.pickupScreen.setup(this._player, items);
                        this.setSubScreen(Game.Screen.pickupScreen);
                        return;
                    }
                } else {
                    // Not a valid key
                    return;
                }
                // ...
    },
    // ...

assets/entity.js

Our inventory is now looking pretty slick! The last thing I'd like to do in this post is notify entities when they walk over items, letting them know what the items are! Let's modify our tryMove function to do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
Game.Entity.prototype.tryMove = function(x, y, z, map) {
    // ...
    // 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);
        // Notify the entity that there are items at this position
        var items = this.getMap().getItemsAt(x, y, z);
        if (items) {
            if (items.length === 1) {
                Game.sendMessage(this, "You see %s.", [items[0].describeA()]);
            } else {
                Game.sendMessage(this, "There are several objects here.");
            }
        }
        return true;
    // Check if the tile is diggable
    // ...
};

Conclusion

This was a big post but I think it was well worth it as we can now pick up and drop items and have a fully functioning inventory! Our game is quickly starting to ressemble an actual roguelike game - good going us! The next part will cover adding a food (mainly corpses) and hunger system to the game so make sure to stick around!

I hope you enjoyed this post! Remember that all the code for this part can be found at the part 10b 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 11 - Hungry Heroes Need Food