Mechanics Explained: Turn Based RPG System

by Marty Wallace

Being primarily a server-side web developer and not having worked seriously on a game for close to 8 years has left me with very limited experience when it comes to writing code that deals with complex real-time, interactive content. My current RPG style project has been eye opening and trying in terms of how I should go about coding certain blocks of functionality - a big hurdle being how the turn-based combat should work.

Turn based combat has always been a favourite of mine with games like Tactics Ogre, Heroes of Might and Magic and entries in the Final Fantasy series being a big inspiration for this project. A small difference between those titles and my vision for this game is the fact that the game transitions from exploration to turn-based battle in the same context (there's not a new screen, enemies randomly spawn around the party), but otherwise the combat itself will be quite traditional.

I had a tough time tinkering with different ways the system itself could work, but ultimately ended up with something I think is perfect and that I'm very happy with. The driving force in the system I developed is the use of Promises, which make it very easy to hand the state of the battle over to a creature and allow anything to happen during their turn, taking any amount of time, before ultimately resolving and moving to the next creature, continuing until there are no more enemies (or all heroes have run out of HP).

A primitive example of what the system looks like begins with the Battle class I created, which manages the participants, turn timeline and victory/defeat events. The first thing this class does is accept a list of the heroes (player's characters) and enemies that are participating in the battle. My version of the battle class also contains a timeline, which is a list of the creatures mapped against a counter that determines the order of battle, but for this example we will omit that for simplicity.

class Battle {
  constructor(heroes, enemies) {
    this.heroes = heroes;
    this.enemies = enemies;
    this.creatures = heroes.concat(enemies);
  }
}

The project uses ES6 with Babel, so we will be using the class syntax, arrow functions and other ES6 features.

This allows us to construct a Battle and access the enemies, heroes and a combined list of both (for things like working out whose turn is next). The next two pieces of functionality serve to determine who should take the next turn and what that turn should look like:

class Battle {
  constructor(heroes, enemies) {
    this.heroes = heroes;
    this.enemies = enemies;
    this.creatures = heroes.concat(enemies);

    this.action();
  }

  next() {
    // Determine and return the next creature to take a turn.
    // ...
  }

  action() {
    // If there are no enemies, the battle is won.
    // If all heroes are dead, the battle is lost.

    // else:
    this.next()
      .action(this)
      .then(() => this.action());
  }
}

Now we have a battle that once instantiated will immediately begin the loop of determining who should act next, performing their action and repeating until there are either no more enemies or all the heroes have perished. This code assumed that each creature has a method action which accepts the Battle instance managing the creature and returns a Promise. Once that promise resolves, the battle will continue on to the next creature. Our creatures should all inherit a class that looks something like the following:

class Creature {
  action(battle) {
    return new Promise((resolve, reject) => {
      // Perform some action.
      // ...

      resolve();
    });
  }
}

This provides the perfect foundation to allow the action of each creature to take full control of the battle; move the camera around, perform some animation, deal damage to other creatures in the battle provided by the convenient battle argument and then simply resolve() once they are done. Anything as simple as:

action(battle) {
  return new Promise((resolve, reject) => {
    battle.enemies[0].health =- 3;
    resolve();
  });
}

Through to:

action(battle) {
  return new Promise((resolve, reject) => {
    const enemy = battle.pickRandomEnemy();

    battle.camera.moveTo(enemy, 100)
      .then(() => this.graphics.animate('attack'))
      .then(() => {
          enemy.takeDamage(3);
          return battle.camera.moveTo(this, 100);
      })
      .then(() => resolve());
  });
}

Functionality described in these examples (pickRandomEnemy(), .health, .camera, etc) are pseudo-code.

This works great for player turns where a menu of possible actions needs to be constructed and selected from and a target needs to be chosen - simply resolve() after all those choices are made.

The final piece of functionality is to notify the game that a battle has finished so that it can go back to exploration mode on victory, or a game over screen on defeat. This is as trivial as extending an event emitter (provided by Olical/EventEmitter in my case) and emitting a victory or defeat event, making your Batte.action() method look something like:

action() {
  if (enemies.length === 0) {
    this.emit('victory');
  } else {
    const deadHeroes = this.heroes.reduce((total, hero) => total + (hero.dead ? 1 : 0), 0);

    if (deadHeroes === heroes.length) {
      this.emit('defeat');
    } else {
      const creature = this.next();
      creature.action(this).then(result => this.action());
    }
  }
}

As your game becomes more advanced, you can even include some information about the result of the battle to the listener for victory, like what kind of loot was produced, and add it to your inventory:

this.emit('victory', { loot: [{ 'type': 'sword', damage: 10 }] });

With all of this in mind, a complete example of starting and handling the end of a battle is as straightforward as:

const battle = new Battle(heroes, enemies);

battle.on('victory', result => {
  result.loot.forEach(item => inventory.add(item));
  heroes.forEach(hero => hero.experience += result.experience);

  // Resume exploration.
  // ...
});

battle.on('defeat', () => {
  // Show game over screen, go to checkpoint, whatever.
  // ...
});

As I said, I'm very happy with this setup and I think it offers all the flexibility required for complex turns complete with any behavior you could dream up, as well as providing extremely clear exit points for your battle via the victory and defeat events.

  • javascript
  • systems