Source: models/WorldEvent.js

import { fromNow, parseDate, faction, languageString, node, syndicate } from 'warframe-worldstate-data/utilities';

import mdConfig from '../supporting/MarkdownSettings.js';

import WorldstateObject from './WorldstateObject.js';
import SyndicateJob from './SyndicateJob.js';
import Reward from './Reward.js';

/**
 * Interim step for an event reward system.
 * @typedef {object} InterimStep
 * @property {number} goal          Goal amount
 * @property {Reward} reward        Reward for reaching the step
 * @property {number} winnerCount   Amount of players at this step
 * @property {object} message       Message received when reaching the interim step
 */

/**
 * Progress for one of multiple stages
 * @typedef {object} ProgressStep
 * @property {string} type         Type of progress
 * @property {number} progressAmt Amount of progress
 */

/**
 * Represents an in-game special event
 * @augments {WorldstateObject}
 */
export default class WorldEvent extends WorldstateObject {
  /**
   * Asynchronously build a new WorldEvent
   * @param   {object}            data            The event data
   * @param   {object}            deps            The dependencies object
   * @param   {string}            deps.locale     Locale to use for translations
   * @returns {Promise.<WorldEvent>}              The created WorldEvent object
   */
  static async build(data, deps) {
    const event = new WorldEvent(data, deps);
    if (data.Jobs) {
      const jobs = [];
      // eslint-disable-next-line no-restricted-syntax
      for await (const job of data.Jobs ?? []) {
        jobs.push(await SyndicateJob.build(job, event.expiry, deps));
      }
      event.jobs = jobs;
    }
    if (data.PreviousJobs) {
      const previousJobs = [];
      // eslint-disable-next-line no-restricted-syntax
      for await (const job of data.PreviousJobs ?? []) {
        previousJobs.push(await SyndicateJob.build(job, event.expiry, deps));
      }
      event.previousJobs = previousJobs;
    }
    return event;
  }

  /**
   * @param   {object}            data            The event data
   * @param   {object}            deps            The dependencies object
   * @param   {string}            deps.locale     Locale to use for translations
   */
  constructor(data, { locale = 'en' } = { locale: 'en' }) {
    super(data);

    const opts = {
      locale,
    };

    /**
     * The date and time at which the event ends
     * @type {Date}
     */
    this.expiry = parseDate(data.Expiry);

    /**
     * The event's main score goal
     * @type {number}
     */
    this.maximumScore = Number.parseInt(data.Goal, 10);

    /**
     * The current score on the event
     * @type {number}
     */
    this.currentScore = Number.parseInt(data.Count, 10);

    /**
     * The first intermediate score goal
     * @type {?number}
     */
    this.smallInterval = Number.parseInt(data.GoalInterim, 10);

    /**
     * The second intermediate score goal
     * @type {?number}
     */
    this.largeInterval = Number.parseInt(data.GoalInterim2, 10);

    /**
     * The faction that the players must fight in the event
     * @type {string}
     */
    this.faction = faction(data.Faction, locale);

    /**
     * The description of the event
     * @type {string}
     */
    this.description = languageString(data.Desc, locale);

    /**
     * Tooltip for the event
     * @type {?string}
     */
    this.tooltip = languageString(data.ToolTip, locale);

    /**
     * The node where the event takes place
     * @type {?string}
     */
    this.node = node(data.Node, locale);

    /**
     * The other nodes where the event takes place
     * @type {string[]}
     */
    this.concurrentNodes = data.ConcurrentNodes ? data.ConcurrentNodes.map((n) => node(n), locale) : [];

    /**
     * The victim node
     * @type {?string}
     */
    this.victimNode = node(data.VictimNode, locale);

    /**
     * The score description
     * @type {?string}
     */
    this.scoreLocTag = data.Fomorian ? 'Fomorian Assault Score' : languageString(data.ScoreLocTag, locale);

    /**
     * The event's rewards
     * @type {Reward[]}
     */
    this.rewards = Object.keys(data)
      .filter((k) => k.includes('Reward') || k.includes('reward'))
      .map((k) => new Reward(data[k], opts));

    /**
     * Whether or not this is expired (at time of object creation)
     * @type {boolean}
     */
    this.expired = this.getExpired();

    /**
     * Health remaining for the target
     * @type {number}
     */
    this.health =
      typeof data.HealthPct !== 'undefined' ? Number.parseFloat(((data.HealthPct || 0.0) * 100).toFixed(2)) : undefined;

    this.jobs = [];
    this.previousJobs = [];

    /**
     * Previous job id
     * @type {string}
     */
    this.previousId = (data.JobPreviousVersion || {}).$oid;

    /**
     * Array of steps
     * @type {InterimStep[]}
     */
    this.interimSteps = [];

    (data.InterimRewards || []).forEach((reward, index) => {
      const msg = (data.InterimRewardMessages || [])[index] || {};
      this.interimSteps[index] = {
        goal: Number.parseInt(data.InterimGoals[index], 10),
        reward: reward ? new Reward(reward, opts) : undefined,
        message: {
          sender: languageString(msg.sender, locale),
          subject: languageString(msg.subject, locale),
          message: languageString(msg.message, locale),
          senderIcon: msg.senderIcon,
          attachments: msg.attachments,
        },
        // eslint-disable-next-line no-underscore-dangle
        winnerCount: (data._interimWinnerCounts || [])[index],
      };
    });

    /**
     * Progress Steps, if any are present
     * @type {ProgressStep[]}
     */
    this.progressSteps = [];

    if (data.IsMultiProgress) {
      data.Types.forEach((type, index) => {
        this.progressSteps[index] = {
          type: languageString(type, locale),
          progressAmt: Number.parseInt(data.MultiProgress[index], 10),
        };
      });

      /**
       * Total of all MultiProgress
       * @type {number}
       */
      this.progressTotal = Number.parseFloat(data.MultiProgress.reduce((accumulator, val) => accumulator + val));
    }

    /**
     * Whether to show the total score at the end of the mission
     * @type {boolean}
     */
    this.showTotalAtEndOfMission = data.ShowTotalAtEOM;
    /**
     * Whether the event is personal
     * @type {boolean}
     */
    this.isPersonal = data.Personal;
    /**
     * Whether the event is community
     * @type {boolean}
     */
    this.isCommunity = data.Community;

    /*
     * Affectors for this mission
     * @type {string[]}
     */
    this.regionDrops = (data.RegionDrops || []).map((drop) => languageString(drop, locale));

    /**
     * Archwing Drops in effect while this event is active
     * @type {string[]}
     */
    this.archwingDrops = (data.ArchwingDrops || []).map((drop) => languageString(drop, locale));

    this.asString = this.toString();

    /**
     * Metadata provided by DE
     * @type {object}
     */
    this.metadata = JSON.parse((data.Metadata || '{}').replace('" ', '"'));

    /**
     * Bonuses given for completion
     * @type {Array.<number>}
     */
    this.completionBonuses = data.CompletionBonus || [];

    this.scoreVar = data.ScoreVar;

    this.altExpiry = parseDate(data.AltExpiry);
    this.altActivation = parseDate(data.AltActivation);

    this.nextAlt = {
      expiry: parseDate(data.NextAltExpiry),
      activation: parseDate(data.NextAltActivation),
    };

    if (data.JobAffiliationTag) {
      this.affiliatedWith = syndicate(data.JobAffiliationTag, locale);
    }
  }

  /**
   * Get whether the event has expired
   * @returns {boolean} whether the event has expired
   */
  getExpired() {
    return fromNow(this.expiry) < 0;
  }

  /**
   * The event's string representation
   * @returns {string} the event's string representation
   */
  toString() {
    let lines = [];
    if (this.faction) {
      lines.push(`${this.description} : ${this.faction}`);
    } else {
      lines.push(this.description);
    }

    if (this.scoreLocTag && this.maximumScore) {
      lines.push(`${this.scoreLocTag} : ${this.maximumScore}`);
    }

    if (this.rewards.length) {
      lines.push('Rewards:');
      lines = lines.concat(this.rewards.map((r) => r.toString()));
    }
    if (this.node) {
      lines.push(`Battle on ${this.node}`);
    }
    if (this.victim) {
      lines.push(`Protect ${this.victimNode}`);
    }
    if (this.health) {
      lines.push(`${this.health}% Remaining`);
    }

    if (this.affiliatedWith && this.jobs) {
      lines.push(
        `${this.affiliatedWith} will reward you for performing ` +
          `${this.jobs.map((job) => job.type).join(', ')} job${this.jobs.length > 1 ? 's' : ''}`
      );
    }

    return lines.join(mdConfig.lineEnd);
  }
}