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

Battle Outcomes and Map Transitions

Hello! Welcome back to our JRPG development journey.

In our last lesson, we designed a robust damage calculation pipeline. Characters can now attack, defend, and deal variable damage based on classic JRPG formulas. The logical next step, and the focus of today's lesson, is to handle what happens when a combatant's HP drops to zero.

This lesson fulfills the learning outcome: Implement victory and defeat condition checks, and the transition from the battle scene back to the map. We will architect the end-of-battle flow, check for win/loss conditions, and manage the technical process of returning the player from the combat screen to the world map.

This is a critical piece of the game loop. From your experience in front-end development, you can think of this as managing application states or routes—transitioning from a "battle" view back to the "map" view, ensuring the previous state is restored correctly.

1. The Architectural Challenge: Returning from Battle

When a battle ends, we need to return to the map scene exactly as it was: the player character in the same position, other NPCs still on their paths, and game flags unchanged. A naive approach of simply destroying the map scene to create the battle scene and then recreating it from scratch is inefficient and loses state.

A much cleaner and more professional architecture, common in game development, is the State Stack.

How to Build a JRPG: A Primer for Game Developers - Code

The article 'How to Build a JRPG: A Primer for Game Developers' provides an excellent explanation of using a state stack to manage game flow. This concept is fundamental to handling transitions between menus, maps, and battles.

Please read the section titled 'Making Game Logic Easier With a State Stack'. Pay close attention to the diagram and the pseudocode for the StateStack class. Notice how states can be 'pushed' on top of each other and 'popped' off to resume the previous state.

As the article explains, the flow works like this:

  1. The game starts, and the MapScene is pushed onto the stack. It becomes the active scene, running its update and render methods.
  2. A random encounter occurs. Instead of destroying the MapScene, we simply push a new BattleScene onto the stack.
  3. The BattleScene is now at the top of the stack, so it becomes the active scene. The MapScene is paused but remains in memory.
  4. The battle concludes. We pop the BattleScene from the stack.
  5. The MapScene is once again at the top of the stack and resumes, exactly where it left off.

This is conceptually similar to how a modal dialog works in a web application, overlaying the main view without destroying it. Our SceneManager from Module 3 can be adapted to work like this stack.

2. Managing the Battle Flow: An Internal State Machine

Just as the overall game has states (map, battle, menu), the battle itself has its own internal sequence of states. Managing this flow prevents bugs, like the player attacking during the enemy's turn or victory music playing prematurely.

A common way to model this is with an enum or a set of constants representing the battle's current phase.

Turn based battle and transition from a game world Unity

This Unity tutorial, 'Turn based battle and transition from a game world', provides a clear, practical example of defining and using battle states to control the flow of combat.

Read the subsections 'Controlling the battle flow' and 'The Start()'. Note the BattleState enum: START, PLAYERTURN, ENEMYTURN, WIN, LOST. This structure dictates what can happen at any given moment in the battle.

We will adopt this pattern. In our BattleScene, we'll define these states:

const BattleState = {
    START: 'START',         // Initial setup
    PLAYER_TURN: 'PLAYER_TURN', // Waiting for player input
    ENEMY_TURN: 'ENEMY_TURN',   // AI is making a move
    WIN: 'WIN',             // Player won
    LOST: 'LOST'            // Player lost
};

Our BattleScene will have a property, e.g., this.currentState, which will hold one of these values. The scene's update loop will use this state to decide what to do.

3. Implementing Victory and Defeat Checks

Now we can connect our damage formula from the last lesson to this state machine. A battle's outcome is decided when one side has no more active combatants.

When to Check?
The check must happen immediately after any action that could defeat a combatant, primarily after damage is applied.

How to Check?
We'll create a new method in our BattleScene, checkBattleEndConditions().

// In BattleScene.js

// This function is called after any combatant takes damage and their HP is updated.
checkBattleEndConditions() {
    // Check for victory: are all enemies defeated?
    const allEnemiesDefeated = this.enemies.every(enemy => enemy.hp <= 0);
    if (allEnemiesDefeated) {
        this.currentState = BattleState.WIN;
        this.endBattle();
        return; // Stop further checks
    }

    // Check for defeat: are all party members defeated?
    const allActorsDefeated = this.party.every(actor => actor.hp <= 0);
    if (allActorsDefeated) {
        this.currentState = BattleState.LOST;
        this.endBattle();
    }
}

Now, let's integrate this into our executeAttack method from the previous lesson.

// In BattleScene.js, inside executeAttack(attacker, defender)

// ... after damage is calculated and applied ...
defender.hp -= damage;

// Clamp HP to 0
if (defender.hp < 0) {
    defender.hp = 0;
}

console.log(`${defender.name}'s HP is now ${defender.hp}`);

// Check if this defeat ended the battle
if (defender.hp <= 0) {
    console.log(`${defender.name} has been defeated!`);
    // We could add logic here to make the sprite disappear or play a death animation.
    this.checkBattleEndConditions();
}
Test your understanding!

Imagine you are implementing a 'Poison' status effect that deals damage to an actor at the end of their turn. Where would you need to call checkBattleEndConditions() to correctly handle a character being defeated by poison?

Show answer

You would need to call checkBattleEndConditions() immediately after applying the poison damage at the end of the turn. If the poison damage reduces the last party member's HP to zero, the battle should end in a 'LOST' state right away, without proceeding to the next turn.

4. Ending the Battle and Transitioning

When checkBattleEndConditions changes the state to WIN or LOST, the endBattle method is called. This method is responsible for displaying the result and triggering the transition.

The implementation is guided by the same principles seen in the Unity tutorial.

Turn based battle and transition from a game world Unity

Let's look at the final piece of the puzzle from the Unity tutorial: the function that handles the end-of-battle logic and scene transition.

Read the sections 'Player's turn', 'Enemy's turn', and 'Ending conditions'. Notice how after an attack, the code checks if the target's health is <= 0. If it is, the battle state is changed to WIN or LOST, and EndBattle() is called. The EndBattle() function then handles the final logic before loading the next level.

Let's write our own endBattle method. We'll use setTimeout to create a brief pause, allowing the player to read the result message before the screen transitions.

// In BattleScene.js

endBattle() {
    // Stop all combat processing by changing the state.
    // The main update loop should no longer accept player input or run enemy AI.
    
    // Disable UI elements like the command menu
    this.hideCommandMenu(); 

    if (this.currentState === BattleState.WIN) {
        console.log("VICTORY!");
        // Display a victory message on the screen.
        // We will add UI for this later.

        // After a delay, we'll pop the scene.
        setTimeout(() => {
            // In the next lesson, we'll add EXP and loot distribution here.
            console.log("Returning to the map...");
            this.sceneManager.pop(); // Assuming sceneManager has a pop method
        }, 2000); // 2-second delay

    } else if (this.currentState === BattleState.LOST) {
        console.log("DEFEAT...");
        // Display a game over message.

        setTimeout(() => {
            console.log("GAME OVER. Returning to title screen...");
            // For now, we'll just pop back to the map.
            // A real game might pop all scenes and push a 'TitleScene'.
            this.sceneManager.pop();
        }, 3000); // 3-second delay
    }
}

This function is our simplified version of the "Battle End Events" concept. While tools like RPG Maker let you visually script complex cutscenes for victory or defeat, the underlying mechanism is the same: the battle result triggers a predefined sequence of commands.

Battle End Events Plugin - RPG Maker MV

To see this concept in action within a game engine, this video on RPG Maker's 'Battle End Events' plugin shows how different outcomes (win, lose, escape) can trigger different scripted events.

Watch the first minute (00:00 - 01:00). This demonstrates how a 'Win' condition is tied to a specific 'Common Event' that displays dialogue. Our endBattle function serves the same purpose in code.

Conclusion

You have now architected and implemented one of the most important loops in any JRPG: entering combat, fighting to a conclusion, and returning to the world map. This structure is robust and scalable, ready for the features we'll build on top of it.

Key Takeaways:

  • State Stacks for Game Flow: Using a push/pop stack mechanism in your SceneManager is the standard, efficient way to handle temporary scenes like battles and menus while preserving the state of the underlying scene.
  • Internal State Machines: A scene's internal state machine (e.g., PLAYER_TURN, WIN, LOST) brings order and control to complex sequences, preventing invalid operations.
  • Centralized End-of-Battle Logic: Checks for victory or defeat should be centralized in a single function (checkBattleEndConditions) that is called whenever a combatant's status changes.
  • Decoupling Outcome from Transition: The endBattle function acts as a handler. It first processes the outcome (WIN or LOST) and then, after an appropriate delay for user feedback, triggers the scene transition.

Preview of the next lesson:
Our endBattle function currently has a placeholder comment for when the player wins. In the next lesson, we will fill that in by implementing a core pillar of JRPG progression: distributing experience points and loot upon victory. We will design the data structures and logic to reward the player for their success in battle.

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

Sign up