Entropy.js

import * as stampit from 'stampit';
import { isObject } from './helpers';

import EventEmitter from './EventEmitter';
import Engine from './Engine';
import Ticker from './Ticker';
import Query from './Query';

import EntityStore from './EntityStore';
import ComponentStore from './ComponentStore';
import SystemStore from './SystemStore';

const PLUGINS = [];

/**
 * Main framework factory method. This is the only factory, that needs to be called by user.
 *
 * @class Entropy
 * @borrows ComponentStore#register as Entropy#registerComponent
 * @borrows ComponentStore#registerMany as Entropy#registerComponents
 * @borrows ComponentStore#create as Entropy#createComponent
 * @borrows EntityStore#register as Entropy#registerEntity
 * @borrows EntityStore#registerMany as Entropy#registerEntitys
 * @borrows EntityStore#create as Entropy#createEntity
 * @borrows SystemStore#register as Entropy#registerSystem
 * @borrows SystemStore#registerMany as Entropy#registerSystems
 * @borrows SystemStore#create as Entropy#createSystem
 * @extends EventEmitter
 * @param {Object} config
 * @param {Boolean} config.pauseOnVisibilityChange
 *
 * @example
 * const game = Entropy() // creating game with default configuration
 * game.addEntity('Ball', {
 *   x: 0,
 *   y: 0,
 * })
 */
const Entropy = stampit.compose(EventEmitter, {
  statics: {
    stampit,
    registerPlugin(factoryFunction) {
      PLUGINS.push(factoryFunction);
    },
  },
  init(config = {}) {
    // welcome message
    const styles = `
      background: white;
      display: block;
      color: black;
      box-shadow: 1px 1px 3px black;
      padding: 5px;
      text-align: center;
      font-weight: bold;`;

    console.log('%cEntropy 1.0.0-alpha.1 - Entity System Framework for JavaScript', styles);

    /**
     * Stores components for later reuse.
     *
     * @memberof Entropy#
     * @name component
     * @type ComponentStore
     */
    this.component = ComponentStore();

    /**
     * Stores entities for later reuse.
     *
     * @memberof Entropy#
     * @name entity
     * @type EntityStore
     */
    this.entity = EntityStore({
      game: this,
    });

    /**
     *
     * Stores entities for later reuse.
     *
     * @private
     * @memberOf Entropy#
     * @name system
     * @type SystemStore
     */
    this.system = SystemStore({
      game: this,
    });

    /**
     * Instance of {@link Engine}.
     *
     * @memberof Entropy#
     * @name engine
     * @type {Engine}
     */
    this.engine = Engine({
      game: this,
    });

    /**
     * Instance of Ticker class.
     *
     * @memberof Entropy#
     * @name ticker
     * @type {Ticker}
     */
    this.ticker = Ticker({
      game: this,
    });

    // initialize plugins
    PLUGINS.forEach((factoryFunction) => {
      this[factoryFunction.propName] = factoryFunction({
        game: this,
        config: config[factoryFunction.propName],
      });
    });

    // update engine when ticker updates
    this.ticker.on('update', (...args) => this.engine.update(...args));

    // browser only code
    if (typeof window !== 'undefined') {
      // Set the name of the hidden property and the change event for visibility
      let hidden;
      let visibilityChange;

      if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
        hidden = 'hidden';
        visibilityChange = 'visibilitychange';
      } else if (typeof document.msHidden !== 'undefined') {
        hidden = 'msHidden';
        visibilityChange = 'msvisibilitychange';
      } else if (typeof document.webkitHidden !== 'undefined') {
        hidden = 'webkitHidden';
        visibilityChange = 'webkitvisibilitychange';
      }

      document.addEventListener(visibilityChange, (e) => {
        this.emit('visibilityChange', {
          originalEvent: e,
          hidden: document.hidden,
        });
      }, false);

      if (config.pauseOnHide) {
        this.on('visibilityChange', (e) => {
          if (e[hidden]) {
            this.pause();
          } else {
            this.resume();
          }
        });
      }
    }
  },
  methods: {
    /**
     * Starts the game. See Ticker's {@link Ticker#start} method for more details.
     *
     * @memberof Entropy#
     * @return {Boolean} succesfuly started or not
     */
    start() {
      this.ticker.start();
      this.emit('start');
    },
    /**
     * Pauses the game.
     *
     * @memberof Entropy#
     */
    pause() {
      this.ticker.pause();
    },
    /**
     * Resumes paused game.
     *
     * @memberof Entropy#
     */
    resume() {
      this.ticker.resume();
    },
    /**
     * Stops the game. See Ticker's {@ink Ticker@stop} method for more details.
     *
     * @memberof Entropy#
     * @param {Boolean} clearEngine if `true`, engine will be cleared before ticker stop
     * @return {Entropy} game instance for chaining
     */
    stop(clearEngine) {
      if (clearEngine) {
        this.engine.once('clear', () => this._stopAndEmit());

        // schedule engine clearing
        this.engine.clear();
      } else {
        this._stopAndEmit();
      }
    },
    registerEntity(...args) {
      this.entity.register(...args);

      return this;
    },
    registerEntities(...args) {
      this.entity.registerMany(...args);

      return this;
    },
    createEntity(...args) {
      return this.entity.create(...args);
    },
    /**
     * Adds (or schedules adding) entity to engine. If first argument is entity type (string), then entity instance is created and then added.
     *
     * @public
     * @memberof Entropy#
     * @method addEntity
     * @param {Entity|String} typeOrEntity entity instance or entity type to create
     * @param {...Any}        args         arguments passed to `onCreate` method of entity descriptor, when first argument is entity type
     */
    addEntity(typeOrEntity, ...args) {
      const entity = isObject(typeOrEntity) ?
        typeOrEntity : this.createEntity(typeOrEntity, ...args);

      this.engine.addEntity(entity);

      return this;
    },
    /**
     * Removes (or schedules removing) entity from engine.
     *
     * @public
     * @memberof Entropy#
     * @method removeEntity
     * @param {Entity} entity entity instance
     */
    removeEntity(entity) {
      this.engine.removeEntity(entity);
    },
    registerComponent(...args) {
      this.component.register(...args);

      return this;
    },
    registerComponents(...args) {
      this.component.registerMany(...args);

      return this;
    },
    createComponent(...args) {
      return this.component.create(...args);
    },
    registerSystem(...args) {
      this.system.register(...args);

      return this;
    },
    registerSystems(...args) {
      this.system.registerMany(...args);

      return this;
    },
    createSystem(...args) {
      return this.system.create(...args);
    },
    /**
     * Adds system to engine. If first argument is system type (string), then system instance is created and then added.
     *
     * @public
     * @memberof Entropy#
     * @method addSystem
     * @param {System|String} typeOrSystem system instance or system type to create
     * @param {...Any}        args         arguments passed to `onCreate` method of system descriptor, when first argument is system type
     */
    addSystem(typeOrSystem, ...args) {
      const system = isObject(typeOrSystem) ?
        typeOrSystem : this.createSystem(typeOrSystem, ...args);

      this.engine.addSystem(system);
    },
    /**
     * Removes (or schedules removing) system from engine.
     *
     * @public
     * @memberof Entropy#
     * @method removeSystem
     * @param {System|String} typeOrSystem system instance or system type
     */
    removeSystem(typeOrSystem) {
      this.engine.removeSystem(typeOrSystem);
    },
    /**
     * Creates new query from criterions. See {@link Query} for details about available criterions.
     *
     * @public
     * @memberof Entropy#
     * @method createQuery
     * @param {Object|Array} criterions criterions
     * @returns {Query} query instance
     */
    createQuery(criterions) {
      const query = Query({
        componentsIdsMap: this.component._componentsIdsMap,
        criterions,
      });

      return query;
    },
    /**
     * Returns entities that matches provided query.
     *
     * @public
     * @memberof Entropy#
     * @method getEntities
     * @param {Query} query query
     * @return {Object} object with `length` and `entities` properties
     */
    getEntities(query) {
      return this.engine.getEntities(query);
    },
    /**
     * Checks if ticker is running.
     *
     * @public
     * @memberof Entropy#
     * @method isRunning
     * @returns {Boolean}
     */
    isRunning() {
      return this.ticker.isRunning();
    },
    _stopAndEmit() {
      const stop = this.ticker.stop();

      if (stop) {
        this.emit('stop');
      }
    },
  },
});

module.exports = Entropy;