Creating an Inventory

In this example we will create an inventory using the Gameface components library.

The inventory sample shows how to create, style, nest and reuse custom components. The inventory will hold a list of inventory items - weapons and consumables. Each item will be presented with an image. Clicking on an item with the left mouse button will open a details panel for the selected element. The right mouse button will equip/use weapon/consumable respectively.

The complete project can be found in Samples/uiresources/Components along with the asset.

For this tutorial we’ll create the following components:

  1. Inventory - will hold all items
  2. InventoryItem - will support mouse interaction and will handle the display of the details window
  3. Weapon - a type of InventoryItem that has an image and a name
  4. Consumable - a type of InventoryItem that will be usable and will have a quantity

and use the pre-made modal component.

Getting Started

Installing the CLI

To get started we first need to install the (coherent-guic-cli)[https://coherentlabs.github.io/GameUIComponents/en/command-line-interface/cli/]

npm i -g coherent-guic-cli

The coherent-guic-cli is a tool that helps you create and develop components and can also setup a development environment allowing an easier way to create a UI using the components.

Setting up the project

To set up the project, we can now use the command line interface we’ve just installed to run the following command

coherent-guic-cli create-project webpack ./inventory-ui

This will create a inventory-ui folder with the following structure

    node_modules/
    index.html
    index.js
    package-lock.json
    package.json
    README.md
    style.css
    webpack.config.js

Adding the modal component

To add the pre-made modal component we just need to do:

npm i coherent-gameface-modal

Creating a custom component

To create a custom component we will use the CLI with the coherent-guic-cli create-component my-component ./my-components-folder command.

Here my-component is the name of the component and my-components-folder will be the folder where all of the component should be located.

Components created with the CLI will have the following structure:

    coherent-gameface-components-theme.css
    demo.js
    index.html
    index.js
    package.json
    README.md
    script.js
    style.css
    template.html

Where script.js will be the main logic of the component, template.html the template of the component and the style.css - the component styling.

To be able to use the component we’ve created, we have to build it by using the CLI again. We can do that by navigating to the component folder and running coherent-guic-cli build. If we don’t want to run the build command every time we change something we can also do coherent-guic-cli watch.

If we use the components in a larger project, we also have the option to preview them in a Playground like setting before integrating them in our UI. To do that we have to run coherent-guic-cli build-demo which will create a /demo folder where you can see the component.

script.js

The script.js file is the main script of our component. It handles all of the logic of the component.

The most important methods here that we need to focus on are:

  • init where we render the template.
  • connectedCallback which is called when the component is added to the DOM.

It’s also recommended to use the constructor for setting up the initial state, event handlers, or the template. You shouldn’t inspect the element’s attributes and children as they might not be available yet. Access them in the connectedCallback.

Components Library

To streamline component development we’ve created the coherent-gameface-components library. Its purpose is to provide a BaseComponent that all other components are built upon and utility methods to create these components.

These are some of the most used methods from the library:

  • whenDefined - returns a Promise that resolves when the named element is defined.
  • defineCustomElement - defines a custom component.

template.html

The template file has the HTML markup that will be rendered with the custom component. The one thing that we have to note here is the availability of the component-slot custom HTML element which is modeled after the existing web <slot> element

It allows you to insert additional markup at runtime. For example in the default template code we can see the following:

<div class="my-component">
    <span>Hello </span>
    <component-slot data-name="name">there!</component-slot>
</div>

Where we have a component-slot for the name. So now if we render our component and do

<my-component class="my-component-component">
    <component-slot data-name="name">my-component</component-slot>
</my-component>

it will render Hello my-component instead of Hello there!

Demo files

The demo.js and index.html files are used to create the demo to test the custom component. If there is a need for additional JavaScript or we need to test different variations of the component, it should be added to these files.

Creating the components for this tutorial

Inventory

The first component we’ll start with is the Inventory component. This is the one that will hold all of our components.

To make it, we just need to run the following command in the root folder:

coherent-guic-cli create-component game-inventory

This will create a game-inventory folder where our new component is.

First let’s change the template file. The CLI has generated a div element with a component-slot and a simple text in a span:

<div class="GameInventory">
    <span>Hello </span>
    <component-slot data-name="name">there!</component-slot>
</div>

We won’t need any of these, so let’s replace it with a container that has an info box and a grid of slots where the item will be held:

<div class="inventory-container">
    <div class="info">
        <span>Left click on an item to show details.</span>
        <span>Right click on an item to use/equip.</span>
    </div>
    <div class="slots">
        <div class="slot"></div>
        <div class="slot"></div>
        <div class="slot"></div>
        <div class="slot"></div>
    </div>
</div>

For this example we’ll be using 24 div.slot elements.

The Inventory should have two states - opened and closed. We’ll use a boolean value to represent that so we’ll add this to the constructor:

this.state = {
    display: false
};

Now that we have the correct template, let’s load it in the component. Since the CLI has generated the template loading part for us we just need to add the items to the inventory. To do that let’s create an onTemplateLoaded function that we’ll call in the init function:

init(data) {
    this.setupTemplate(data, () => {
        components.renderOnce(this);
        // attach event handlers here
        this.onTemplateLoaded();
    });
}

Then we add the function bellow the connectedCallback:

/**
* Called when the component's template was loaded.
*/
onTemplateLoaded() {
    this.style.display = this.state.display ? '' : 'none';
}

Inside for now we will just show or hide the inventory based on the state.

To complete the inventory we’ll also need to add the following methods.

  • addInventoryItems - adds the items from the InventoryItems model to the Inventory
  • addInventoryItem - adds a single item
  • addItemAt - adds an item at a specific slot
  • findFreeSocketId - finds the id of a socket that has no item in it
  • isSocketFree - checks if a socket has an item in it
  • show - shows the inventory

Which will be added to the script.js file


/**
* Adds the inventory items to the inventory.
* InventoryItems is the data binding model registered in the global
* namespace by Gameface.
*/
addInventoryItems() {
    const itemsIds = Object.keys(InventoryItems.list);

    for (let itemId of itemsIds) {
        this.addInventoryItem(InventoryItems.list[itemId], itemId);
    }
}

/**
* Creates an inventory item instance and adds it into an available slot
* in the inventory.
* @param {Object} item - the inventory item from the model (InventoryItems)
* @param {string} itemId - the item's identifier
* @param {number} [socketId=0] - the inventory socket's id into which the
* item should added. The default is 0 - this will add it in the next free socket.
*/
addInventoryItem(item, itemId, socketId = 0) {
    let WrappedComponent = 'inventory-consumable';
    if (item.typeId === 0) WrappedComponent = 'inventory-weapon';

    const inventoryItem = document.createElement('inventory-item');
    inventoryItem.socket = socketId;
    inventoryItem.itemid = itemId;
    inventoryItem.imageurl = `{{InventoryItems.list.${itemId}.image}}`;
    inventoryItem.description = item.name;
    inventoryItem.WrappedComponent = WrappedComponent;
    this.addItemAt(inventoryItem, socketId);
}

/**
* Adds an inventory item instance to a given inventory socket.
* @param {Object} item - the inventory item from the model (InventoryItems)
* @param {number} socketId - the inventory socket's id into which the
* item should added
*/
addItemAt(item, socketId) {
    const itemSlotElements = this.getElementsByClassName('slot');

    if (!this.isSocketFree(socketId)) socketId = this.findFreeSocketId();
    if (socketId === undefined) return showMessage(`I can't cary anymore!`);

    this.itemSlots[socketId] = socketId;
    itemSlotElements[socketId].appendChild(item);
}

/**
* Finds the first free inventory socket.
* @returns {number} - the id of the socket.
*/
findFreeSocketId() {
    for (let i = 0; i < this.itemSlots.length; i++) {
        if (this.itemSlots[i] === undefined) return i;
    }
}

/**
* Checks if an inventory socket with a given id is free.
* @returns {boolean} - true if it's free, false if it's not
*/
isSocketFree(socketId) {
    return this.itemSlots[socketId] === undefined;
}

/**
* Show the inventory instance.
*/
show() {
    this.state.display = true;
    this.style.display = '';
}

and then we’ll finish everything by adding the last bit of code to the onTemplateLoaded function:

/**
* Called when the component's template was loaded.
*/
onTemplateLoaded() {
    // inventory itemSlots, not to be confused with Web Component <slot>
    this.itemSlots = new Array(this.getElementsByClassName('slot').length);
    this.addInventoryItems();
    this.style.display = this.state.display ? '' : 'none';
}

The Inventory component is an ES6 module. It uses import and export statements. Meaning that in order to use it, we will need to import it into our project. In order to do that we have to build the component first.

As previously discussed, to build the component we have to navigate to the Inventory folder and execute:

coherent-guic-cli build

The build command will generate the following:

game-inventory
+---dist
|       GameInventory.development.js
|       GameInventory.production.min.js
|

We’ll import the development build to the index.js file like so:

import './game-inventory/dist/game-inventory.development';
import './game-inventory/style.css';

along with the styles.

File structure

We’ll generate most of the files using the CLI. This is how your project should look at the end of this guide:

Components:
|   cohtml.js
|   index.html
|   model.js
|   package-lock.json
|   package.json
|   styles.css
|
+---consumable
|   |   index.js
|   |   package.json
|   |   README.md
|   |   script.js
|   |   style.css
|   |   template.html
|   |
|   \---dist
|           consumable.development.js
|           consumable.production.min.js
|
+---images
|
+---inventory
|   |   index.js
|   |   package.json
|   |   README.md
|   |   script.js
|   |   style.css
|   |   template.html
|   |
|   \---dist
|           inventory.development.js
|           inventory.production.min.js
|
+---inventory-item
|   |   index.js
|   |   inventoryItem.js
|   |   package.json
|   |   README.md
|   |   script.js
|   |   style.css
|   |   template.html
|   |
|   \---dist
|           inventory-item.development.js
|           inventory-item.production.min.js
|
+---node_modules
|
\---weapon
    |   index.js
    |   package.json
    |   README.md
    |   script.js
    |   style.css
    |   template.html
    |
    +---dist
    |       weapon.development.js
    |       weapon.production.min.js
    |

We strongly recommend a domain-driven file structure. It is more maintainable and it scales better in big applications. If we group the files by nature - for example, we put all components in one folder, all styles are another we’ll end up with huge folders and we’ll have to add a file into each folder every time we want to add a new component.

Inventory Item

Type coherent-guic-cli create-component inventory-item to create a new component. As we mentioned earlier in the tutorial the inventory item will have a details panel that will be opened on click.

Inventory items will of two types - consumable and weapon. Where the consumable item will have quantity and the weapon - won’t.

We’ll be making new components for the weapon and consumable that will be wrapped in an inventory-item in order to share functionality.

The inventory-item will populate itself with either a weapon or a consumable depending on the type of the current item.

To do that we’ll create a setup function in the script.js of the component and use it in the connectedCallback.

connectedCallback() {
    this.classList.add('inventory-item');

    this.setup();
}

setup() {
    const wrappedComponent = document.createElement(this.WrappedComponent);
    wrappedComponent.itemid = this.itemid;
    wrappedComponent.imageurl = this.imageurl;
    wrappedComponent.description = this.description;
    wrappedComponent.onClick = this.onClick;
    this.appendChild(wrappedComponent);
}

In order to show the details of each item, we’ll be using the modal component that we added when we were setting up the project.

The modal component has 3 slots for the header, body and footer. We’ll be adding the item title in the header and the image and description of the item in the body. We’ll leave the footer empty.

We’ll be creating these elements dynamically through JavaScript and then appending them to a gameface-modal element. To do that we’ll create a createDetailsSlots and showDetailsModal functions

/**
* Creates the elements which will be added to slots and nests them into a
* wrapper element.
* @returns {HTMLElement} content
*/
createDetailsSlots() {
    const content = document.createElement('div');
    const body = document.createElement('div');
    const header = document.createElement('div');
    const footer = document.createElement('div');

    body.setAttribute('slot', 'body');
    header.setAttribute('slot', 'header');
    footer.setAttribute('slot', 'footer');

    const headerContent = document.createElement('div');
    headerContent.textContent = 'Item Details';
    headerContent.className = 'item-details';
    header.appendChild(headerContent);

    const imageItem = document.createElement('div');
    imageItem.style.backgroundImage = `url(${InventoryItems.list[this.itemid].image})`;
    imageItem.classList.add('info-image');
    body.appendChild(imageItem);

    const description = document.createElement('div');
    description.textContent = this.description;
    body.appendChild(description);

    content.appendChild(body);
    content.appendChild(header);
    content.appendChild(footer);

    return content;
}

/**
* Creates a modal window component and passes the elements created in
* createDetailsSlots to be put in the modal's slots.
*/
showDetailsModal() {
    const details = document.createElement('gameface-modal');
    details.appendChild(this.createDetailsSlots());

    const detailsContainer = document.getElementById('details-container');
    detailsContainer.innerHTML = '';
    detailsContainer.appendChild(details);
    document.querySelector('gameface-modal').style.display = 'flex';
}

and we’ll have to show the modal so we’ll add a click listener in the component constructor:

constructor() {
    super();
    this.onClick = () => this.showDetailsModal();
}

Then we have to run coherent-guic-cli build and again import the style and bundled JS files in our project index.js

import './inventory-item/dist/inventory-item.development';
import './inventory-item/style.css';

Weapon

Just like with the previous components, we’ll use the CLI to create it. We need to run coherent-guic-cli create-component weapon in our root project folder.

The weapon component is going to have logic for setting up the content - the image and the description and a method for equipping. We will set up the content and attach the event listeners in the connectedCallback.

Apart from that we’ll also need to call engine.synchronizeModels() to make sure that the data binding attributes are updated.

First we’ll create our setupContent and equip functions:

    /**
    * Sets the html content of the consumable item and sets the data binding attributes.
    */
    setupContent() {
        this.classList.add('inventory-weapon');
        this.querySelector('.image')
            .setAttribute('data-bind-style-background-image-url', this.imageurl);
        this.querySelector('.weapon')
            .setAttribute('data-bind-class-toggle', `disabled:{{InventoryItems.list.${this.itemid}.equipped}} == true`);
    }

    /**
    * Equips the weapon by setting its equipped property to true.
    */
    equip() {
        if (InventoryItems.list[this.itemid].equipped) return;

        InventoryItems.list[this.itemid].equipped = true;
        engine.updateWholeModel(InventoryItems);
        engine.synchronizeModels();
    }

Then we need to add the click function from the inventory-item component, which we’ll do in the constructor:

    constructor() {
        super();
        this.onClickBound = (e) => {
            if (e.button === 2) return this.equip();
            // on click is assigned in InventoryItem
            this.onClick();
        };
        this.template = template;
        this.init = this.init.bind(this);
    }

and then attach the event listener for this function in the init method:

    init(data) {
        this.setupTemplate(data, () => {
            components.renderOnce(this);
            // attach event handlers here
            this.setupContent();
            this.addEventListener('mousedown', this.onClickBound);
            engine.synchronizeModels();
        });
    }

And since by standard we can’t have a component with a single name (they have to be two separated by a hyphen), we need to change:

components.defineCustomElement("inventory-weapon", Weapon);

Finally we need to change the template to add the image and description.

<div class="weapon">
    <div class="image"></div>
    <div class="description"></div>
</div>

And as always we run coherent-guic-cli build and include the bundle and styles in the main index.js:

import './weapon/dist/weapon.development';
import './weapon/style.css';

Consumable

For our last component we’ll use the CLI to create it again coherent-guic-cli create-component consumable.

Just like the weapon component, the consumable is also an inventory item, but instead of equipping it as right mouse button interaction, it can be used using the left mouse button. Its template will also be a little different than the weapon’s component as the consumable has to have a quantity info badge.

And like the weapon, we need to set it up in a setupContent function:

    /**
    * Sets the html content of the consumable item and sets the data binding attributes.
    */
    setupContent() {
        this.classList.add('inventory-consumable');

        this.querySelector('.image').setAttribute('data-bind-style-background-image-url', this.imageurl);
        this.querySelector('.consumable').setAttribute('data-bind-class-toggle', `disabled:{{InventoryItems.list.${this.itemid}.quantity}} == 0`);

        this.querySelector('.quantity').setAttribute('data-bind-value', `{{InventoryItems.list.${this.itemid}.quantity}}`);
    }

but unlike the weapon, here we’ll have a use method, rather than equip

    /**
    * Uses one of the consumable items by decreasing its quantity by 1.
    */
    use() {
        InventoryItems.list[this.itemid].quantity -= 1;
        engine.updateWholeModel(InventoryItems);
        engine.synchronizeModels();
    }

Then we add it to the constructor just like with the weapon:

    constructor() {
        super();
        this.template = template;
        this.init = this.init.bind(this);
        this.onClickBound = (e) => {
            if (InventoryItems.list[this.itemid].quantity === 0) return;
            // right mouse button
            if (e.button === 2) return this.use();
            this.onClick();
        };
    }

and we set it up in the init method:

    init(data) {
        this.setupTemplate(data, () => {
            components.renderOnce(this);
            // attach event handlers here
            this.setupContent();
            this.addEventListener('mousedown', this.onClickBound);
            engine.synchronizeModels();
        });
    }

Same as for the weapon, we need to change the defined custom element:

components.defineCustomElement('inventory-consumable', Consumable);

And change the template:

<div class="consumable">
    <div class="image"></div>
    <div class="quantity"></div>
</div>

Run coherent-guic-cli build to build it and add the bundle and style to the root index.js:

import './consumable/dist/consumable.development';
import './consumable/style.css';

Bringing it all together

To finish this example, we’ll need to replace the content inside the <body> tag with:

<body>
    <div id ="toggle_inventory" class="button">Open Inventory</div>
    <div class="ui">
        <div id="inventory-wrapper"></div>
        <div id="details-container"></div>
    </div>
</body>

And add our inventory inside the div.inventory-wrapper

<body>
    <div id="toggle_inventory" class="button">Open Inventory</div>
    <div class="ui">
        <div id="inventory-wrapper">
            <game-inventory></game-inventory>
        </div>
        <div id="details-container"></div>
    </div>
</body>

The last steps before finishing this sample are to add functionality to the Open Inventory button. We do this by adding a script tag to the bottom of our <body> element:

    <script>
        const toggleInventoryBtn = document.getElementById('toggle_inventory');

        toggleInventoryBtn.addEventListener('click', () => {
            const inventory = document.querySelector('game-inventory');
            inventory.show();
        });
    </script>
</body>

And we’ll create a model.js file that will have the information about our inventory items:

(() => {
    engine.createJSModel('InventoryItems', {
        list: {
            'icon_Axe_1_Small': {
                typeId: 0,
                name: 'Axe 1',
                image: 'images/icon_Axe_1_Small.png'
            },
            'icon_Axe_2_Small': {
                typeId: 0,
                name: 'Axe 2',
                image: 'images/icon_Axe_2_Small.png'
            },
            'icon_Axe_3_Small': {
                typeId: 0,
                name: 'Axe 3',
                image: 'images/icon_Axe_3_Small.png'
            },
            'icon_Book_Small': {
                typeId: 1,
                quantity: 8,
                name: 'Book',
                image: 'images/icon_Book_Small.png'
            },
            'icon_Bracelet_Small': {
                typeId: 0,
                name: 'Bracelet',
                image: 'images/icon_Bracelet_Small.png'
            },
            'icon_Dagger_Small': {
                typeId: 0,
                name: 'Dagger',
                image: 'images/icon_Dagger_Small.png'
            },
            'icon_HealthPotion_Small': {
                typeId: 1,
                quantity: 10,
                name: 'HealthPotion',
                image: 'images/icon_HealthPotion_Small.png'
            },
            'icon_PoisonVial_Small': {
                typeId: 1,
                quantity: 2,
                name: 'PoisonVial',
                image: 'images/icon_PoisonVial_Small.png'
            },
            'icon_Ring1_Small': {
                typeId: 0,
                quantity: 5,
                name: 'Ring 1',
                image: 'images/icon_Ring1_Small.png'
            },
            'icon_Ring2_Small': {
                typeId: 1,
                quantity: 5,
                name: 'Ring 2',
                image: 'images/icon_Ring2_Small.png'
            },
            'icon_Scroll1_Small': {
                typeId: 1,
                quantity: 5,
                name: 'Scroll 1',
                image: 'images/icon_Scroll1_Small.png'
            },
            'icon_Scroll2_Small': {
                typeId: 1,
                quantity: 5,
                name: 'Scroll 2',
                image: 'images/icon_Scroll2_Small.png'
            },
        }
    });
})();

Finally we add the model.js along with the cohtml.js library to the root index.html in the <head> tag:

    <script src="./cohtml.js"></script>
    <script src="./model.js"></script>

and we run npm run build in the project root to build our UI.

In the /build