// Game.js
(function () {
/* global adventurejs A */
/**
* @class adventurejs.Game
* @ajsconstruct var MyGame = new adventurejs.Game( "MyGame", "MyGameDisplay" )
* @ajsnavheading FrameworkClasses
* @summary Constructs a new game object.
* @keywords append output, prepend output, overwrite output
* @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>.
* @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";
/**
* An enum for storing game_states values.
* @var {Object} adventurejs.Game#game_states
* @default {}
*/
this.game_states = {
STATIC: -1,
CONSTRUCTION: 0,
VALIDATE_ASSETS: 1,
CREATE_DEFERRED_ASSETS: 2,
INITIALIZE_ASSETS: 3,
SORT_LOOKUP_VALUES: 4,
SET_DEFAULT_ASSETS: 5,
INIT_PLAYER: 6,
SAVE_BASELINE: 7,
PLAYING: 8,
};
this.game_state = this.game_states.CONSTRUCTION;
/**
* A place to store arbitrary game state variables, such as
* whether the statusbar should be hidden/shown.
* @var {Object} adventurejs.Game#state_vars
* @default {}
*/
this.state_vars = {};
// knows_about and has_seen are used for temporary knowledge during init
// knowledge will be transferred to player
this.knows_about = {}; // ok
this.has_seen = {};
/**
* 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;
// attach a reference to our clearPrompt function to window
// so that we can add/remove event listeners as needed
window[`${this.game_name}_clearPrompt`] = this.game.clearPrompt.bind(
this.game
);
/**
* An object used to manage javascript events.
* @var {Object} adventurejs.Game#reactor
* @default {}
*/
this.reactor = new adventurejs.Reactor(this);
this.reactor.addEventListener("inputParseComplete", (e) => {
const game = e.detail.game; // safe alias
game.log(
"L1002",
"log",
"high",
"Event > inputParseComplete",
"events"
);
});
/**
* <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("L1004", "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 _room 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!_vars
*/
this.world._vars = {};
this.world._game_name = this.game_name;
this.world._room = "";
this.world._player = "";
this.world._version = "";
this.world._title = "";
this.world._author = "";
this.world._score = "";
this.world._ifid = "";
this.world._timestamp = new Date().getTime().toString();
this.world._intervals = {};
this.NPCs = {};
this.pre_scripts = {};
this.post_scripts = {};
this.UIDs = {};
this.vessels = {};
/**
* <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>longest_key</code> stores the world_lookup
* key with the highest word count, which is used
* for iterating through world_lookup from longest
* to shortest.
* @var adventurejs.Game#longest_key
*/
this.longest_key = 0;
/**
* <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();
// create display
this.display = new adventurejs.Display(displayElId, this);
// create print queue
this.print_queue = [];
this.enqueue_output = false;
// create parser
this.parser = new adventurejs.Parser(this).set({ display: this.display });
// create asset manager
// this.assetManager = new adventurejs.AssetManager(this);
// check whether this browser supports the methods
// needed for save/restore of local files
this.fileReaderUnsupported =
"undefined" === typeof window.File ||
"undefined" === typeof window.FileReader ||
"undefined" === typeof window.FileList ||
"undefined" === typeof window.Blob;
var a = document.createElement("a");
this.downloadUnsupported = "undefined" === typeof a.download;
if (this.fileReaderUnsupported || this.downloadUnsupported) {
this.display.displayEl.classList.add("ajs-file-reader-unsupported");
}
// create save manager
this.saveManager = new adventurejs.SaveManager(this);
// create restore manager
this.restoreManager = new adventurejs.RestoreManager(this);
// create goal manager
this.goalManager = new adventurejs.GoalManager(this);
// create hint manager
this.hintManager = new adventurejs.HintManager(this);
// create user manager
this.userManager = new adventurejs.UserManager(this);
// create global objects
this.createAsset({ class: "All", name: "all" });
this.createAsset({ class: "Void", name: "void" });
this.createAsset({ class: "Null", name: "null" });
this.createGlobalConcepts();
this.createGlobalExits();
this.createGlobalWalls();
this.createGlobalFloors();
this.createGlobalCeilings();
this.createGlobalScenery();
/**
* <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>hintcard</code> is a singleton class
* that stores all the game's hint data
* and functions for managing hints.
* @var {Object} adventurejs.Game#hintcard
*/
this.hintcard = new adventurejs.Hintcard(this);
/**
* <code>goalcard</code> is a singleton class
* that stores all the game's goal data
* and functions for managing goals.
* @var {Object} adventurejs.Game#goalcard
*/
this.goalcard = new adventurejs.Goalcard(this);
/**
* <code>introcard</code> is a singleton class
* used for printing the game's intro screen.
* @var {Object} adventurejs.Game#introcard
*/
this.introcard = new adventurejs.Introcard(this);
/**
* <code>outrocard</code> is a singleton class
* used for printing the game's outro screen.
* @var {Object} adventurejs.Game#outrocard
*/
this.outrocard = new adventurejs.Outrocard(this);
this.clear_on_continue = false;
this.deferredObjects = [];
this.description = "";
this.world_history = [];
/**
* If <code>doStartGame</code> is set to a function,
* it will be called when the game starts. Provided
* to allow authors to call arbitrary code on game start.
* @var {Object} adventurejs.Game#doStartGame
*/
this.doStartGame = null;
return this;
}
// unused
initialize(params) {
return this;
}
/**
* Takes an input and immediately forwards it to the parser.
* @memberOf adventurejs.Game
* @method adventurejs.Game#sendToParser
*/
sendToParser(value) {
this.reactor.emit("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();
}
/**
* 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;
}
// 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#room
* @deprecated Use game.getRoom() instead.
*/
get room() {
return this.world[this.world._room];
}
/**
* 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
createParser(parser) {
return this.parser.createParser(parser);
}
enableParser(parser) {
return this.parser.enableParser(parser);
}
disableParser(parser) {
return this.parser.disableParser(parser);
}
/**
* Convenience method is a shortcut to
* game.parser.input_history[ 0 ].appendOutput().
* Provides a method for author to append custom
* text after default output for the turn.
* @memberof adventurejs.Game
* @method adventurejs.Game#appendOutput
* @param {String} msg
* @returns {boolean}
*/
appendOutput(msg) {
return this.getInput().appendOutput(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#overrideOutput
* @param {String} msg
* @returns {boolean}
*/
overrideOutput(msg) {
return this.getInput().overrideOutput(msg);
}
/**
* This convenience method is a shortcut to
* game.parser.input_history[ 0 ].prependOutput().
* Provides a method for author to prepend custom
* text before default output for the turn.
* @memberof adventurejs.Game
* @method adventurejs.Game#prependOutput
* @param {String} msg
* @returns {boolean}
*/
prependOutput(msg) {
return this.getInput().prependOutput(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);
}
/**
* Look for a common word in the dictionary.
* This convenience method is a shortcut to
* {@link adventurejs.Dictionary#hasCommonWord|game.dictionary.hasCommonWord()}.
* @memberof adventurejs.Game
* @method adventurejs.Game#hasCommonWord
* @param {string} word
* @returns {boolean}
*/
hasCommonWord(word) {
return this.dictionary.hasCommonWord(word);
}
/**
* 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) {
return this.dictionary.replaceVerb(oldVerb, 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) {
return this.dictionary.combineVerbs(verbs, intoVerb);
}
/**
* This convenience method is a shortcut to
* {@link adventurejs.Dictionary#modifyVerb|game.dictionary.modifyVerb()} ).
* @memberof adventurejs.Game
* @method adventurejs.Game#modifyVerb
* @param {object} verb
* @returns {object}
*/
modifyVerb(verb) {
return this.dictionary.modifyVerb(verb);
}
/**
* This convenience method is a shortcut to
* {@link adventurejs.Dictionary#addPronouns|game.dictionary.addPronouns()}.
* @memberof adventurejs.Game
* @method adventurejs.Game#addPronouns
* @param {object} oldVerb
* @param {object} newVerb
* @returns {object}
*/
addPronouns(pronouns) {
return this.dictionary.addPronouns(pronouns);
}
// DISPLAY SHORTCUT METHODS FOR AUTHORS
/**
* This convenience method is a shortcut to
* {@link adventurejs.Display#hideStatusbar|game.display.hideStatusbar()}.
* @memberof adventurejs.Game
* @method adventurejs.Game#hideStatusbar
* @returns {boolean}
*/
hideStatusbar() {
return this.display.hideStatusbar();
}
/**
* This convenience method is a shortcut to
* {@link adventurejs.Display#hideStatusbar|game.display.showStatusbar()}.
* @memberof adventurejs.Game
* @method adventurejs.Game#showStatusbar
* @returns {boolean}
*/
showStatusbar() {
return this.display.showStatusbar();
}
//
/**
* idleNPCs performs actions or prints messages defined in
* NPC.idle, for NPCs in the current room.
* Properties can be strings or arrays or functions.
* @memberof adventurejs.Game
* @method adventurejs.Game#idleNPCs
*/
idleNPCs() {
this.game.log("L1615", "log", "high", `[Game.js] idleNPCs()`, "Game");
var game = this;
let input = game.getInput();
Object.keys(this.NPCs).forEach(function (id) {
const npc = game.getAsset(id);
// npc must be present to idle
if (!npc.is.present) return;
// npc can't have been this turn's subject
if (npc.id === game.getInput().getSubject().id) return;
// npc can't have been this turn's object
if (input.isObject(npc.id)) return;
// has npc got an do_idle string or doIdle function?
if ("function" === typeof npc.doIdle) {
return npc.doIdle();
} else if ("undefined" !== typeof npc.do_idle) {
if (game.settings.quiet && game.settings.respect_quiet) return;
let results = A.getSAF.call(game, npc.do_idle, npc);
if ("string" === typeof results) game.print(results);
}
});
}
/**
* callPreScripts calls any arbitrary functions defined by the author.
* @memberof adventurejs.Game
* @method adventurejs.Game#callPreScripts
*/
callPreScripts(input) {
this.game.log(
"L1613",
"log",
"high",
`[Game.js] callPreScripts()`,
"Game"
);
var game = this;
Object.keys(this.pre_scripts).forEach(function (id) {
game.pre_scripts[id](input);
});
}
/**
* callPostScripts calls any arbitrary functions defined by the author.
* @memberof adventurejs.Game
* @method adventurejs.Game#callPostScripts
*/
callPostScripts() {
this.game.log(
"L1614",
"log",
"high",
`[Game.js] callPostScripts()`,
"Game"
);
var game = this;
Object.keys(this.post_scripts).forEach(function (id) {
game.post_scripts[id]();
});
}
/**
* endTurn calls methods to update displays and print per-turn
* statements.
* @memberof adventurejs.Game
* @method adventurejs.Game#endTurn
*/
endTurn() {
this.game.log("L1611", "log", "high", `[Game.js] endTurn()`, "Game");
this.display.update();
this.printAreaEvents();
this.callIntervals();
this.idleNPCs();
this.callPostScripts();
this.reactor.emit("inputParseComplete");
}
/**
* midTurn calls methods to update displays during queued verb actions.
* @memberof adventurejs.Game
* @method adventurejs.Game#midTurn
*/
midTurn() {
this.game.log("L1612", "log", "high", `[Game.js] midTurn()`, "Game");
this.display.updateInventoryDocks();
this.display.updateCompasses();
}
/**
* This convenience property is a shortcut to game.world._vars,
* which is a container for any game 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#vars
*/
get vars() {
return this.world._vars;
}
/**
* Get a custom var stored in this.world._vars. Vars stored
* here are written to save files.
* @memberof adventurejs.Game
* @method adventurejs.Game#getVar
*/
getVar(key) {
if ("undefined" === typeof this.world._vars[key]) return null;
return this.world._vars[key];
}
/**
* Set a custom var stored in this.world._vars. Vars stored
* here are written to save files.
* @memberof adventurejs.Game
* @method adventurejs.Game#setVar
*/
setVar(key, value) {
if ("object" === typeof value) {
let msg = `Game.setVar() received a value that is not a primitive. `;
this.game.log("L1578", "warn", 0, msg, "Game");
return false;
}
this.world._vars[key] = value;
}
/**
* Increment a custom var stored in this.world._vars. Default value is 1.
* Vars stored here are written to save files.
* @memberof adventurejs.Game
* @method adventurejs.Game#incVar
*/
incVar(key, value = 1) {
if (
"undefined" === typeof this.world._vars[key] ||
isNaN(this.world._vars[key])
) {
let msg = `Game.incVar() called on a var that is not a number. `;
this.game.log("L1624", "warn", 0, msg, "Game");
return false;
}
if (isNaN(value)) {
let msg = `Game.incVar() received a value that is not a number. `;
this.game.log("L1623", "warn", 0, msg, "Game");
return false;
}
this.world._vars[key] = this.world._vars[key] + value;
}
/**
* Decrement a custom var stored in this.world._vars. Default value is 1.
* Vars stored here are written to save files.
* @memberof adventurejs.Game
* @method adventurejs.Game#decVar
*/
decVar(key, value = 1) {
if (
"undefined" === typeof this.world._vars[key] ||
isNaN(this.world._vars[key])
) {
let msg = `Game.decVar() called on a var that is not a number. `;
this.game.log("L1625", "warn", 0, msg, "Game");
return false;
}
if (isNaN(value)) {
let msg = `Game.decVar() received a value that is not a number. `;
this.game.log("L1626", "warn", 0, msg, "Game");
return false;
}
this.world._vars[key] = this.world._vars[key] + value;
}
/**
* Test if there is a custom var stored in this.world._vars.
* Vars stored here are written to save files.
* @memberof adventurejs.Game
* @method adventurejs.Game#getVar
*/
hasVar(key) {
return "undefined" !== typeof this.world._vars[key];
}
/**
* Swap the active player character. If a room is passed,
* new player is moved to that room. If no room is passed
* and player is in a room, that room is set to current room.
* If no room is passed and player is not in a room, player
* is moved to current room.
* @memberof adventurejs.Game
* @method adventurejs.Game#setPlayer
* @param {*} new_player id or object representing new player
* @returns {Object}
*/
setPlayer(new_player, room = null) {
if ("string" === typeof new_player) {
new_player = this.getAsset(new_player);
}
if (!new_player.id || !new_player.hasClass("Player")) {
return null;
}
// deactivate current player
this.getPlayer().is.active = false;
// activate new player
new_player.is.active = true;
// save reference back to game
this.world._player = new_player.id;
// update world_lookup for me/myself
this.world_lookup.me.IDs[0] = new_player.id;
this.world_lookup.myself.IDs[0] = new_player.id;
// if method was called without room, use new_player location
if (!room) {
room = new_player.getRoomAsset();
}
// if new_player has no location, use current room
if (!room) {
room = this.getRoom();
}
this.world._room = room.id;
// update this turn's input to refer to new player
this.getInput().setSubject(new_player);
this.setRoom(room);
return new_player;
}
}
var p = Game.prototype;
p.$setVar = p.setVar;
p.$getVar = p.getVar;
p.$hasVar = p.hasVar;
adventurejs.Game = Game;
})();