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

  /**
   * @ajspath adventurejs.Atom.Asset.Matter.Tangible.Room
   * @augments adventurejs.Tangible
   * @class adventurejs.Room
   * @ajsconstruct MyGame.createAsset({ "class":"Room", "name":"foo", [...] })
   * @ajsconstructedby adventurejs.Game#createAsset
   * @ajsnavheading RoomAssets
   * @param {String} game_name The name of the top level game object.
   * @param {String} name A name for the object, to be serialized and used as ID.
   * @summary Where all the action happens.
   * @tutorial CreateRoom
   * @ajstangiblecontainer in
   * @ajstangiblecontainer attached
   * @classdesc
   * <p>
   * <strong>Room</strong> is a subclass of
   * {@link adventurejs.Tangible|Tangible} and the class for
   * all {@link adventurejs.Game|Game} locations. Player is always
   * in a Room. Rooms connect to other Rooms via
   * {@link adventurejs.Exit|Exits}, and can be decorated with
   * {@link adventurejs.Scenery|Scenery} and random events.
   * To learn more, see
   * <a href="/doc/GetStarted_CreateARoom.html">Create a Room</a>.
   * </p>
   *
   * <h3 class="examples">Exits:</h3>
   * <p>
   * There are two methods to define Exits, which can be mixed & matched.
   * The first method is a simple shortcut that defines an Exit
   * as part of a Room definition, using a direction and a
   * Room name, as in the following example. Exits are one-way, and
   * the other Rooms will need Exits back to this Room.
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "West of House",
   *   exits: {
   *     south: "South of House",
   *     north: "North of House",
   *     west: "Forest",
   *     east: "The door is boarded and you can't remove the boards. "
   *   }
   * })
   * </code></pre>
   * <p>
   * In the example above, the south, north and west Exits all lead to
   * other Rooms. There is no east Exit, but by providing a string
   * instead of a Room name, that string will be returned to players
   * who try to go east.
   * </p>
   * <p>
   * This next example uses the second method of defining an Exit,
   * which is to create the Room and its Exits as distinct objects.
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "Inside the Barrow",
   * });
   * MyGame.createAsset({
   *   class: "Exit",
   *   direction: "south",
   *   place: { in: "Inside the Barrow" },
   *   destination: "Narrow Tunnel",
   * });
   * </code></pre>
   * <p>
   * This second method supports greater complexity and allows
   * for more granular control once you get into custom coded Exits.
   * To learn more, see
   * <a href="/doc/GetStarted_CreateAnExit.html">Create an Exit</a>.
   * </p>
   *
   * <h3 class="examples">Scenery:</h3>
   * <p>
   * By default,
   * {@link adventurejs.Game|Game} instances include a number of
   * {@link adventurejs.Scenery|Scenery}
   * {@link adventurejs.Asset|Assets} which are available to use
   * in any Room. Scenery Assets can be sensed but not interacted
   * with, like sun and sky, rain and room tone, odors, walls,
   * and non-existent exits (to provide responses when players try
   * to use exits that don't exist).
   * Default Scenery objects can be enabled, customized, and disabled
   * at will by Room, by {@link adventurejs.Zone|Zone}, and globally.
   * It's also possible to create new custom Scenery Assets.
   * In the example below, we enable and set custom descriptions
   * for the global <code>sky</code>, <code>clouds</code>,
   * <code>sun</code>, and <code>rain</code>;
   * and create a new custom Scenery <code>crows</code>.
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "Creepy Playground",
   *   descriptions: {
   *     at: "This creepy looking playground is dominated by a rusted jungle gym. ",
   *   },
   *   room_scenery: {
   *     global_sky: {
   *       enabled: true,
   *       description: "The clouded sky is dark and foreboding. ",
   *     },
   *     global_clouds: {
   *       enabled: true,
   *       description: "Heavy, black, and pendulous. ",
   *     },
   *     global_thunder: {
   *       enabled: true,
   *       description: "Occasional thunder plays in the distance. ",
   *     },
   *     global_lightning: {
   *       enabled: true,
   *       description: "None yet, but it wouldn't surprise you. ",
   *     },
   *     global_sun: {
   *       enabled: true,
   *       description: "You can barely see it through the dark clouds. ",
   *     },
   *     global_rain: {
   *       enabled: true,
   *       description: "The rain patters onto the playground's worn rubber mats. ",
   *     },
   *     crows: {
   *       create: true,
   *       enabled: true,
   *       description: "Evil looking crows circle the playground. ",
   *     },
   *   },
   * });
   * </code></pre>
   * <p>
   * To learn more about using the default Scenery Assets, see
   * <a href="/doc/NextSteps_GlobalScenery.html">Global Scenery</a>.
   * </p>
   *
   * <h3 class="examples">Events:</h3>
   * <p>
   * Events are arbitrary messages that can be attached to a Room
   * and set so that they are appended to any other output for the
   * turn. Use the <code>frequency</code> property of
   * <code>room_events</code> to set the frequency of events.
   * A frequency of 1 will play an event on every turn; .1 will play
   * an event every 10th turn; etc. The <code>randomize</code>
   * property determines whether events are selected to play randomly.
   * If <code>randomize</code> is set to true, events will play in
   * random order, otherwise events will play in the order they are
   * set in the array, looping as needed.
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "Creepy Playground",
   *   descriptions: {
   *     at: "This creepy looking playground is dominated by a rusted jungle gym. ",
   *   },
   *   room_events: [
   *     {
   *       frequency: .1,
   *       randomize: true,
   *     },
   *     "A crow shrieks loudly, startling you. ",
   *     "Rain pings metallically off the jungle gym. ",
   *     "The rain raises a tarry odor from the playground's rubber mats. ",
   *     "Distant thunder rolls through the clouds. ",
   *     "The barest hint of lightning flicks in the distance. ",
   *   ],
   * });
   * </code></pre>
   * <p>
   * To learn more about using events, see
   * <a href="/doc/NextSteps_SceneEvents.html">Scene Events</a>.
   * </p>
   **/
  class Room extends adventurejs.Tangible {
    constructor(name, game_name) {
      super(name, game_name);
      this.class = "Room";

      this.synonyms = ["room"];
      this.singlePluralPairs = [["room", "rooms"]];

      this.indefinite_article = "a";
      this.definite_article = "the";
      this.use_definite_article_in_lists = true;
      this.default_aspect = "in";
      this.exits = {};
      //this.split_name_for_world_lookup = false;

      this.room_scenery = {
        global_air: { enabled: null },
        global_moon: { enabled: null },
        global_sky: { enabled: null },
        global_sun: { enabled: null },
        global_stars: { enabled: null },
        global_sound: { enabled: null },
        global_rain: { enabled: null },
        global_wind: { enabled: null },
        global_clouds: { enabled: null },
        global_lightning: { enabled: null },
        global_floor: { enabled: null },
        global_ceiling: { enabled: null },

        global_north_wall: { enabled: null },
        global_east_wall: { enabled: null },
        global_west_wall: { enabled: null },
        global_south_wall: { enabled: null },
        global_aft_wall: { enabled: null },
        global_fore_wall: { enabled: null },
        global_northeast_wall: { enabled: null },
        global_northwest_wall: { enabled: null },
        global_port_wall: { enabled: null },
        global_southeast_wall: { enabled: null },
        global_southwest_wall: { enabled: null },
        global_starboard_wall: { enabled: null },

        global_north: { enabled: true },
        global_east: { enabled: true },
        global_west: { enabled: true },
        global_south: { enabled: true },
        global_aft: { enabled: true },
        global_fore: { enabled: true },
        global_northeast: { enabled: true },
        global_northwest: { enabled: true },
        global_port: { enabled: true },
        global_southeast: { enabled: true },
        global_southwest: { enabled: true },
        global_starboard: { enabled: true },
        global_up: { enabled: true },
        global_down: { enabled: true },
      };

      this.zone = "";

      this.dimensions.height = 1;
      this.player_has_visited = false;

      // unset these verbs inherited from Matter / Tangible
      this.unsetDOVs(["push", "take", "give", "drop", "move", "pull", "throw"]);
      this.unsetIOV("throw");
      this.setIOVs(["take", "drop", "put"]);

      /**
       * If set true, when player tries to go in a direction that hasn't
       * got an {@link adventurejs.Exit|Exit},
       * we'll print the current room's Exits as a reminder
       * of what Exits are available.
       * <br><br>
       * Setting this to true or false per room will override global
       * {@link adventurejs.Settings|Settings} for that room.
       * @var {Boolean} adventurejs.Room#when_travel_fails_list_exits
       * @default true
       */
      this.when_travel_fails_list_exits = null;

      /**
       * If set true,
       * {@link adventurejs.Exit|Exit} descriptions can include
       * the name of the {@link adventurejs.Room|Room} that the
       * Exit leads to. (The logic for this may also consider other
       * conditions such as whether the player knows about the
       * other Room.)
       * <br><br>
       * Setting this to true or false per room will override global
       * {@link adventurejs.Settings|Settings} for that room.
       * @var {Boolean} adventurejs.Room#show_room_names_in_exit_descriptions
       * @default null
       */
      this.show_room_names_in_exit_descriptions = null;

      /**
       * If set true,
       * {@link adventurejs.Exit|Exit} descriptions can include
       * the name of the {@link adventurejs.Room|Room} that the
       * Exit leads to once player knows about the destination Room.
       * Generally player must visit a room to know about it, but
       * there can be exceptions.
       * <br><br>
       * Setting this to true or false per room will override global
       * {@link adventurejs.Settings|Settings} for that room.
       * @var {Boolean} adventurejs.Settings#show_room_names_in_exit_descriptions_only_when_room_is_known
       * @default null
       */
      this.show_room_names_in_exit_descriptions_only_when_room_is_known = null;

      /**
       * If set true,
       * {@link adventurejs.Exit|Exit} descriptions can include
       * the name of the {@link adventurejs.Room|Room} that the
       * Exit leads to once player has used the Exit.
       * <br><br>
       * Setting this to true or false per room will override global
       * {@link adventurejs.Settings|Settings} for that room.
       * @var {Boolean} adventurejs.Settings#show_room_names_in_exit_descriptions_only_after_exit_has_been_used
       * @default null
       */
      this.show_room_names_in_exit_descriptions_only_after_exit_has_been_used =
        null;

      this.dimensions.size = 1;

      this.aspects.in = new adventurejs.Aspect("in", this.game_name).set({
        parent_id: this.id,
        player: {
          preposition: "in",
          posture: "stand",
          initial_position: { x: 0, y: 0, z: 0 },
          can: {
            bounce: true,
            crawl: true,
            enter: true,
            exit: true,
            hear: true,
            hop: true,
            jump: true,
            kneel: true,
            lie: true,
            ride: true,
            see: true,
            sit: true,
            stand: true,
            swim: false,
            walk: true,
          },
        },
      });

      this.aspects.in.vessel = new adventurejs.Vessel("in", this.game_name).set(
        {
          maxvolume: Infinity,
          vessel_is_known: true,
          is_body_of_substance: true,
        },
      );

      this.aspects.attached = new adventurejs.Aspect(
        "attached",
        this.game_name,
      ).set({
        parent_id: this.id,
      });

      /**
       * A collection of properties that defines whether a player may
       * enter this aspect, what actions they are allowed to perform
       * in it, and the default posture they will take upon entering.
       * Aspects and Rooms both share these properties.
       * @var {boolean} adventurejs.Room#player
       * @default {}
       */
      this.player = new adventurejs.Aspect_Player(
        "player",
        this.game_name,
        this.id,
        "in",
      ).set({
        parent_id: this.parent_id,
        preposition: "in",
        can: {
          bounce: true,
          crawl: true,
          enter: true,
          exit: true,
          hear: true,
          hop: true,
          jump: true,
          kneel: true,
          lie: true,
          ride: true,
          see: true,
          sit: true,
          stand: true,
          swim: false,
        },
      });

      this.descriptions.listen = "$(We) hear the room tone.";

      this.is_vacuum = false;
      this.location_unneccessary = true;

      this.is.closed = null;
    }

    // returns array of strings
    getAllContents() {
      var contents = this.aspects.in.contents;
      return contents;
    }

    onMoveThatToThis(object) {
      this.game.log(
        "log",
        "low",
        "move " + object.name + " to " + this.name + ".",
        "Room",
        "Room",
      );
      var results = true;

      results = super.onMoveThatToThis(object);
      if ("undefined" !== typeof results) return results;

      if (object.id === this.game.world._player) {
        this.showRoomToPlayer(object);
      }
      return;
    }

    showRoomToPlayer(object) {
      this.player_has_visited = true;
      this.setKnown();
      this.setSeen();

      // player recognizes exits
      var keys = Object.keys(this.exits);
      for (var i = 0; i < keys.length; i++) {
        var exitID = this.exits[keys[i]];
        var exit = this.game.world[exitID];
        if (
          exit.is.hidden ||
          this.game.world[this.game.world._player].is.blind
        ) {
          /* || TODO dark / no visibility */
        } else {
          exit.setKnown();
          exit.setSeen();
        }
      }
    }

    /**
     * Room inherits validate from {@link adventurejs.Tangible|Tangible}
     * and adds some additional chunks to handle
     * {@link adventurejs.Scenery|Scenery} and
     * {@link adventurejs.Exit|Exits}. If the Room's definition includes
     * Scenery or Exits that haven't been defined distinctly, they will be
     * added to a list of deferredObjects to be constructed after the
     * {@link adventurejs.Game|Game's} main validation/initialization pass.
     * @method adventurejs.Room#validate
     * @memberof adventurejs.Room
     */
    validate(game) {
      super.validate(game);

      this.zone = A.serialize(this.zone);

      var sceneryCount = Object.keys(this.room_scenery).length;
      if (sceneryCount > 0) {
        var sceneries = Object.keys(this.room_scenery);
        for (var i = 0; i < sceneries.length; i++) {
          if (!this.game.getAsset(sceneries[i])) {
            var newScenery = {};
            var already_deferred = false;

            newScenery = Object.assign(
              newScenery,
              this.room_scenery[sceneries[i]],
            );
            newScenery.name = sceneries[i];
            newScenery.class = "Scenery";
            if (newScenery.is && newScenery.is.global) {
              // if it's global, it must be known in order
              // for player to refer to it from anywhere
              newScenery.is.known = true;
            } else {
              // otherwise put it in this room
              newScenery.place = { in: this.id };
            }

            // if it's global, it can be listed in multiple rooms,
            // but we only want to construct it once,
            // and we only want to construct the one that has create:true
            for (var d = 0; d < this.game.deferredObjects.length; d++) {
              if (sceneries[i] === this.game.deferredObjects[d].name) {
                if (true === this.game.deferredObjects[d].create) {
                  // created by that other instance
                  already_deferred = true;
                } else if (true === newScenery.create) {
                  // created by this instance
                  this.game.deferredObjects[d] = newScenery;
                }
              }
            }
            if (false === already_deferred) {
              this.game.deferredObjects.push(newScenery);
            }
          }
        }
      }

      var exitCount = Object.keys(this.exits).length;
      if (exitCount === 0) {
        var msg = "Room.js > " + this.name + " has no exits. ";
        //this.game.log( "warn", "critical", msg , 'Room' , 'Room' );
      }

      if (exitCount > 0) {
        var directions = Object.keys(this.exits);
        for (var i = 0; i < directions.length; i++) {
          var direction = directions[i];
          var exit = this.exits[direction];
          if (typeof exit === "string") {
            // at minimum we need location & direction, secondarily destinationName
            // does the string match a Room ID?
            // if not treat it as a description
            var newDirection = { descriptions: {} };
            var destinationName = exit;
            var destinationID = A.serialize(destinationName);
            newDirection.place = { in: this.name };
            newDirection.direction = direction;
            newDirection.class = "Exit";

            if (
              "undefined" !== typeof this.game.world[destinationID] &&
              this.game.world[destinationID] instanceof adventurejs.Room
            ) {
              // exit string matches a Room
              newDirection.destination = destinationName;
            } else {
              // doesn't match a room so treat it as a description
              newDirection.descriptions.look = destinationName;
            }

            // push it to deferredObjects for construction/validation
            // after the initial validation pass
            this.game.deferredObjects.push(newDirection);

            // erase the intitial string
            delete this.exits[direction];
          } else if (typeof this.exits[direction] === "object") {
            var newDirection = {};
            newDirection = Object.assign(newDirection, this.exits[direction]);
            newDirection.class = "Exit";
            newDirection.direction = direction;
            newDirection.place = { in: this.id };
            this.game.deferredObjects.push(newDirection);
          }
        } // for( var i = 0; i < directions.length
      } // if( exitCount

      return true;
    }

    /**
     * Rooms never have a place/parent, but they get queried as a Tangible class.
     * Returns false to cover bases.
     * @memberOf adventurejs.Room
     * @method adventurejs.Room#getPlaceAssetId
     * @returns {Boolean}
     */
    getPlaceAssetId() {
      return false;
    }
    /**
     * Rooms never have a place/parent, but they get queried as a Tangible class.
     * Returns false to cover bases.
     * @memberOf adventurejs.Room
     * @method adventurejs.Room#getPlaceAsset
     * @returns {Boolean}
     */
    getPlaceAsset() {
      return false;
    }

    /**
     * Rooms never have a place/parent, but they get queried as a Tangible class.
     * Returns false to cover bases.
     * @memberOf adventurejs.Room
     * @method adventurejs.Room#getPlaceAspect
     * @returns {Boolean}
     */
    getPlaceAspect() {
      return false;
    }

    initialize(game) {
      super.initialize(game);

      // add to list of rooms, which we use for "go to room"
      this.game.room_lookup.push(this.id);

      return true;
    }

    /**
     * Get this.exits.
     * @memberOf adventurejs.Room
     * @method adventurejs.Room#$exits
     * @returns {Boolean}
     */
    $exits() {
      return this.exits;
    }

    /**
     * Get a list of directions leaving this room.
     * @memberOf adventurejs.Room
     * @method adventurejs.Room#$directions
     * @returns {Boolean}
     */
    $directions() {
      return Object.keys(this.exits);
    }
  }

  adventurejs.Room = Room;
})();