Source: hg/NormalEvent.js

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

/**
 * Event that can happen in a game.
 *
 * @memberof HungryGames
 * @inner
 * @augments HungryGames~Event
 */
class NormalEvent extends HungryGames.Event {
  /**
   * @description Creates a HungryGames Normal Event.
   *
   * @param {string} message The message to show.
   * @param {number} [numVictim=0] The number of victims in this event.
   * @param {number} [numAttacker=0] The number of attackers in this event.
   * @param {string} [victimOutcome=nothing] The outcome of the victims from
   * this event.
   * @param {string} [attackerOutcome=nothing] The outcome of the attackers from
   * this event.
   * @param {boolean} [victimKiller=false] Do the victims kill anyone in this
   * event. Used for calculating kill count.
   * @param {boolean} [attackerKiller=false] Do the attackers kill anyone in
   * this event. Used for calculating kill count.
   * @param {boolean} [battle] Is this event a battle?
   * @param {number} [state=0] State of event if there are multiple attacks
   * before the event.
   * @param {HungryGames~NormalEvent[]} [attacks=[]] Array of attacks that take
   * place before the event.
   */
  constructor(
      message, numVictim = 0, numAttacker = 0, victimOutcome = 'nothing',
      attackerOutcome = 'nothing', victimKiller = false, attackerKiller = false,
      battle = false, state = 0, attacks = []) {
    super(message);
    /**
     * The action to format into a message if this is a weapon event.
     *
     * @public
     * @type {?string}
     * @default
     */
    this.action = null;
    /**
     * Information about the victims in this event.
     *
     * @public
     * @type {object}
     * @property {number} count Number of victims. Negative means "at least" the
     * magnitude.
     * @property {string} outcome The outcome of the victims.
     * @property {boolean} killer Do the victims kill the attackers.
     * @property {?{id: string, count: number}} weapon The weapon information to
     * give to the player.
     */
    this.victim = {
      count: numVictim,
      outcome: victimOutcome,
      killer: victimKiller,
      weapon: null,
    };
    /**
     * Information about the attackers in this event.
     *
     * @public
     * @type {object}
     * @property {number} count Number of attackers. Negative means "at least"
     * the magnitude.
     * @property {string} outcome The outcome of the attackers.
     * @property {boolean} killer Do the attackers kill the victims.
     * @property {?{id: string, count: number}} weapon The weapon information to
     * give to the player.
     */
    this.attacker = {
      count: numAttacker,
      outcome: attackerOutcome,
      killer: attackerKiller,
      weapon: null,
    };
    /**
     * Is this event a battle event.
     *
     * @public
     * @type {boolean}
     * @default false
     */
    this.battle = battle;
    /**
     * The current state of printing the battle messages.
     *
     * @public
     * @type {number}
     * @default 0
     */
    this.state = state;
    /**
     * The attacks in a battle to show before the message.
     *
     * @public
     * @type {HungryGames~Event[]}
     * @default []
     */
    this.attacks = attacks;
    /**
     * Amount of consumables used if this is a weapon event.
     *
     * @public
     * @type {?number|string}
     * @default
     */
    this.consumes = null;
  }

  /**
   * @description Compare this Event to another to check if they are equivalent.
   * @public
   * @param {HungryGames~NormalEvent} two Other NormalEvent to compare against.
   * @returns {boolean} If they are equivalent.
   */
  equal(two) {
    return NormalEvent.equal(this, two);
  }
  /**
   * @description Finalize this instance.
   * @public
   * @param {HungryGames~GuildGame} game Game context.
   * @param {HungryGames~Player[]} affected An array of all players affected by
   * this event.
   * @returns {HungryGames~FinalEvent} The finalized event.
   */
  finalize(game, affected) {
    return new HungryGames.FinalEvent(this, game, affected);
  }

  /**
   * @description Format an event string based on specified users.
   * @public
   * @static
   * @param {string} message The message to show.
   * @param {HungryGames~Player[]} affectedUsers An array of all users affected
   * by this event.
   * @param {number} numVictim Number of victims in this event.
   * @param {number} numAttacker Number of attackers in this event.
   * @param {string} victimOutcome The outcome of the victims from this event.
   * @param {string} attackerOutcome The outcome of the attackers from this
   * event.
   * @param {HungryGames~GuildGame} game The GuildGame to make this event for.
   * Used for settings and fetching other players not affected by this event if
   * necessary.
   * @returns {HungryGames~FinalEvent} The final event that was created and
   * formatted ready for display.
   */
  static finalize(
      message, affectedUsers, numVictim, numAttacker, victimOutcome,
      attackerOutcome, game) {
    return new HungryGames.FinalEvent(
        new NormalEvent(
            message, numVictim, numAttacker, victimOutcome, attackerOutcome),
        game, affectedUsers);
  }

  /**
   * @description Compare two events to check if they are equivalent.
   * @public
   * @static
   * @param {HungryGames~NormalEvent} e1 First event.
   * @param {HungryGames~NormalEvent} e2 Second event to compare.
   * @returns {boolean} If the two given events are equivalent.
   */
  static equal(e1, e2) {
    if (!e1 || !e2) return false;
    if (e1.message != e2.message) return false;
    if (e1.action != e2.action) return false;
    if (e1.consumes != e2.consumes) return false;
    if (e1.type != e2.type) return false;
    if (!e1.battle != !e2.battle) return false;
    const v1 = e1.victim;
    const v2 = e2.victim;
    if (v1 && v2) {
      if (v1.count != v2.count) return false;
      if (v1.outcome != v2.outcome) return false;
      if (!v1.killer != !v2.killer) return false;
      if (v1.weapon && v2.weapon) {
        if (v1.weapon.id != v2.weapon.id) return false;
        if (v1.weapon.count != v2.weapon.count) return false;
      } else if (!(!v1.weapon && !v2.weapon)) {
        return false;
      }
    } else if (!(!v1 && !v2)) {
      return false;
    }
    const a1 = e1.attacker;
    const a2 = e2.attacker;
    if (a1 && a2) {
      if (a1.count != a2.count) return false;
      if (a1.outcome != a2.outcome) return false;
      if (!a1.killer != !a2.killer) return false;
      if (a1.weapon && a2.weapon) {
        if (a1.weapon.id != a2.weapon.id) return false;
        if (a1.weapon.count != a2.weapon.count) return false;
      } else if (!(!a1.weapon && !a2.weapon)) {
        return false;
      }
    } else if (!(!a1 && !a2)) {
      return false;
    }
    return true;
  }

  /**
   * @description Validate that the given data is properly typed and structured
   * to be converted to a NormalEvent. Also coerces values to correct types if
   * possible.
   * @public
   * @static
   * @param {HungryGames~NormalEvent} evt The event data to verify.
   * @returns {?string} Error string, or null if no error.
   */
  static validate(evt) {
    const err = HungryGames.Event.validate(evt);
    if (err) return err;

    if (evt.action && (typeof evt.action !== 'string' ||
                       evt.action.length === 0 || evt.action.length > 1000)) {
      return 'BAD_ACTION';
    }

    if (evt.battle != null && typeof evt.battle !== 'boolean' &&
        evt.battle !== 'true' && evt.battle !== 'false') {
      return 'BAD_BATTLE';
    } else if (evt.battle != null) {
      evt.battle = evt.battle === 'true';
    }

    if (evt.state != null && !Number.isSafeInteger(evt.state *= 1)) {
      return 'BAD_STATE';
    }

    if (evt.attacks && !Array.isArray(evt.attacks)) {
      return 'BAD_ATTACKS';
    } else if (evt.attacks) {
      let outerr;
      let index;
      evt.attacks.find((el, i) => {
        outerr = NormalEvent.validate(el);
        index = i;
        return outerr;
      });
      if (outerr) {
        return `BAD_ATTACK_${index}_${outerr}`;
      }
    }

    if (evt.consumes &&
        (typeof evt.consumes !== 'string' || evt.consumes.length > 100 ||
         evt.consumes.length === 0) &&
        !Number.isSafeInteger(evt.consumes * 1)) {
      return 'BAD_CONSUMES';
    }

    if (evt.victim) {
      switch (evt.victim.outcome) {
        case 'nothing':
        case 'dies':
        case 'wounded':
        case 'revived':
        case 'thrives':
          break;
        default:
          return 'BAD_VICTIM_OUTCOME';
      }
      if (!Number.isSafeInteger(evt.victim.count *= 1)) {
        return 'BAD_VICTIM_COUNT';
      }
      if (typeof evt.victim.killer !== 'boolean') {
        if (evt.victim.killer && evt.victim.killer !== 'true' &&
            evt.victim.killer !== 'false') {
          return 'BAD_VICTIM_KILLER';
        } else {
          evt.victim.killer = evt.victim.killer === 'true';
        }
      }
      if (evt.victim.weapon &&
          (typeof evt.victim.weapon.id !== 'string' ||
           !Number.isSafeInteger(evt.victim.weapon.count *= 1))) {
        return 'BAD_VICTIM_WEAPON';
      }
    }
    if (evt.attacker) {
      switch (evt.attacker.outcome) {
        case 'nothing':
        case 'dies':
        case 'wounded':
        case 'revived':
        case 'thrives':
          break;
        default:
          return 'BAD_ATTACKER_OUTCOME';
      }
      if (!Number.isSafeInteger(evt.attacker.count *= 1)) {
        return 'BAD_ATTACKER_COUNT';
      }
      if (typeof evt.attacker.killer !== 'boolean') {
        if (evt.attacker.killer && evt.attacker.killer !== 'true' &&
            evt.attacker.killer !== 'false') {
          return 'BAD_ATTACKER_KILLER';
        } else {
          evt.attacker.killer = evt.attacker.killer === 'true';
        }
      }
      if (evt.attacker.weapon &&
          (typeof evt.attacker.weapon.id !== 'string' ||
           !Number.isSafeInteger(evt.attacker.weapon.count *= 1))) {
        return 'BAD_ATTACKER_WEAPON';
      }
    }
    return null;
  }

  /**
   * @description Create a new NormalEvent object from a NormalEvent-like
   * object. Similar to copy-constructor.
   *
   * @public
   * @static
   * @param {object} obj Event-like object to copy.
   * @returns {HungryGames~NormalEvent} Copy of event.
   */
  static from(obj) {
    const out = new NormalEvent(obj.message);
    out.fill(obj);
    if (obj.victim) {
      out.victim.count = obj.victim.count || 0;
      out.victim.outcome = obj.victim.outcome || 'nothing';
      out.victim.killer = obj.victim.killer || false;
      if (obj.victim.weapon) {
        out.victim.weapon = {
          id: obj.victim.weapon.id,
          count: obj.victim.weapon.count,
        };
      }
    }
    if (obj.attacker) {
      out.attacker.count = obj.attacker.count || 0;
      out.attacker.outcome = obj.attacker.outcome || 'nothing';
      out.attacker.killer = obj.attacker.killer || false;
      if (obj.attacker.weapon) {
        out.attacker.weapon = {
          id: obj.attacker.weapon.id,
          count: obj.attacker.weapon.count,
        };
      }
    }
    if (Array.isArray(obj.attacks)) {
      out.attacks = obj.attacks.map((el) => NormalEvent.from(el));
    }
    if (typeof obj.action === 'string') out.action = obj.action;
    if (typeof obj.battle === 'boolean') out.battle = obj.battle;
    if (typeof obj.state === 'number') out.state = obj.state;
    if (obj.consumes) out.consumes = obj.consumes;

    return out;
  }
}

module.exports = NormalEvent;