Source

battle.js

/* global rpgcode, gui */
let battle = new Battle();

/**
 * The builtin turn-based battle system.
 * 
 * @class
 * @constructor
 * 
 * @returns {Battle}
 */
function Battle() {
   this.context = {
      MENU: "MENU",
      ITEM_SELECTION: "ITEM_SELECTION",
      ENEMY_SELECTION: "ENEMY_SELECTION",
      ENEMY_TURN: "ENEMY_TURN"
   };
}

/**
 *  Shows the default turn-based battle system based the supplied config.
 *  
 *  Currently battles ranging from 1v1 to 4v4 are supported, the same enemy or
 *  character can be used multiple times.
 * 
 * @example
 * let config = {
 *  enemies: ["evil-eye.enemy", "evil-eye.enemy", "evil-eye.enemy" "evil-eye.enemy"],
 *  characters: ["Hero.character", "Hero.character", "Hero.character", "Hero.character"],
 *  backgroundImage: "battle-background.png",
 *  battleMusic: "Battle.ogg",
 *  itemSoundEffect: "item.ogg"
 * }
 * battle.show(config, function(result) {
 *  rpgcode.log("The battle has ended! result.status=" + result.status);
 *  rpgcode.endProgram();   
 * });
 * 
 * @param {Object} config
 * @param {Callback} callback
 * @returns {undefined}
 */
Battle.prototype.show = function(config, callback) {
   if (!config.enemies || !config.characters || config.enemies.length < 1 || config.characters.length < 1) {
      rpgcode.endProgram();
   } else {
      this.config = config;
      this.callback = callback;
      config.enemies = config.enemies.slice(0, 4);
      config.characters = config.characters.slice(0, 4);
      this._loadAssets(config, function(config) {
         this._setup(config);
         this._createStage(config);
         this._placeEnemies(this._state.enemies);
         this._placeCharacters(this._state.characters);
         rpgcode.stopSound(rpgcode.getBoard().backgroundMusic);
         rpgcode.playSound("battle.stage", true, 1.0);
      }.bind(this));
   }
};

Battle.prototype._end = function(status) {
   clearInterval(this._state.drawInterval);
   clearInterval(this._state.flashInterval);
   this._state.characters.forEach(function(character) {
      rpgcode.destroySprite(character.id);
   });
   this._state.enemies.forEach(function(enemy) {
      rpgcode.destroySprite(enemy.id);
   });
   this._clearCanvases();
   rpgcode.getBoard().removeLayer(-1);
   rpgcode.stopSound("battle.stage");
   rpgcode.playSound(rpgcode.getBoard().backgroundMusic, true, 1.0);
   if (this.callback) {
      this.callback({
         status: status
      });
   } else {
      rpgcode.endProgram();
   }
};

Battle.prototype._setup = function(config) {
   this._scale = rpgcode.getScale();
   this.stage = {
      canvasId: "battle.stageCanvas",
      width: 640,
      height: 340,
      x: 0,
      y: 0,
      cursor: {
         canvasId: "battle.cursorCanvas",
         width: 25,
         height: 25,
         x: 0,
         y: 0
      }
   };
   this.window = {
      width: 640,
      height: 140,
      x: 100,
      y: 100,
      radius: 15,
      linePadding: 10,
      borderWidth: 5,
      area: {
         width: 490,
         height: 140,
         x: 0,
         y: 0,
         padding: {
            x: 15,
            y: 35
         },
         bar: {
            height: 8
         },
         selectedIndex: 1
      },
      menu: {
         width: 150,
         height: 140,
         x: 490,
         y: 0,
         padding: {
            x: 15,
            y: 35
         },
         selectedIndex: 1
      }
   };

   this.window.x = Math.floor((rpgcode.getViewport().width) / 2) - Math.floor(this.window.width / 2);
   this.window.y = Math.floor(rpgcode.getViewport().height - this.window.height);
   rpgcode.createCanvas(this.stage.width, this.stage.height, this.stage.canvasId);
   rpgcode.createCanvas(this.stage.cursor.width, this.stage.cursor.height, this.stage.cursor.canvasId);
   rpgcode.setCanvasPosition(0, 0, this.stage.canvasId);
   rpgcode.setCanvasPosition(0, 0, this.stage.cursor.canvasId);

   this.areaFrame = gui.createFrame({
      id: "Battle.areaFrameCanvas",
      width: 490,
      height: 140,
      x: 0,
      y: 340
   });
   this.areaFrame.setVisible(true);

   this.menuFrame = gui.createFrame({
      id: "Battle.menuFrameCanvas",
      width: 150,
      height: 140,
      x: rpgcode.getViewport().width - 150,
      y: 340
   });
   this.menuFrame.setVisible(true);

   this._state = {
      stageLayer: rpgcode.getBoard().layers.length,
      turnsTaken: 0,
      playerTurn: true,
      processingInput: false,
      currentContext: this.context.MENU,
      selectedCharacterIndex: 1,
      selectedEnemyIndex: 1,
      selectedItemIndex: 1,
      flashMenuSelection: true,
      drawInterval: setInterval(this._draw.bind(this), 50),
      flashInterval: setInterval(function() {
         this._state.flashMenuSelection = !this._state.flashMenuSelection;
      }.bind(this), 500),
      enemies: config.enemies,
      characters: config.characters,
      items: [],
      messages: []
   };
   rpgcode.registerKeyDown("E", this._handleInput.bind(this), false);
   rpgcode.registerKeyDown("UP_ARROW", this._handleInput.bind(this), false);
   rpgcode.registerKeyDown("DOWN_ARROW", this._handleInput.bind(this), false);
   rpgcode.registerKeyDown("W", this._handleInput.bind(this), false);
   rpgcode.registerKeyDown("S", this._handleInput.bind(this), false);
};

Battle.prototype._loadAssets = function(config, callback) {
   let assets = {
      "images": [config.backgroundImage],
      "audio": {
         "battle.stage": config.battleMusic,
         "battle.item": config.itemSoundEffect
      }
   };
   rpgcode.loadAssets(assets, function() {
      this._loadEnemies(config, callback);
   }.bind(this));
};

Battle.prototype._loadEnemies = function(config, callback) {
   let enemies = [];
   let loaded = function(enemy, index) {
      enemies.push(enemy);
      if (config.enemies.length < 1) {
         config.enemies = enemies;
         this._loadCharacters(config, callback);
      } else {
         this._loadSprite(config.enemies.pop(), "enemy", index, loaded);
      }
   }.bind(this);
   this._loadSprite(config.enemies.pop(), "enemy", 1, loaded);
};

Battle.prototype._loadCharacters = function(config, callback) {
   let characters = [];
   let loaded = function(character, index) {
      characters.push(character);
      if (config.characters.length < 1) {
         config.characters = characters;
         callback(config);
      } else {
         this._loadSprite(config.characters.pop(), "character", index, loaded);
      }
   }.bind(this);
   this._loadSprite(config.characters.pop(), "character", 1, loaded);
};

Battle.prototype._loadSprite = function(file, type, index, callback) {
   let sprite = {
      "name": file,
      "id": type + "-" + index,
      "thread": "",
      "startingPosition": {
         "x": -100,
         "y": 100,
         "layer": rpgcode.getBoard().layers.length
      },
      "events": []
   };
   rpgcode.addSprite(sprite, function() {
      callback({
         "id": sprite.id,
         "data": rpgcode.getSprite(sprite.id).sprite[type]
      }, ++index);
   }.bind(this));
};

Battle.prototype._createStage = function(config) {
   let viewport = rpgcode.getViewport();
   let layer = {
      "tiles": [],
      "vectors": [],
      "images": [{
         "src": config.backgroundImage,
         "x": -Crafty.viewport._x - rpgwizard.craftyBoard.xShift,
         "y": -Crafty.viewport._y - rpgwizard.craftyBoard.yShift,
         "id": "battle.background"
      }],
      "name": "battle.stage"
   };
   rpgcode.getBoard().addLayer(layer);
};

Battle.prototype._getMenuItems = function() {
   return [{
         text: "Attack",
         execute: this._startEnemySelection.bind(this)
      },
      {
         text: "Item",
         execute: this._startItemSelection.bind(this)
      },
      {
         text: "Flee",
         execute: this._flee.bind(this)
      }
   ];
};

Battle.prototype._getStatItems = function() {
   let statItems = [];
   let entities = this._state.currentContext === this.context.ENEMY_SELECTION ? this._state.enemies : this._state.characters;
   entities.forEach(function(entity) {
      statItems.push(this._getStatItem(entity.data));
   }.bind(this));
   return statItems;
};

Battle.prototype._getStatItem = function(entity) {
   let name = entity.name.padEnd(12);
   let hp = "HP " + entity.health.toString().padStart(4);
   let mp = "MP " + entity.magic.toString().padStart(4);
   let dp = "DP " + entity.defence.toString().padStart(4);
   const item = {
      text: name + hp + "  " + mp + "  " + dp,
      bars: [{
            position: name.length,
            value: entity.health,
            maxValue: entity.maxHealth,
            maxWidth: "1234567"
         },
         {
            position: name.length + hp.length + 2,
            value: entity.magic,
            maxValue: entity.maxMagic,
            maxWidth: "1234567"
         },
         {
            position: name.length + hp.length + 2 + mp.length + 2,
            value: entity.defence,
            maxValue: entity.maxDefence,
            maxWidth: "1234567"
         }
      ]
   };
   return item;
};

Battle.prototype._getInventoryItems = function() {
   let inventoryItems = [];
   let inventory = rpgcode.getCharacter().inventory;
   Object.keys(inventory).forEach(function(key) {
      if (inventory[key].length > 0) {
         if (inventory[key][0].type.toLowerCase() === "battle") {
            inventoryItems.push(this._getInventoryItem(inventory[key][0], inventory[key].length, key));
         }
      }
   }.bind(this));
   this._state.items = inventoryItems;
   return inventoryItems;
};

Battle.prototype._getInventoryItem = function(inventoryItem, count, key) {
   return {
      text: (inventoryItem.name.padEnd(15) + " " + count.toString().padStart(2)).substring(0, 18),
      fileName: key,
      effects: inventoryItem.effects
   };
};

Battle.prototype._getCurrentSelection = function() {
   let selection = {
      activeAnimation: null,
      location: null
   };
   if (this._state.currentContext === this.context.ENEMY_SELECTION) {
      let selectedEnemy = this._state.enemies[this._state.selectedEnemyIndex - 1];
      if (!selectedEnemy) {
         return null;
      }
      let sprite = rpgcode.getSprite(selectedEnemy.id);
      if (!sprite) {
         return null;
      }
      selection.activeAnimation = sprite.sprite.enemy.spriteGraphics.active;
      selection.location = rpgcode.getSpriteLocation(selectedEnemy.id, false, true);
   } else {
      let selectedCharacter = this._state.characters[this._state.selectedCharacterIndex - 1];
      if (!selectedCharacter) {
         return null;
      }
      let sprite = rpgcode.getSprite(selectedCharacter.id);
      if (!sprite) {
         return null;
      }
      selection.activeAnimation = sprite.sprite.character.spriteGraphics.active;
      selection.location = rpgcode.getSpriteLocation(selectedCharacter.id, false, true);
   }
   return selection;
};

//
// Additional Setup Functions
//

Battle.prototype._placeEnemies = function(enemies) {
   let cells = {};
   let grid = {
      width: this.stage.width * 0.50,
      height: this.stage.height,
      center: {
         x: this.stage.width * 0.25,
         y: this.stage.height * 0.50
      }
   };
   if (enemies.length === 1) {
      cells = [{
         x: 0,
         y: 0,
         center: {
            x: grid.width * 0.5,
            y: grid.height * 0.5
         }
      }];
   } else {
      cells = [{
            x: 0,
            y: 0,
            center: {
               x: grid.width * 0.25,
               y: grid.height * 0.30
            }
         },
         {
            x: grid.width * 0.50,
            y: 0,
            center: {
               x: grid.width * 0.75,
               y: grid.height * 0.30
            }
         },
         {
            x: grid.width * 0,
            y: grid.height * 0.50,
            center: {
               x: grid.width * 0.25,
               y: grid.height * 0.70
            }
         },
         {
            x: grid.width * 0.50,
            y: grid.height * 0.50,
            center: {
               x: grid.width * 0.75,
               y: grid.height * 0.70
            }
         }
      ];
   }
   let xShift = -Crafty.viewport._x - rpgwizard.craftyBoard.xShift;
   let yShift = -Crafty.viewport._y - rpgwizard.craftyBoard.yShift
;
   for (let i = 0; i < enemies.length; i++) {
      rpgcode.setSpriteLocation(enemies[i].id, cells[i].center.x + xShift, cells[i].center.y + yShift, this._state.stageLayer, false);
      rpgcode.setSpriteStance(enemies[i].id, "EAST");
   }
};

Battle.prototype._placeCharacters = function(characters) {
   let cells = {};
   let grid = {
      width: this.stage.width,
      height: this.stage.height,
      center: {
         x: this.stage.width * 0.25,
         y: this.stage.height * 0.50
      }
   };
   if (characters.length === 1) {
      cells = [{
         x: 0,
         y: 0,
         center: {
            x: grid.width * 0.75,
            y: grid.height * 0.5
         }
      }];
   } else {
      cells = [{
            x: grid.width * 0.50,
            y: 0,
            center: {
               x: grid.width * 0.84,
               y: grid.height * 0.20
            }
         },
         {
            x: grid.width * 0.75,
            y: 0,
            center: {
               x: grid.width * 0.86,
               y: grid.height * 0.40
            }
         },
         {
            x: grid.width * 0.50,
            y: grid.height * 0.50,
            center: {
               x: grid.width * 0.88,
               y: grid.height * 0.60
            }
         },
         {
            x: grid.width * 0.75,
            y: grid.height * 0.50,
            center: {
               x: grid.width * 0.90,
               y: grid.height * 0.80
            }
         }
      ];
   }
   let xShift = -Crafty.viewport._x - rpgwizard.craftyBoard.xShift;
   let yShift = -Crafty.viewport._y - rpgwizard.craftyBoard.yShift
;
   for (let i = 0; i < characters.length; i++) {
      rpgcode.setSpriteLocation(characters[i].id, cells[i].center.x + xShift, cells[i].center.y + yShift, this._state.stageLayer, false);
      rpgcode.setSpriteStance(characters[i].id, "WEST");
   }
   rpgcode.moveSprite(this._state.characters[this._state.selectedCharacterIndex - 1].id, "WEST", 50);
};

//
// Battle Functions
//

Battle.prototype._checkEndConditions = function() {
   return this._state.characters.length < 1 || this._state.enemies.length < 1;
};

Battle.prototype._endTurn = function() {
   if (this._checkEndConditions()) {
      this._end(this._state.characters.length < 1 ? "LOSE" : "WIN");
   } else {
      if (this._state.playerTurn) {
         this._endCharacterTurn();
      } else {
         this._endEnemyTurn();
      }
      this._state.turnsTaken++;
      let totalTurns = this._state.playerTurn ? this._state.characters.length : this._state.enemies.length;
      if (this._state.turnsTaken >= totalTurns) {
         this._state.playerTurn = !this._state.playerTurn;
         this._state.turnsTaken = 0;
         if (this._state.playerTurn) {
            this._state.selectedCharacterIndex = this._state.selectedEnemyIndex = this.window.area.selectedIndex = 1;
            this._state.currentContext = this.context.MENU;
            this._startNextCharacterTurn();
         } else {
            this._state.selectedCharacterIndex = this._state.selectedEnemyIndex = this.window.area.selectedIndex = 1;
            this._state.currentContext = this.context.ENEMY_TURN;
            this._startNextEnemyTurn();
         }
      } else {
         if (this._state.playerTurn) {
            this._startNextCharacterTurn();
         } else {
            this._startNextEnemyTurn();
         }
      }
   }
};

Battle.prototype._startNextCharacterTurn = function() {
   rpgcode.moveSprite(this._state.characters[this._state.selectedCharacterIndex - 1].id, "WEST", 50);
};

Battle.prototype._endCharacterTurn = function() {
   rpgcode.moveSprite(this._state.characters[this._state.selectedCharacterIndex - 1].id, "EAST", 50);
   if (this._state.currentContext === this.context.ENEMY_SELECTION) {
      this._endEnemySelection();
   } else {
      this._endItemSelection(true);
   }
};

Battle.prototype._startNextEnemyTurn = function() {
   let enemy = this._state.enemies[this._state.selectedEnemyIndex - 1];
   this._state.selectedCharacterIndex = Math.round(Math.random() * ((this._state.characters.length) - 1) + 1);
   rpgcode.delay(1000, this._attackCharacter.bind(this));
};

Battle.prototype._endEnemyTurn = function() {
   this._state.selectedEnemyIndex++;
};

Battle.prototype._attackCharacter = function() {
   let enemy = this._state.enemies[this._state.selectedEnemyIndex - 1];
   let character = this._state.characters[this._state.selectedCharacterIndex - 1];
   let attackPower = this._determineAttackPower(enemy.data.attack);
   let location = rpgcode.getSpriteLocation(character.id, false, true);
   this._showToastMessage({
      text: attackPower,
      x: location.x,
      y: location.y
   });
   let playing = 2;
   let callback = function() {
      playing--;
      if (playing < 1) {
         character.data.health -= attackPower;
         if (character.data.health < 1) {
            this._removeCharacter(character, this._state.selectedCharacterIndex - 1);
         }
         this._endTurn();
      }
   };
   rpgcode.animateSprite(enemy.id, "ATTACK", callback.bind(this));
   rpgcode.animateSprite(character.id, "DEFEND", callback.bind(this));
};

Battle.prototype._removeCharacter = function(character, arrIndex) {
   this._state.characters.splice(arrIndex, 1);
   this._state.selectedCharacterIndex = this.window.area.selectedIndex = 1;
   rpgcode.animateSprite(character.id, "DIE", function() {
      rpgcode.destroySprite(character.id);
   }.bind(this));
};

Battle.prototype._attackEnemy = function() {
   let enemy = this._state.enemies[this._state.selectedEnemyIndex - 1];
   let character = this._state.characters[this._state.selectedCharacterIndex - 1];
   let attackPower = this._determineAttackPower(character.data.attack);
   let location = rpgcode.getSpriteLocation(enemy.id, false, true);
   this._showToastMessage({
      text: attackPower,
      x: location.x,
      y: location.y
   });
   let playing = 2;
   let callback = function() {
      playing--;
      if (playing < 1) {
         enemy.data.health -= attackPower;
         if (enemy.data.health < 1) {
            this._removeEnemy(enemy, this._state.selectedEnemyIndex - 1);
         }
         this._endTurn();
      }
   };
   rpgcode.animateSprite(enemy.id, "DEFEND", callback.bind(this));
   rpgcode.animateSprite(character.id, "ATTACK", callback.bind(this));
};

Battle.prototype._removeEnemy = function(enemy, arrIndex) {
   this._state.enemies.splice(arrIndex, 1);
   this._state.selectedEnemyIndex = this.window.area.selectedIndex = 1;
   rpgcode.animateSprite(enemy.id, "DIE", function() {
      rpgcode.destroySprite(enemy.id);
   }.bind(this));
};

Battle.prototype._flee = function() {
   this._end("FLEE");
};

//
// Item Functions
//

Battle.prototype._useItem = function() {
   let selectedItem = this._state.items[this._state.selectedItemIndex - 1];
   if (selectedItem) {
      let character = this._state.characters[this._state.selectedCharacterIndex - 1].data;
      character.health += selectedItem.effects.health;
      if (character.health > character.maxHealth) {
         character.health = character.maxHealth;
      }
      character.attack += selectedItem.effects.attack;
      if (character.attack > character.maxAttack) {
         character.attack = character.maxAttack;
      }
      character.defence += selectedItem.effects.defence;
      if (character.defence > character.maxDefence) {
         character.defence = character.maxDefence;
      }
      character.magic += selectedItem.effects.magic;
      if (character.magic > character.maxMagic) {
         character.magic = character.maxMagic;
      }
      rpgcode.takeItem(selectedItem.fileName, "");
      rpgcode.playSound("battle.item", false, 1.0);
      this._endTurn();
   } else {
      this._endItemSelection(false);
   }
};

//
// AI Functions
//

Battle.prototype._determineAttackPower = function(attack) {
   return Math.round(Math.random() * ((attack * 1.1) - (attack * 0.6)) + (attack * 0.6));
};

//
// Input Functions
//

Battle.prototype._handleInput = function(e) {
   if (!this._state.playerTurn || this._state.processingInput) {
      return;
   }
   this._state.processingInput = true;
   switch (e.key) {
      case 69:
         this._handleAction();
         break;
      case 38: // UP_ARROW
      case 87: // W
         this._handleUpArrow();
         break;

      case 40: // DOWN_ARROW
      case 83: // S
         this._handleDownArrow();
         break;
      default:
         return;
   }
};

Battle.prototype._handleAction = function() {
   if (this._state.currentContext === this.context.MENU) {
      switch (this.window.menu.selectedIndex) {
         case 1:
         case 2:
         case 3:
         case 4:
            this._getMenuItems()[this.window.menu.selectedIndex - 1].execute();
            return;
         default:
            return;
      }
   } else if (this._state.currentContext === this.context.ENEMY_SELECTION) {
      this._attackEnemy();
   } else if (this._state.currentContext === this.context.ITEM_SELECTION) {
      this._useItem();
   }
};

Battle.prototype._handleUpArrow = function() {
   if (this._state.currentContext === this.context.MENU) {
      this.window.menu.selectedIndex = this.window.menu.selectedIndex > 1 ? this.window.menu.selectedIndex - 1 : this.window.menu.selectedIndex;
   } else if (this._state.currentContext === this.context.ENEMY_SELECTION) {
      this.window.area.selectedIndex = this._state.selectedEnemyIndex = this._state.selectedEnemyIndex > 1 ? this._state.selectedEnemyIndex - 1 : this._state.selectedEnemyIndex;
   } else if (this._state.currentContext === this.context.ITEM_SELECTION) {
      this.window.area.selectedIndex = this._state.selectedItemIndex = this._state.selectedItemIndex > 1 ? this._state.selectedItemIndex - 1 : this._state.selectedItemIndex;
   }
   this._state.processingInput = false;
};

Battle.prototype._handleDownArrow = function() {
   if (this._state.currentContext === this.context.MENU) {
      this.window.menu.selectedIndex = this.window.menu.selectedIndex < this._getMenuItems().length ? this.window.menu.selectedIndex + 1 : this.window.menu.selectedIndex;
   } else if (this._state.currentContext === this.context.ENEMY_SELECTION) {
      this.window.area.selectedIndex = this._state.selectedEnemyIndex = this._state.selectedEnemyIndex < this._state.enemies.length ? this._state.selectedEnemyIndex + 1 : this._state.selectedEnemyIndex;
   } else if (this._state.currentContext === this.context.ITEM_SELECTION) {
      this.window.area.selectedIndex = this._state.selectedItemIndex = this._state.selectedItemIndex < this._state.items.length + 1 ? this._state.selectedItemIndex + 1 : this._state.selectedItemIndex;
   }
   this._state.processingInput = false;
};

Battle.prototype._startEnemySelection = function() {
   this._state.currentContext = this.context.ENEMY_SELECTION;
   this.window.area.selectedIndex = this._selectedEnemyIndex = 1;
   this._state.processingInput = false;
};

Battle.prototype._endEnemySelection = function() {
   this._state.currentContext = this.context.MENU;
   this.window.area.selectedIndex = ++this._state.selectedCharacterIndex;
   this._state.processingInput = false;
};

Battle.prototype._startItemSelection = function() {
   this._state.currentContext = this.context.ITEM_SELECTION;
   this.window.area.selectedIndex = this._state.selectedItemIndex = 1;
   this._state.processingInput = false;
};

Battle.prototype._endItemSelection = function(advance) {
   this._state.currentContext = this.context.MENU;
   this.window.area.selectedIndex = advance ? ++this._state.selectedCharacterIndex : this._state.selectedCharacterIndex;
   this._state.processingInput = false;
};


//
// Drawing Functions
//

Battle.prototype._clearCanvases = function() {
   rpgcode.clearCanvas(this.stage.canvasId);
   rpgcode.clearCanvas(this.stage.cursor.canvasId);
   rpgcode.clearCanvas(this.window.canvasId);
   rpgcode.clearCanvas(this.areaFrame.id);
   rpgcode.clearCanvas(this.menuFrame.id);
};

Battle.prototype._draw = function() {
   this._clearCanvases();
   this.areaFrame.draw();
   this.menuFrame.draw();
   this._drawMenu(this._getMenuItems());
   if (this._state.currentContext === this.context.ITEM_SELECTION) {
      this._drawInventory(this._getInventoryItems());
   } else {
      this._drawStats(this._getStatItems());
   }
   this._drawSelection();
   this._drawMessages();
   rpgcode.renderNow(this.areaFrame.id);
   rpgcode.renderNow(this.menuFrame.id);
   rpgcode.renderNow(this.stage.canvasId);
};

Battle.prototype._drawStats = function(statItems) {
   this._drawStatSelection();
   for (let i = 0; i < statItems.length; i++) {
      this._drawStatItem(statItems[i], i);
   }
   rpgcode.drawOntoCanvas(this.areaFrame.id, this.window.area.x, this.window.area.y, this.window.area.width, this.window.area.height, this.window.canvasId);
};

Battle.prototype._drawStatItem = function(item, index) {
   rpgcode.font = gui.getFont();
   let x = this.window.area.padding.x;
   let y = this.window.area.padding.y + ((gui.getFontSize() + this.window.linePadding) * index);
   item.bars.forEach(function(bar) {
      this._drawStatBar(bar, x, y);
   }.bind(this));
   gui.prepareTextColor();
   rpgcode.drawText(x, y, item.text, this.areaFrame.id);
};

Battle.prototype._drawStatBar = function(bar, x, y) {
   let charWidth = rpgcode.measureText("0").width;
   let maxWidth = rpgcode.measureText(bar.maxWidth).width;
   let width = maxWidth * (bar.value / bar.maxValue);
   let height = this.window.area.bar.height;
   let barX = x + (bar.position * charWidth);
   let barY = y - (height / 2);
   gui.prepareStatBarColor();
   rpgcode.fillRect(barX, barY, width, height, this.areaFrame.id);
};

Battle.prototype._drawStatSelection = function() {
   gui.prepareSelectionColor();
   let x = this.window.area.padding.x - (this.window.area.padding.x / 2);
   let y = this.window.area.padding.y + ((gui.getFontSize() + this.window.linePadding) * (this.window.area.selectedIndex - 1)) - gui.getFontSize() - (this.window.linePadding / 4);
   let width = this.window.area.width - this.window.area.padding.x;
   let height = gui.getFontSize() + (this.window.linePadding);
   rpgcode.fillRect(x, y, width, height, this.areaFrame.id);
};

Battle.prototype._drawInventory = function(inventoryItems) {
   this._drawInventorySelection();
   let i = 0;
   for (; i < inventoryItems.length; i++) {
      this._drawInventoryItem(inventoryItems[i], i);
   }
   this._drawInventoryItem({
      text: "<- Back"
   }, i);
   rpgcode.drawOntoCanvas(this.areaFrame.id, this.window.area.x, this.window.area.y, this.window.area.width, this.window.area.height, this.window.canvasId);
};

Battle.prototype._drawInventoryItem = function(item, index) {
   rpgcode.font = gui.getFont();
   let x = index < 4 ? this.window.area.padding.x : this.window.area.padding.x + (this.window.area.width / 2);
   let y = this.window.area.padding.y + ((gui.getFontSize() + this.window.linePadding) * (index < 4 ? index : index - 4));
   gui.prepareTextColor();
   rpgcode.drawText(x, y, item.text, this.areaFrame.id);
};

Battle.prototype._drawInventorySelection = function() {
   gui.prepareSelectionColor();
   let index = this.window.area.selectedIndex - 1;
   let x = index < 4 ? this.window.area.padding.x : this.window.area.padding.x + (this.window.area.width / 2);
   let y = this.window.area.padding.y + ((gui.getFontSize() + this.window.linePadding) * (index < 4 ? index : index - 4)) - gui.getFontSize() - (this.window.linePadding / 4);
   let width = (this.window.area.width / 2) - (this.window.area.padding.x * 2);
   let height = gui.getFontSize() + (this.window.linePadding);
   rpgcode.fillRect(x, y, width, height, this.areaFrame.id);
};

Battle.prototype._drawMenu = function(menuItems) {
   if (this._state.flashMenuSelection && this._state.currentContext !== this.context.ENEMY_TURN) {
      this._drawMenuSelection();
   }
   for (let i = 0; i < menuItems.length; i++) {
      this._drawMenuItem(menuItems[i], i);
   }
   rpgcode.drawOntoCanvas(this.menuFrame.id, this.window.menu.x, this.window.menu.y, this.window.menu.width, this.window.menu.height, this.window.canvasId);
};

Battle.prototype._drawMenuItem = function(item, index) {
   rpgcode.font = gui.getFont();
   gui.prepareTextColor();
   let x = this.window.menu.padding.x;
   let y = this.window.menu.padding.y + ((gui.getFontSize() + this.window.linePadding) * index);
   rpgcode.drawText(x, y, item.text, this.menuFrame.id);
};

Battle.prototype._drawMenuSelection = function() {
   gui.prepareSelectionColor();
   let x = this.window.menu.padding.x - (this.window.menu.padding.x / 2);
   let y = this.window.menu.padding.y + ((gui.getFontSize() + this.window.linePadding) * (this.window.menu.selectedIndex - 1)) - gui.getFontSize() - (this.window.linePadding / 4);
   let width = this.window.menu.width - this.window.menu.padding.x;
   let height = gui.getFontSize() + (this.window.linePadding);
   rpgcode.fillRect(x, y, width, height, this.menuFrame.id);
};

Battle.prototype._drawSelection = function() {
   let selection = this._getCurrentSelection();
   if (!selection) {
      return;
   }
   gui.prepareSelectionColor();
   rpgcode.fillRect(0, 0, this.stage.cursor.width, this.stage.cursor.height, this.stage.cursor.canvasId);
   this.stage.cursor.width = selection.activeAnimation.width;
   this.stage.cursor.height = selection.activeAnimation.height;
   this.stage.cursor.x = selection.location.x - (this.stage.cursor.width / 2);
   this.stage.cursor.y = selection.location.y - (this.stage.cursor.height / 2);
   rpgcode.drawOntoCanvas(this.stage.cursor.canvasId, this.stage.cursor.x, this.stage.cursor.y, this.stage.cursor.width, this.stage.cursor.height, this.stage.canvasId);
};

Battle.prototype._drawMessages = function() {
   gui.prepareTextColor();
   this._state.messages.forEach(function(message) {
      rpgcode.drawText(message.x, message.y, message.text, this.stage.canvasId);
   }.bind(this));
};

Battle.prototype._showToastMessage = function(message, location) {
   rpgcode.font = gui.getFont();
   message.x -= rpgcode.measureText(message.text).width / 2;
   this._state.messages.push(message);
   let callback = function(_this, message, startY) {
      message.y--;
      if (startY - message.y < 25) {
         rpgcode.delay(10, function() {
            callback(_this, message, startY);
         });
      } else {
         _this._state.messages.shift();
      }
   };
   rpgcode.delay(10, function() {
      callback(this, message, message.y);
   }.bind(this));
};