Source: models/ZarimanCycle.js

import { fromNow, timeDeltaToString } from 'warframe-worldstate-data/utilities';

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

import WorldstateObject from './WorldstateObject.js';

// This is a confirmed starting time for Corpus (in millis)
// All faction operation should use this as a calculation point
// Unless there's a better logic
const corpusTimeMillis = 1655182800000;
const fullCycle = 18000000;
const stateMaximum = 9000000;

/**
 * Represents the current Zariman Corpus/Grineer Cycle
 * @augments {WorldstateObject}
 */
export default class ZarimanCycle extends WorldstateObject {
  /**
   * The current zariman cycle, for calculating the other fields
   * @type {object}
   * @private
   */
  #ec;
  /**
   * The end of the Zariman bounties timer, the faction changes exactly half way through
   * @type {Date}
   * @private
   */
  #bountiesEndDate;

  /**
   * @param {Date} bountiesEndDate The current Zariman cycle expiry
   */
  constructor(bountiesEndDate) {
    super({ _id: { $oid: 'zarimanCycle0' } });

    this.#bountiesEndDate = bountiesEndDate;
    this.#ec = this.getCurrentZarimanCycle();

    /**
     * The date and time at which the event ends
     * @type {Date}
     */
    this.expiry = this.#ec.expiry;

    /**
     * The date and time at which the event started
     * @type {Date}
     */
    this.activation = new Date(this.#ec.start);

    /**
     * Whether or not this it's corpus or grineer
     * @type {boolean}
     */
    this.isCorpus = this.#ec.isCorpus;

    /**
     * Current cycle state. One of `corpus`, `grineer`
     * @type {string}
     */
    this.state = this.#ec.state;

    /**
     * Time remaining string
     * @type {string}
     */
    this.timeLeft = this.#ec.timeLeft;

    this.id = `zarimanCycle${this.expiry.getTime()}`;

    this.shortString = `${this.timeLeft.replace(/\s\d*s/gi, '')} to ${this.isCorpus ? 'grineer' : 'corpus'}`;
  }

  /**
   * Get whether the event has expired
   * @returns {boolean} whether this is expired
   */
  getExpired() {
    return this.expiry ? fromNow(this.expiry) < 0 : /* istanbul ignore next */ true;
  }

  getCurrentZarimanCycle() {
    const now = Date.now();
    // determine if it is corpus cycle or grineer cycle based on bounty end time
    // we subtract 5000 millis (5 seconds) to ensure the corpus/grineer calculation is correct
    const bountiesClone = this.#bountiesEndDate.getTime() - 5000;
    const millisLeft = fromNow(new Date(bountiesClone));
    // the following line is a modulus operation
    // this ensures that our calculation is correct if bountiesClone is before corpusTimeMillis
    // if you really care, read https://torstencurdt.com/tech/posts/modulo-of-negative-numbers/
    const cycleTimeElapsed = (((bountiesClone - corpusTimeMillis) % fullCycle) + fullCycle) % fullCycle;
    const cycleTimeLeft = fullCycle - cycleTimeElapsed;
    // if timeInCycle is more than 2.5 hours, it is corpus, otherwise it is grineer
    const isCorpus = cycleTimeLeft > stateMaximum;

    const minutesCoef = 1000 * 60;
    const expiry = new Date(Math.round((now + millisLeft) / minutesCoef) * minutesCoef);
    const state = isCorpus ? 'corpus' : 'grineer';

    return {
      isCorpus,
      timeLeft: timeDeltaToString(millisLeft),
      expiry,
      expiresIn: millisLeft,
      state,
      start: expiry.getTime() - stateMaximum,
    };
  }

  /**
   * The event's string representation
   * @returns {string} string representation
   */
  toString() {
    const lines = [
      `Operator, Zariman Ten Zero is currently occupied by ${this.state}`,
      `Time remaining until ${this.isCorpus ? 'grineer' : 'corpus'} takeover: ${this.timeLeft}`,
    ];

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