Source: Game.js

const core = require('engine/core');
const EventEmitter = require('engine/EventEmitter');
const { removeItems } = require('engine/utils/array');
const Entity = require('engine/Entity');

/**
 * Game is the main hub for a game. A game made with LesserPanda
 * is a combination of different `Games`(menu, shop, game, game-over .etc).
 *
 * @class Game
 * @extends {EvenetEmitter}
 */
class Game extends EventEmitter {
  /**
   * @constructor
   */
  constructor() {
    super();

    /**
     * Desired FPS this scene should run
     * @property {Number} desiredFPS
     * @default 60
     */
    this.desiredFPS = 60;

    /**
     * Map of added systems
     * @property {Object} systems
     */
    this.systems = {};

    /**
     * List of system updating order.
     * Note: `systemOrder` should only be modified after systems added!
     * @property {Array<String>} systemOrder
     */
    this.systemOrder = [];

    /**
     * List of entities in the game world.
     * @type {Array<Entity>}
     */
    this.entities = [];
    /**
     * Holding all the named entities(has `name` been set).
     * @type {Object}
     */
    this.namedEntities = {};
    /**
     * Holding all the tagged entities(has `tag` been set).
     * @type {Object}
     */
    this.taggedEntities = {};

    /**
     * Caches update informations
     * @type {Object}
     * @private
     */
    this.updateInfo = {
      spiraling: 0,
      last: -1,
      realDelta: 0,
      deltaTime: 0,
      lastCount: 0,
      step: 0,
      slowStep: 0,
      count: 0,
    };
  }

  /**
   * Called each single frame by engine core, support both idle and fixed update.
   * Using a modified fixed update implementation from Phaser by @photonstorm
   * @param {Number} timestamp Timestamp at this invoking
   * @protected
   */
  run(timestamp) {
    let updateInfo = this.updateInfo;

    if (updateInfo.last > 0) {
      updateInfo.realDelta = timestamp - updateInfo.last;
    }
    updateInfo.last = timestamp;

    // If the logic time is spiraling upwards, skip a frame entirely
    if (updateInfo.spiraling > 1) {
      // Reset the deltaTime accumulator which will cause all pending dropped frames to be permanently skipped
      updateInfo.deltaTime = 0;
      updateInfo.spiraling = 0;
    }
    else {
      // Step size
      updateInfo.step = 1000.0 / this.desiredFPS;
      updateInfo.slowStep = updateInfo.step * core.speed;
      updateInfo.slowStepSec = updateInfo.step * 0.001 * core.speed;

      // Accumulate time until the step threshold is met or exceeded... up to a limit of 3 catch-up frames at step intervals
      updateInfo.deltaTime += Math.max(Math.min(updateInfo.step * 3, updateInfo.realDelta), 0);

      // Call the game update logic multiple times if necessary to "catch up" with dropped frames
      // unless forceSingleUpdate is true
      updateInfo.count = 0;

      while (updateInfo.deltaTime >= updateInfo.step) {
        updateInfo.deltaTime -= updateInfo.step;

        // Fixed update
        this.fixedUpdate(updateInfo.slowStep, updateInfo.slowStepSec);

        updateInfo.count += 1;
      }

      // Detect spiraling (if the catch-up loop isn't fast enough, the number of iterations will increase constantly)
      if (updateInfo.count > updateInfo.lastCount) {
        updateInfo.spiraling += 1;
      }
      else if (updateInfo.count < updateInfo.lastCount) {
        // Looks like it caught up successfully, reset the spiral alert counter
        updateInfo.spiraling = 0;
      }

      updateInfo.lastCount = updateInfo.count;
    }

    // Idle update
    this.update(updateInfo.realDelta, updateInfo.realDelta * 0.001);
  }

  /**
   * Awake is called when this scene is activated.
   * @method awake
   * @memberof Game#
   */
  awake() {
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].awake();
    }

    this.emit('awake');
  }

  /**
   * Update is called every single frame.
   * @method update
   * @memberof Game#
   * @param {Number} delta    Delta time in millisecond
   * @param {Number} deltaSec Delta time in second
   */
  update(delta, deltaSec) {
    let i, sys, ent;

    // Update entities
    for (i = 0; i < this.entities.length; i++) {
      ent = this.entities[i];
      if (!ent.isRemoved && ent.canEverTick) {
        ent.update(delta, deltaSec);
      }

      if (ent.isRemoved) {
        if (ent.CTOR.canBePooled) {
          ent.CTOR.recycle(ent);
        }
        removeItems(this.entities, i--, 1);
      }
    }

    // Update systems
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].update(delta, deltaSec);
    }

    this.emit('update', delta, deltaSec);
  }

  /**
   * Fixed update is called in a constant frenquence decided by `desiredFPS`.
   * @method fixedUpdate
   * @memberof Game#
   * @param {Number} delta    Delta time in millisecond
   * @param {Number} deltaSec Delta time in second
   */
  fixedUpdate(delta, deltaSec) {
    let i, sys, ent;

    // Update entities
    for (i = 0; i < this.entities.length; i++) {
      ent = this.entities[i];
      if (!ent.isRemoved && ent.canFixedTick) {
        ent.fixedUpdate(delta, deltaSec);
      }

      if (ent.isRemoved) {
        if (ent.CTOR.canBePooled) {
          ent.CTOR.recycle(ent);
        }
        removeItems(this.entities, i--, 1);
      }
    }

    // Update systems
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].fixedUpdate(delta, deltaSec);
    }

    this.emit('fixedUpdate', delta, deltaSec);
  }

  /**
   * Freeze is called when this scene is deactivated(switched to another one)
   * @method freeze
   * @memberof Game#
   */
  freeze() {
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].freeze();
    }

    this.emit('freeze');
  }

  /**
   * Add a system instance to this game.
   * @method addSystem
   * @memberof Game#
   * @param {System} sys System instance to add
   * @return {Game} Self for chaining
   */
  addSystem(sys) {
    if (sys.name.length === 0) {
      console.log(`System name "${sys.name}" is invalid!`);
      return this;
    }

    if (this.systemOrder.indexOf(sys.name) >= 0) {
      console.log(`System "${sys.name}" already added!`);
      return this;
    }

    this.systems[sys.name] = sys;
    this.systemOrder.push(sys.name);
    this[`sys${sys.name}`] = sys;
    sys.game = this;

    return this;
  }

  /**
   * System pause callback.
   * @method pause
   * @memberof Game#
   */
  pause() {
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].onPause();
    }

    this.emit('pause');
  }
  /**
   * System resume callback.
   * @method resume
   * @memberof Game#
   */
  resume() {
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].onResume();
    }

    this.emit('resume');
  }

  /**
   * Spawn an `Entity` into game world.
   * @method spawnEntity
   * @memberof Game#
   * @param  {Class} type       Entity class
   * @param  {Number} x         X coordinate
   * @param  {Number} y         Y coordinate
   * @param  {String} layer     Name of the layer to added to
   * @param  {Object} settings  Instance settings
   * @return {Entity}           Entity instance
   */
  spawnEntity(type, x, y, layer, settings) {
    let ctor = type;
    if (typeof(type) === 'string') {
      ctor = Entity.types[type];
      if (!ctor) {
        console.log(`[WARNING]: Entity type "${type}" does not exist!`);
        return undefined;
      }
    }

    // Create entity instance
    let ent;
    if (ctor.canBePooled) {
      ent = ctor.create(x, y, settings);
    }
    else {
      ent = new ctor(x, y, settings);
      ent.CTOR = ctor;
    }
    ent.layer = layer;
    ent.game = this;

    // Add to list
    this.entities.push(ent);

    // Add to name list
    if (ent.name) {
      this.namedEntities[ent.name] = ent;
    }

    // Add to tag list
    if (ent._tag) {
      if (!this.taggedEntities.hasOwnProperty(ent._tag)) {
        this.taggedEntities[ent._tag] = [];
      }
      this.taggedEntities[ent._tag].push(ent);
    }

    // Notify systems
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].onEntitySpawn(ent);
    }

    // Entity is ready to rock :D
    ent.ready();

    return ent;
  }
  /**
   * Remove an entity instance from this game
   * @memberof Game#
   * @param  {Entity} ent Entity instance
   */
  removeEntity(ent) {
    // Mark as removed
    ent.isRemoved = true;

    // Remove from name list
    if (ent.name) {
      delete this.namedEntities[ent.name];
    }

    // Remove from tag list
    if (ent._tag && this.taggedEntities.hasOwnProperty(ent._tag)) {
      let idx = this.taggedEntities[ent._tag].indexOf(ent);

      if (idx !== -1) {
        removeItems(this.taggedEntities[ent._tag], idx, 1);
      }
    }

    // Notify systems
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].onEntityRemove(ent);
    }
  }
  /**
   * Change tag of an entity instance
   * @memberof Game#
   * @param  {Entity} ent Entity instance
   * @param {String} tag  Tag to change to
   */
  changeEntityTag(ent, tag) {
    // Remove from tag list
    if (ent._tag && this.taggedEntities.hasOwnProperty(tag)) {
      let idx = this.taggedEntities[tag].indexOf(ent);

      if (idx !== -1) {
        removeItems(this.taggedEntities[tag], idx, 1);
      }
    }

    // Add to new tag group
    if (!this.taggedEntities.hasOwnProperty(tag)) {
      this.taggedEntities[tag] = [];
    }
    this.taggedEntities[tag].push(ent);

    // Notify systems
    let i, sys;
    for (i = 0; i < this.systemOrder.length; i++) {
      sys = this.systemOrder[i];
      this.systems[sys] && this.systems[sys].onEntityTagChange(ent, tag);
    }

    // Change entity tag value
    ent._tag = tag;
  }
  /**
   * Find an entity with specific name.
   * @memberof Game#
   * @param  {string} name Name of the entity
   * @return {Entity} Entity with the name
   */
  getEntityByName(name) {
    return this.namedEntities[name];
  }
  /**
   * Find entities with a specific tag.
   * @memberof Game#
   * @param  {string} tag Tag of the entities
   * @return {Array<Entity>|null} Entities with the tag
   */
  getEntitiesByTag(tag) {
    if (this.taggedEntities.hasOwnProperty(tag)) {
      return this.taggedEntities[tag];
    }
    return null;
  }

  /**
   * Resize callback.
   * @method resize
   * @memberof Game#
   * @param {Number} w New window width
   * @param {Number} h New window height
   */
  resize(w, h) {} /* eslint no-unused-vars:0 */
}

/**
 * @example <captain>Create a new game class</captain>
 * const Game = require('engine/Game');
 * class MyGame extends Game {}
 *
 * @example <captain>Switch to another game</captain>
 * const core = require('engine/core');
 * const MyGame = require('engine/MyGame');
 * core.setGame(MyGame);
 *
 * @exports engine/Game
 * @requires engine/core
 * @requires engine/EventEmitter
 * @requires engine/utils/math
 */
module.exports = Game;