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

Item Equipping and Stat Calculation

Hello! Let's dive into the next part of building our JRPG engine.

In our last lesson, we laid the architectural groundwork for our equipment system. We designed the data structures for equipment items (with slot and statModifiers) and for our characters (separating baseStats from the equipment they wear). This design gives us a clean, data-driven way to represent gear.

Today, we'll bring that design to life. The goal for this lesson is to implement the logic for equipping items and calculating a character's final stats. We will write the functions that handle swapping a character's gear and the code that computes their resulting combat statistics. This is the "engine" part of the equipment system, connecting your item database to a character's actual power.

1. Managing Equipment: The equip and unequip Logic

A good architecture isolates responsibilities. Instead of putting equipment logic directly on the character object, we'll create a dedicated service or "manager" to handle it. This approach, common in both game engines and large web applications, keeps our code organized and easier to maintain.

This manager will be responsible for two core actions:

  1. equip(character, newItemId): Puts a new piece of gear on a character.
  2. unequip(character, slot): Removes gear from a specific slot.

The equip function is the most interesting, as it needs to handle cases where a slot is already occupied. If a character equips a "Steel Sword" while already holding an "Iron Sword," the Iron Sword shouldn't just vanish—it needs to be returned to the player's inventory.

EQUIPMENT - Making an RPG in Unity (E07)

To see a practical implementation of this, let's watch a video from Brackeys' RPG series. Although it's in Unity/C#, the architectural concepts are identical to what we're building in JavaScript. Focus on how the EquipmentManager handles placing an item in a slot and swapping it with an old item.

Watch from 03:50 to 10:21. Pay close attention to: The Equip method (starts around 05:40): Notice how it uses the item's equipSlot to determine the correct index in an array. This is analogous to our character's equipment object and its slot keys. Item Swapping (starts around 08:35): See how it checks if an oldItem exists in the slot. If so, it adds that old item back to the inventory before placing the new one. This is the crucial logic for a smooth user experience.

Let's translate this logic into JavaScript. Imagine we have a global game object that holds the player's inventory and the itemDatabase.

// Assumed data structures from previous lesson
// game.player.inventory = { items: [{ itemId: 'broad_sword', quantity: 1 }, ...] }
// game.player.party.ryu = { name: "Ryu", baseStats: {...}, equipment: { weapon: 'old_sword', ... } }
// game.itemDatabase = { broad_sword: { name: 'Broad Sword', slot: 'weapon', ... }, ... }

const EquipmentManager = {
  equip: function(character, newItemId) {
    const newItem = game.itemDatabase[newItemId];
    if (!newItem || newItem.type !== 'equipment') {
      console.error("Attempted to equip a non-equippable item.");
      return;
    }

    const slot = newItem.slot;
    const oldItemId = character.equipment[slot];

    // 1. If there's an old item, return it to the inventory.
    if (oldItemId) {
      InventoryManager.addItem(oldItemId, 1); // Assuming you have an InventoryManager
    }

    // 2. Place the new item on the character.
    character.equipment[slot] = newItemId;

    // 3. Remove the new item from the inventory.
    InventoryManager.removeItem(newItemId, 1);

    // We'll discuss this more later, but we need to signal that stats have changed.
    this.onEquipmentChanged(character, newItem, game.itemDatabase[oldItemId]);
  },

  unequip: function(character, slot) {
    const oldItemId = character.equipment[slot];
    if (!oldItemId) {
      return; // Nothing to unequip
    }

    // 1. Add the unequipped item back to the inventory.
    InventoryManager.addItem(oldItemId, 1);

    // 2. Clear the slot on the character.
    character.equipment[slot] = null;
    
    this.onEquipmentChanged(character, null, game.itemDatabase[oldItemId]);
  },
  
  // This is a placeholder for a callback/event system.
  onEquipmentChanged: function(character, newItem, oldItem) {
    console.log(`${character.name} equipped ${newItem?.name}, replacing ${oldItem?.name}.`);
    // This is where we will trigger the stat recalculation.
  }
};

This implementation cleanly separates the logic of managing equipment from the data itself, which is a hallmark of robust software design.

2. The Heart of the System: Calculating Final Stats

Now for the second critical piece: calculating a character's final, "derived" stats. Our architecture is designed for this to be a pure calculation: we always start from baseStats and add the bonuses from all currently equipped items. This is more robust than trying to manually add/subtract stats, which can lead to bugs and desynchronization.

The process is straightforward:

  1. Create a temporary stats object, initialized with the character's baseStats.
  2. Iterate through each slot in the character's equipment object.
  3. For each equipped item, retrieve its statModifiers.
  4. For each modifier, add its value to the corresponding stat in our temporary object.
  5. Return the final, calculated stats object.

How to Make an RPG: Stats

The article "How to Make an RPG: Stats" provides a fantastic conceptual model for this process. It explains the idea of modifiers and shows a clear algorithm for applying them to base stats to get a final, modified value.

Read the sections titled "Modifiers" and the implementation that follows (from "There are two types of modifier..." to "...Everything is working as expected."). Although the code is in Lua, focus on the Get(id) function's logic. It starts with a base value, then iterates through all active modifiers to calculate the final stat. This is exactly what we will implement.

Let's write a JavaScript function based on this principle. We can add it to a CharacterStatsManager.

const CharacterStatsManager = {
  calculateFinalStats: function(character) {
    // Start with a deep copy of baseStats to avoid modifying the original.
    const finalStats = JSON.parse(JSON.stringify(character.baseStats));

    // Get an array of all equipped item IDs (filter out null/empty slots).
    const equippedItemIds = Object.values(character.equipment).filter(id => id);

    for (const itemId of equippedItemIds) {
      const item = game.itemDatabase[itemId];
      if (!item || !item.statModifiers) continue;

      // Apply each modifier from the item.
      for (const modifier of item.statModifiers) {
        const { stat, value } = modifier;
        if (finalStats.hasOwnProperty(stat)) {
          finalStats[stat] += value;
        } else {
          // It's good practice to initialize the stat if it doesn't exist on the base character.
          finalStats[stat] = value;
        }
      }
    }
    return finalStats;
  }
};

// --- Example Usage ---
// Let's say Ryu has base attack 25 and equips a 'broad_sword' (+7 ATK)
// and 'leather_armor' (+4 DEF).

const ryu = game.player.party.ryu; // has equipment: { weapon: 'broad_sword', body: 'leather_armor' }
ryu.finalStats = CharacterStatsManager.calculateFinalStats(ryu);

console.log(ryu.baseStats.attack);    // Output: 25
console.log(ryu.finalStats.attack);   // Output: 32
console.log(ryu.baseStats.defense);   // Output: 20
console.log(ryu.finalStats.defense);  // Output: 24

This function is pure and predictable. No matter how many times you call it, it will always produce the correct result based on the character's current state.

3. Tying It Together: The Equipment Change Lifecycle

We now have an equip function and a calculateFinalStats function. How do they work together? We need to ensure stats are recalculated whenever a character's equipment changes. This is where an event-based or callback system becomes invaluable.

Look back at the onEquipmentChanged placeholder in our EquipmentManager. This function acts as a hook. Other parts of our engine can "listen" for this event.

EQUIPMENT - Making an RPG in Unity (E07)

The Brackeys video briefly touches on this exact concept toward the end. This is a crucial architectural pattern for creating decoupled systems.

Watch from 00:12:12 to 00:13:17. The video introduces a 'callback method' (onEquipmentChanged). Notice how it's invoked whenever equipment changes. Other scripts (like a stats manager or a UI controller) can 'subscribe' to this callback to react to the change without being tightly coupled to the EquipmentManager.

In a simple JavaScript implementation, we can make this explicit by having our equip function directly call the stat calculation.

// A more complete EquipmentManager
const EquipmentManager = {
  // ... (equip and unequip methods from before) ...

  equip: function(character, newItemId) {
    // ... (logic from before) ...
    this.onEquipmentChanged(character); // Call the hook
  },

  unequip: function(character, slot) {
    // ... (logic from before) ...
    this.onEquipmentChanged(character); // Call the hook
  },

  onEquipmentChanged: function(character) {
    // The 'listener' updates the character's final stats.
    character.finalStats = CharacterStatsManager.calculateFinalStats(character);
    console.log(`${character.name}'s stats have been updated!`);
    
    // In the future, a UI Manager could also listen to this to redraw the screen.
  }
};

This pattern ensures that a character's stats are always up-to-date after their gear changes. The equipment screen you see in games like Octopath Traveler relies on this exact lifecycle: you select a new item, the equip logic runs, an onEquipmentChanged event fires, and both the stats display and the character model are updated in response.

JRPG Equipment Screen with Character Stats
This screenshot from a modern JRPG shows the end result of the logic we've built today. The 'Attributes' on the right are the character's **final stats**, calculated from their base stats plus all modifiers from the equipped Sword, Polearm, Shield, and other gear.
Test your understanding!

A character has baseStats: { attack: 10 } and equipment: { weapon: 'rusty_sword' }.
The rusty_sword gives +2 attack.
The shiny_sword gives +8 attack.

You call EquipmentManager.equip(character, 'shiny_sword').

Walk through the steps:

  1. What happens to the rusty_sword?
  2. What is the new value of character.equipment.weapon?
  3. What function is called at the end of the equip process?
  4. What will the character's final calculated attack be?
Show answer
  1. The rusty_sword is returned to the player's inventory.
  2. character.equipment.weapon is now 'shiny_sword'.
  3. The onEquipmentChanged hook is called, which in turn triggers CharacterStatsManager.calculateFinalStats(character).
  4. The final attack will be 10 (base) + 8 (shiny_sword) = 18. The +2 from the rusty sword is no longer part of the calculation because it's not in an equipment slot.

Conclusion

Today, we've implemented the functional core of our equipment system. You've written the logic to manage equipping and unequipping gear, and you've created a robust function to calculate a character's final stats based on their base abilities and worn items.

Key Takeaways:

  • Encapsulate Logic: Using managers (EquipmentManager, CharacterStatsManager) keeps your code clean, testable, and organized.
  • Swapping is Key: The equip function must account for returning previously worn items to the player's inventory.
  • Calculate, Don't Mutate: Deriving final stats from baseStats + modifiers is more reliable than trying to add/subtract changes incrementally.
  • Decouple with Callbacks: An onEquipmentChanged event or callback allows different systems (stats, UI) to react to equipment changes without being tangled together.

Preview of the next lesson:
We have the data and the logic. What's missing? The user interface! In the next module, we'll begin building the UI for our game, starting with the design of a reusable component architecture for menus and windows. We will then build the very screens where the player can see their inventory and use the equip logic we've created today.

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

Sign up