Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Building an Inventory Management API

Hello! Welcome back to our JRPG-building journey.

In our last lesson, we designed the fundamental data structures for our game's items and inventory. We created a blueprint for an items.json file to act as our item "catalog" and defined the player's inventory as an array of objects, each holding an itemId and a quantity.

This lesson brings those blueprints to life. We will now implement the core logic that manages the inventory. Our goal is to achieve the learning outcome: Implement an API to add, remove, and query items in the player's inventory.

Think of this as building the "engine" for your player's backpack. Instead of having different parts of your game code directly manipulate the inventory array, we'll create a single, reliable set of functions—an Application Programming Interface (API)—to handle all interactions. This concept is very similar to how you might use a state management library like Redux or Vuex in front-end development, where you dispatch actions to modify the state rather than changing it directly. This keeps our code clean, predictable, and easier to debug.

1. The Inventory Class: A Home for Our Logic

To neatly package our inventory data and the functions that manage it, we'll use a JavaScript class. This object-oriented approach encapsulates the inventory's internal workings, exposing only the public API methods we want the rest of our game to use.

Our Inventory class will be responsible for:

  • Holding the array of items.
  • Enforcing an inventory size limit.
  • Providing methods to add, remove, and check for items.

Here's a basic skeleton for our class:

class Inventory {
  constructor(maxSlots = 20) {
    // The player's items, e.g., [{ itemId: 'potion_sm', quantity: 5 }]
    this.slots = [];
    this.maxSlots = maxSlots;
  }

  // --- Our API methods will be defined here ---
}

This idea of creating a dedicated object to manage inventory logic is a universal pattern in game development. While we're using JavaScript, the same structure applies across different engines and languages.

Data models - using data to create extensible, maintainable games in Godot

To see this concept in action, let's watch a segment from the 'Data models' video by Godotneers. It shows the creation of an 'Inventory' script that serves the exact same purpose as our JavaScript class.

Watch from 19:56 to 22:55. Notice how the presenter creates a simple object (not a complex game 'node') with an array to hold the contents and functions like add_item, remove_item, and get_items. This is a direct parallel to the API we are about to build.

2. The "Write" API: Adding and Removing Items

These are the methods that modify the inventory's state.

Adding an Item

Our addItem function needs to be smart. If the player picks up a Potion and they already have some, we should increase the quantity of the existing stack. If it's a new item, we need to add it to a new slot, but only if there's space.

Our logic will be:

  1. Find if an item with the same itemId already exists in a slot.
  2. If it exists, simply add the new quantity to the existing one.
  3. If it doesn't exist, check if the inventory has a free slot (slots.length < maxSlots).
  4. If there's a free slot, create a new object { itemId, quantity } and push it to the slots array.
  5. If there's no free slot, the action fails. The function should return true on success and false on failure, which is useful for things like preventing a pickup animation from playing if the inventory is full.

Here is the implementation inside our Inventory class:

// Inside the Inventory class
addItem(itemId, quantity = 1) {
  const existingSlot = this.slots.find(slot => slot.itemId === itemId);

  if (existingSlot) {
    // Item already in inventory, just increase quantity
    existingSlot.quantity += quantity;
    return true;
  } else if (this.slots.length < this.maxSlots) {
    // New item, add to a new slot
    this.slots.push({ itemId, quantity });
    return true;
  } else {
    // Inventory is full
    console.warn(`Inventory is full. Could not add ${itemId}.`);
    return false;
  }
}

This pattern of creating a dedicated function to handle adding items, including checks for inventory limits, is a core part of building a robust system.

Full Item and Item Inventory System in GMS2!

The video 'Full Item and Item Inventory System in GMS2!' provides a great step-by-step example of building this functionality. Let's see how it creates an item_add function and then refines it to handle inventory limits.

First, watch from 33:20 to 36:40, where a reusable item_add function is created. Then, watch from 01:00:47 to 01:03:55, where this function is updated to check against a maximum inventory size and return a boolean indicating success or failure. This perfectly illustrates the logic we just implemented.

Removing an Item

The removeItem function is the reverse. It needs to find the item and decrease its quantity. If the quantity drops to zero, the entire slot should be removed from the inventory.

Our logic:

  1. Find the index of the slot containing the itemId.
  2. If the item isn't found, the action fails.
  3. If found, decrease its quantity.
  4. If the quantity becomes 0 or less, remove the slot from the array entirely.
// Inside the Inventory class
removeItem(itemId, quantity = 1) {
  const slotIndex = this.slots.findIndex(slot => slot.itemId === itemId);

  if (slotIndex > -1) {
    const slot = this.slots[slotIndex];
    if (slot.quantity > quantity) {
      slot.quantity -= quantity;
    } else {
      // Remove the entire slot if quantity is depleted
      this.slots.splice(slotIndex, 1);
    }
    return true;
  } else {
    console.warn(`Item ${itemId} not found in inventory.`);
    return false;
  }
}
Test your understanding!

Given an Inventory instance with maxSlots: 3 and the following state:
inventory.slots = [{ itemId: 'potion_sm', quantity: 2 }]

What will inventory.slots look like after these two operations are run in sequence?

  1. inventory.addItem('ether', 1);
  2. inventory.removeItem('potion_sm', 2);
Show answer
  1. After inventory.addItem('ether', 1), a new slot is added because 'ether' is not in the inventory and there is space. The state becomes:
    [{ itemId: 'potion_sm', quantity: 2 }, { itemId: 'ether', quantity: 1 }]

  2. After inventory.removeItem('potion_sm', 2), the quantity of 'potion_sm' becomes 2 - 2 = 0. Since the quantity is zero, the entire slot is removed. The final state is:
    [{ itemId: 'ether', quantity: 1 }]

3. The "Read" API: Querying the Inventory

These methods allow other parts of the game to ask questions about the inventory without being able to change it.

  • getQuantity(itemId): Returns how many of a specific item the player has.
  • hasItem(itemId): A simple boolean check if the player has at least one of an item.
  • isFull(): Checks if there are no free slots.
  • getSlots(): Returns the full list of inventory slots, mainly for UI rendering.

Here are their implementations:

// Inside the Inventory class

getQuantity(itemId) {
  const slot = this.slots.find(s => s.itemId === itemId);
  return slot ? slot.quantity : 0;
}

hasItem(itemId) {
  return this.getQuantity(itemId) > 0;
}

isFull() {
  return this.slots.length >= this.maxSlots;
}

// Returns a copy to prevent external modification of the original array
getSlots() {
  return [...this.slots];
}

Note the use of the spread syntax [...this.slots] in getSlots(). This returns a shallow copy of the array, preventing code outside our class from accidentally modifying the inventory directly (e.g., by calling push or pop on the returned array). This enforces the principle that all modifications must go through our API.

4. The Complete Inventory Class

Putting it all together, we now have a powerful, self-contained Inventory manager.

class Inventory {
  constructor(maxSlots = 20) {
    this.slots = [];
    this.maxSlots = maxSlots;
  }

  /**
   * Adds an item to the inventory.
   * @param {string} itemId The ID of the item to add.
   * @param {number} quantity The amount to add.
   * @returns {boolean} True if the item was added, false if inventory is full.
   */
  addItem(itemId, quantity = 1) {
    const existingSlot = this.slots.find(slot => slot.itemId === itemId);

    if (existingSlot) {
      existingSlot.quantity += quantity;
      return true;
    } else if (this.slots.length < this.maxSlots) {
      this.slots.push({ itemId, quantity });
      return true;
    } else {
      console.warn(`Inventory is full. Could not add ${itemId}.`);
      return false;
    }
  }

  /**
   * Removes an item from the inventory.
   * @param {string} itemId The ID of the item to remove.
   * @param {number} quantity The amount to remove.
   * @returns {boolean} True if the item was removed, false if not found.
   */
  removeItem(itemId, quantity = 1) {
    const slotIndex = this.slots.findIndex(slot => slot.itemId === itemId);

    if (slotIndex > -1) {
      const slot = this.slots[slotIndex];
      if (slot.quantity > quantity) {
        slot.quantity -= quantity;
      } else {
        this.slots.splice(slotIndex, 1);
      }
      return true;
    } else {
      console.warn(`Item ${itemId} not found in inventory.`);
      return false;
    }
  }

  /**
   * Gets the quantity of a specific item.
   * @param {string} itemId The ID of the item to check.
   * @returns {number} The quantity of the item, or 0 if not found.
   */
  getQuantity(itemId) {
    const slot = this.slots.find(s => s.itemId === itemId);
    return slot ? slot.quantity : 0;
  }

  /**
   * Checks if the player has at least one of a specific item.
   * @param {string} itemId The ID of the item to check.
   * @returns {boolean}
   */
  hasItem(itemId) {
    return this.getQuantity(itemId) > 0;
  }

  /**
   * Checks if the inventory is full.
   * @returns {boolean}
   */
  isFull() {
    return this.slots.length >= this.maxSlots;
  }

  /**
   * Returns a copy of the current inventory slots.
   * @returns {Array<{itemId: string, quantity: number}>}
   */
  getSlots() {
    return [...this.slots];
  }
}
Test your understanding! (Advanced)

A crafting system is a common feature in JRPGs. Let's say a recipe is defined as an object like this:
const swordRecipe = { ingredients: [{ itemId: 'iron_ore', quantity: 5 }, { itemId: 'wood_plank', quantity: 2 }] };

Using the API methods of an inventory instance, write a function canCraft(recipe) that returns true if the player has all the necessary ingredients, and false otherwise.

Show answer

The function would iterate through the recipe's ingredients and use the inventory.getQuantity() method to check if the player has enough of each one.

function canCraft(recipe, inventory) {
  for (const ingredient of recipe.ingredients) {
    const playerQuantity = inventory.getQuantity(ingredient.itemId);
    if (playerQuantity < ingredient.quantity) {
      // Found an ingredient the player doesn't have enough of.
      return false;
    }
  }
  // If the loop completes, the player has all ingredients.
  return true;
}

// Example usage:
const playerInventory = new Inventory();
playerInventory.addItem('iron_ore', 10);
playerInventory.addItem('wood_plank', 1);

const swordRecipe = { ingredients: [{ itemId: 'iron_ore', quantity: 5 }, { itemId: 'wood_plank', quantity: 2 }] };

console.log(canCraft(swordRecipe, playerInventory)); // This will log 'false'

This is a perfect example of why having a clean API is so useful. The canCraft function doesn't need to know how the inventory is stored; it just needs to ask the right questions. The Godotneers video demonstrates a very similar has_all function in its crafting system implementation (starting around 01:05:30).

Conclusion

Congratulations! You've just built a complete, encapsulated inventory management system. By creating a dedicated Inventory class with a clear API, you've established a robust foundation that the rest of your game can rely on. This approach isolates complex logic, prevents bugs, and makes your overall game architecture much cleaner and more professional.

Key Takeaways:

  • Encapsulation: We bundled our inventory data (slots) and logic (methods) into a single Inventory class, hiding the internal complexity from the outside world.
  • API-Driven Design: We created a set of public methods (addItem, removeItem, getQuantity, etc.) that form a contract for how other game systems will interact with the inventory.
  • State Integrity: Our API methods contain the necessary logic to handle stacking, inventory limits, and removing depleted items, ensuring the inventory state is always valid.
  • Immutability (for queries): By returning a copy of the inventory data in getSlots(), we prevent other parts of the code from making unauthorized changes, directing all modifications through our controlled API.

Preview of the next lesson:
Now that we have the machinery to manage our inventory, it's time to put it to use. In the next lesson, "Implement the logic for using consumable items from both the menu and in battle," we will use the API we just built. When the player selects "Use Potion," our code will call inventory.removeItem('potion_sm', 1) and then apply the healing effect to the character—connecting our systems together into a tangible gameplay feature.

Can't find a good explanation? Sign up and we'll make it for you

Sign up