// 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;
})();