Create your own
Lesson illustration

Skill Progression and Leveling Up

Hello! Welcome back to our JRPG development journey.

In our previous lesson, we established a robust, data-driven Class system. We designed a Class data structure that dictates a character's stat growth, what equipment they can use, and, crucially, a skill_learnset that maps specific levels to skills. We also sketched out a checkForNewSkills method to be triggered on level up.

This lesson will build directly on that foundation to fully implement the skill learning mechanics tied to leveling up within a class. We'll move from a simple array of skillId strings to a complete system where those IDs correspond to actual, defined abilities. This process is fundamental to making character progression feel meaningful and rewarding, as each level-up can now grant the player new tools for their strategic arsenal.

1. What is a Skill? Designing the Skill Database

In our last session, the skill_learnset in our classes.json looked like this: [{ "level": 5, "skillId": "power_strike" }]. This is a great start, but the game engine doesn't know what "power_strike" means. Is it a healing spell? A powerful attack? We need to define it.

Just as we created data files for characters and classes, we'll now create a data/skills.json. This file will serve as our central database for all abilities in the game. Your experience with data modeling for front-end applications will make this structure feel very natural. It's another example of a data-driven approach, separating the game's logic from its content.

Here's a sample structure for skills.json:

{
  "power_strike": {
    "id": "power_strike",
    "name": "Power Strike",
    "description": "A powerful blow that costs a small amount of MP.",
    "mpCost": 5,
    "type": "physical",
    "power": 150,
    "target": "single_enemy",
    "animationId": "slash_effect"
  },
  "fire": {
    "id": "fire",
    "name": "Fire",
    "description": "Engulfs one enemy in flames.",
    "mpCost": 4,
    "type": "magic",
    "element": "fire",
    "power": 120,
    "target": "single_enemy",
    "animationId": "fire_effect"
  },
  "cure": {
    "id": "cure",
    "name": "Cure",
    "description": "Restores a small amount of HP to one ally.",
    "mpCost": 3,
    "type": "magic",
    "element": "healing",
    "power": 100,
    "target": "single_ally",
    "animationId": "heal_effect"
  }
}

Let's break down the fields:

  • id: The unique identifier we've been using.
  • name & description: Text for display in menus and battle.
  • mpCost: How many Magic Points are consumed to use the skill.
  • type: Broad category (e.g., physical, magic). This can affect how damage is calculated or defended against.
  • element: (Optional) An elemental affinity like fire or ice, used for calculating weaknesses and resistances.
  • power: A base value used in the damage/healing formula. We'll implement this formula in the combat module, but for now, think of it as the skill's relative strength.
  • target: Defines who the skill can be used on (single_enemy, all_allies, etc.).
  • animationId: A reference to the visual effect to play when the skill is used in battle.

With this structure, a skillId is no longer just a string; it's a key that unlocks a rich set of data defining a complete game mechanic.

2. Visualizing the Concept in RPG Maker

Before refining our code, let's see how this exact concept is implemented in RPG Maker, a tool central to your learning goals. This will help bridge the gap between our abstract data structures and a tangible game development interface.

RPG Maker MZ: Basics EP-5: How to make a Custom Class

The video 'RPG Maker MZ: Basics EP-5: How to make a Custom Class' provides a clear look at the engine's interface for this system. It directly visualizes the relationship we are building between classes and skills.

Please watch from 02:50 to 03:50 and then from 08:35 to 09:30. Pay close attention to the 'Skills to Learn' list in the Class tab. Notice how the user selects a level and then chooses a skill from a dropdown. This is the UI representation of creating one of the { level, skillId } objects in our skill_learnset array.

As you can see, the data we're designing in JSON files mirrors the database-driven architecture of established game-making tools. A "Class" contains a list of skills, and each entry in that list specifies the level at which the skill is learned.

3. Refining the Implementation

In the last lesson, we added a placeholder checkForNewSkills method. Let's refine our Character class logic to be more robust. A key consideration is handling characters who don't start at level 1. If a character joins the party at level 10, they should already know all the skills their class would have learned from levels 1 through 10.

Good software design practice suggests that an object's constructor should be responsible for setting up its complete initial state, while other methods handle state transitions (like levelUp).

Here is the refined logic for our Character class:

// Assume GameData.skills is loaded from our new skills.json

class Character {
    constructor(definition) {
        this.definition = definition;
        this.class = GameData.classes[definition.classId];
        this.level = definition.initialLevel;
        this.xp = 0; // Or calculate based on level
        
        // This is where we'll store the skillId strings
        this.skills = []; 

        // Set up base stats...
        this.stats = { ...definition.baseStats };

        // ...then correctly initialize skills and stats for the starting level
        this.initializeSkills();
        this.recalculateStats();
        
        this.xpToNextLevel = this.calculateXpForLevel(this.level + 1);
    }

    // New method to handle the initial skill set
    initializeSkills() {
        const learnset = this.class.skill_learnset;
        for (const learnable of learnset) {
            if (learnable.level <= this.level) {
                // Ensure no duplicates are added
                if (!this.skills.includes(learnable.skillId)) {
                    this.skills.push(learnable.skillId);
                }
            }
        }
        console.log(`${this.definition.name} starts with skills: ${this.skills.map(id => GameData.skills[id].name).join(', ')}`);
    }

    levelUp() {
        this.level++;
        console.log(`${this.definition.name} is now Level ${this.level}!`);
        this.recalculateStats();
        
        // Check for new skills learned AT THIS SPECIFIC LEVEL
        const learnset = this.class.skill_learnset;
        for (const learnable of learnset) {
            if (learnable.level === this.level && !this.skills.includes(learnable.skillId)) {
                this.skills.push(learnable.skillId);
                const skillName = GameData.skills[learnable.skillId].name;
                console.log(`🎉 ${this.definition.name} learned ${skillName}!`);
            }
        }
        
        this.xpToNextLevel = this.calculateXpForLevel(this.level + 1);
        // ... and other level-up logic like healing ...
    }
    
    // recalculateStats, addXP, etc. remain as they were.
}

This approach is clean and robust:

  1. The constructor calls initializeSkills(), which populates the character's skill list with everything they should know up to their starting level.
  2. The levelUp() method handles the simple case of checking for skills learned at the new level only.

This cleanly separates initialization from state updates, a principle you're likely familiar with from managing component lifecycles in front-end development.

Test your understanding!

Imagine your classes.json for a "Knight" has the following skill_learnset:
[{ "level": 1, "skillId": "defend" }, { "level": 5, "skillId": "power_strike" }, { "level": 10, "skillId": "protect" }]

A new party member, "Gideon", who is a Knight, joins the party. His initialLevel is 7. According to our refined logic, which method (initializeSkills or levelUp) is called, and what will the contents of his skills array be immediately after he is created?

Show answer

The constructor will be called, which in turn calls initializeSkills(). This method will loop through the skill_learnset and add any skills with a level less than or equal to 7.

Therefore, Gideon's skills array will be ["defend", "power_strike"]. He will not have "protect" yet, as he is not level 10.

4. Design Patterns for Skill Progression

The system we've just implemented—learning specific skills automatically at set levels—is a cornerstone of classic JRPG design. It provides a steady, predictable sense of progression and makes every level-up an exciting event. But it's not the only way.

How you tackle Classes and Skills Progression ...

To understand where our system fits in the broader landscape of game design, let's look at a forum thread where different developers discuss their approaches to skill progression. This provides valuable insight into alternative patterns.

Please read the post by the user 'Wavelength' (it's post #5 in the thread). They outline five different systems they've used. Pay special attention to their description of the 'Learn by Leveling Up' system, as it perfectly describes what we have just implemented.

As Wavelength notes, this tried-and-true system makes "every level-up feel exciting!" This is our primary design goal here. The other systems mentioned, like skill trees or designing your own skills, offer more player choice but also introduce more complexity in terms of balance and implementation.

Our current system provides a strong, reliable foundation. We can always build upon it later by introducing skill trees or other mechanics, but mastering this core progression loop is the essential first step.

RPG Maker Skill Tree Example: Berserk
This image shows a more complex skill tree system, often seen in modern RPGs. Here, players spend points earned on level-up to unlock skills. While different from our automatic system, notice the core components: skills have prerequisites (like level or other skills) and costs.

Conclusion

In this lesson, we have successfully implemented a complete, data-driven skill learning system. By connecting our Class definitions to a new Skill database, we've given tangible meaning to character progression.

Key Takeaways:

  • A skill database (e.g., skills.json) is essential for defining the properties of abilities, such as their cost, power, and target.
  • The Character constructor is the correct place to handle the initial state of a character, including populating their list of known skills based on their starting level.
  • The levelUp method should handle the state transition, checking for and adding only the skills learned at that specific new level.
  • Automatic, level-based skill learning is a classic and effective design pattern for making player progression feel consistently rewarding.

Preview of the next lesson:
Now that our characters have stats, classes, and skills, they need things to hold and use! In our next lesson, we will design a database structure for items (consumable, equipment, key) and an inventory data structure. This will involve creating items.json and deciding how to represent the party's shared inventory, setting the stage for using potions in battle and equipping the swords and armor our classes are now permitted to use.

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

Sign up