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

  /**
   * @augments adventurejs.Tangible
   * @class adventurejs.Exit
   * @ajsconstruct MyGame.createAsset({ <br>&nbsp;&nbsp;"class":"Exit", <br>&nbsp;&nbsp;"direction":"x", <br>&nbsp;&nbsp;"place":{in:"room a"},<br>&nbsp;&nbsp;"destination":"room b",<br>&nbsp;&nbsp;[...]<br> })
   * @ajsconstructedby adventurejs.Game#createAsset
   * @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.
   * @ajsnavheading DoorExitClasses
   * @summary Makes travel between Rooms possible.
   * @tutorial CreateExit
   * @tutorial CreateKey
   * @classdesc
   * <p>
   * <strong>Exit</strong> allows travel between
   * {@link adventurejs.Room|Rooms}.
   * An Exit must be assigned a
   * location, a direction, and a destination.
   * Unlike other classes, no name is required.
   * Though Exits are created via
   * <a href="/doc/adventurejs.Game.html#createAsset">Game.createAsset()</a>
   * like other
   * {@link adventurejs.Asset|Assets},
   * they are unique in that they don't take a
   * <a href="#name">name</a>
   * to generate an
   * <a href="#id">id</a>.
   * Rather, an id is generated from
   * the exit's location and direction.
   * </p>
   * <p>
   * Exits have no physical properties of their own. By default,
   * they allow travel with no special actions from the player.
   * If there's a north exit, typing
   * <code class="property">go north</code>
   * take the player north.
   * To add physical elements to an Exit, it must be paired with an
   * {@link adventurejs.Aperture|Aperture},
   * which is a class that includes subclasses such as lockable
   * {@link adventurejs.Door|Doors} and
   * {@link adventurejs.Window|Windows}, etc.
   * Apertures provide
   * {@link adventurejs.Tangible|Tangible}
   * Assets
   * with all the usual methods for physical interactions.
   * </p>
   * <p>
   * Exits are one-way. To create a two-way passage, you'll need
   * to create two Exits, one in each Room. Ordinary Exits
   * are singular, meaning that they only exist in the Room
   * they are assigned to.
   * </p>
   * <p>
   * The exception to the singular rule are GlobalExits.
   * GlobalExits are a set of predefined Exits with their
   * <code class="property">{@link adventurejs.Asset#is!global|is.global}</code>
   * property set to true.
   * GlobalExits capture direction inquiries
   * in rooms without exits in those directions.
   * For example, if a player inputs
   * <code class="property">go north</code>
   * in a Room with no north Exit, Game will return
   * the description for {@link global_north},
   * which should be some version of
   * <code class="property">You don't see any exit to the north.</code>
   * GlobalExit descriptions can be customized,
   * so you can write a different snarky comment
   * for each direction, if you so desire. To learn more about
   * customizing GlobalExits, see
   * <a href="/doc/NextSteps_GlobalScenery.html">Global Scenery</a>.
   * </p>
   * <p>
   * But let's say you want to provide Room-specific messages
   * when a player tries to travel in a wrong direction. Maybe
   * you've built a maze and you want to provide hints. That's
   * possible too, by creating Exits with no destination. When
   * a player tries to travel, only a description is returned.
   * Keep reading to see examples of this below.
   * </p>
   * <h3 class="examples">Example:</h3>
   * <p>
   * There are two ways to create an Exit.
   * If you don't need an Aperture, you can use this
   * simple shortcut: define a Room with an
   * <code class="property">exits</code> property,
   * with nested properties for each direction you
   * want to place an Exit. Set each
   * <a class="external"
   * href="https://www.w3schools.com/Js/js_json_objects.asp">key/value pair</a>
   * to a direction / a name of a Room to travel to.
   * This provides the minimum information for the game
   * to automatically construct new Exits at runtime.
   * In this example, we define a Room
   * called "Field of Dreams" with Exits to north and south.
   * This example assumes that there are additional Rooms called
   * "Corn Field" and "Front Yard".
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "Field of Dreams",
   *   descriptions: { look: "If you build it, he will come. ", },
   *   exits: {
   *     north: "Corn Field"
   *     south: "Front Yard",
   *   }
   * });
   * </code></pre>
   * <p>
   * This method can also be used to create non-functioning
   * exits that return an error when a player tries to use a
   * non-existent exit. Instead of providing a Room name,
   * simply provide a string to return as a message. This
   * method can provide useful feedback to players. (This
   * provides results that are very similar to setting custom
   * GlobalExit messages as mentioned above. It's just
   * another way of doing it.)
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "Middle Beach",
   *   descriptions: {
   *     look: {
   *       "A sandy strip between a rough tide to the
   *       east and a rocky sea wall to the west. You can walk north
   *       or south along the water's edge. ",
   *     },
   *   },
   *   exits: {
   *     north: "North Beach",
   *     south: "South Beach",
   *     east: "The rough tides turn you back. ",
   *     west: "A towering rock wall blocks passage to the west. ",
   *   },
   * });
   * </code></pre>
   * <p>
   * This results in an interaction like the following example.
   * </p>
   * <pre class="display border outline">
   * <span class="input">&gt; go west</span>
   * A towering rock wall blocks passage to the west.
   * </pre>
   * <hr>
   * <p>
   * Alternately, Exits can be defined like other distinct Assets.
   * In this example, we create a Room, an Exit, a lockable Aperture,
   * and a {@link adventurejs.Key|Key}. Note that the Exit is
   * given a direction, a location, and a destination, and a
   * name for the Aperture it should be linked with. The main benefit
   * of this arrangement is that it provides more granular control
   * over the Exit details.
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *   class: "Room",
   *   name: "Ice King's Living Room",
   *   descriptions: {
   *     look: "It's the Ice King's Living Room. Man that guy is a slob.
   *     No wonder he can't get a Princess. Schmowza! ",
   *   },
   * });
   *
   * MyGame.createAsset({
   *   class: "Exit",
   *   direction: "north",
   *   place: { in: "Ice King's Living Room" },
   *   destination: "Ice King's Boudoir",
   *   aperture: "icy door",
   *   descriptions: {
   *     look: "It looks frosty that way. Brrr. ",
   *     for_exits_list: "north to the Boudoir",
   *   }
   * });
   *
   * MyGame.createAsset({
   *   class: "Door",
   *   name: "icy door",
   *   adjectives: "icy, north",
   *   synonyms: ["icy north door"],
   *   place: { in: "Ice King's Living Room" },
   *   direction: "north",
   *   descriptions: {
   *     look: "The icy door is very slightly translucent.
   *     You can't see through it, but it has a bit of a glow. ",
   *     open: "The icy door is open. ",
   *     closed: "The icy door is closed. ",
   *     touch: "The icy door is cold to the touch. ",
   *     through: function()
   *     {
   *       if( MyGame.$( "icy door" ).$is("open") )
   *       {
   *         return "Through the open door you can see
   *         the Ice King's Boudoir. "
   *       }
   *       else
   *       {
   *         return "Though the closed door appears to be
   *         very slightly translucent, you can't see through it. "
   *       }
   *     },
   *   }
   *   dov: {
   *     unlock: { with_assets: ['glass key'], },
   *   },
   *   is: {
   *         closed: true,
   *         locked: true
   *   },
   *   linked_asset: "boudoir door",
   * });
   *
   * MyGame.createAsset({
   *   class: "Key",
   *   name: "Boudoir key",
   *   article: "the",
   *   opacity: .5,
   *   iov: {
   *     unlock: { then_destroy: { on_success: 'The fragile key crumbles into pieces after use. ' }, },
   *   },
   * });
   * </code></pre>
   * <p>
   * As with the simpler method,
   * it's also possible to construct a non-functioning Exit
   * that returns a message when a player tries to go that way.
   * Though the outcome is the same as with the simpler method,
   * you might find that this method provides more granular
   * control over the Exit Asset.
   * </p>
   * <pre class="display"><code class="language-javascript">MyGame.createAsset({
   *  class: "Room",
   *  name: "Pantheon",
   *  descriptions: {
   *    look: "One of the most inspiring sites in Rome. ",
   *  ),
   *  exits: {
   *    north: "Piazza della Rotonda",
   *  },
   * });
   *
   * MyGame.createAsset({
   *   class: "Exit",
   *   name: "hole in the roof",
   *   place: { in: "Pantheon" },
   *   direction: "up",
   *   descriptions: {
   *     look: {
   *       "You see a dot of blue sky through a
   *       tiny hole in the roof, but you could never reach it. ",
   *       split_name_for_world_lookup: false,
   *     },
   *   },
   * });
   * </code></pre>
   * <p>
   * It's ok to mix the two methods. For instance a Room might
   * have one exit with an Aperture and another without.
   * It's fine to construct an Exit instance to go with the
   * Aperture, and use the simple method for the other Exit.
   * To learn more about Apertures and Exits, see
   * <a href="/doc/GetStarted_CreateAnExit.html">Create an Exit</a>.
   * </p>
   **/
  class Exit extends adventurejs.Tangible {
    constructor(name, game_name) {
      super(name, game_name);
      this.class = "Exit";

      this.is = new adventurejs.Exit_Is("is", this.game_name, this.id).set({
        parent_id: this.id,
      });

      this.singlePluralPairs = [
        ["exit", "exits"],
        ["passage", "passages"],
      ];
      //this.name = this.direction + " passage";

      // TODO I had this excluded and can't remember specifically why
      // but then disabled because of stoomphing_room exit "hole" in ground
      //this.exclude_from_lookup = true;

      this.is.listed_in_room = false;
      this.split_name_for_world_lookup = false;

      /**
       * Exit instances aren't named in the usual way. Instead
       * they're given an id that is parent room + direction.
       * @nestedproperty
       * @var {Boolean} adventurejs.Exit#is!nameless
       * @default false
       */
      this.is.nameless = true;

      /**
       * Set an ID representing an Aperture. Used chiefly for Exit class to set a
       * physical component corresponding to the exit.
       * @var {String} adventurejs.Tangible#aperture
       * @default ""
       */
      this.aperture = ""; // for use with exits

      //this.is.known = false;
      //this.is.seen = false;
      //this.is.used = false;
    }

    initialize(game) {
      super.initialize(game);
      return this;
    }

    validate(game) {
      super.validate(game);
      /**
       * The simple way to define an exit's destination
       * is to set destination to a Room name.
       * If we find a destination,
       * we save it to destinationName and Serialize it to destinationID.
       *
       * It is also permissible to leave destination empty,
       * in which case trying to exit in this direction returns a description.
       *
       * In order to support more advanced features such as
       * randomized destination or custom functions that return
       * different destinations depending on state, we invite
       * authors to set destinationID directly. This means we
       * need to check destinationID for advanced methods.
       *
       */

      // check the exit's direction
      if (!this.direction && !this.is.global) {
        var msg = "Exit " + this.name + "has no direction.";
        //console.error( msg );
        this.game.log("error", "critical", msg, "Exit");
        return false;
      }

      if (!this.name) {
        this.name = this.direction + " passage";
      }

      // add all synonyms for this exit's direction, as adjectives
      // so player can refer to, eg, "east exit" or "e exit"
      //console.warn( "this.direction" );
      //console.warn( this );
      //console.warn( this.direction );
      var directionSynonyms =
        this.game.dictionary.directionLookup[this.direction].synonyms;
      for (var i = 0; i < directionSynonyms.length; i++) {
        this.adjectives.push(directionSynonyms[i]);
      }

      // also add direction adjectives so player can refer to, eg, "eastern exit"
      var directionAdjectives =
        this.game.dictionary.directionLookup[this.direction].adjectives;
      for (var i = 0; i < directionAdjectives.length; i++) {
        this.adjectives.push(directionAdjectives[i]);
      }

      /**
       * Global exits are a special case which only exist to catch
       * direction queries in rooms with no query matches.
       * They don't need destination or location or aperture,
       * so we're done validating.
       */
      if (this.is.global) {
        return true;
      }

      /**
       * The author hasn't set a destination.
       * This is permissible and the exit will be treated
       * as a description if player attempts to travel.
       */
      if (!this.destination) {
        var msg =
          "Exit.js > " +
          this.id +
          " has no destination. " +
          "Treating it as a non-travel description rather than an exit.";
        this.game.log("warn", "critical", msg, "Exit");
        //console.warn( msg );
      } else if (
        /**
         * Author provided a string in destination so Serialize it for an ID.
         */
        "string" === typeof this.destination &&
        "" !== this.destination
      ) {
        this.destinationName = this.destination;
        this.destinationID = A.serialize(this.destination);
      }

      /**
       * If destination is not an array or a function
       * or a non-travel string,
       * by now we should have a serialized destinationID,
       * so validate the destination.
       */
      if (!this.destination) {
        // do nothing - we've accepted exit as a non-travel string
      } else if ("string" === typeof this.destinationID) {
        var destinationObject = this.game.getAsset(this.destinationID);
        if (!destinationObject) {
          var msg = `Exit ${this.name}'s destination ${this.destinationID} doesn't map to a valid object. `;
          this.game.log("error", "critical", msg, "Exit");
          //console.error( msg );
          return false;
        }

        if (false === destinationObject instanceof adventurejs.Room) {
          var msg =
            "Exit " +
            this.name +
            "'s destination is set to " +
            destinationObject.name +
            ", which is a " +
            destinationObject.constructor.name +
            " rather than a Room.";
          this.game.log("error", "critical", msg, "Exit");
          //console.error( msg );
          return false;
        }

        //this.destination = destinationObject;
        this.destinationID = destinationObject.id;
        // we should be good to go on
      } // if( "string" === typeof this.destinationID )

      /**
       * destination can also be an array,
       * which needs to have each item validated.
       */
      else if (Array.isArray(this.destination)) {
        for (var i = 0; i < this.destination; i++) {
          /**
           * Each item in an array must be a string,
           * and we assume each one represents a unique Room.
           */
          if (
            "string" !== typeof this.destination[i] ||
            "" === this.destination[i]
          ) {
            var msg =
              "Exit " +
              this.name +
              "'s destination is an array, and item " +
              i +
              " is either empty or not a string.";
            this.game.log("error", "critical", msg, "Exit");
            //console.error( msg );
            return false;
          }

          // keep a copy of the original for error messaging
          var inputID = this.destination[i];

          // make sure it's serialized
          this.destination[i] = A.serialize(this.destination[i]);

          var destinationObject = this.game.getAsset(this.destination[i]);

          if (!destinationObject) {
            var msg =
              "Exit " +
              this.name +
              "'s destination is an array, and item " +
              i +
              ", " +
              inputID +
              " is invalid.";
            this.game.log("error", "critical", msg, "Exit");
            //console.error( msg );
            return false;
          }

          if (false === destinationObject instanceof adventurejs.Room) {
            var msg =
              "Exit " +
              this.name +
              "'s destination is an array, and item " +
              i +
              ", " +
              inputID +
              " refers to " +
              destinationObject.name +
              " which is a " +
              destinationObject.constructor.name +
              " rather than a Room.";
            this.game.log("error", "critical", msg, "Exit");
            //console.error( msg );
            return false;
          }
        } // for( var i = 0; i < destinationID

        /**
         * We're leaving this.destination as provided by author
         * because it's a valid array.
         * We should be good to proceed.
         */
      } // if( Array.isArray( destinationID )

      /**
       * Destination can also be a custom function,
       * which needs to be validated.
       */
      else if ("function" === typeof this.destination) {
        /**
         * Try calling the function.
         * This is tricky because while we can require
         * that custom functions return a string,
         * they may depend on state which isn't available prior to gameplay.
         */
        var test = this.destination();

        if ("string" !== typeof test) {
          var msg =
            "Exit " +
            this.name +
            "'s destination is a function " +
            "that returns something other than a string.";
          this.game.log("error", "critical", msg, "Exit");
          //console.error( msg );
          return false;
        }

        if ("string" === typeof test) {
          var testinput = test;
          test = A.serialize(test);
          var destinationObject = this.game.getAsset(test);

          if (!destinationObject) {
            var msg =
              "Exit " +
              this.name +
              "'s destination is a function that returns " +
              testinput +
              ", which  is invalid. Allowing this to validate, " +
              "but be sure your function works as you expect.";
            this.game.log("warn", "critical", msg, "Exit");
            //console.warn( msg );
            // don't return false and hope author knows what they're doing
          }

          if (false === destinationObject instanceof adventurejs.Room) {
            var msg =
              "Exit " +
              this.name +
              "'s destination is a function that returns " +
              testinput +
              ", " +
              " which refers to " +
              destinationObject.name +
              " which is a " +
              destinationObject.constructor.name +
              " rather than a Room. Allowing this to validate, " +
              "but be sure your function works as you expect.";
            this.game.log("warn", "critical", msg, "Exit");
            //console.warn( msg );
            // don't return false and hope author knows what they're doing
          }
        } // if( "string" === typeof test )

        /**
         * As long as it returns a string,
         * we're leaving this.destination() as provided by author.
         * We don't know for sure that it's a valid function
         * but we've done our best to warn the author.
         */
      } // else if( "function" === typeof this.destination )

      /**
       * If place has not been set...
       */
      if (!this.place || Object.keys(this.place).length === 0) {
        var msg = "Exit " + this.name + "'s location is undefined.";
        this.game.log("error", "critical", msg, "Exit");
        //console.error( msg );
        return false;
      }

      /**
       * place set to something other than a string?
       */
      if ("string" !== typeof this.place[Object.keys(this.place)[0]]) {
        var msg = "Exit " + this.name + "'s location is not a string.";
        this.game.log("error", "critical", msg, "Exit");
        //console.error( msg );
        return false;
      }

      // save a temp copy of the original string for error messaging
      var myplace = this.getPlaceAssetId();

      // get the object referred to by the ID
      var placeAsset = this.game.getAsset(myplace);

      // no such object
      if (!placeAsset) {
        var msg =
          "Exit " + this.name + "'s location " + myplace + " is invalid.";
        this.game.log("error", "critical", msg, "Exit");
        return false;
      }

      // object isn't a Room
      if (!(placeAsset instanceof adventurejs.Room)) {
        var msg =
          "Exit " +
          this.name +
          "'s location refers to " +
          placeAsset.name +
          " which is a " +
          placeAsset.constructor.name +
          " rather than a Room.";
        this.game.log("error", "critical", msg, "Exit");
        return false;
      }

      /**
       * Oh yeah and also check the aperture.
       * An exit is not required to have an aperture,
       * though an aperture is required to have an exit,
       * and each will ensure that the other has a reference to it.
       */
      if (this.aperture && "string" !== typeof this.aperture) {
        var msg =
          "Exit " +
          this.name +
          "'s aperture " +
          this.aperture +
          " is not a string.";
        this.game.log("error", "critical", msg, "Exit");
        return false;
      }

      // no such object
      if (this.aperture) {
        var inputAperture = this.aperture;
        this.aperture = A.serialize(this.aperture);
        var apertureObject = this.game.getAsset(this.aperture);

        if (!apertureObject) {
          var msg =
            "Exit " +
            this.name +
            "'s aperture " +
            inputAperture +
            " is invalid.";
          this.game.log("error", "critical", msg, "Exit");
          return false;
        }

        // not actually an Aperture
        if (!(apertureObject instanceof adventurejs.Aperture)) {
          var msg =
            "Exit " +
            this.name +
            "'s aperture " +
            apertureObject.name +
            " is a " +
            placeAsset.constructor.name +
            " rather than an Aperture.";
          this.game.log("error", "critical", msg, "Exit");
          return false;
        }

        // looks like we have a valid aperture
        //this.aperture = apertureObject;
        // no longer saving object - now keeping string

        // looks like we're good to go with aperture
        //this.aperture.exit = this;
        // no longer saving object - now keeping string
        apertureObject.exit = this.id;
      }

      // BOOM! Add it to its room's exits object.
      //placeAsset.exits[ this.direction ] = this; // EtoS
      placeAsset.exits[this.direction] = this.id;

      return this;
    }

    /**
     * Get a description, such as "look in book", where
     * "in" has been defined as a key at
     * asset.descriptions.in.
     * "look" is always the default description.
     * This is modified from asset.getDescription to allow
     * exit descriptions to be stored on their apertures.
     * @memberOf adventurejs.Asset
     * @method adventurejs.Asset#getDescription
     * @param {String} description
     * @return {String}
     */
    getDescription(description) {
      console.warn("Exit getDescription");
      description = description || "look";

      if (this.aperture) {
        let aperture = this.game.getAsset(this.aperture);
        if (
          aperture &&
          aperture.descriptions &&
          aperture.descriptions[description]
        ) {
          return aperture.getDescription(description);
        }
      }

      if (!this.descriptions[description]) description = "look";
      if (this.descriptions[description]) {
        if (
          Array.isArray(this.descriptions[description]) ||
          "string" === typeof this.descriptions[description] ||
          "function" === typeof this.descriptions[description]
        ) {
          return A.getSAF.call(this.game, this.descriptions[description]);
        }

        if (
          "object" === typeof this.descriptions[description] &&
          this.descriptions[description].default
        ) {
          return A.getSAF.call(
            this.game,
            this.descriptions[description].default
          );
        }
      }

      return `${this.Articlename} is undescribed. `;
    }
  }
  adventurejs.Exit = Exit;
})();