Source: hg/GuildGame.js

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

/**
 * A single instance of a game in a guild.
 *
 * @memberof HungryGames
 * @inner
 */
class GuildGame {
  /**
   * @description Create a game instance for a single guild.
   * @param {string} bot User id of the current bot instance.
   * @param {string} id Guild id of the Guild that this object is for.
   * @param {object<number|boolean|string|object>} options The game options.
   * @param {string} [name] Name of this game to be passed to the Game object.
   * @param {string[]|HungryGames~Player[]} [includedUsers] Array of user IDs
   * that will be included in the next game, or array of Players to include.
   * @param {string[]} [excludedUsers] Array of user IDs that have been
   * excluded from the games.
   * @param {HungryGames~NPC[]} [includedNPCs] Array of NPC objects to include
   * in the game.
   * @param {HungryGames~NPC[]} [excludedNPCs] Array of NPC objects to exclude
   * from the game.
   * @param {{
   *   bloodbath: string[],
   *   player: string[],
   *   weapon: string[],
   *   arena: string[],
   *   battle: {
   *     starts: string[],
   *     attacks: string[],
   *     outcomes: string[]
   *   }
   * }} customEventIds Array of IDs of custom events to load.
   * @param {string[]} disabledEventIds Array of IDs of events to be disabled
   * from game.
   */
  constructor(
      bot, id, options, name, includedUsers, excludedUsers, includedNPCs,
      excludedNPCs, customEventIds, disabledEventIds) {
    /**
     * The ID of the current bot account.
     *
     * @public
     * @type {string}
     * @constant
     */
    this.bot = bot;
    /**
     * The ID of the Guild this is for.
     *
     * @public
     * @type {string}
     * @constant
     */
    this.id = id;
    /**
     * Array of user IDs that will be included in the next game.
     *
     * @public
     * @type {string[]}
     * @default []
     */
    this.includedUsers = [];
    if (Array.isArray(includedUsers)) {
      for (let i = 0; i < includedUsers.length; i++) {
        if (typeof includedUsers[i] === 'string') {
          this.includedUsers.push(includedUsers.splice(i, 1)[0]);
          i--;
        } else {
          this.includedUsers.push(includedUsers[i].id);
        }
      }
    }
    /**
     * Array of user IDs that will be excluded from the next game.
     *
     * @public
     * @type {string[]}
     * @default []
     */
    this.excludedUsers = excludedUsers || [];
    /**
     * Array of NPCs that will be included in the game.
     *
     * @public
     * @type {HungryGames~NPC[]}
     * @default []
     */
    this.includedNPCs = includedNPCs || [];
    /**
     * Array of NPCs that will be excluded from the game.
     *
     * @public
     * @type {HungryGames~NPC[]}
     * @default []
     */
    this.excludedNPCs = excludedNPCs || [];
    /**
     * Game options.
     *
     * @public
     * @type {object}
     */
    this.options = options;
    /**
     * Is this game autoplaying?
     *
     * @public
     * @type {boolean}
     * @default false
     */
    this.autoPlay = false;
    /**
     * Does this game currently have any long running operations being
     * performed.
     *
     * @public
     * @type {boolean}
     * @default false
     */
    this.loading = false;
    /**
     * Is this game automatically stepping, or are steps controlled manually.
     *
     * @private
     * @type {boolean}
     * @default false
     */
    this._autoStep = false;
    /**
     * @description Storage manager for all custom events.
     * @todo Currently each guild will cache all events on its own, this will be
     * find for now, but would be more efficient if a global cache was used
     * instead.
     * @public
     * @type {HungryGames~EventContainer}
     * @constant
     */
    this.customEventStore = new HungryGames.EventContainer(customEventIds);

    /**
     * Current game information.
     *
     * @public
     * @type {HungryGames~Game}
     * @default
     */
    this.currentGame = new HungryGames.Game(name, includedUsers);
    /**
     * @description List of IDs of events to disable per-category.
     * @public
     * @type {{
     *   bloodbath: string[],
     *   player: string[],
     *   arena: string[],
     *   weapon: string[],
     *   battle: {
     *     starts: string[],
     *     attacks: string[],
     *     outcomes: string[]
     *   }
     * }}
     */
    this.disabledEventIds = disabledEventIds || {
      bloodbath: [],
      player: [],
      arena: [],
      weapon: [],
      battle: {
        starts: [],
        attacks: [],
        outcomes: [],
      },
    };

    /**
     * The channel id a command was last sent from that affected this guild
     * game.
     *
     * @public
     * @type {?string}
     * @default
     */
    this.channel = null;
    /**
     * The id of the user that last sent a command which interacted with this
     * guild game.
     *
     * @public
     * @type {?string}
     * @default
     */
    this.author = null;

    /**
     * The channel id where the game messages are currently being sent in.
     *
     * @public
     * @type {?string}
     * @default
     */
    this.outputChannel = null;

    /**
     * Message ID of the message to fetch reactions from for join via react.
     *
     * @public
     * @type {?{id: string, channel: string}}
     * @default
     */
    this.reactMessage = null;

    /**
     * The ID of the currently active {@link HungryGames~StatGroup} tracking
     * stats.
     *
     * @public
     * @type {?string}
     * @default
     */
    this.statGroup = null;

    /**
     * The actions to perform when certain events occur.
     *
     * @public
     * @type {HungryGames~ActionStore}
     * @default
     */
    this.actions = new HungryGames.ActionStore();

    /**
     * Interval for day events.
     *
     * @private
     * @type {?Timeout}
     * @default
     */
    this._dayEventInterval = null;
    /**
     * The timeout to continue autoplaying after the day ends. Used for
     * cancelling if user ends the game between days.
     *
     * @private
     * @type {?Timeout}
     * @default
     */
    this._autoPlayTimeout = null;

    /**
     * Function to call when state is modified.
     *
     * @private
     * @type {?HungryGames~GuildGame~StateUpdateCB}
     * @default
     */
    this._stateUpdateCallback = null;
    /**
     * Manages all stats for all players.
     *
     * @private
     * @type {HungryGames~StatManager}
     * @constant
     */
    this._stats = new HungryGames.StatManager(this);

    this.step = this.step.bind(this);
    this.modifyPlayerWeapon = this.modifyPlayerWeapon.bind(this);
  }

  /**
   * @description Get a serializable version of this class instance. Strips all
   * private variables, and all functions. Assumes all public variables are
   * serializable if they aren't a function.
   * @public
   * @returns {object} Serializable version of this instance.
   */
  get serializable() {
    const all = Object.entries(Object.getOwnPropertyDescriptors(this));
    const output = {};
    for (const one of all) {
      if (typeof one[1].value === 'function' || one[0].startsWith('_')) {
        continue;
      } else if (one[1].value && one[1].value.serializable) {
        output[one[0]] = one[1].value.serializable;
      } else {
        output[one[0]] = one[1].value;
      }
    }
    return output;
  }

  /**
   * @description Callback to fire when game state is about to be modified.
   * @callback HungryGames~GuildGame~StateUpdateCB
   * @param {boolean} dayComplete True if this update is after a day has ended,
   * false if the state is still during a day.
   * @param {boolean} doSim True if the next day should be simulated and
   * started.
   */

  /**
   * @description Add users to teams, and remove excluded users from teams.
   * Deletes empty teams, and adds teams once all teams have teamSize of
   * players.
   *
   * @public
   * @returns {?string} Null if success, string if error.
   */
  formTeams() {
    if (this.options.teamSize < 0) this.options.teamSize = 0;
    if (this.options.teamSize == 0) {
      this.currentGame.teams = [];
      return;
    }

    let corruptTeam = false;

    const teamSize = this.options.teamSize;
    const numTeams =
        Math.ceil(this.currentGame.includedUsers.length / teamSize);
    // If teams already exist, update them. Otherwise, create new teams.
    if (this.currentGame.teams && this.currentGame.teams.length > 0) {
      this.currentGame.teams.forEach((obj) => {
        obj.players.forEach((p) => {
          if (typeof p !== 'string' && typeof p !== 'number') {
            corruptTeam = true;
            console.error(
                '(PreTeamForm) Player in team is invalid: ' + typeof p +
                ' in team ' + obj.id + ' guild: ' + this.id + ' players: ' +
                JSON.stringify(obj.players));
          }
        });
      });

      this.currentGame.teams.sort((a, b) => a.id - b.id);

      const notIncluded = this.currentGame.includedUsers.slice(0);
      // Remove players from teams if they are no longer included in game.
      for (let i = 0; i < this.currentGame.teams.length; i++) {
        const team = this.currentGame.teams[i];
        team.id = i;
        for (let j = 0; j < team.players.length; j++) {
          if (!this.currentGame.includedUsers.find(
              (obj) => obj.id === team.players[j])) {
            team.players.splice(j, 1);
            j--;
          } else {
            notIncluded.splice(
                notIncluded.findIndex((obj) => obj.id === team.players[j]), 1);
          }
        }
        if (team.players.length == 0) {
          this.currentGame.teams.splice(i, 1);
          i--;
        }
      }
      // Add players who are not on a team, to a team.
      for (let i = 0; i < notIncluded.length; i++) {
        let found = false;
        for (let j = 0; j < this.currentGame.teams.length; j++) {
          const team = this.currentGame.teams[j];
          if (team.players.length < teamSize) {
            team.players.push(notIncluded[i].id);
            found = true;
            break;
          }
        }
        if (found) continue;
        // Add a team if all existing teams are full.
        this.currentGame.teams[this.currentGame.teams.length] =
            new HungryGames.Team(
                this.currentGame.teams.length,
                'Team ' + (this.currentGame.teams.length + 1),
                [notIncluded[i].id]);
      }
    } else {
      // Create all teams for players.
      this.currentGame.teams = [];
      for (let i = 0; i < numTeams; i++) {
        this.currentGame.teams[i] = new HungryGames.Team(
            i, `Team ${i + 1}`,
            this.currentGame.includedUsers
                .slice(i * teamSize, i * teamSize + teamSize)
                .map((obj) => obj.id));
      }
    }
    // Reset team data.
    this.currentGame.teams.forEach((obj) => {
      obj.numAlive = obj.players.length;
      obj.rank = 1;
      obj.players.forEach((p) => {
        if (typeof p !== 'string' && typeof p !== 'number') {
          corruptTeam = true;
          console.error(
              '(PostTeamForm) Player in team is invalid: ' + typeof p +
              ' in team ' + obj.id + ' guild: ' + this.id + ' players: ' +
              JSON.stringify(obj.players));
        }
      });
    });

    if (corruptTeam) {
      return 'Teams appeared to be corrupted, teams may have been ' +
          'rearranged.\nIf you have more information, please report this bug.';
    }
    return null;
  }


  /**
   * @description Force this current game to end immediately.
   * @public
   */
  end() {
    this.currentGame.inProgress = false;
    this.currentGame.isPaused = true;
    this.currentGame.ended = true;
    this.autoPlay = false;
    this.clearIntervals();
    if (this.currentGame.day.state === 1) this.currentGame.day.state = 0;
  }
  /**
   * @description Clear all timeouts and intervals.
   * @public
   */
  clearIntervals() {
    if (this._dayEventInterval) {
      clearInterval(this._dayEventInterval);
      this._dayEventInterval = null;
    }
    if (this._autoPlayTimeout) {
      clearInterval(this._autoPlayTimeout);
      this._autoPlayTimeout = null;
    }
    this._autoStep = false;
    this.currentGame.isPaused = true;
  }
  /**
   * @description Create an interval for this guild. Calls the callback every
   * time the game state is about to be modified. State is updated immediately
   * after the callback completes. This also sets `_autoStep` to true.
   * @public
   * @param {HungryGames~GuildGame~StateUpdateCB} [cb] Callback to fire on the
   * interval. Optional only if set via {@link setStateUpdateCallback} prior to
   * call to this function.
   */
  createInterval(cb) {
    if (cb && typeof cb !== 'function') {
      throw new Error('Callback must be a function');
    } else if (
      typeof cb !== 'function' &&
        typeof this._stateUpdateCallback !== 'function') {
      throw new Error('Callback must be a function');
    }
    if (this._dayEventInterval) {
      throw new Error(
          'Attempted to register second listener for existing interval.');
    }
    this.currentGame.isPaused = false;
    this._autoStep = true;
    if (cb) this._stateUpdateCallback = cb;
    const delay = this.options.disableOutput ? 1 : this.options.delayEvents;
    this.step();
    this._dayEventInterval = setInterval(this.step, delay);
  }

  /**
   * @description Set the state update callback function.
   * @public
   * @param {HungryGames~GuildGame~StateUpdateCB} cb Callback to fire when
   * stepped.
   */
  setStateUpdateCallback(cb) {
    if (typeof cb !== 'function') {
      throw new Error('Callback must be a function');
    }
    this._stateUpdateCallback = cb;
  }

  /**
   * @description Progress to the next game state. Calls `_stateUpdateCallback`
   * prior to any action, if it's set.
   * @public
   */
  step() {
    const day = this.currentGame.day;
    const index = day.state - 2;
    const dayOver = index >= day.events.length;
    if (typeof this._stateUpdateCallback === 'function') {
      this._stateUpdateCallback(dayOver, index < 0 && !this.currentGame.ended);
    }
    if (this._autoPlayTimeout) {
      clearTimeout(this._autoPlayTimeout);
      this._autoPlayTimeout = null;
    }
    if (dayOver) {
      if (this._dayEventInterval) {
        clearInterval(this._dayEventInterval);
        this._dayEventInterval = null;
      }
      if (this.autoPlay && this._autoStep && !this.currentGame.isPaused) {
        const delay = this.options.disableOutput ? 1 : this.options.delayDays;
        this._autoPlayTimeout = setTimeout(this.step, delay);
      }
      day.state = 0;
      this._stats.parseDay();
    } else if (index < 0) {
      return;
    } else if (
      day.events[index].battle &&
        day.events[index].state < day.events[index].attacks.length) {
      day.events[index].state++;
    } else {
      day.state++;
    }
  }

  /**
   * @description Create a GuildGame from data parsed from file. Similar to copy
   * constructor.
   *
   * @public
   * @static
   * @param {object} data GuildGame like object.
   * @param {Discord~Client} client Discord client reference for
   * creating {@link HungryGames~ActionStore}.
   * @returns {HungryGames~GuildGame} Created GuildGame.
   */
  static from(data, client) {
    const game = new GuildGame(
        data.bot, data.id, data.options, data.name, data.includedUsers,
        data.excludedUsers, data.includedNPCs, data.excludedNPCs);
    game.autoPlay = data.autoPlay || false;
    game.reactMessage = data.reactMessage || null;

    // Legacy custom event storage.
    if (data.customEvents) {
      if (!game.legacyEvents) game.legacyEvents = {};
      game.legacyEvents.bloodbath = data.customEvents.bloodbath || [];
      game.legacyEvents.player = data.customEvents.player || [];
      game.legacyEvents.arena = data.customEvents.arena || [];
      game.legacyEvents.weapon = data.customEvents.weapon || {};
      if (data.customEvents.battle) {
        if (!game.legacyEvents.battle) game.legacyEvents.battle = {};
        game.legacyEvents.battle.starts = data.customEvents.battle.starts || [];
        game.legacyEvents.battle.attacks =
            data.customEvents.battle.attacks || [];
        game.legacyEvents.battle.outcomes =
            data.customEvents.battle.outcomes || [];
      }
    }
    if (data.legacyEvents) {
      game.legacyEvents = data.legacyEvents;
    }
    // End legacy custom events.

    if (data.customEventStore) {
      game.customEventStore.updateAndFetchAll(data.customEventStore);
    }
    if (data.disabledEventIds) {
      game.disabledEventIds.bloodbath = data.disabledEventIds.bloodbath || [];
      game.disabledEventIds.player = data.disabledEventIds.player || [];
      game.disabledEventIds.arena = data.disabledEventIds.arena || [];
      game.disabledEventIds.weapon = data.disabledEventIds.weapon || [];
      if (data.disabledEventIds.battle) {
        game.disabledEventIds.battle.starts =
            data.disabledEventIds.battle.starts || [];
        game.disabledEventIds.battle.attacks =
            data.disabledEventIds.battle.attacks || [];
        game.disabledEventIds.battle.outcomes =
            data.disabledEventIds.battle.outcomes || [];
      }
    }

    game.channel = data.channel || null;
    game.author = data.author || null;
    game.outputChannel = data.outputChannel || null;
    game.statGroup = data.statGroup || null;
    if (data.currentGame) {
      game.currentGame = HungryGames.Game.from(data.currentGame);
    }
    if (data.actions) {
      game.actions =
          HungryGames.ActionStore.from(client, game.id, data.actions);
    }
    return game;
  }
  /**
   * @description Force a player to have a certain outcome in the current day
   * being simulated, or the next day that will be simulated. This is acheived
   * by adding a custom event in which the player will be affected after their
   * normal event for the day.
   *
   * @public
   * @static
   * @param {HungryGames~GuildGame} game The game context.
   * @param {string[]} list The array of player IDs of which to affect.
   * @param {string} state The outcome to force the players to have been
   * victims of by the end of the simulated day. ("living", "dead", "wounded",
   * or "thriving").
   * @param {HungryGames~Messages} messages Reference to current Messages
   * instance.
   * @param {string|HungryGames~NormalEvent[]} text Message to show when the
   * user is affected, or array of default events if not specifying a specific
   * message.
   * @param {?string} locale Language locale to use for string lookup.
   * @param {Function} cb Callback once complete. Single parameter is the
   * output message to tell the user of the outcome of the operation.
   */
  static forcePlayerState(game, list, state, messages, text, locale, cb) {
    if (!Array.isArray(list)) {
      messages = state;
      text = list.text;
      state = list.state;
      list = list.list;
    }
    if (!Array.isArray(list) || list.length == 0) {
      cb('effectPlayerNoPlayer');
      return;
    }
    if (typeof state !== 'string') {
      cb('effectPlayerNoOutcome');
      return;
    }
    const players = [];
    const outcomes = {};
    list.forEach((p) => {
      const player = game.currentGame.includedUsers.find((el) => el.id == p);
      if (!player) return;
      players.push(player.name);

      let living = player.living;
      let currentState = player.state;

      if (game.currentGame.day.state <= 1) {
        game.currentGame.nextDay.events.forEach((evt) => {
          const found = evt.icons.find((icon) => icon.id === player.id);
          if (!found) return;
          const group = found.settings.victim ? evt.victim : evt.attacker;
          switch (group.outcome) {
            case 'dies':
              living = false;
              currentState = 'dead';
              break;
            case 'revived':
              living = true;
              currentState = 'zombie';
              break;
            case 'thrives':
              living = true;
              currentState = 'living';
              break;
            case 'wounded':
              living = true;
              currentState = 'wounded';
              break;
          }
        });
      }
      let outcome;
      if (living && state === 'dead') {
        outcome = 'dies';
      } else if (
        !living && (state === 'living' || state === 'thriving')) {
        outcome = 'revived';
      } else if (currentState === 'wounded' && state === 'thriving') {
        outcome = 'thrives';
      } else if (living && currentState !== 'wounded' && state === 'wounded') {
        outcome = 'wounded';
      } else {
        return;
      }
      if (!outcomes[outcome]) outcomes[outcome] = [];
      outcomes[outcome].push(player);
    });

    /**
     * @description Find and apply an event to players.
     * @private
     * @param {HungryGames~Player[]} affected Array of players to affect.
     * @param {string} outcome The outcome to apply.
     */
    const findEvent = function(affected, outcome) {
      let evt;
      if (typeof text !== 'string' && Array.isArray(text) &&
          game.options.anonForceOutcome) {
        let eventPool = text.concat(game.customEventStore.getArray('player'));
        eventPool = eventPool.filter((el) => {
          const checkVictim = el.victim.outcome === outcome &&
              (el.victim.count === affected.length ||
               (el.victim.count < 0 &&
                el.victim.count * -1 <= affected.length));
          const checkAttacker = el.attacker.outcome === outcome &&
              (el.attacker.count === affected.length ||
               (el.attacker.count < 0 &&
                el.attacker.count * -1 <= affected.length));
          const checkCount = el.attacker.count === 0 || el.victim.count === 0;
          return (checkVictim || checkAttacker) && checkCount &&
              !game.disabledEventIds.player.includes(el.id);
        });
        if (eventPool.length > 0) {
          const pick = eventPool[Math.floor(eventPool.length * Math.random())];
          text = pick.message;
          const vC = pick.victim.count == 0 ? 0 : affected.length;
          const aC = pick.attacker.count == 0 ? 0 : affected.length;
          evt = HungryGames.NormalEvent.finalize(
              text, affected, vC, aC, outcome, outcome, game,
              pick.victim.killer, pick.attacker.killer, pick.victim.weapon,
              pick.attacker.weapon);
        }
      }
      if (typeof text !== 'string') {
        switch (state) {
          case 'dead':
            text = messages.get('forcedDeath', locale);
            break;
          case 'thriving':
            text = messages.get('forcedHeal', locale);
            break;
          case 'wounded':
            text = messages.get('forcedWound', locale);
            break;
        }
      }
      if (!evt) {
        evt = HungryGames.NormalEvent.finalize(
            text, affected, affected.length, 0, outcome, 'nothing', game);
      }
      if (game.currentGame.day.state > 1) {
        for (const player of affected) {
          if (!HungryGames.Simulator._applyOutcome(
              game, player, 0, null, outcome)) {
            break;
          }
        }
        game.currentGame.day.events.push(evt);
      } else {
        game.currentGame.nextDay.events.push(evt);
      }
    };

    game.customEventStore.waitForReady(() => {
      for (const outcome in outcomes) {
        if (!outcomes[outcome] || outcomes[outcome].length == 0) continue;
        const affected = outcomes[outcome];
        if (affected.length < 7) {
          findEvent(affected, outcome);
        } else {
          do {
            findEvent(affected.splice(0, 7), outcome);
          } while (affected.length > 0);
        }
      }
      if (players.length == 0) {
        cb('effectPlayerNoPlayerFound');
      } else if (players.length < 5) {
        const names = players.map((el) => `\`${el}\``).join(', ');
        cb(messages.get('forceStateSuccessFew', locale, names, state));
      } else {
        cb(messages.get(
            'forceStateSuccessMany', locale, players.length, state));
      }
    });
  }

  /**
   * @description Give or take a weapon from a player.
   * @public
   * @param {string} player The ID of the player to modify.
   * @param {string} weapon The weapon ID to give/take.
   * @param {?string|HungryGames} [text=null] The message text to show, or
   * reference to object storing default events. If no value is given, a random
   * message is chosen from `./save/hgMessages.json`.
   * @param {number} [count=1] The amount to give to the player. Negative to
   * take away.
   * @param {boolean} [set=false] Set the amount to `count` instead of
   * incrementing.
   * @param {Function} [cb] Callback once complete. First parameter is string
   *     key, following are optional values to fill template.
   */
  modifyPlayerWeapon(player, weapon, text = null, count = 1, set = false, cb) {
    if (typeof cb !== 'function') cb = function() {};
    const game = this;
    if (!game.currentGame || !game.currentGame.includedUsers) {
      cb('noGameInProgress');
      return;
    }
    player = game.currentGame.includedUsers.find((el) => el.id == player);
    if (!player) {
      cb('unableToFindPlayer');
      return;
    }

    let current = player.weapons[weapon] || 0;
    if (game.currentGame.day.state <= 1) {
      for (const evt of game.currentGame.nextDay.events) {
        if (evt.consumer === player.id) {
          for (const w of evt.consumes) {
            if (w.id !== weapon) continue;
            current -= w.count;
          }
        }
        const icon = evt.icons.find((icon) => icon.id === player.id);
        if (icon) {
          const list =
              icon.settings.victim ? evt.victim.weapons : evt.attacker.weapons;
          for (const w of list) {
            if (w.id !== weapon) continue;
            current += w.count * 1;
          }
        }
      }
    }

    const diff = (set ? count - current : count) || 0;
    if (!diff) {
      cb('modifyPlayerCountNonZero');
      return;
    }
    count = Math.max(0, current + diff);

    const defaultEvents =
        text.getDefaultEvents && text.getDefaultEvents().get('weapon');
    game.customEventStore.waitForReady(() => {
      const customWeapons = game.customEventStore.get('weapon');
      const weapons = {};
      if (player.weapons) {
        for (const w in player.weapons) {
          if (!player.weapons[w]) continue;
          const existing = customWeapons[w] || defaultEvents[w];
          if (!existing) {
            console.error('Unable to find weapon:', w, 'in guild', game.id);
            continue;
          }
          weapons[w] = new HungryGames.WeaponEvent(
              [], existing.consumable, existing.name);
        }
      }
      if (!weapons[weapon]) {
        const existing = customWeapons[weapon] || defaultEvents[weapon];
        weapons[weapon] = new HungryGames.WeaponEvent(
            [], existing && existing.consumable,
            existing && existing.name || weapon);
      }

      const name = weapons[weapon].name;

      let evt;
      if (text && typeof text === 'object' && game.options.anonForceOutcome) {
        const defaultWeapon = defaultEvents[weapon];
        const custom = customWeapons[weapon];

        weapons.action = HungryGames.WeaponEvent.action;
        weapons[weapon] = new HungryGames.WeaponEvent(
            [], (custom && custom.consumable) ||
                (defaultWeapon && defaultWeapon.consumable),
            (custom && custom.name) || (defaultWeapon && defaultWeapon.name));

        let eventPool;
        if (diff < 0) {
          if (defaultWeapon && custom) {
            eventPool = defaultWeapon.outcomes.concat(custom.outcomes);
          } else if (defaultWeapon) {
            eventPool = defaultWeapon.outcomes;
          } else if (custom) {
            eventPool = custom.outcomes;
          } else {
            cb('modifyPlayerUnableToFindWeapon');
            return;
          }
          weapons[weapon].outcomes = eventPool.slice(0);
          const disabled = game.disabledEventIds.weapon;
          eventPool = eventPool.filter((el) => {
            return Math.abs(el.victim.count) + Math.abs(el.attacker.count) ===
                1 &&
                !disabled.includes(el.id);
          });
        } else {
          eventPool = text.getDefaultEvents().getArray('player').concat(
              game.customEventStore.getArray('player'));
          const disabled = game.disabledEventIds.player;
          eventPool = eventPool.filter((el) => {
            const aW = el.attacker.weapon;
            const aCheck = aW && aW.name === weapon && aW.count > 0;
            const vW = el.victim.weapon;
            const vCheck = vW && vW.name === weapon && vW.count > 0;
            return (aCheck || vCheck) &&
                (Math.abs(el.attacker.count) + Math.abs(el.victim.count) ===
                 1) &&
                !disabled.includes(el.id);
          });
          weapons[weapon].outcomes = eventPool.slice(0);
        }
        if (eventPool.length > 0) {
          const pick = HungryGames.NormalEvent.from(
              eventPool[Math.floor(eventPool.length * Math.random())]);

          text = pick.message = pick.message.replace(/\{owner\}/g, 'their');
          evt = pick.finalize(game, [player]);
        }
      }
      if (text && typeof text === 'object') {
        if (diff < 0) {
          text = text.messages.get('takeWeapon');
        } else {
          text = text.messages.get('giveWeapon');
        }
      }
      if (!evt) {
        const name = weapons[weapon].name;
        text = text.replace(
            /\{weapon\}/g, Math.abs(diff) === 1 ? `their ${name}` : `${name}s`);
        text = text.replace(
            /\[W([^|]*)\|([^\]]*)\]/g, (Math.abs(diff) == 1 ? '$1' : '$2'));
        evt = HungryGames.NormalEvent.finalize(
            text, [player], 0, 1, 'nothing', 'nothing', game);
      }

      const nameFormat = game.options.useNicknames ? 'nickname' : 'username';
      if (diff < 0) {
        const ownerName =
            HungryGames.Grammar.formatMultiNames([player], nameFormat);
        const firstAttacker = true;
        evt.consumer = player.id;
        evt.consumes = [{id: weapon, count: -diff}];
        evt.subMessage = HungryGames.Simulator.formatWeaponEvent(
            evt, player, ownerName, firstAttacker, weapon, weapons, count);
      } else {
        const aW = evt.attacker.weapons.find((w) => w.id === weapon);
        if (!aW) {
          evt.attacker.weapons.push({id: weapon, count: diff});
        } else {
          aW.count = diff;
        }
        const vW = evt.victim.weapons.find((w) => w.id === weapon);
        if (!vW) {
          evt.victim.weapons.push({id: weapon, count: diff});
        } else {
          vW.count = diff;
        }
      }

      if (game.currentGame.day.state > 1) {
        if (count <= 0) {
          count = 0;
          delete player.weapons[weapon];
        } else {
          player.weapons[weapon] = count;
        }
        evt.subMessage += HungryGames.Simulator.formatWeaponCounts(
            evt, [player], weapons, nameFormat);
        // State - 2 = the event index, + 1 is the next index to get shown.
        /* let lastIndex = game.currentGame.day.state - 1;
        for (let i = game.currentGame.day.events.length - 1; i > lastIndex; i--)
        {
          if (game.currentGame.day.events[i].icons.find(
              (el) => el.id == player.id)) {
            lastIndex = i + 1;
            break;
          }
        }
        if (lastIndex < game.currentGame.day.events.length) {
          game.currentGame.day.events.splice(lastIndex, 0, evt);
        } else { */
        game.currentGame.day.events.push(evt);
        // }
        cb('modifyPlayerNowHas', player.name, count, name);
        return;
      } else {
        game.currentGame.nextDay.events.push(evt);
        cb('modifyPlayerWillHave', player.name, count, name);
        return;
      }
    });
  }
}

module.exports = GuildGame;