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