Source: rpg.js

'use strict';

// ### Types
// Types are mostly imported from `@fabric/core` — an SDK for
// building distributed applications.  We'll cover this in more
// detail later, but there's [an entire site dedicated to docs][docs]
// if you're interested in learning more now.
const Collection = require('@fabric/core/types/collection');
const Entity = require('@fabric/core/types/entity');
const Key = require('@fabric/core/types/key');
const Remote = require('@fabric/core/types/remote');
const Service = require('@fabric/core/types/service');
const Verse = require('@fabric/rpg');

// HTTP
const Swarm = require('@fabric/http/types/swarm');

// #### Internal Types
const Queue = require('./queue');
const Character = require('./character');
const Player = require('./player');
const Universe = require('./universe');

/**
 * The core {@link RPG} class provides all functions necessary
 * for interacting with the game world, as powered by {@link Verse}.
 * @property {Collection} entities
 */
class RPG extends Service {
  /**
   * Instantiate an instance of {@link RPG} using `new RPG(settings)`, where
   * `settings` is an `Object` containing various optional settings.
   * @param {Object} [settings]
   * @param {String} [settings.authority=localhost:9999] Host and port combination reflecting intended authority. 
   */
  constructor (settings = {}) {
    super(settings);

    this.settings = Object.assign({
      authority: 'localhost:9999',
      framerate: 1,
      sync: false
    }, settings);

    // Internals
    this.cache = [];
    this.timer = null;
    this.queue = new Queue();
    this.remote = new Remote(this.settings);
    this.swarm = new Swarm(this.settings.swarm);

    // Collections
    this.characters = new Collection({
      type: Character
    });

    this.players = new Collection({
      type: Player
    });

    this.universes = new Collection({
      type: Universe
    });

    // internal collections
    this.entities = new Collection();
    this.messages = new Collection();

    // Assign Job Types
    this.queue.use(this.boot);
    this.queue.use(this.tick);

    // Flags
    this.status = 'waiting';

    // State
    this._state = {
      clocks: {
        last: Date.now()
      }
    };
  }

  /**
   * Provides a simple list of named {@link Collection} types to manage.
   */
  get handles () {
    // TODO: document public APIs for api.roleplaygateway.com
    return [
      `universes`,
      `authors`,
      `characters`,
      `snippets`
    ];
  }

  /**
   * Provides an {@link Entity} representing the current game state.
   * @property {String} id Hex-encoded unique identifier for the game state.
   */
  get state () {
    let state = {
      status: this.status,
      queue: this.queue
    };

    for (let i = 0; this.handles.length; i++) {
      state[this.handles[i]] = this._state[this.handles[i]];
    }

    return new Entity(state);
  }

  set state (obj) {
    if (!this.cache) this.cache = [];
    /* this.cache.push({
      '@method': 'SET',
      '@params': [`/state`, obj]
    }); */
    return this;
  }

  async _connectSwarm () {
    // TODO: implement
  }

  async _registerPlayer (player) {
    let actor = await this.players.create(player);
  }

  /**
   * Call `start()` to begin processing the game state, including
   * the clock beginning to advance for the in-game world.
   */
  async start () {
    // console.log('[RPG:LITE]', '[ENGINE]', 'Starting...');
    this.status = 'starting';

    let exchange = this;

    // run dependencies
    if (this.settings.sync) await this._sync();
    if (this.settings.fabric) await this._connectSwarm();

    this.queue._addWork({
      method: 'boot'
    });

    await this.queue._setState(this._state);
    await this.queue.start();
    await this.swarm.start();

    this.timer = setTimeout(function () {
      exchange.queue._addWork({
        '@method': 'tick'
      });
    }, this.settings.framerate / 1000);

    this.status = 'started';
    // console.log('[RPG:LITE]', '[ENGINE]', 'Started!');
  }

  async stop () {
    if (this.timer) clearInterval(this.timer);
    await this.swarm.stop();
    await this.queue.stop();
  }

  /**
   * Load information from the configured {@link Authority}.
   */
  async _sync () {
    this.log('[RPG:LITE]', '[ENGINE]', 'Syncing...');
    this.status = 'syncing';

    console.log('[RPG:LITE]', '[ENGINE]', 'Remote used:', this.remote);

    for (let i = 0; i < this.handles.length; i++) {
      let handle = this.handles[i];

      try {
        this._state[handle] = await this.remote._GET(`/${handle}`);
      } catch (E) {
        console.error('[RPG:LITE]', '[ENGINE]', 'Could not sync:', E);
      }
    }

    this.status = 'synced';
    this.log('[RPG:LITE]', '[ENGINE]', 'Synced!');
  }

  async _write () {
    for (let i = 0; i < this.handles.length; i++) {
      let handle = this.handles[i];
    }
  }


  boot () {
    console.log('[RPG:LITE]', '[BOOT]', 'Booting...');
    this.status = 'booting';
    let seed = new Key();
    this._state.seed = seed.toObject();
    this.status = 'booted';
    return this._state;
  }

  log (...msg) {
    let params = [ `@[${Date.now()}]` ].concat(msg);
    let entity = this.messages.create({
      '@type': 'Event',
      '@data': { params }
    });

    // TODO: document & upstream
    console.log.apply(null, params);
  }

  tick () {
    let now = Date.now();
    let dt = (now - this._state.clocks.last) / 1000.0;

    this._advanceClockSeconds(dt);
    this.render();

    this._state.clocks.last = now;
    this._requestAnimationFrame();
  }

  _advanceClockSeconds (dt) {
    // update game state and entities here...
    console.log('Advancing clock seconds... delta:', dt);
  }

  _requestAnimationFrame () {
    console.log('Requesting animation frame...');
    requestAnimFrame(this.tick.bind(this));
  }
}

module.exports = RPG;

// fabric: https://github.com/FabricLabs/fabric
// docs: https://dev.fabric.pub