Source: hg/actions/Action.js

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

/**
 * @description Handler function for a generic action.
 * @typedef HungryGames~Action~ActionHandler
 * @type {Function}
 *
 * @param {HungryGames} hg HG context.
 * @param {HungryGames~GuildGame} game Game context.
 */

/**
 * @description Base for actions to perform in response to certain things that
 * happen during a hunger games.
 *
 * @memberof HungryGames
 * @inner
 * @interface
 */
class Action {
  /**
   * @description Create action.
   * @param {HungryGames~Action~ActionHandler} handler Action handler override.
   * @param {number} [delay=0] Delay calling the handler by this many
   * milliseconds after triggered.
   */
  constructor(handler, delay = 0) {
    if (typeof handler !== 'function') {
      throw new TypeError('Handler is not a function.');
    }
    /**
     * @description The unique ID for this action. Probably globally unique,
     * definitely unique per-trigger in a guild.
     * @public
     * @type {string}
     */
    this.id = Action.createID();
    /**
     * @description Passed handler to fire once triggered.
     * @private
     * @type {HungryGames~Action~ActionHandler}
     * @constant
     */
    this._handler = handler;
    /**
     * @description Delay handler call by this many milliseconds after
     * triggered.
     * @public
     * @default 0
     * @type {number}
     */
    this.delay = delay || 0;
    /**
     * @description Data injected into save file that the `create` function uses
     * to restore data. Must be serializable.
     * @type {object}
     * @private
     * @default
     */
    this._saveData = {};
  }

  /**
   * @description Convert this object to serializable format for saving to file.
   * Injects data from `this._saveData`.
   * @public
   * @returns {{className: string, data: ?object}} Serializable object that can
   * be saved to file.
   */
  get serializable() {
    return {
      id: this.id,
      className: this.constructor.name,
      delay: this.delay,
      data: this._saveData,
    };
  }

  /**
   * @description Trigger the action to be performed.
   *
   * @type {HungryGames~Action~ActionHandler}
   * @public
   * @param {HungryGames} hg HG context.
   * @param {HungryGames~GuildGame} game Game context.
   * @param {...*} [args] Additional arguments to pass.
   */
  trigger(hg, game, ...args) {
    if (this.delay && !game.options.disableOutput) {
      setTimeout(() => this._handler(hg, game, ...args), this.delay);
    } else {
      this._handler(hg, game, ...args);
    }
  }

  /**
   * @description Create action from save data.
   * @public
   * @static
   * @abstract
   * @param {Discord~Client} client Bot client context to get object
   * references.
   * @param {string} id Guild ID this action is for.
   * @param {object} obj The parsed data from file.
   * @returns {HungryGames~Action} The created action.
   */
  static create(client, id, obj) {
    id;
    obj;
    client;
    throw new Error('Create function not overridden.');
  }

  /**
   * @description Generate an ID for an Action. Does not check for collisions.
   * @public
   * @static
   * @returns {string} Generated ID.
   */
  static createID() {
    return crypto.randomBytes(8).toString('hex').toUpperCase();
  }

  /**
   * @description Create an Action object from save data. Looks up action from
   * {@link HungryGames~Action~actionList}.
   * @public
   * @static
   * @param {Discord~Client} client Client reference for obtaining
   * discord object references.
   * @param {string} id The Guild ID this action is for.
   * @param {object} obj The object data from save file.
   * @returns {?Action} The created action, or null if failed to find the
   * action.
   */
  static from(client, id, obj) {
    if (typeof obj.className !== 'string' ||
        obj.className.length === 0) {
      console.error(obj.className, 'is not an Action name');
      return null;
    }
    const action = Action[obj.className];
    let out = null;
    if (action) {
      try {
        out = action.create(client, id, obj.data);
      } catch (err) {
        console.error(err);
      }
      if (!out) {
        console.error('Action.js: Unable to create', obj.className, id, obj);
        return null;
      }
      if (typeof obj.delay === 'number') {
        out.delay = obj.delay;
      }
      if (typeof obj.id === 'string') {
        out.id = obj.id;
      }
    } else {
      console.error(obj.className, 'is not an Action');
      return null;
    }
    return out;
  }
}

/**
 * @description List of available actions.
 * @public
 * @static
 * @type {Array.<{path: string, pickable: boolean}>}
 */
Action.actionList = [
  {path: './MemberAction.js'},
  {path: './ChannelAction.js'},
  {
    path: './GiveRoleAction.js',
    type: 'member',
    args: [{name: 'role', type: 'role'}],
  },
  {
    path: './TakeRoleAction.js',
    type: 'member',
    args: [{name: 'role', type: 'role'}],
  },
  {
    path: './SendMessageAction.js',
    type: 'channel',
    args: [{name: 'message', type: 'text'}],
  },
  {
    path: './RunCommandAction.js',
    type: 'channel',
    args: [{name: 'command', type: 'text'}, {name: 'author', type: 'member'}],
  },
  {path: './SendDayStartMessageAction.js', type: 'channel'},
  {path: './SendDayEndMessageAction.js', type: 'channel'},
  {path: './SendPlayerRankMessageAction.js', type: 'channel'},
  {path: './SendStatusListAction.js', type: 'channel'},
  {path: './SendTeamRankMessageAction.js', type: 'channel'},
  {path: './SendVictorAction.js', type: 'channel'},
  {path: './SendEventMessageAction.js', type: 'channel'},
  {path: './SendAutoplayingMessageAlertAction.js', type: 'channel'},
];

/**
 * @description Map of metadata for available triggers, to aid with UIs.
 * @public
 * @static
 * @type {object.<{
 *   order: number,
 *   types: string[],
 *   description: string
 * }>}
 */
Action.triggerMeta = {
  gameStart: {
    order: 10,
    types: ['member', 'channel'],
    description: 'Prior to game starting.',
  },
  dayStart: {
    order: 20,
    types: ['member', 'channel'],
    description: 'Prior to day starting.',
  },
  eventInstant: {
    order: 30,
    types: ['member', 'channel'],
    description: 'Moment the event occurs.',
  },
  eventPlayerDeath: {
    order: 31,
    types: ['member'],
    description: 'Players who died during the event.',
  },
  eventPlayerRevive: {
    order: 31,
    types: ['member'],
    description: 'Players who were revived during the event.',
  },
  eventPlayerWound: {
    order: 31,
    types: ['member'],
    description: 'Players who were wounded during the event.',
  },
  eventPlayerHealed: {
    order: 31,
    types: ['member'],
    description: 'Players who were healed during the event.',
  },
  eventPlayerKilled: {
    order: 31,
    types: ['member'],
    description: 'Players who killed another player during the event.',
  },
  eventPlayerGainWeapon: {
    order: 31,
    types: ['member'],
    description: 'Players who gained a weapon during the event.',
  },
  eventPlayerLoseWeapon: {
    order: 31,
    types: ['member'],
    description: 'Players who lost a weapon during the event.',
  },
  eventPlayerUseWeapon: {
    order: 31,
    types: ['member'],
    description: 'Players who used a weapon during the event, ' +
        'and neither gained nor lost consumables.',
  },
  eventPlayerUnAffected: {
    order: 31,
    types: ['member'],
    description: 'Players whose state did not change during the event, ' +
        'but took part in the event.',
  },
  eventPlayerAffected: {
    order: 31,
    types: ['member'],
    description: 'All players in the event.',
  },
  dayEnd: {
    order: 40,
    types: ['member', 'channel'],
    description: 'After the day has ended.',
  },
  dayPlayerDead: {
    order: 41,
    types: ['member'],
    description: 'After day has ended, for all players that are dead.',
  },
  dayPlayerAlive: {
    order: 41,
    types: ['member'],
    description: 'After day has ended, for all players that are alive.',
  },
  dayPlayerWounded: {
    order: 41,
    types: ['member'],
    description: 'After day has ended, for all players that are wounded.',
  },
  gameEnd: {
    order: 50,
    types: ['member', 'channel'],
    description: 'After game has ended.',
  },
  gameAbort: {
    order: 50,
    types: ['member', 'channel'],
    description: 'If game is ended early with command.',
  },
  gamePlayerDead: {
    order: 51,
    types: ['member', 'channel'],
    description: 'After game ended, for all players that are dead.',
  },
  gamePlayerAlive: {
    order: 51,
    types: ['member', 'channel'],
    description: 'After game ended, for all players that are alive.',
  },
  gamePlayerWounded: {
    order: 51,
    types: ['member', 'channel'],
    description: 'After game ended, for all players that are wounded.',
  },
  gamePlayerWin: {
    order: 52,
    types: ['member'],
    description: 'After game ended, for all players that won.',
  },
  gamePlayerLose: {
    order: 52,
    types: ['member'],
    description: 'After game ended, for all players that lost.',
  },
};

module.exports = Action;

Action.actionList.forEach(
    (el) => delete require.cache[require.resolve(el.path)]);
Action.actionList.forEach((el) => {
  try {
    const obj = require(el.path);
    Action[obj.name] = obj;
    el.name = obj.name;
  } catch (err) {
    console.error(err);
  }
});