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

Agile Turn-Based Combat

Hello! Welcome back.

In our last lesson, we laid the crucial groundwork for our combat system by defining the data structures for actors and enemies. We now have a clear blueprint for what a "combatant" is, complete with core stats like HP, Attack, and, most importantly for today, Agility (AGI).

This lesson addresses the next logical step in building our combat engine: Implement a turn-ordering system based on character agility (e.g., Active Time Battle). We'll shift our focus from the "who" of combat to the "when." Using the AGI stat we just defined, we'll build the system that dictates the flow and rhythm of battle, deciding which character gets to act and how often.

We will explore the famous Active Time Battle (ATB) system, a hallmark of the Final Fantasy series you're fond of. This will be our first major step in implementing the logic of the combat state machine we designed.

1. From Turn-Based to Active Time

Traditional JRPGs, like the original Dragon Quest or Final Fantasy I, used a simple, strict turn-based system. Your party inputs all commands, then the enemy party inputs theirs, and then all actions resolve in a sequence. In these systems, Agility often just helps determine who goes first within a round.

However, starting with Final Fantasy IV, a more dynamic system was introduced that has become iconic: the Active Time Battle (ATB) system.

What Does ATB Add to a Game?

To start, let's get a high-level overview of what the ATB system is and how it changes the feel of combat. This video from ActualLizard provides a concise introduction.

Watch the first part of the video, from the beginning to 01:38. Focus on how it contrasts ATB with traditional turn-based combat and the core concept of individual meters filling up over time.

As the video explains, the fundamental shift with ATB is that time is always flowing. Instead of discrete "rounds," each character has their own readiness gauge that fills up independently. Once a character's gauge is full, they can take an action. This small change has significant design implications.

2. Why Choose ATB? The Design Advantages

Opting for an ATB system isn't just about mimicking a classic; it provides tangible benefits that make combat more engaging and stats more meaningful.

The Active-Time Model

The article 'The Active-Time Model' by magnolienne breaks down the key advantages of using an ATB system. This will help solidify why it's such a compelling design choice.

Read the section titled 'Why ATB?'. The author highlights three main advantages. Pay close attention to the first point about how it makes the speed stat more consistently impactful.

The key takeaways from that reading are:

  • Impactful Agility: In ATB, a higher AGI stat doesn't just mean you act first; it means you act more often. This makes investing in the stat valuable throughout the entire game, from short skirmishes to long boss battles.
  • Added Tension: Because enemy gauges are filling while you make decisions, there's a constant, low-level pressure. You can't endlessly deliberate on the perfect move, making battles feel more dynamic and less like a solved puzzle.
  • Animated Play: ATB bridges the gap between pure turn-based and action RPGs, making the combat feel faster and more fluid.

3. The Mechanics of an ATB System

Now let's translate this concept into a concrete algorithm. The core idea is to give every combatant a numerical counter that we'll call their "ATB gauge."

Battle system | Final Fantasy Wiki - Fandom

The Final Fantasy Wiki provides a factual breakdown of how ATB was implemented in the series. This will give us the historical context and technical details for our own implementation.

Read the section on 'Active Time Battle'. Focus on the description of the ATB gauge, its relation to the Speed stat, and the difference between 'Active' and 'Wait' modes.

From this, we can derive a clear algorithm for our engine:

  1. Initialization: When a battle begins, every combatant (both actors and enemies) has their ATB gauge set to 0.
  2. The "Tick" Loop: In every frame of our game loop, we iterate through all combatants.
  3. Increment Gauges: For each combatant that is alive and not currently acting, we increase their ATB gauge value. The amount of this increase is directly proportional to their AGI stat.
  4. Check for Readiness: After incrementing, we check if any combatant's gauge has reached a predefined maximum value (e.g., 1000).
  5. Action Turn: The first combatant to reach the maximum is now "ready." The game loop pauses the ATB-incrementing process and transitions the combat FSM to the INPUT state for that character.
  6. Reset: After the character has performed their action, their ATB gauge is reset to 0, and the ATB-incrementing process resumes.

This creates a continuous cycle where faster characters will complete the "0 to 1000" race more frequently than slower ones.

Test your understanding!

Imagine a battle with three combatants:

  • Hero: AGI of 50
  • Mage: AGI of 30
  • Goblin: AGI of 40

Let's say the ATB_MAX is 100, and in each "tick," a character's gauge increases by their AGI value. Everyone starts at 0.

Who takes the first turn? After that character acts and their gauge resets, who takes the second turn of the battle?

Show answer
  1. Turn 1:

    • After 1 tick: Hero (50), Mage (30), Goblin (40)
    • After 2 ticks: Hero (100 - Ready!), Mage (60), Goblin (80)
    • The Hero acts first.
  2. Turn 2:

    • The Hero's gauge resets to 0. The others continue from where they were.
    • After 2 ticks (from the start): Hero (0), Mage (60), Goblin (80)
    • Tick 3: Hero (50), Mage (90), Goblin (120 - Ready!).
    • The Goblin acts second. Even though the Hero is faster, the Goblin had a head start on their gauge from the first "race."

4. Implementation in Code

Given your background, translating this algorithm into JavaScript should be straightforward. This logic would live inside your BattleScene class, specifically within its update method, which is called by the main game loop via requestAnimationFrame.

Here is a conceptual implementation. We will add a currentAtb property to our combatant objects in memory when a battle starts.

// --- In your BattleScene class ---

const ATB_MAX = 1000; // The target value for a full gauge

class BattleScene {
    constructor(party, enemies) {
        // ...
        this.fsm = new StateMachine(/* ... */); // Our combat state machine
        
        // Combine actors and enemies into a single list for easy processing
        this.combatants = [...party.members, ...enemies];
        
        // Initialize ATB gauges for all combatants
        this.combatants.forEach(c => {
            // Give a small random initial boost to prevent ties at the start
            c.currentAtb = Math.random() * (ATB_MAX / 4); 
            c.isReady = false;
        });

        this.activeCombatant = null;
    }

    update(deltaTime) {
        // Let the FSM dictate the flow. We only advance time in the 'IDLE' state.
        if (this.fsm.currentState.name === 'IDLE') {
            this.updateAtbGauges(deltaTime);
            this.checkForReadyCombatant();
        }
        // ... other FSM-related updates
    }

    updateAtbGauges(deltaTime) {
        this.combatants.forEach(c => {
            // Only increment for active, non-ready characters
            if (c.hp > 0 && !c.isReady) {
                // Base tick rate + AGI. deltaTime makes it frame-rate independent.
                // AGI values might need to be scaled to feel right.
                c.currentAtb += (c.stats.agi + 20) * deltaTime;
            }
        });
    }

    checkForReadyCombatant() {
        // Find the first combatant whose gauge is full
        const readyCombatant = this.combatants.find(c => c.currentAtb >= ATB_MAX && c.hp > 0);

        if (readyCombatant) {
            readyCombatant.isReady = true;
            readyCombatant.currentAtb = ATB_MAX; // Clamp to max
            this.activeCombatant = readyCombatant;
            
            // Tell the FSM to transition to the next phase
            this.fsm.transition('INPUT', { combatant: this.activeCombatant });
        }
    }

    onActionComplete(combatant) {
        combatant.currentAtb = 0;
        combatant.isReady = false;
        this.activeCombatant = null;
        
        // Return the FSM to the idle/watching state
        this.fsm.transition('IDLE');
    }
}

A key detail here is deltaTime, which you'll get from your requestAnimationFrame loop. Multiplying the ATB increment by deltaTime ensures that the gauges fill at the same speed regardless of whether the player's computer is running the game at 60 FPS or 144 FPS.

5. Architectural Variation: The "Impulse Grid"

The real-time nature of ATB is its defining feature, but there are other ways to make Agility meaningful. The article you read earlier mentions the "Impulse Grid," a concept borrowed from tabletop wargames.

This system, also seen in a refined form in Final Fantasy X's "Conditional Turn-Based" (CTB) system, pre-calculates the turn order.

  • Imagine a timeline of "ticks."
  • For each character, you calculate how many ticks it takes for them to get a turn (e.g., TicksToAction = 1000 / AGI).
  • You can then build a queue of the next 10-15 actions and display it to the player.

This removes the real-time pressure of ATB while retaining the core benefit: faster characters act more frequently. It's a design choice that pushes the system more towards strategy and away from reflexes. While we will stick with the classic ATB for our project, it's valuable to know that these hybrid designs exist.

Conclusion

You have now designed the "heartbeat" of our combat system. By implementing a turn-ordering system based on Agility, we've created a dynamic flow that directly ties a character's stats to their performance in battle.

Key Takeaways:

  • The Active Time Battle (ATB) system uses individual timers (gauges) for each combatant, which fill up in real-time.
  • A combatant's Agility (AGI) stat is the primary factor determining how quickly their ATB gauge fills.
  • The core implementation involves incrementing each combatant's ATB counter in the game's update loop until it reaches a maximum value, at which point they can act.
  • Using deltaTime in your calculations is crucial for creating a frame-rate independent game speed.
  • Design choices like Active vs. Wait mode allow you to tune the level of real-time pressure on the player.

Preview of the next lesson:
We now have a system that can select who is acting. The very next step is to define what they can do. In the next lesson, we will begin implementing the commands for the active character: "Implement core combat actions: Attack, Defend, and Escape." This will involve taking player input and beginning the EXECUTION phase of our combat FSM.

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

Sign up