Pre-release
Adventure.js Docs Downloads
Score: 0 Moves: 0
// Game.js
(function () {
  /*global adventurejs A*/
  "use strict";

  /**
   * @class adventurejs.Game
   * @ajsconstruct var MyGame = new adventurejs.Game( "MyGame", "MyGameDisplay" )
   * @ajsnavheading FrameworkReference
   * @param {String} [game_name = ""]
   * A string that matches the name
   * of the var used to store the new <strong>Game</strong> instance.
   * Used to give the game a reference back to itself, relative to the
   * browser window. If no name is provided, the instance name will
   * default to "MyGame".
   * <!--This name gets scoped to
   * window, and is used by all classes as a reference back to the game
   * instance. Instead of storing 'this.game', objects store
   * 'this.game_name', and refer back to the game by calling
   * window[this.game_name]. We do this to avoid circular object
   * references, which create problems when writing/reading json, which
   * we use to manage save/restore operations.-->
   * The chief reason you might want to use a custom game_name
   * is if you want to run multiple instance of adventure.js on a
   * single page; give them distinct names to prevent them from colliding.
   * You also may want to refer to your Game instance if you write any
   * custom javascript.
   * @param {String} [displayElId = ""]
   * A string that matches the ID of an existing DOM element into which
   * to insert the game {@link adventurejs.Display|Display}. If no
   * ID is provided, a DOM element will be created with an ID of
   * "MyGameDisplay" and class of "ajs-display", and appended to <BODY>.
   * @summary Constructs a new game object.
   * @classdesc
   * <p>
   * <strong>Game</strong> is used to construct a new game instance.
   * This instance contains the entire game and all of its methods.
   * If you open the browser inspector window, go to the console, and
   * type "MyGame" (or whatever game_name you have provided),
   * you can see the properties scoped to the Game object.
   * Every object in the game is given a reference back to the main
   * Game object. This means that you can run multiple games in a
   * single HTML page.
   * </p>
   * <h3 class="example">Example:</h3>
   * <p>
   * A minimal game defines at least one {@link adventurejs.Room|Room}
   * and one {@link adventurejs.Player|Player}
   * {@link adventurejs.Character|Character}. If you
   * don't provide either of these, one will be created automatically.
   * </p>
   * <pre class="display"><code class="language-javascript">var MyGame = new adventurejs.Game( "MyGame", "MyGameDisplay" )
   * .set({
   *   title: "Welcome Game",
   *   author: "Ivan Cockrum",
   *   description: "This is my great game! Thanks for playing!",
   *   version: "0.0.1",
   * });
   *
   * MyGame.createAsset({
   *   class: "Room",
   *   name: "Welcome Room",
   *   descriptions:{ look: "You've entered the Welcome Room. " },
   * });
   *
   * MyGame.createAsset({
   *   class: "Player",
   *   name: "Guest",
   *   is: { active: true, },
   *   place: { in: "Welcome Room" },
   * });
   * </code></pre>
   *
   **/
  class Game {
    constructor(game_name = "MyGame", displayElId = "MyGameDisplay") {
      this.class = "Game";

      this.game_states = {
        static: -1,
        construction: 0,
        validateAssets: 1,
        createDeferredAssets: 2,
        initializeAssets: 3,
        sortLookupValues: 4,
        worldSetDefaultAssets: 5,
        worldSaveBaseline: 6,
        playing: 7,
      };
      this.game_state = this.game_states.construction;

      /**
       * A reference back to this <strong>Game</strong> object.
       * Chiefly used for consistency so we can refer to this.game,
       * which is how we refer back to the Game from many functions.
       * @var {Object} adventurejs.Game#game
       * @default {}
       */
      this.game = window[game_name] = this;

      /**
       * game_name holds a copy of the variable name used to store
       * the current game instance, to which every asset in the game world
       * holds a reference. (By default, we use "MyGame", but you can use
       * any name.) The variable is scoped to window, and in order
       * to get the game object, we call window[this.game_name].
       * Though it would be easier to use a direct object reference,
       * doing so creates a circular reference, which complicates
       * JSON encoding, which is how we save/restore the game state.
       * @var {String} adventurejs.Game#game_name
       */
      this.game_name = game_name;

      /**
       * @member
       */
      // event management
      this.reactor = new adventurejs.Reactor(this);
      this.events = new adventurejs.Events(this);

      /**
       * <code>settings</code> is a singleton class
       * that stores all the game's global settings.
       * @var {Object} adventurejs.Game#settings
       */
      this.settings = new adventurejs.Settings(this);

      this.log("log", 1, "Game.js > Constructing " + this.game_name + ".");

      //this.version = "0.0.0";

      /**
       * <code>world</code> stores all the game asset
       * objects. It also stores a handful of variables
       * such as _player and _currentRoom which are used
       * for convenience. It also stores any global vars
       * created by the game's author. This last is done
       * to give authors a place to store vars that can
       * be written out with saved games.
       * @var adventurejs.Game#world
       */
      this.world = {};

      /**
       * <code>world._vars</code> is a namespace for
       * authors to store any custom vars that they
       * would like to be written out with saved games,
       * so that they can be restored along with other
       * saved game data in order to retain game state.
       * @var adventurejs.Game#world_lookup
       */
      this.world._vars = {};

      this.world._game_name = this.game_name;
      this.world._currentRoom = "";
      this.world._player = "";
      this.world._version = "";
      this.world._title = "";
      this.world._titleSerialized = "";
      this.world._author = "";
      this.world._score = "";
      this.world._timestamp = new Date().getTime().toString();
      this.world._intervals = {};

      /**
       * <code>baseline</code> stores a copy of the initial
       * game world, for comparison in save/restore and undo.
       * @var adventurejs.Game#baseline
       */
      this.baseline = { world: {} };
      // this.baseline = { world: {}, vars: {} };

      /**
       * <code>world_lookup</code> stores a lookup table
       * containing indexed keywords for all game assets.
       * @var adventurejs.Game#world_lookup
       */
      this.world_lookup = {};

      /**
       * <code>class_lookup</code> stores a lookup table
       * containing the ids of all assets by class.
       * @var adventurejs.Game#class_lookup
       */
      this.class_lookup = {};

      /**
       * <code>room_lookup</code> stores an array
       * containing the ids of all rooms.
       * @var adventurejs.Game#room_lookup
       */
      this.room_lookup = [];

      /**
       * <code>image_lookup</code> stores a lookup table
       * for optional images supplied by author.
       * @var adventurejs.Game#image_lookup
       */
      this.image_lookup = {};

      // create dictionary
      this.dictionary = new adventurejs.Dictionary(this);

      // create standard dictionary verbs
      this.dictionary.initStandardVerbs();

      this.display = new adventurejs.Display(displayElId, this);

      // create parser
      this.parser = new adventurejs.Parser(this).set({ display: this.display });

      this.saveManager = new adventurejs.SaveManager(this);

      this.restoreManager = new adventurejs.RestoreManager(this);

      this.userManager = new adventurejs.UserManager(this);

      // create global objects
      this.createAsset({ class: "All", name: "all" });
      this.createGlobalExits();
      this.createGlobalWalls();
      this.createGlobalFloors();
      this.createGlobalCeilings();
      this.createGlobalScenery();
      this.createAsset({
        class: "GlobalString",
        id: "global_string",
      }); // for string-in-string

      /**
       * <code>scorecard</code> is a singleton class
       * that stores all the game's score events
       * and functions for managing score.
       * @var {Object} adventurejs.Game#scorecard
       */
      this.scorecard = new adventurejs.Scorecard(this);

      /* *
       * <code>compasses</code> is an array that
       * stores references to any custom compasses.
       * @var {Object} adventurejs.Game#compasses
       */
      // this.compasses = [];

      /* *
       * <code>verbdocks</code> is an array that
       * stores references to any custom verbdocks.
       * @var {Object} adventurejs.Game#verbdocks
       */
      // this.verbdocks = [];

      /**
       * <code>inventorydocks</code> is an array that
       * stores references to any custom inventorydocks.
       * @var {Object} adventurejs.Game#inventorydocks
       */
      // this.inventorydocks = [];

      this.deferredObjects = [];
      this.description = "";
      this.world_history = [];

      return this;
    }

    initialize(params) {
      return this;
    }

    /**
     * Takes an input and immediately forwards it to the parser.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#sendToParser
     */
    sendToParser(value) {
      this.game.reactor.dispatchEvent(this.game.events.inputEnter);
      this.parser.input_string = value;
      this.parser.parseInput(value);
      this.display.clearInput();
    }

    /**
     * An alias to method display.sendToInput.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#sendToInput
     */
    sendToInput(value) {
      this.display.sendToInput(value);
    }

    /**
     * An alias to method display.clearInput.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#clearInput
     */
    clearInput(value) {
      this.display.clearInput();
    }

    // Getter / Setter Properties
    // These few props have getter/setter methods in order to pass
    // values to the display.

    /**
     * Get/set game title and pass to Display.
     * @var {String} adventurejs.Game#title
     * @default ""
     */
    get title() {
      return this.__title;
    }
    set title(title) {
      this.display.set({ title: (this.__title = title) });
      // double underscore solves recursion
      this.titleSerialized = A.serialize(title);
      this.world._title = title;
      this.world._titleSerialized = A.serialize(title);
    }

    /**
     * Get/set game author and pass to Display.
     * @var {String} adventurejs.Game#author
     * @default ""
     */
    get author() {
      return this.__author;
    }
    set author(author) {
      this.display.set({ author: (this.__author = author) });
      // double underscore solves recursion
      this.world._author = author;
    }

    /**
     * Concatenate room name + player situation
     * and save to game.displayRoomName. If there's
     * a room image, update that.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#updateDisplayRoom
     */
    updateDisplayRoom() {
      var player = this.getPlayer();
      var situation = "";
      var nest_asset = player.getNestAsset();
      let room = this.game.getAsset(this.world._currentRoom);

      if (nest_asset) {
        situation = ` (${player.getPostureGerund()} 
        ${player.getNestPreposition()} 
        ${nest_asset.articlename})`;
      } else if (player.isOnFloor()) {
        situation = ` (${player.getPostureGerund()} on the floor)`;
      }
      // @TODO floating in water or zero-G

      situation = situation
        ? `<span class='situation'>${situation}</span>`
        : "";
      let name = room.name + (situation && situation);

      //this.display.set({ "room" : name });
      this.display.setRoomName(name).setRoomImage(room.image);
      //this.display.setRoomImage(room.image);
      // @TODO set image
    }

    /**
     * Write current room's exits to the display's compass rose.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#updateDisplayCompasses
     */
    updateDisplayCompasses() {
      let exitnames = "";
      let exits = this.getCurrentRoom().exits;
      for (var id in exits) {
        let exit = this.getAsset(exits[id]);
        if (exit.destination) exitnames += ` ${exit.direction}`;
      }
      this.display.updateCompasses(exitnames);
    }

    /**
     * Update the verbs in verb docks.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#updateDisplayVerbs
     * @TODO this has no logic yet
     */
    updateDisplayVerbs() {
      this.display.updateVerbDocks();
    }

    /**
     * Update the inventory in inventory docks.
     * @memberOf adventurejs.Game
     * @method adventurejs.Game#updateDisplayInventory
     */
    updateDisplayInventory() {
      // getInventory doesn't return nested items
      // this.display.updateInventoryDocks( this.game.getPlayer().getInventory() );

      // getAllNestedContents might reveal unknown nested items
      this.display.updateInventoryDocks(
        this.game.getPlayer().getAllNestedContents()
      );
    }

    /* *
     * Get/set game room name and pass to Display.
     * @var {String} adventurejs.Game#displayRoomName
     * @default ""
     */
    // Object.defineProperty(p, "displayRoomName",
    // {
    // 	get: function()
    //   {
    // 		return this.__displayRoomName;
    // 	},
    // 	set: function(displayRoomName)
    //   {
    // 		this.display.set({ "room" : this.__displayRoomName = displayRoomName });
    // 		// double underscore solves recursion
    // 	}
    // });

    /**
     * Get/set game score and pass to Display.
     * @var {String} adventurejs.Game#score
     * @default ""
     */
    get score() {
      return this.__score;
    }
    set score(score) {
      this.display.set({ score: (this.__score = score) });
      this.world._score = score;
    }

    /**
     * Get/set game version and pass to Display.
     * @var {String} adventurejs.Game#version
     * @default ""
     */
    get version() {
      return this.__version;
    }
    set version(version) {
      this.display.set({ version: (this.__version = version) });
      // double underscore solves recursion
      this.world._version = version;
    }

    // SHORTCUT ALIASES FOR AUTHORS

    /**
     * Convenience method to get the player.
     * @var {Object} adventurejs.Game#player
     * @deprecated Use game.getPlayer() instead.
     */
    get player() {
      return this.getAsset(this.world._player);
    }

    /**
     * Convenience method to get the player's current room.
     * @var {Object} adventurejs.Game#currentRoom
     * @deprecated Use game.getCurrentRoom() instead.
     */
    get currentRoom() {
      return this.world[this.world._currentRoom];
    }

    /**
     * Convenience method to get this turn's input.
     * @var {Object} adventurejs.Game#input
     * @deprecated Use game.getInput() instead.
     */
    get input() {
      return this.parser.input_history[0];
    }

    /**
     * Convenience method to get last turn.
     * @var {Object} adventurejs.Game#last_turn
     * @deprecated Use game.getLastTurn() instead.
     */
    get last_turn() {
      return this.parser.input_history[1];
    }

    // PARSER SHORTCUT METHODS FOR AUTHORS

    createCustomParser(parser) {
      return this.parser.createCustomParser(parser);
    }
    enableCustomParser(parser) {
      return this.parser.enableCustomParser(parser);
    }
    disableCustomParser(parser) {
      return this.parser.disableCustomParser(parser);
    }

    /**
     * Convenience method is a shortcut to
     * game.parser.input_history[ 0 ].appendToOutput().
     * Provides a method for author to append custom
     * text after default output for the turn.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#appendToOutput
     * @param {String} msg
     * @returns {boolean}
     */
    appendToOutput(msg) {
      return this.input.appendToOutput(msg);
    }

    /**
     * This convenience method is a shortcut to
     * game.parser.input_history[ 0 ].overrideOutput().
     * Provides a method for author to override default
     * text output for the turn.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#appendToOutput
     * @param {String} msg
     * @returns {boolean}
     */
    overrideOutput(msg) {
      return this.input.overrideOutput(msg);
    }

    /**
     * This convenience method is a shortcut to
     * game.parser.input_history[ 0 ].prependToOutput().
     * Provides a method for author to prepend custom
     * text before default output for the turn.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#prependToOutput
     * @param {String} msg
     * @returns {boolean}
     */
    prependToOutput(msg) {
      return this.input.prependToOutput(msg);
    }

    // DICTIONARY SHORTCUT METHODS FOR AUTHORS

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#getStringLookup|game.dictionary.getStringLookup()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#getStringLookup
     * @param {object} verbs
     * @returns {object}
     */
    getStringLookup(type, value) {
      return this.dictionary.getStringLookup(type, value);
    }

    /**
     * Get the dictionary verb corresponding to provided string.
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#getVerb|game.dictionary.getVerb()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#getVerb
     * @param {string} verb
     * @returns {object}
     */
    getVerb(verb) {
      return this.dictionary.getVerb(verb);
    }

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#disableVerbs|game.dictionary.disableVerbs()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#disableVerbs
     * @param {object} verbs
     * @returns {object}
     */
    disableVerbs(verbs) {
      return this.dictionary.disableVerbs(verbs);
    }

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#disableAllVerbsBut|game.dictionary.disableAllVerbsBut()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#disableAllVerbsBut
     * @param {object} verbs
     * @returns {object}
     */
    disableAllVerbsBut(verbs) {
      return this.dictionary.disableAllVerbsBut(verbs);
    }

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#enableVerbs|game.dictionary.enableVerbs()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#enableVerbs
     * @param {object} verbs
     * @returns {object}
     */
    enableVerbs(verbs) {
      return this.dictionary.enableVerbs(verbs);
    }

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#createVerb|game.dictionary.createVerb()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#createVerb
     * @param {object} verb
     * @returns {object}
     */
    createVerb(verb) {
      return this.dictionary.createVerb(verb);
    }

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#replaceVerb|game.dictionary.replaceVerb()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#replaceVerb
     * @param {object} oldVerb
     * @param {object} newVerb
     * @returns {object}
     */
    replaceVerb(oldVerb, newVerb) {
      newVerb = this.dictionary.replaceVerb(oldVerb, newVerb);
      return newVerb;
    }

    /**
     * This convenience method is a shortcut to
     * {@link adventurejs.Dictionary#combineVerbs|game.dictionary.combineVerbs()}.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#combineVerbs
     * @param {object} verbs
     * @param {object} intoVerb
     * @returns {object}
     */
    combineVerbs(verbs, intoVerb) {
      intoVerb = this.dictionary.combineVerbs(verbs, intoVerb);
      return intoVerb;
    }

    /**
     * <strong>patchVerb</strong> enables an author to revise the properties
     * of an existing verb. For example, let's say you want to revise the
     * verb "plug" so that it works in the dark - meaning that players
     * it can operate on assets the player can't see. You might copy the
     * <code>plug.phrase1</code> and in your game file, call patchVerb with
     * line <code>visible: true</code> removed.
     * <pre class="display"><code class="language-javascript">MyGame.patchVerb({
     *   name: "plug",
     *   phrase1:{
     *     accepts_noun: true,
     *     requires_noun: true,
     *     accepts_preposition: true,
     *     noun_must_be:
     *     {
     *       known: true,
     *       tangible: true,
     *       present: true,
     *       <span class="strikethrough">visible: true,</span>
     *       reachable: true,
     *     },
     *   },
     * });</code></pre>
     * <br><br>
     * ( game.patchVerb() actually forwards to
     * {@link adventurejs.Dictionary#patchVerb|game.dictionary.patchVerb()} ).
     * @memberof adventurejs.Game
     * @method adventurejs.Game#patchVerb
     * @param {object} patch_verb
     * @returns {object}
     */
    patchVerb(patch_verb) {
      patch_verb = this.dictionary.patchVerb(patch_verb);
      return patch_verb;
    }

    /**
     * Intervals are callback functions used to fire in-game events
     * every turn. For example, turn on a water faucet and the faucet
     * pours water every turn until it's turned off.
     * registerInterval adds functions to the list.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#registerInterval
     * @param {string} id id of Asset which owns the function.
     * @param {string} callback The function to call on interval.
     * @todo add number of turns as a param and only call per x turn
     */
    registerInterval(id, callback) {
      //console.warn( "Game_registerInterval " + id + ", " + callback );
      if ("undefined" === typeof this.world._intervals[id]) {
        this.world._intervals[id] = {};
      }
      this.world._intervals[id][callback] = true;
    }

    /**
     * Intervals are callback functions used to fire in-game events
     * every turn. For example, turn on a water faucet and the faucet
     * pours water every turn until it's turned off.
     * unregisterInterval removes callback functions from the list.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#unregisterInterval
     * @param {string} id id of Asset which owns the function.
     * @param {string} callback The function to call on interval.
     */
    unregisterInterval(id, callback) {
      //console.warn( "Game_unregisterInterval " + id + ", " + callback );
      if ("undefined" === typeof this.world._intervals[id]) {
        return;
      }
      if ("undefined" === typeof this.world._intervals[id][callback]) {
        return;
      }
      delete this.world._intervals[id][callback];
      if (0 === Object.keys(this.world._intervals[id]).length) {
        delete this.world._intervals[id];
      }
    }

    /**
     * callIntervals uses pointers to asset properties to
     * print messages or apply custom logic every turn.
     * For example, turn on a water faucet and have
     * the faucet pour water every turn until it's turned
     * off, or turn on a radio and have it play music lyrics.
     * Properties can be strings or arrays or functions.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#unregisterInterval
     */
    callIntervals() {
      var game = this;
      Object.keys(this.world._intervals).forEach(function (id) {
        Object.keys(game.world._intervals[id]).forEach(function (callback) {
          let results;
          let asset;
          asset = game.getAsset(id);
          if (!asset || !asset[callback]) return;
          results = A.getSAF.call(game, asset[callback], asset);
          if ("string" === typeof results) game.print(results);
        });
      });
    }

    /**
     * This convenience property is a shortcut to game.world._vars,
     * which is a container for any custom variables set by authors.
     * Variables stored here are saved within the scope of data that
     * gets written to save files.
     * @memberof adventurejs.Game
     * @method adventurejs.Game#unregisterInterval
     */
    get vars() {
      return this.world._vars;
    }
  }
  adventurejs.Game = Game;
})();