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

Equipment Data Structures for Character Stats

Hello! Welcome to the next lesson in your JRPG development journey.

In our last session, we built a flexible system for consumable items. We focused on how to define and trigger an item's active effect—a one-time action like healing HP. The core principle was data-driven design, where the item's data specifies its function.

Today, we shift from active, one-time effects to passive, continuous effects. This brings us to the world of equipment. Our learning outcome is to design and implement a data structure for equipment that modifies character stats. We will architect how a "Broad Sword" is defined and how a character's stats are structured to accommodate its bonus. We're building the blueprint today; in the next lesson, we'll implement the logic to actually equip the items and see the stat changes.

1. Defining Equipment: An Extension of "Item"

An equipment piece is, at its core, just another type of item. It has a name, a description, and exists in our game's database. We can extend the item data structure we've been using to include properties specific to equipment.

Your experience as a developer has shown that extending existing data models is a common task. A clean, extensible data structure is crucial for maintainability.

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

To see how naturally a data model can be extended, let's watch a short segment from the "Data models" video by Godotneers. While the context is Godot, the principle is universal. The presenter discusses adding a 'price' to an item, demonstrating how easily a well-structured data model accommodates new properties.

Watch from 01:22:05 to 01:23:37. Notice how adding a new price field to the base item script is a simple, non-disruptive change. We will apply this exact same thinking to add equipment-specific properties to our item data.

Following this principle, let's define the new properties our equipment items will need in our items.json database:

  1. type: To differentiate equipment from consumables or key items. We'll use "equipment".
  2. slot: A string indicating where the item can be equipped. This prevents equipping a sword to the 'head' slot. Common slots in classic JRPGs include 'weapon', 'shield', 'head', 'body', and 'accessory'.
  3. statModifiers: The heart of the system. This will define the stat changes the item provides.

For statModifiers, we have a design choice. A simple object like {"ATK": 5, "DEF": 2} works, but a more flexible and extensible approach—one better suited for a robust engine—is an array of objects:

"statModifiers": [
  { "stat": "ATK", "value": 5 },
  { "stat": "DEF", "value": 2 }
]

This structure is more verbose but allows for future expansion. For instance, we could later add a "type": "percentage" to a modifier without changing the entire structure. Given your preference for solid architecture, this is the superior choice.

Here is what two new items, a sword and some armor, would look like in our items.json:

{
  "broad_sword": {
    "name": "Broad Sword",
    "description": "A sturdy sword for a starting warrior.",
    "type": "equipment",
    "slot": "weapon",
    "statModifiers": [
      { "stat": "ATK", "value": 7 }
    ]
  },
  "leather_armor": {
    "name": "Leather Armor",
    "description": "Simple armor made from boiled leather.",
    "type": "equipment",
    "slot": "body",
    "statModifiers": [
      { "stat": "DEF", "value": 4 }
    ]
  }
}

2. Architecting Character Stats for Modification

Now that we know what an equipment item looks like, we need a character data structure that can properly utilize it. A simple character.attack property is insufficient because we need to distinguish between a character's natural strength and the bonus they get from their sword.

This is a classic problem in game development, and the best practice is to favor composition over inheritance. A character has stats; they aren't defined by them.

How to store and organize my data in a JRPG style game?

This StackExchange answer provides an excellent, code-driven overview of this very architectural problem. It advocates for separating a character's stats into distinct data containers.

Read the answer by user DMGregory. Pay close attention to the distinction between BaseStats (the character's inherent values, like our level-up stats) and RuntimeStats (the mutable, in-game instance). Also, note the suggestion to use an Equipment component, reinforcing the 'has-a' relationship. We will model our JavaScript data structures on this concept.

Inspired by this, we'll structure our character data with three distinct layers of statistical information:

  1. baseStats: An object holding the character's stats from their level and class, before any equipment is applied. This is the "source of truth" for the character's innate abilities.
  2. equipment: An object representing the character's equipment slots, storing the ID of the item currently in each slot.
  3. Final Stats: These are not stored but are derived whenever needed. They are the result of baseStats + bonuses from equipment + any temporary in-battle effects.

Here's what a character object would look like with this architecture:

const ryu = {
  name: "Ryu",
  level: 5,
  baseStats: {
    maxHp: 150,
    maxMp: 50,
    attack: 25,
    defense: 20,
    agility: 18
  },
  equipment: {
    weapon: "broad_sword",
    shield: null,
    head: null,
    body: "leather_armor",
    accessory: null
  }
  // Note: current HP and MP would be stored at the top level, e.g., ryu.hp = 125
};

This design is clean and robust. To change a character's base attack, you modify baseStats.attack. To change their weapon, you modify equipment.weapon. The final calculated attack value will update automatically without you having to manually track stat changes.

3. The Stat Modifier Pattern

We now have the two main data structures: the equipment in our database and the character who will use it. The statModifiers array is the bridge between them. The general concept of a "modifier" is a powerful pattern in game development.

When a character equips an item, that item's statModifiers are conceptually "attached" to the character. When we need to know the character's final attack, our game will perform a calculation: it starts with baseStats.attack and then adds the value from any equipped item's modifier where stat is "attack".

EASY Stats and Modifiers in Unity | Broker Chain Pattern

For a deeper look at how modifiers can be managed, let's watch a brief clip from a video on the Broker Chain pattern. While we won't implement this full pattern today, it superbly visualizes the concept of an equippable item providing a modifier that is then applied to a character.

Watch from 00:14:51 to 00:15:52. The video demonstrates an IEquipable interface. When an item is equipped, it provides a StatModifier that is added to a mediator. When unequipped, it's marked for removal. This is the exact lifecycle we're designing for: an equipped item grants a set of modifiers, and an unequipped item revokes them.

This concept of modifiers is highly extensible. The article "Tactics RPG Stats" discusses how modifiers can be additive or multiplicative and how their application order can be crucial. For our classic JRPG, a simple additive model is sufficient, but our data structure is ready for more if needed.

Test your understanding!

You need to create a "Wizard Hat" that provides +5 to maxMp and +3 to defense. How would you define this item in your items.json database using the structure we've designed?

Show answer
"wizard_hat": {
  "name": "Wizard Hat",
  "description": "A pointy hat that slightly boosts magical reserves.",
  "type": "equipment",
  "slot": "head",
  "statModifiers": [
    { "stat": "maxMp", "value": 5 },
    { "stat": "defense", "value": 3 }
  ]
}

Conclusion

Today, we've designed the essential data structures for a robust equipment system. You've learned how to represent equipment and its effects within a data-driven framework and how to architect your character data to cleanly separate base stats from modified stats.

Key Takeaways:

  • Equipment is Data: Equipment is an "item" with special properties: type, slot, and statModifiers.
  • Composition for Characters: Characters have stats. We separate baseStats (innate) from equipment (worn gear) to allow for dynamic calculation of final stats. This avoids data corruption and makes the system predictable.
  • Modifiers as a Bridge: The statModifiers array in our item data is the fundamental link that allows equipment to influence character stats. Our array-based structure is designed for future extensibility.

Preview of the next lesson:
We have the blueprints. In the next lesson, "Implement the logic for equipping items and calculating a character's final stats," we will write the code that brings these data structures to life. We'll build the function that calculates the final stats and the logic that allows a player to change their gear, immediately seeing the effects.

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

Sign up