Source: src/hg/Battle.js

// Copyright 2019 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (dev@campbellcrowley.com)
const HungryGames = require('./HungryGames.js');

/**
 * @description A single battle in an Event.
 * @memberof HungryGames
 * @inner
 * @augments HungryGames~Event
 */
class Battle extends HungryGames.Event {
  /**
   * @description Create a single battle.
   * @param {string} message The message of this battle event.
   * @param {number} attacker The damage done to the attacker.
   * @param {number} victim The damage done to the victim.
   */
  constructor(message, attacker, victim) {
    super(message);
    /**
     * Information about attacker.
     *
     * @public
     * @type {{damage: number}}
     */
    this.attacker = {damage: attacker};
    /**
     * Information about victim.
     *
     * @public
     * @type {{damage: number}}
     */
    this.victim = {damage: victim};
  }
  /**
   * The file path to read attacking left image.
   *
   * @private
   * @static
   * @readonly
   * @returns {string} Path relative to project root.
   */
  static get _fistLeft() {
    return './img/fist_left.png';
  }
  /**
   * The file path to read attacking right image.
   *
   * @private
   * @static
   * @readonly
   * @returns {string} Path relative to project root.
   */
  static get _fistRight() {
    return './img/fist_right.png';
  }
  /**
   * The file path to read attacking both directions image.
   *
   * @private
   * @static
   * @readonly
   * @returns {string} Path relative to project root.
   */
  static get _fistBoth() {
    return './img/fist_both.png';
  }

  /**
   * Make an event that contains a battle between players before the main event
   * message.
   *
   * @public
   * @static
   * @param {HungryGames~Player[]} affectedUsers All of the players involved in
   * the event.
   * @param {number} numVictim The number of victims in this event.
   * @param {number} numAttacker The number of attackers in this event.
   * @param {boolean} mention Should every player be mentioned when their name
   * comes up?
   * @param {HungryGames~GuildGame} game The GuildGame this battle is for. This
   * is for settings checking and fetching non-affected users.
   * @param {HungryGames~Battle[]} battles Array of all possible battle events
   * to choose from.
   * @returns {HungryGames~NormalEvent} The event that was created.
   */
  static finalize(
      affectedUsers, numVictim, numAttacker, mention, game, battles) {
    const useNicknames = game.options.useNicknames;
    const outcomeMessage =
        battles.outcomes[Math.floor(Math.random() * battles.outcomes.length)];
    const finalEvent = HungryGames.NormalEvent.finalize(
        outcomeMessage, affectedUsers, numVictim, numAttacker, 'dies',
        'nothing', game);
    finalEvent.attacker.killer = true;
    finalEvent.battle = true;
    finalEvent.state = 0;
    finalEvent.attacks = [];

    const userHealth = new Array(affectedUsers.length).fill(0);
    const maxHealth = game.options.battleHealth * 1;
    let numAlive = numVictim;
    let duplicateCount = 0;
    let lastAttack = {index: 0, attacker: 0, victim: 0, flipRoles: false};

    const startMessage =
        battles.starts[Math.floor(Math.random() * battles.starts.length)];
    const battleString = '**A battle has broken out!**';
    let healthText =
        affectedUsers
            .map(
                (obj, index) => '`' +
                    (useNicknames ? (obj.nickname || obj.name) : obj.name) +
                    '`: ' + Math.max((maxHealth - userHealth[index]), 0) + 'HP')
            .sort()
            .join(', ');
    finalEvent.attacks.push(
        HungryGames.NormalEvent.finalize(
            `${battleString}\n${startMessage}\n${healthText}`, affectedUsers,
            numVictim, numAttacker, 'nothing', 'nothing', game));

    let loop = 0;
    do {
      loop++;
      if (loop > 1000) {
        throw new Error('INFINITE LOOP');
      }
      const eventIndex = Math.floor(Math.random() * battles.attacks.length);
      const eventTry = battles.attacks[eventIndex];
      const attackerEventDamage = eventTry.attacker.damage * 1;
      const victimEventDamage = eventTry.victim.damage * 1;

      const flipRoles = Math.random() > 0.7;
      const attackerIndex = Math.floor(Math.random() * numAttacker) + numVictim;

      if (loop == 999) {
        console.log(
            'Failed to find valid event for battle!\n', eventTry, flipRoles,
            userHealth, '\nAttacker:', attackerIndex, '\nUsers:',
            affectedUsers.length, '\nAlive:', numAlive, '\nFINAL:', finalEvent);
      }

      if ((!flipRoles &&
           userHealth[attackerIndex] + attackerEventDamage >= maxHealth) ||
          (flipRoles &&
           userHealth[attackerIndex] + victimEventDamage >= maxHealth)) {
        continue;
      }

      let victimIndex = Math.floor(Math.random() * numAlive);

      let count = 0;
      for (let i = 0; i < numVictim; i++) {
        if (userHealth[i] < maxHealth) count++;
        if (count == victimIndex + 1) {
          victimIndex = i;
          break;
        }
      }

      const victimDamage =
          (flipRoles ? attackerEventDamage : victimEventDamage);
      const attackerDamage =
          (!flipRoles ? attackerEventDamage : victimEventDamage);

      userHealth[victimIndex] += victimDamage;
      userHealth[attackerIndex] += attackerDamage;

      if (userHealth[victimIndex] >= maxHealth) {
        numAlive--;
      }

      if (lastAttack.index == eventIndex &&
          lastAttack.attacker == attackerIndex &&
          lastAttack.victim == victimIndex &&
          lastAttack.flipRoles == flipRoles) {
        duplicateCount++;
      } else {
        duplicateCount = 0;
      }
      lastAttack = {
        index: eventIndex,
        attacker: attackerIndex,
        victim: victimIndex,
        flipRoles: flipRoles,
      };

      healthText =
          affectedUsers
              .map((obj, index) => {
                const health = Math.max((maxHealth - userHealth[index]), 0);
                const prePost = health === 0 ? '~~' : '';
                return prePost + '`' +
                    (useNicknames ? (obj.nickname || obj.name) : obj.name) +
                    '`: ' + health + 'HP' + prePost;
              })
              .sort()
              .join(', ');
      let messageText = eventTry.message;
      if (duplicateCount > 0) {
        messageText += ' x' + (duplicateCount + 1);
      }

      const newEvent = HungryGames.NormalEvent.finalize(
          battleString + '\n' + messageText + '\n' + healthText,
          [
            affectedUsers[flipRoles ? attackerIndex : victimIndex],
            affectedUsers[flipRoles ? victimIndex : attackerIndex],
          ],
          1, 1, !flipRoles && userHealth[victimIndex] >= maxHealth ? 'dies' :
                                                                     'nothing',
          flipRoles && userHealth[victimIndex] >= maxHealth ? 'dies' :
                                                              'nothing',
          game);

      if (victimDamage && attackerDamage) {
        newEvent.icons.splice(1, 0, {url: Battle._fistBoth});
      } else if (attackerDamage) {
        newEvent.icons.splice(
            1, 0, {url: flipRoles ? Battle._fistLeft : Battle._fistRight});
      } else if (victimDamage) {
        newEvent.icons.splice(
            1, 0, {url: flipRoles ? Battle._fistRight : Battle._fistLeft});
      }

      finalEvent.attacks.push(newEvent);
    } while (numAlive > 0);
    return finalEvent;
  }
}

module.exports = Battle;