// Exit.js
(function () {
/*global adventurejs A*/
"use strict";
/**
* @augments adventurejs.Tangible
* @class adventurejs.Exit
* @ajsconstruct MyGame.createAsset({ <br> "class":"Exit", <br> "direction":"x", <br> "place":{in:"room a"},<br> "destination":"room b",<br> [...]<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">> 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;
})();