How to Make a Turn-Based RPG Game in Phaser – Part 2

In the last tutorial, we created the BattleState for our turn-based RPG. Now, we are going to create a WorldState were the player can explore and eventually find enemies. In addition, we will improve our battle system to consider units speed. The following topics will be covered in this tutorial:

  • Creating a WorldState which the player can navigate
  • Creating enemy spawners that will initiate the BattleState
  • Improve the battle system to consider units speed

To read this tutorial, it is important that you are familiar with the following concepts:

  • Javascript and object-oriented concepts.
  • Basic Phaser concepts, such as: states, sprites, groups and arcade physics
  • Creating maps using Tiled

Learn Phaser by building 15 games

If you want to master Phaser and learn how to publish Phaser games as native games for iOS and Android feel free to check Zenva‘s online course The Complete Mobile Game Development Course – Build 15 Games.

Assets copyright

The monsters assets used in this tutorial are available in http://opengameart.org/content/2d-rpg-enemy-set and were created by the following artists: Brett Steele (Safir-Kreuz), Joe Raucci (Sylon), Vicki Beinhart (Namakoro), Tyler Olsen (Roots). The characters assets are available in http://opengameart.org/content/24×32-characters-with-faces-big-pack and were created by Svetlana Kushnariova (email: [email protected]). All assets are available through the Creative Commons license.

Source code files

You can download the tutorial source code files here.

Creating the WorldState

We are going to create a WorldState which will read a Tiled map (in JSON format) and allow the player to navigate through it. The figure below shows the map I created. Since the focus of this tutorial is not on creating Tiled maps, I’m not going into the details of it, and you can create your own or use the one provided in the source code. However, two things are important about the map creating, due to the way we are going to create WorldState:

map

  1. Any collidable tile layer must have a collision property set to true.
  2. All game prefabs must be defined in the objects layer and each object must contain in its properties at least: name, type, group and texture. Any additional properties must also be defined.

The figures below show the properties of a collision layer and the player object, to illustrate those two conditions.

collision_layer object_properties

In addition to the Tiled map, WorldState will read another JSON file, such as the one below. Notice that the JSON file must specify the assets, groups and map information.

{
    "assets": {
        "map_tileset": {"type": "image", "source": "assets/images/open_tileset.png"},
        "enemy_spawner_image": {"type": "image", "source": "assets/images/enemy_spawner.png"},
        "male_fighter_spritesheet": {"type": "spritesheet", "source": "assets/images/characters/fighter_m.png", "frame_width": 24, "frame_height": 32},
        
        "level_tilemap": {"type": "tilemap", "source": "assets/maps/map1.json"}
    },
    "groups": [
        "players",
        "enemy_spawners"
    ],
    "map": {
        "key": "level_tilemap",
        "tilesets": ["map_tileset", "male_fighter_spritesheet"]
    }
}

The code below shows the WorldState. The “init” method initializes the physics engine and creates the map form the JSON file. It also creates a “party_data” object which contains the stats of all player units. Notice that this object can be passed as a parameter, which will be done after each battle.

var <a class="wpil_keyword_link" href="https://gamedevacademy.org/best-rpg-tutorials/" target="_blank" rel="noopener" title="RPG" data-wpil-keyword-link="linked">RPG</a> = RPG || {};

RPG.WorldState = function () {
    "use strict";
    Phaser.State.call(this);
    
    this.prefab_classes = {
        "player": RPG.Player.prototype.constructor,
        "enemy_spawner": RPG.EnemySpawner.prototype.constructor
    };
};

RPG.WorldState.prototype = Object.create(Phaser.State.prototype);
RPG.WorldState.prototype.constructor = RPG.WorldState;

RPG.WorldState.prototype.init = function (level_data, extra_parameters) {
    "use strict";
    var tileset_index;
    this.level_data = this.level_data || level_data;
    
    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
    this.scale.pageAlignHorizontally = true;
    this.scale.pageAlignVertically = true;
    
    // start physics system
    this.game.physics.startSystem(Phaser.Physics.ARCADE);
    this.game.physics.arcade.gravity.y = 0;
    
    // create map and set tileset
    this.map = this.game.add.tilemap(this.level_data.map.key);
    tileset_index = 0;
    this.map.tilesets.forEach(function (tileset) {
        this.map.addTilesetImage(tileset.name, this.level_data.map.tilesets[tileset_index]);
        tileset_index += 1;
    }, this);
    
    // if no party data is in the parameters, initialize it with default values
    this.party_data = extra_parameters.party_data || {
        "fighter": {
            "type": "player_unit",
            "position": {"x": 250, "y": 70},
            "properties": {
                "texture": "male_fighter_spritesheet",
                "group": "player_units",
                "frame": 10,
                "stats": {
                    "attack": 15,
                    "defense": 5,
                    "health": 100,
                    "speed": 15
                }
            }
        },
        "mage": {
            "type": "player_unit",
            "position": {"x": 250, "y": 150},
            "properties": {
                "texture": "female_mage_spritesheet",
                "group": "player_units",
                "frame": 10,
                "stats": {
                    "attack": 20,
                    "defense": 2,
                    "health": 100,
                    "speed": 15
                }
            }
        }
    };
    
    if (extra_parameters.restart_position) {
        this.player_position = undefined;
    }
};

RPG.WorldState.prototype.create = function () {
    "use strict";
    var group_name, object_layer, collision_tiles;
    
    // create map layers
    this.layers = {};
    this.map.layers.forEach(function (layer) {
        this.layers[layer.name] = this.map.createLayer(layer.name);
        if (layer.properties.collision) { // collision layer
            collision_tiles = [];
            layer.data.forEach(function (data_row) { // find tiles used in the layer
                data_row.forEach(function (tile) {
                    // check if it's a valid tile index and isn't already in the list
                    if (tile.index > 0 && collision_tiles.indexOf(tile.index) === -1) {
                        collision_tiles.push(tile.index);
                    }
                }, this);
            }, this);
            this.map.setCollision(collision_tiles, true, layer.name);
        }
    }, this);
    // resize the world to be the size of the current layer
    this.layers[this.map.layer.name].resizeWorld();
    
    // create groups
    this.groups = {};
    this.level_data.groups.forEach(function (group_name) {
        this.groups[group_name] = this.game.add.group();
    }, this);
    
    this.prefabs = {};
    
    for (object_layer in this.map.objects) {
        if (this.map.objects.hasOwnProperty(object_layer)) {
            // create layer objects
            this.map.objects[object_layer].forEach(this.create_object, this);
        }
    }
    
    // if we came from BattleState, move the player to the previous position
    if (this.player_position) {
        this.prefabs.player.reset(this.player_position.x, this.player_position.y);
    }
};

RPG.WorldState.prototype.create_object = function (object) {
    "use strict";
    var object_y, position, prefab;
    // tiled coordinates starts in the bottom left corner
    object_y = (object.gid) ? object.y - (this.map.tileHeight / 2) : object.y + (object.height / 2);
    position = {"x": object.x + (this.map.tileHeight / 2), "y": object_y};
    // create object according to its type
    if (this.prefab_classes.hasOwnProperty(object.type)) {
        prefab = new this.prefab_classes[object.type](this, object.name, position, object.properties);
    }
    this.prefabs[object.name] = prefab;
};

The “create” method must initialize the map layers and game groups and prefabs. The layers were all available in the Tiled map and were already read in the “init” method, so we just have to iterate through them creating each one. However, we must detect the collision layers (by means of the added property) and set all its tiles as collidable.

The groups are easily created by iterating through the ones defined in the level JSON file. However, to create the prefabs we must iterate through each object in the objects layer and instantiate the correct prefab. The prefab instantiation is done in the “create_object” method, which also adjust the prefab position due to the different coordinates used by Tiled. To properly instantiate each prefab, we define a “prefab_classes” property in the constructor that maps each prefab type to its constructor, as we did in the BattleState. As in BattleState, this is possible because all prefabs have the same constructor.

The last piece of code in the “create” method resets the player to a previously saved position. This must be done because the WorldState can be called after a BattleState, so we must keep saved the player previous position. If the battle is lost, we reset the game using the “reset_position” parameter in the “init” method.

Adding the Player prefab

Now we are going to add the player prefab, which can navigate through the world. The code below shows the Player prefab. In the constructor it must initialize the walking speed, animations, and physics body. Then, in the “update” method, it is controlled by the keyboard arrow keys (obtained from the “cursors” object). We move the player to a given direction if its respective arrow key is pressed and if the player is not already moving to the opposite direction. Also, when the player starts moving to a given direction it plays the corresponding animation. When the player stops, the animation stops and reset its frame to the correct stopped frame, according to its facing direction.

var RPG = RPG || {};

RPG.Player = function (game_state, name, position, properties) {
    "use strict";
    RPG.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.walking_speed = +properties.walking_speed;
    
    this.animations.add("walking_down", [6, 7, 8], 10, true);
    this.animations.add("walking_left", [9, 10, 11], 10, true);
    this.animations.add("walking_right", [3, 4, 5], 10, true);
    this.animations.add("walking_up", [0, 1, 2], 10, true);
    
    this.stopped_frames = [7, 10, 4, 1, 7];

    this.game_state.game.physics.arcade.enable(this);
    this.body.setSize(16, 16, 0, 8);
    this.body.collideWorldBounds = true;

    this.cursors = this.game_state.game.input.keyboard.createCursorKeys();
};

RPG.Player.prototype = Object.create(RPG.Prefab.prototype);
RPG.Player.prototype.constructor = RPG.Player;

RPG.Player.prototype.update = function () {
    "use strict";
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.collision_back);
    this.game_state.game.physics.arcade.collide(this, this.game_state.layers.collision_front);
    
    if (this.cursors.left.isDown && this.body.velocity.x <= 0) {
        // move left
        this.body.velocity.x = -this.walking_speed;
        if (this.body.velocity.y === 0) {
            this.animations.play("walking_left");
        }
    } else if (this.cursors.right.isDown && this.body.velocity.x >= 0) {
        // move right
        this.body.velocity.x = +this.walking_speed;
        if (this.body.velocity.y === 0) {
            this.animations.play("walking_right");
        }
    } else {
        this.body.velocity.x = 0;
    }

    if (this.cursors.up.isDown && this.body.velocity.y <= 0) {
        // move up
        this.body.velocity.y = -this.walking_speed;
        if (this.body.velocity.x === 0) {
            this.animations.play("walking_up");
        }
    } else if (this.cursors.down.isDown && this.body.velocity.y >= 0) {
        // move down
        this.body.velocity.y = +this.walking_speed;
        if (this.body.velocity.x === 0) {
            this.animations.play("walking_down");
        }
    } else {
        this.body.velocity.y = 0;
    }
    
    if (this.body.velocity.x === 0 && this.body.velocity.y === 0) {
        // stop current animation
        this.animations.stop();
        this.frame = this.stopped_frames[this.body.facing];
    }
};

You can already try moving the player in the WorldState. Check if the collisions are working properly. Notice that we check for collisions with all collision layers in the update method.

world

Adding the EnemySpawner prefab

The EnemySpawner will be an immovable prefab that overlaps with the player. When such overlap occur, it will check for possible enemy encounters according to their probabilities. We will start by defining the enemy encounters in the JSON level file, as shown below. Each enemy encounter has a probability and the enemy data.

"enemy_encounters": [
        {"probability": 1,
         "enemy_data": {
            "bat1": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 90},
                "properties": {
                    "texture": "bat_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 10,
                        "defense": 1,
                        "health": 30
                    }
                }
            },
            "bat2": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 170},
                "properties": {
                    "texture": "bat_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 10,
                        "defense": 1,
                        "health": 30
                    }
                }
            }
         }
        }
    ]

Now, we can create the EnemySpawner to check for one of the possible encounters. Its code is shown below. In the “update” method we check for overlaps with the player and call the “check_for_spawn” method when an overlap occure. Notice that, to call this method only once for each overlap we use the “overlapping” variable, and check for spawns only when it was false.

var RPG = RPG || {};

RPG.EnemySpawner = function (game_state, name, position, properties) {
    "use strict";
    RPG.Prefab.call(this, game_state, name, position, properties);
    
    this.game_state.game.physics.arcade.enable(this);
    this.body.immovable = true;
    
    this.overlapping = true;
};

RPG.EnemySpawner.prototype = Object.create(RPG.Prefab.prototype);
RPG.EnemySpawner.prototype.constructor = RPG.EnemySpawner;

RPG.EnemySpawner.prototype.update = function () {
    "use strict";
    this.overlapping = this.game_state.game.physics.arcade.overlap(this, this.game_state.groups.players, this.check_for_spawn, null, this);
};

RPG.EnemySpawner.prototype.check_for_spawn = function () {
    "use strict";
    var spawn_chance, encounter_index, enemy_encounter;
    // check for spawn only once for overlap
    if (!this.overlapping) {
        spawn_chance = this.game_state.game.rnd.frac();
        // check if the enemy spawn probability is less than the generated random number for each spawn
        for (encounter_index = 0; encounter_index < this.game_state.level_data.enemy_encounters.length; encounter_index += 1) {
            enemy_encounter = this.game_state.level_data.enemy_encounters[encounter_index];
            if (spawn_chance <= enemy_encounter.probability) {
                // save current player position for later
                this.game_state.player_position = this.game_state.prefabs.player.position;
                // call battle state
                this.game_state.game.state.start("BootState", false, false, "assets/levels/battle.json", "BattleState", {enemy_data: enemy_encounter.enemy_data, party_data: this.game_state.party_data});
                break;
            }
        }
    }
};

The “check_for_spawn” method generates a random number using Phaser random data generator and compares it with the enemy encounters probabilities, choosing the first one whose probability is higher than the generated number. Notice that, for this to work the encounters must be sorted in ascending order of probability, prioritizing less likely encounters. When an encounter occurs, it calls BattleState with the correct enemy data and player party data.

Updating BattleState

Finally, we must update our BattleState to work with our last modifications. Instead of reading the enemy and player units from a JSON file, they will be passed as parameters in the “init” method. Then, we just have to iterate through them in the “create” method and create all their prefabs, using the same “create_prefab” method. Notice that the enemy units were stored in the enemy encounters data from the WorldState, while the player units were stored in the “party_data” variable from WorldState (shown before).

Now, we must properly go back to WorldState when the battle is finished. In the “next_turn” method, before making the next unit act, we check if there are remaining enemy and player units. If there are no remaining enemy units, we call an “end_battle” method and, if there are no remaining player units we call a “game_over” method.

The “end_battle” method will switch back to WorldState updating the “party_data” to reflect this battle. So, we must iterate through all player units saving their stats in the “party_data” variable. On the other hand, the “game_over” method will switch back to WorldState without sending any “party_data”, which will reset it. Also, we must tell the WorldState to restart the player position, instead of keeping the last one as showed in the WorldState code. The code below shows the modifications in the BattleState.

var RPG = RPG || {};

RPG.BattleState = function () {
    "use strict";
    Phaser.State.call(this);
    
    this.prefab_classes = {
        "background": RPG.TilePrefab.prototype.constructor,
        "rectangle": RPG.Prefab.prototype.constructor,
        "player_unit": RPG.PlayerUnit.prototype.constructor,
        "enemy_unit": RPG.EnemyUnit.prototype.constructor
    };
    
    this.TEXT_STYLE = {font: "14px Arial", fill: "#FFFFFF"};
};

RPG.BattleState.prototype = Object.create(Phaser.State.prototype);
RPG.BattleState.prototype.constructor = RPG.BattleState;

RPG.BattleState.prototype.init = function (level_data, extra_parameters) {
    "use strict";
    this.level_data = level_data;
    this.enemy_data = extra_parameters.enemy_data;
    this.party_data = extra_parameters.party_data;
    
    this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
    this.scale.pageAlignHorizontally = true;
    this.scale.pageAlignVertically = true;
};

RPG.BattleState.prototype.create = function () {
    "use strict";
    var group_name, prefab_name, player_unit_name, enemy_unit_name;
    
    // create groups
    this.groups = {};
    this.level_data.groups.forEach(function (group_name) {
        this.groups[group_name] = this.game.add.group();
    }, this);
    
    // create prefabs
    this.prefabs = {};
    for (prefab_name in this.level_data.prefabs) {
        if (this.level_data.prefabs.hasOwnProperty(prefab_name)) {
            // create prefab
            this.create_prefab(prefab_name, this.level_data.prefabs[prefab_name]);
        }
    }
    
    // create enemy units
    for (enemy_unit_name in this.enemy_data) {
        if (this.enemy_data.hasOwnProperty(enemy_unit_name)) {
            // create enemy units
            this.create_prefab(enemy_unit_name, this.enemy_data[enemy_unit_name]);
        }
    }
    
    // create player units
    for (player_unit_name in this.party_data) {
        if (this.party_data.hasOwnProperty(player_unit_name)) {
            // create player units
            this.create_prefab(player_unit_name, this.party_data[player_unit_name]);
        }
    }
    
    this.init_hud();
    
    this.units = [];
    this.units = this.units.concat(this.groups.player_units.children);
    this.units = this.units.concat(this.groups.enemy_units.children);
    
    this.next_turn();
};

RPG.BattleState.prototype.next_turn = function () {
    "use strict";
    // if all enemy units are dead, go back to the world state
    if (this.groups.enemy_units.countLiving() === 0) {
        this.end_battle();
    }
    
    // if all player units are dead, restart the game
    if (this.groups.player_units.countLiving() === 0) {
        this.game_over();
    }
    
    // takes the next unit
    this.current_unit = this.units.shift();
    // if the unit is alive, it acts, otherwise goes to the next turn
    if (this.current_unit.alive) {
        this.current_unit.act();
        this.units.push(this.current_unit);
    } else {
        this.next_turn();
    }
};

RPG.BattleState.prototype.game_over = function () {
    "use strict";
    // go back to WorldState restarting the player position
    this.game.state.start("BootState", true, false, "assets/levels/level1.json", "WorldState", {restart_position: true});
};

RPG.BattleState.prototype.end_battle = function () {
    "use strict";
    // save current party health
    this.groups.player_units.forEach(function (player_unit) {
        this.party_data[player_unit.name].properties.stats = player_unit.stats;
    }, this);
    // go back to WorldState with the current party data
    this.game.state.start("BootState", true, false, "assets/levels/level1.json", "WorldState", {party_data: this.party_data});
};

By now, you can already try playing with the EnemySpawners.

enemy_spawner

Considering units speed for the turns

In this tutorial, each unit will have a speed stat, which will be used to calculate the next turn that unit will act. The code below shows the modifications in the Unit prefab, and how it calculates the next act turn based on the current turn and its speed. Notice that I used an arbitrary rule for calculating the next turn, and you can use the one you finds best.

var RPG = RPG || {};

RPG.Unit = function (game_state, name, position, properties) {
    "use strict";
    RPG.Prefab.call(this, game_state, name, position, properties);
    
    this.anchor.setTo(0.5);
    
    this.stats = Object.create(properties.stats);
    
    this.attacked_animation = this.game_state.game.add.tween(this);
    this.attacked_animation.to({tint: 0xFF0000}, 200);
    this.attacked_animation.onComplete.add(this.restore_tint, this);
    
    this.act_turn = 0;
};

RPG.Unit.prototype = Object.create(RPG.Prefab.prototype);
RPG.Unit.prototype.constructor = RPG.Unit;

RPG.Unit.prototype.calculate_act_turn = function (current_turn) {
    "use strict";
    // calculate the act turn based on the unit speed
    this.act_turn = current_turn + Math.ceil(100 / this.stats.speed);
}

Now, we will change the BattleState to store the units in a priority queue, instead of an array. A priority queue is a data structure where all elements are always sorted, given a sorting criteria (if you’re not familiar with priority queues, check this wikipedia link). In our case, the sorting criteria will be the unit next acting turn, which means the first unit from the queue is the one that will act earlier. Since the priority queue is a well known data structure, we are going to use the implementation provided by Adam Hooper instead of creating our own.

The code below shows the modifications in the BattleState to use the priority queue. First, in the end of the “create” method we initialize “units” as a priority queue which compares the units act turn, and add all units to the queue, calculating their first acting turns. Then, in the “next_turn” method, we must update the current unit act turn before adding it to the “units” queue again, so it will be put in the correct position.

RPG.BattleState.prototype.create = function () {
    "use strict";
    var group_name, prefab_name, player_unit_name, enemy_unit_name;
    
    // create groups
    this.groups = {};
    this.level_data.groups.forEach(function (group_name) {
        this.groups[group_name] = this.game.add.group();
    }, this);
    
    // create prefabs
    this.prefabs = {};
    for (prefab_name in this.level_data.prefabs) {
        if (this.level_data.prefabs.hasOwnProperty(prefab_name)) {
            // create prefab
            this.create_prefab(prefab_name, this.level_data.prefabs[prefab_name]);
        }
    }
    
    // create enemy units
    for (enemy_unit_name in this.enemy_data) {
        if (this.enemy_data.hasOwnProperty(enemy_unit_name)) {
            // create enemy units
            this.create_prefab(enemy_unit_name, this.enemy_data[enemy_unit_name]);
        }
    }
    
    // create player units
    for (player_unit_name in this.party_data) {
        if (this.party_data.hasOwnProperty(player_unit_name)) {
            // create player units
            this.create_prefab(player_unit_name, this.party_data[player_unit_name]);
        }
    }
    
    this.init_hud();
    
    // store units in a priority queue which compares the units act turn
    this.units = new PriorityQueue({comparator: function (unit_a, unit_b) {
        return unit_a.act_turn - unit_b.act_turn;
    }});
    this.groups.player_units.forEach(function (unit) {
        unit.calculate_act_turn(0);
        this.units.queue(unit);
    }, this);
    this.groups.enemy_units.forEach(function (unit) {
        unit.calculate_act_turn(0);
        this.units.queue(unit);
    }, this);
    
    this.next_turn();
};

RPG.BattleState.prototype.next_turn = function () {
    "use strict";
    // if all enemy units are dead, go back to the world state
    if (this.groups.enemy_units.countLiving() === 0) {
        this.end_battle();
    }
    
    // if all player units are dead, restart the game
    if (this.groups.player_units.countLiving() === 0) {
        this.game_over();
    }
    
    // takes the next unit
    this.current_unit = this.units.dequeue();
    // if the unit is alive, it acts, otherwise goes to the next turn
    if (this.current_unit.alive) {
        this.current_unit.act();
        this.current_unit.calculate_act_turn(this.current_unit.act_turn);
        this.units.queue(this.current_unit);
    } else {
        this.next_turn();
    }
};

Now, you can already try setting some speed values and see if everything is working correctly. Below is the final enemy encounters and party data I used.

"enemy_encounters": [
        {"probability": 0.3,
         "enemy_data": {
            "lizard1": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 100},
                "properties": {
                    "texture": "lizard_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 30,
                        "defense": 10,
                        "health": 50,
                        "speed": 15
                    }
                }
            }
         }
        },
        {"probability": 0.5,
         "enemy_data": {
            "bat1": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 90},
                "properties": {
                    "texture": "bat_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 10,
                        "defense": 1,
                        "health": 30,
                        "speed": 20
                    }
                }
            },
            "bat2": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 170},
                "properties": {
                    "texture": "bat_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 10,
                        "defense": 1,
                        "health": 30,
                        "speed": 20
                    }
                }
            }
         }
        },
        {"probability": 1.0,
         "enemy_data": {
            "scorpion1": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 50},
                "properties": {
                    "texture": "scorpion_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 15,
                        "defense": 1,
                        "health": 20,
                        "speed": 10
                    }
                }
            },
            "scorpion2": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 100},
                "properties": {
                    "texture": "scorpion_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 15,
                        "defense": 1,
                        "health": 20,
                        "speed": 10
                    }
                }
            },
            "scorpion3": {
                "type": "enemy_unit",
                "position": {"x": 100, "y": 150},
                "properties": {
                    "texture": "scorpion_spritesheet",
                    "group": "enemy_units",
                    "stats": {
                        "attack": 15,
                        "defense": 1,
                        "health": 20,
                        "speed": 10
                    }
                }
            }
         }
        }
    ]

this.party_data = extra_parameters.party_data || {
        "fighter": {
            "type": "player_unit",
            "position": {"x": 250, "y": 50},
            "properties": {
                "texture": "male_fighter_spritesheet",
                "group": "player_units",
                "frame": 10,
                "stats": {
                    "attack": 15,
                    "defense": 5,
                    "health": 100,
                    "speed": 15
                }
            }
        },
        "mage": {
            "type": "player_unit",
            "position": {"x": 250, "y": 100},
            "properties": {
                "texture": "female_mage_spritesheet",
                "group": "player_units",
                "frame": 10,
                "stats": {
                    "attack": 20,
                    "defense": 2,
                    "health": 100,
                    "speed": 10
                }
            }
        },
        "ranger": {
            "type": "player_unit",
            "position": {"x": 250, "y": 150},
            "properties": {
                "texture": "female_ranger_spritesheet",
                "group": "player_units",
                "frame": 10,
                "stats": {
                    "attack": 10,
                    "defense": 3,
                    "health": 100,
                    "speed": 20
                }
            }
        }
    };

And now we finished this tutorial. In the next one we are going to add different actions during the battle, such as using magic and items. In addition, the player units will receive experience after each battle, being able to pass levels.