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

Consumable Item Implementation: Menu & Battle

Hello! Welcome back to your JRPG development course.

In our last lesson, you built a robust, API-driven Inventory class to manage adding, removing, and querying items. This was a crucial step in creating clean, professional game architecture, much like establishing a reliable state management pattern in a web application.

Today, we'll make those items come alive. Our goal is to address the learning outcome: Implement the logic for using consumable items from both the menu and in battle. We will connect the act of "using an item" to a tangible effect on a character, and we'll ensure the item is correctly consumed from the inventory you built.

This is where the systems we've been designing start to intersect, forming the core of our gameplay loop.

1. The Architectural Challenge: How to Define an Item's Effect?

Before writing code, let's consider the design. We have items like "Potion" and "Ether." How does our game know what each one does?

A naive approach would be to write a large if/else or switch statement where we check the item's ID:

// The "brittle" way - Don't do this!
function useItem(itemId, target) {
  if (itemId === 'potion_sm') {
    target.hp += 50;
  } else if (itemId === 'ether_sm') {
    target.mp += 20;
  } else if (itemId === 'bomb') {
    target.hp -= 100;
  }
  // ...and so on for every single item.
}

This quickly becomes unmanageable. Adding a new item requires changing this central function, and the logic for all items is tangled together. Your experience in software engineering has likely taught you to avoid such tightly coupled designs.

consumable item design in RPG game java

To see a classic example of this design problem, let's look at a question posted on Stack Overflow about this very topic in Java. The initial code presents a similar, category-based approach that suffers from the same scalability issues.

Read the question to understand the initial, inefficient design using checkCategory(). Then, read the accepted answer, which proposes a much more elegant solution using polymorphism. In this design, each item object knows how to apply its own effect to the player. We will adapt this powerful concept to JavaScript.

The key takeaway is to make our system data-driven. The item's data itself should define its effect, rather than having a monolithic block of code interpret it.

2. A Data-Driven Approach: Functions as Effects

In JavaScript, functions are first-class citizens. This means we can treat them like any other data: store them in variables, pass them as arguments, and—most importantly for us—attach them to objects.

The most flexible way to define an item's effect is to literally store a function within the item's data.

Full Item and Item Inventory System in GMS2!

This video, while for GameMaker Studio, perfectly demonstrates the architecture we're about to build. The presenter refactors the item system to include an 'effect' property, which is a function that gets executed when the item is used.

Watch from 44:22 to 47:57. Pay close attention to how a new effect parameter is added to the item constructor. This parameter is a function that contains the specific logic for that item (e.g., healing HP). This is the core principle we will now implement.

Extending Our Item Database

To implement this, we need to enrich our item definitions. While storing raw functions in a JSON file isn't feasible, we can store an identifier for the function. We'll also add metadata about where and how an item can be used.

Let's expand the structure of an item in our items.json database:

  • consumable: true if it's used up, false otherwise.
  • scope: An array indicating where it can be used (e.g., ['menu', 'battle']).
  • effect: An object containing the details of the effect.
    • type: A string identifier for the effect logic (e.g., HEAL_HP).
    • power: A number representing the strength of the effect (e.g., 50).
    • target: The default target type (e.g., 'ally', 'enemy', 'self').

Here's how a "Potion" might look in items.json:

{
  "potion_sm": {
    "name": "Potion",
    "description": "Restores 50 HP to one ally.",
    "consumable": true,
    "scope": ["menu", "battle"],
    "effect": {
      "type": "HEAL_HP",
      "power": 50,
      "target": "ally"
    }
  },
  "bomb_frag": {
    "name": "Frag Bomb",
    "description": "Deals 75 fire damage to one enemy.",
    "consumable": true,
    "scope": ["battle"],
    "effect": {
      "type": "DAMAGE_HP",
      "power": 75,
      "element": "fire",
      "target": "enemy"
    }
  }
}

3. Implementing the Item Usage Logic

Now we'll build the machinery to interpret this data.

The Effect Registry

First, we create an "Effect Registry" – a simple object that maps our effect.type identifiers from JSON to actual JavaScript functions. This keeps our logic organized and separate from the data.

// This would live in a file like 'itemEffects.js'
const itemEffects = {
  HEAL_HP: (target, effect) => {
    target.hp = Math.min(target.maxHp, target.hp + effect.power);
    console.log(`${target.name} recovers ${effect.power} HP!`);
  },
  RESTORE_MP: (target, effect) => {
    target.mp = Math.min(target.maxMp, target.mp + effect.power);
    console.log(`${target.name} recovers ${effect.power} MP!`);
  },
  DAMAGE_HP: (target, effect) => {
    // We'll ignore the element for now, but this shows how it could be used
    target.hp = Math.max(0, target.hp - effect.power);
    console.log(`${target.name} takes ${effect.power} damage!`);
  },
  // We can add more complex effects later
};

This is analogous to the "Common Events" system in RPG Maker, where an item triggers a predefined script. Here, our item's effect type triggers a predefined function.

The useItem Controller

Next, we create a central controller function. This function orchestrates the entire process of using an item. It will take the itemId, the target character object, the player's inventory instance, and the current context ('menu' or 'battle').

This function acts like an action handler in a state management library. It validates the action, dispatches the effect, and updates the state (inventory).

// Assumes you have access to your full item database (e.g., loaded from items.json)
// and the itemEffects registry from above.

// Let's assume some dummy data for characters and inventory
const playerParty = [
  { name: 'Ryu', hp: 80, maxHp: 150, mp: 30, maxMp: 50 },
  { name: 'Nina', hp: 120, maxHp: 120, mp: 10, maxMp: 80 },
];
const playerInventory = new Inventory(); // The class from our last lesson
playerInventory.addItem('potion_sm', 3);
playerInventory.addItem('bomb_frag', 2);

function useItem(itemId, targetCharacter, inventory, context = 'menu') {
  // 1. Check if the player has the item
  if (!inventory.hasItem(itemId)) {
    console.error(`Attempted to use ${itemId}, but not in inventory.`);
    return false;
  }

  // 2. Get the item's master data
  const itemData = allItems[itemId]; // allItems is our loaded items.json
  if (!itemData) {
    console.error(`Item data for ${itemId} not found.`);
    return false;
  }

  // 3. Check if usable in the current context
  if (!itemData.scope.includes(context)) {
    console.log(`Cannot use ${itemData.name} here.`);
    return false;
  }

  // 4. Find and execute the effect
  const effectFunction = itemEffects[itemData.effect.type];
  if (effectFunction) {
    effectFunction(targetCharacter, itemData.effect);
  } else {
    console.error(`Effect type ${itemData.effect.type} not implemented.`);
    return false;
  }

  // 5. Consume the item if necessary
  if (itemData.consumable) {
    inventory.removeItem(itemId, 1);
    console.log(`Used one ${itemData.name}. Remaining: ${inventory.getQuantity(itemId)}.`);
  }

  return true;
}

// --- Example Usage ---

// Use a Potion on Ryu from the menu
console.log(`Ryu's HP before: ${playerParty[0].hp}`);
useItem('potion_sm', playerParty[0], playerInventory, 'menu');
console.log(`Ryu's HP after: ${playerParty[0].hp}`);

// Try to use a bomb from the menu (should fail)
useItem('bomb_frag', playerParty[0], playerInventory, 'menu');

This structure is clean, scalable, and easy to debug. Each part has a single responsibility.

Test your understanding!

Imagine you want to add a "Phoenix Down" item that revives a character to 25% of their max HP.

  1. What would its entry in items.json look like?
  2. What new function would you need to add to the itemEffects registry?
Show answer
  1. items.json entry:

    "phoenix_down": {
      "name": "Phoenix Down",
      "description": "Revives a KO'd ally.",
      "consumable": true,
      "scope": ["menu", "battle"],
      "effect": {
        "type": "REVIVE",
        "power": 0.25,
        "target": "ally"
      }
    }
    

    Note: power here represents the percentage of HP to restore.

  2. itemEffects function:

    REVIVE: (target, effect) => {
      if (target.hp <= 0) {
        target.hp = Math.floor(target.maxHp * effect.power);
        console.log(`${target.name} has been revived!`);
      } else {
        console.log(`It has no effect on ${target.name}.`);
      }
    },
    

    This shows how easily the system can be extended to handle completely new mechanics without changing the core useItem function.

4. Handling Contexts: Menu vs. Battle

Our useItem controller is generic enough to work in any context. The difference lies in how it's called.

  • From the Menu: This is straightforward. The UI would present the inventory list. When the player selects "Use," it would then show the party list for target selection. The chosen itemId and targetCharacter are then passed to our useItem function. The context is 'menu'.

  • In Battle: The flow is similar but integrated into the turn-based system. When it's a character's turn, if they choose the "Item" command, the game presents the inventory.

    • The scope property is critical here. Your UI should filter the list to only show items that include 'battle' in their scope.
    • The target property in the item's data helps the UI determine whether to prompt for an ally or an enemy target.
    • Once the item and target are selected, you call the exact same useItem function, but with the context set to 'battle'.

This separation of concerns is powerful. The core logic of what an item does is completely independent of the scene it's used in.

5. Advanced Item Logic

This architecture can handle much more than simple healing.

  • Conditional Use: What about a key that only works in front of a specific chest? The videos for RPG Maker and GameMaker both demonstrate this. We can extend our design by adding an optional condition function to an item. The useItem controller would check and run this condition before applying the effect. If the condition fails (e.g., player.isFacing !== 'chest'), the item is not consumed.
  • Item Transformation: The GMS2 video showed a burger becoming a half-eaten burger. Our REVIVE effect already hints at this. The effect function could return a value indicating a transformation, which the useItem controller would handle by adding the new item to the inventory.

Full Item and Item Inventory System in GMS2!

Let's revisit the GameMaker video to see these advanced concepts in action.

First, watch from 51:31 to 55:21. This part shows how to implement a conditional check to prevent an item (a key) from being consumed if its usage condition isn't met. Then, watch from 58:49 to 01:00:47 to see how an item (a burger) can be 'transformed' into another item (a half-eaten burger) upon use. These are excellent patterns to keep in mind for more complex items.

Conclusion

You have now successfully designed and implemented the logic for using items! This is a major step in making your game feel interactive and strategic. By creating a data-driven system, you've built a foundation that is robust, scalable, and easy to maintain—hallmarks of professional game development.

Key Takeaways:

  • Data-Driven Design is Key: We defined item behavior in our data (items.json) rather than in hard-coded logic.
  • The Registry Pattern: An itemEffects registry maps data identifiers to executable functions, decoupling data from implementation.
  • Central Controller Logic: A single useItem function acts as a controller, validating and orchestrating the entire process, making the system predictable.
  • Context is King: The same core logic can be used in different game scenes (menu, battle) by simply passing a context parameter and letting the UI handle the specific user flow.

Preview of the next lesson:
We've covered items that are consumed. Next, we'll tackle items that are worn. In the next lesson, "Design and implement a data structure for equipment that modifies character stats," we'll explore how to handle items like swords, armor, and accessories that provide passive bonuses to a character's stats when equipped. This will involve a different, but related, set of architectural challenges.

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

Sign up