Create your own
Lesson illustration

Class/Job System Design

Hello! Welcome back to our course on building a JRPG engine.

In our last lesson, we constructed the core engine of character progression: the experience and leveling system. We established how a character can gain XP, level up, and have their stats increase according to predefined growth curves. This was a crucial step, but those growth curves were tied directly to the character. What if we want to create archetypes—a strong but slow Knight, a fast but frail Thief, a powerful but fragile Mage—that many different characters could potentially adopt?

This lesson addresses exactly that, fulfilling the learning outcome: Design a class/job system that dictates stat progression and available skills. We will abstract the concepts of progression into a formal "Class" or "Job" system. This is the heart of classic JRPG party customization, allowing you to create distinct, strategic roles for your characters, just like in the Final Fantasy and Breath of Fire games you enjoy.

1. The Philosophy of a Character Class

Before we design the data structure, let's explore the design philosophy behind character classes. A class is more than just a label; it's a promise to the player about a specific "slice of anticipated gameplay." It defines a character's role, their strengths, their weaknesses, and how they interact with the game's mechanics and other party members.

Making Better RPG Classes - What Makes a Class Classic? - Extra Credits

The YouTube channel Extra Credits has an excellent video titled 'Making Better RPG Classes' that breaks down the core principles of class design. It's a great conceptual starting point for thinking about the roles we want to create.

Please watch from 01:07 to 06:23. As you watch, consider these four key concepts: Role: What is the class's primary function (e.g., damage, healing, support)? Playstyle: How does the class feel to play? What mechanics does it lean on? Niche Protection: What does this class do better than any other? Harmony: How does this class work with other classes in a party?

As the video explains, a classic JRPG party is like a band, with each class acting as a different instrument. Our goal today is to design the "sheet music" for each of those instruments—a data structure that defines what makes a Warrior a Warrior and a Mage a Mage.

2. Designing the Class Data Structure

Your experience as a front-end developer has shown you the power of data-driven design. Instead of hard-coding the logic for a "Warrior" into our game, we can define it entirely through data. This makes our system flexible, easy to balance, and simple to expand.

In the last lesson, we put growth curves directly into a character's definition. Now, we'll extract that into a reusable Class structure. A class definition in our data/classes.json file might look like this:

{
  "warrior": {
    "id": "warrior",
    "name": "Warrior",
    "description": "A master of arms, boasting high physical strength and endurance.",
    "growth": {
      "maxHp": [1.0, 1.2, 1.45, 1.7, ...],
      "maxMp": [1.0, 1.0, 1.05, 1.05, ...],
      "attack": [1.0, 1.18, 1.38, 1.6, ...],
      "defense": [1.0, 1.15, 1.3, 1.48, ...],
      "intelligence": [1.0, 1.0, 1.0, 1.0, ...]
    },
    "skill_learnset": [
      { "level": 5, "skillId": "power_strike" },
      { "level": 10, "skillId": "armor_break" }
    ],
    "equipment_permissions": ["sword", "axe", "heavy_armor", "shield"]
  },
  "black_mage": {
    "id": "black_mage",
    "name": "Black Mage",
    "description": "A student of destructive magic, wielding elemental power to vanquish foes.",
    "growth": {
      "maxHp": [1.0, 1.08, 1.15, 1.22, ...],
      "maxMp": [1.0, 1.3, 1.65, 2.0, ...],
      "attack": [1.0, 1.0, 1.0, 1.0, ...],
      "defense": [1.0, 1.05, 1.1, 1.15, ...],
      "intelligence": [1.0, 1.25, 1.55, 1.9, ...]
    },
    "skill_learnset": [
      { "level": 1, "skillId": "fire" },
      { "level": 4, "skillId": "thunder" },
      { "level": 8, "skillId": "sleep" }
    ],
    "equipment_permissions": ["staff", "rod", "robe", "hat"]
  }
}

Let's break down this structure:

  • growth: This is the exact same concept from our previous lesson—an object containing arrays of level-based multipliers for each stat. Now, however, it belongs to the class, defining its inherent statistical identity.
  • skill_learnset: This is a new, critical piece. It's an array that maps levels to skillIds. This data dictates which abilities the character learns and when, directly tying progression to the class.
  • equipment_permissions: An array of strings defining what types of gear this class can equip. This provides another layer of mechanical distinction.

This approach is highly modular. Our Character data structure now becomes much simpler, only needing to reference its class:

// In data/characters.json
{
  "hero": {
    "id": "hero",
    "name": "Alex",
    "classId": "warrior",
    "initialLevel": 1,
    "baseStats": {
      "maxHp": 35, "maxMp": 10, "attack": 12, "defense": 8, "intelligence": 5
    }
  },
  "mage_ally": {
    "id": "mage_ally",
    "name": "Lena",
    "classId": "black_mage",
    "initialLevel": 1,
    "baseStats": {
      "maxHp": 24, "maxMp": 20, "attack": 6, "defense": 4, "intelligence": 14
    }
  }
}

Notice how the character's baseStats still matter. A character with high base attack who is a Warrior will be stronger than a character with low base attack who is also a Warrior. The class provides the multiplier, but the character provides the base, allowing for individual variation within the same class.

3. Integrating the Class System

Now, let's adjust our engine's architecture to use this new data model.

// A global object to hold our game data after loading it
const GameData = {
    classes: { /* loaded from classes.json */ },
    characters: { /* loaded from characters.json */ },
    // ... etc
};

class Character {
    constructor(definition) {
        this.definition = definition;
        
        // Find the class data from our global data store
        this.class = GameData.classes[definition.classId];
        
        this.level = definition.initialLevel;
        this.xp = 0;
        this.xpToNextLevel = this.calculateXpForLevel(this.level + 1);
        
        // The character's own list of learned skills
        this.skills = [];

        // Start with base stats and then calculate for the initial level
        this.stats = { ...definition.baseStats };
        this.recalculateStats();

        // Check for any skills learned at level 1
        this.checkForNewSkills();
    }

    levelUp() {
        this.level++;
        console.log(`${this.definition.name} is now Level ${this.level}!`);

        // Recalculate stats using the CLASS growth curves
        this.recalculateStats();
        
        // Check if any new skills are learned at this new level
        this.checkForNewSkills();

        this.xpToNextLevel = this.calculateXpForLevel(this.level + 1);
    }

    recalculateStats() {
        const growth = this.class.growth; // Use class growth curves!
        const baseStats = this.definition.baseStats;

        for (const stat in growth) {
            const curve = growth[stat];
            // Ensure we don't go out of bounds on the curve
            const multiplier = curve[Math.min(this.level - 1, curve.length - 1)];
            
            this.stats[stat] = Math.round(baseStats[stat] * multiplier);
        }
        // ... heal on level up, etc. ...
    }
    
    checkForNewSkills() {
        for (const learnable of this.class.skill_learnset) {
            if (learnable.level === this.level && !this.skills.includes(learnable.skillId)) {
                this.skills.push(learnable.skillId);
                console.log(`${this.definition.name} learned ${learnable.skillId}!`);
            }
        }
    }
    
    // ... addXP and calculateXpForLevel methods remain the same ...
}

This updated architecture is cleaner and more scalable. The Character class is no longer responsible for its own growth; it simply consults its assigned Class for instructions on how to grow and what to learn.

Test your understanding!

You want to create a "Red Mage" class, a JRPG archetype famous for being a hybrid of warrior and mage. Using the JSON data structure we designed, what would the growth and equipment_permissions look like for this class?

Show answer
  • growth: The growth curves would be balanced. attack and intelligence would both have moderate growth, better than a specialist's weak stat, but not as high as their primary stat. maxHp and maxMp would also see moderate, balanced increases.
  • equipment_permissions: This would reflect their hybrid nature. They could likely use a mix of weapon and armor types, such as ["sword", "staff", "light_armor", "robe"]. This combination is distinct from the highly specialized Warrior or Black Mage.

4. The Relationship Between Stats, Skills, and Class

We've now designed a system where a Class dictates stat growth and the list of available skills. These concepts are deeply intertwined.

RPG Game Design (Fundamentals, Patterns, Mechanics)

The article 'RPG Game Design' provides a solid overview of how character design elements fit together. It reinforces the idea that a class defines a character's role, abilities, and progression.

Please read the following sections from the article: Start at the heading 'How to design a character for RPG games?' and read down to the paragraph just before 'Develop the different races...'. This section connects the concepts of role, class, abilities, and equipment. Next, find the heading 'Develop character progression systems...' and read that short section. It explicitly describes class-based progression. Finally, scroll down to 'What mechanics are used in RPG game design?' and note how 'class progression' is listed as a core mechanic.

As the resource highlights, the class is the central hub connecting all aspects of a character's combat identity.

  • High Strength Growth (from the class) makes the Attack command and physical skills more powerful.
  • High Intelligence Growth makes magical skills deal more damage or heal for more.
  • The skill_learnset provides the actual tools (Fire, Cure, Power Strike) that utilize those stats.

Your class design directly informs your combat design. A party with a Warrior, Black Mage, White Mage, and Thief has a clear, balanced set of strategic options precisely because their underlying class data gives them different stats, skills, and equipment.

Conclusion

Today we've moved from a simple leveling system to a comprehensive, data-driven class architecture. This is a massive step towards realizing the feel of a classic JRPG.

Key Takeaways:

  • A Class is a data structure that acts as a template for character progression, bundling stat growth curves, a skill learning schedule (skill_learnset), and equipment permissions.
  • By abstracting this logic into data (classes.json), we can create, modify, and balance character roles without changing any core game code.
  • The Character object becomes simpler: it holds its current state (level, xp, skills) and refers to its Class data to know how to evolve.
  • This design creates a powerful link between a character's role, their statistical growth, and the abilities they can use in battle.

Preview of the next lesson:
We have designed the skill_learnset data structure and a function to check it on level up. In the next lesson, we will fully implement the skill learning mechanics tied to leveling up within a class. This will involve creating a Skill database, making sure the learned skillIds are stored correctly, and preparing our systems to be able to use these skills in the combat and menu scenes.

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

Sign up