adventure-engine
Version:
Simple text based adventure game library.
1,363 lines (1,240 loc) • 67 kB
JavaScript
//=========================================================
// adventure-engine.js: Primary module for library
//=========================================================
// _______ ___ _______ __ __ _______ __ __
// | || | | || | | || || | | |
// | ___|| | | ___|| |_| ||_ _|| |_| |
// | |___ | | | | __ | | | | | |
// | ___|| | | || || | | | |_ _|
// | |___ | | | |_| || _ | | | | |
// |_______||___| |_______||__| |__| |___| |___|
// _______ _______ ______ _______ __ __
// | || || _ | | || | | |
// | ___|| _ || | || |_ _|| |_| |
// | |___ | | | || |_||_ | | | |
// | ___|| |_| || __ | | | |_ _|
// | | | || | | | | | | |
// |___| |_______||___| |_| |___| |___|
// _______ ______ _______
// | || _ | | |
// | _ || | || | ___|
// | | | || |_||_ | | __
// ___ | |_| || __ || || | ___
// | | | || | | || |_| || |
// |___| |_______||___| |_||_______||___|
//
// Description:
// Defines API for adventure-engine, the text adventure game library.
//
// The game engine allows the player to move around the world that
// you create:
// - See example-world.json for more information on how to define the
// game world.
// - The built-in commands include:
// look, inspect, take, carrying, use, drop, and any exits you
// define as part of your game world.
// - Custom callback routines can be defined for room entry and
// thing use, allowing you to add functionality.
//
// Author: Brian Piltin
//
// Copyright: (C) 2019 Brian Piltin.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Version: 0.6.17
// History:
// 0.0.0: Initial version
// 0.0.1: Add automatic shortcut key generation.
// 0.1.0: (1st published)
// - Expand API to include adding and removing things.
// - Add generic callbacks for beforePrompt etc.
// 0.1.1:
// - Add readLine, println to public API
// - Refactor items into things
// - Add support for printing thing pictures
// 0.1.2:
// - Add clearScreen to public API
// 0.2.0:
// - Add commands object and command callbacks
// - Add automatic callback calling
// - Add onExit callback
// - Add getLastThingUsed, getLastThingInspected, getNextPlayerCommand
// to public API
// - Add generic callbacks with arguments for room and thing callbacks
// - Add isAlive and killThing callbacks
// 0.2.1:
// - Add public API functions to global namespace
// 0.2.2:
// - Add support for descriptions folder
// 0.2.3:
// - Add handle function
// - Add getInventory to public API
// 0.3.0:
// - Change callback naming scheme to use "on" for all functions
// 0.3.1:
// - Change beforePrompt to onPrompt
// - Change handle to use preposition instead of object
// - Fix exits string to not use shortcuts in description
// 0.4.0:
// - Add combat engine
// 0.5.0:
// - Modify public API to use objects instead of globals
// 0.5.1:
// - Remove callbacks from public API functions
// 0.5.2:
// - Add global searching to getThing public API function
// 0.5.3:
// - Add callback for expired weapons
// 0.5.4:
// - Add listAdjacentRooms, and getExitForRoom to public API of getRoom
// - Add high score loading and saving
// - Add templating to descriptions
// - Add addThing to room object API
// 0.5.5:
// - Add return value to delete exit from room
// - Add parameter to thing.expend()
// 0.6.0:
// - Add support for socket.io client/server
// 0.6.1:
// - Add support for serving multiple games
// - Add support for injecting global API into client code
// - Fix bug when adding async callback functions
// 0.6.2:
// - Add support for better error descriptions
// - Add support for player name
// - Fix printing of indefinite article
// - Fix game end by asking for restart
// 0.6.3:
// - Add support for playing synthesized music through terminal
// 0.6.4:
// - Add support for playing songs when entering rooms, etc.
// - Allow for client program to set port of server
// 0.6.5:
// - Add mute/unmute commands
// 0.6.6:
// - Fix misc. bugs
// 0.6.7:
// - Remove printPaused and rely on terminal for paused printing
// 0.6.8:
// - Add support for sub-menus
// - Remove printQ and refactor repl
// - Add support for inspectMe
// 0.6.9:
// - Add support for multiple high scores
// - Add support for high score printing and callback
// 0.6.10:
// - Replace template strings in print function for all screens but pictures
// - Add passing in thing object to use, inspect, take, drop callbacks
// - Add credits command to other menu
// 0.6.11:
// - Add "headless/serverless" capability for debugging purposes
// - Fix printRoomProperty methods for getThing and getRoom
// 0.6.12:
// - Remove unnecessary packages
// - Move server and client to src directory
// 0.6.13:
// - Fix package.json main path
// 0.6.14:
// - Fix location of adventure-engine.js
// 0.6.15:
// - Fix server paths to work as module on repl.it
// 0.6.16:
// - Fix terminal to display %n0 as 10 lines
// - Add support for default advertisement screen
// - Remove "play" function from public API
// 0.6.17:
// - Change advertisement.txt to title.txt for client screen
//
// TODO:
// - Fix inspecting "me"
// - Fix case sensitivty in world keys
// - Add multi-level menu (other)
// - Add callbacks for each command (onPunch) or onCommand(commandName)
// - Add an "onCommand" callback that executes when any command is run
// - Print pictures before callbacks/descriptions
// - Allow player to carry armor by adding to defense based on defense of things
// - Add shortcut keys for things when using or attacking
// - Allow player to inspect himlself by using "me" or "self"
// - Trigger callbacks from within object API calls
// - Handle collateral damage when non-attack use of item
// - Change name of "callbacks" to "handlers"
// - Create multi-level commands for example [o]ther..
// - Looking should give entire description of room minus picture?
// - Timeout old connections and remove from list
// - Set index.html page logo dynamically through library call
// - Create "graphics" mode for screen that "blits" ascii images
// - Fix prompt being printed twice after quit
// - Fix when room picture available in dual screen show blank
// - Rework callbacks to make more sense (onPrintPicture, onPlaySound)
// - Fix bug where background music stops playing when moving rooms
// - Fix template strings where pictures might replace inadvertently
// - Fix high scores printing to be horizontally centered on screen
// - Refactor
//=========================================================
"use strict";
const
fs = require('fs'),
_ = require('underscore'),
sc = require('./song-converter.js'),
utils = require('./utils.js');
let io;
//=========================================================
// Constants
//=========================================================
const
PAUSE_TOKEN = -1,
READ_TOKEN = -2;
//=========================================================
// Globals
//=========================================================
//---------------------------------------------------------
// Factory method for creating games from client code
//---------------------------------------------------------
let ConstructorTemplate;
//---------------------------------------------------------
// Assign a unique player id to each client
//---------------------------------------------------------
let connections = [];
//---------------------------------------------------------
// Game object
//---------------------------------------------------------
function newGame(socket, id, debug) {
//---------------------------------------------------------
// Game instance globals
//---------------------------------------------------------
let
world, // The world object
player, // The player object
callbacks, // Dictionary of client callbacks
commands, // Dictionary of client commands
shortcuts, // Dictionary of command shortcuts
templateStrings, // Dictionary of template strings
input, // Callback function for parsing input string,
playing, // Flag to control looping
terminalConfig ; // Current terminal configuration
//---------------------------------------------------------
// Functions available to client program, promoted globally
//---------------------------------------------------------
let globalAPI = {
getThisPlayerName,
getInventory,
getRoom,
getThing,
addCallback,
addCallbacks,
removeCallback,
addCommand,
removeCommand,
setTemplateString,
end,
readLine,
print,
println,
printPicture,
printDescription,
pause,
clearScreen,
getTerminalConfig,
setTerminalConfig,
playSong,
stop,
muteSound,
unmuteSound,
setLogo,
loadHighScores,
saveHighScore
}
//=========================================================
// Functions
//=========================================================
//=========================================================
// Main
//=========================================================
//---------------------------------------------------------
// Initialize the game. Load world file, set player locaiton
// add commands and create the client instance.
//---------------------------------------------------------
function init() {
callbacks = {};
commands = {};
shortcuts = {};
templateStrings = {};
input = undefined;
world = loadWorld();
player = initPlayer();
addCommands();
setTerminalConfig();
setLogo("logo.txt");
return globalAPI;
}
//---------------------------------------------------------
// Start the game by creating the client instance and entering repl.
//---------------------------------------------------------
async function start() {
socket.on("disconnect", function () {
console.log("Player " + id + " disconnected.");
});
socket.on("newline", function (str) { input && input(str); });
init();
await createClientInstance();
await welcome();
playing = true;
repl();
return { getSocket: function () { return socket; } }
}
//---------------------------------------------------------
// Rework the client game factory function by injecting the
// global API functions into the global namespace.
// Instatiate the game by eval()ing this new function object.
// TODO: see if there is a better way to do this so that
// the call stack can be used to trace errors.
//---------------------------------------------------------
async function createClientInstance() {
let
code = ConstructorTemplate.toString(),
injection = "var ";
// Store the name of the function for error reporting purposes
var funcName = code.match(/function\s(\w+)\(\w*\)\s\{/)[1];
// Cut out the original function
code = code.slice(code.indexOf("{") + 1, code.lastIndexOf("}"));
// Inject the modified code section
for (const key in globalAPI) {
injection += key + " = this." + key + ", ";
}
injection = injection.substr(0, injection.length - 2) + ";";
// Create a function oject from the modified code
let Constructor = new Function(injection + "\n" + code);
// TODO: get async version of function to work by wrapping all calls in
// async functions and then calling those functions inline. It's weird
// but it should work.
// const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
// Constructor = new AsyncFunction(injection + "\n" + code);
// Set the constructer function API for each new instance
// This breaks if the function is async because there is no prototype
for (const key in globalAPI) {
Constructor.prototype[key] = globalAPI[key];
}
// Eval the modified function
try {
await (new Constructor());
} catch (error) {
// TODO: Fix this up and try to determine the line number of the error
console.error(error.name + ": " + error.message + " when creating game" +
" in function " + funcName +
(error.lineNumber ? " in line " + error.lineNumber + "." : "."));
}
}
//---------------------------------------------------------
// Officially begin the game here and present the user with
// the command prompt.
//---------------------------------------------------------
async function welcome() {
stop();
if (readFile("./pictures/title.txt")) {
printPicture("title.txt");
} else {
print(readFile(ASSETS_DIR + "/pictures/advertisement.txt"), "pictures");
}
// TODO: The following line is necssary in order to begin playing music
// based on browser permissions...
await readLine("Press ENTER to BEGIN...");
if (!(await handle("before", "welcome"))) {
printRoomProperties("welcome");
}
await handle("after", "welcome");
}
function setLogo(textOrFilename) {
var file = readFile('./pictures/' + textOrFilename);
if (file) {
socket && socket.emit("setlogo", file);
}
}
//=========================================================
// Player
//=========================================================
//---------------------------------------------------------
// Sets the player's location regardless of current location.
// Can be used for teleportation.
// Parameter: the name for the destination room as a string.
//---------------------------------------------------------
async function setPlayerLocation(toRoomName) {
let fromRoomName = player.location;
// Player exits the current room
await handle("on", "exit", fromRoomName, toRoomName);
// There are three "entry" callbacks, before, on, and after
await handle("before", "enter", toRoomName, fromRoomName);
// Move the player in the world
world[toRoomName]["things"] = world[toRoomName]["things"] || {};
world[toRoomName]["things"][player.name] = player;
world[fromRoomName]["things"] && delete world[fromRoomName]["things"][player.name];
player.location = toRoomName;
// beforeEnter can override the printing of the description
if (!(await handle("on", "enter", toRoomName, fromRoomName))) {
printRoomProperty(player.location, "song");
look();
}
await handle("after", "enter", player.location, fromRoomName);
return player.location;
}
//=========================================================
// Rooms
//=========================================================
//---------------------------------------------------------
// Moves from one room to another given the direction.
// Also executes the callback function if there is one.
// Parameters:
// inDirection: the direction to move in as a string
// Return: the name of the new location, undefined if not found.
//---------------------------------------------------------
async function move(inDirection) {
for (const key in world[player.location]["exits"]) {
if (inDirection == key || shortcuts[inDirection] == key) {
let toRoomName = world[player.location]["exits"][key];
if (!(await handle("on", "move to", toRoomName, player.location))) {
return await setPlayerLocation(toRoomName);
} else {
return true;
}
}
}
}
//=========================================================
// Things
//=========================================================
//---------------------------------------------------------
// Gets the list of things as a printable string.
// Parameters:
// forRoomName: the name of the room to get the things for.
// Return: the things in this room as a printable string.
//---------------------------------------------------------
function getThingsString(forRoomName) {
let
thingsStr = "",
things = world[forRoomName]["things"];
for (let key in things) {
let thing = getThing(key, forRoomName);
if (key != player.name && thing.isVisible()) {
if (thingsStr == "") {
thingsStr = " " + artFor(key) + " " + key;
} else {
thingsStr = thingsStr + ", " + artFor(key) + " " + key;
}
}
}
if (thingsStr == "") {
thingsStr = " nothing";
}
// Replace the last , with the word and
let idx = thingsStr.lastIndexOf(",");
if (idx > -1) {
thingsStr = thingsStr.substr(0, idx) + " and" + thingsStr.substr(idx + 1);
}
return thingsStr;
}
//---------------------------------------------------------
// Prints the property of a thing.
// Parameters:
// thingName: the name of the thing to print the property for.
// thing: the thing to print the property for.
// propName: the name of the property to print.
//---------------------------------------------------------
function printThingProperty(thingName, thing, propName) {
let text = getThingProperty(thingName, thing, propName)
if (text) {
if (propName == "picture") {
print(text, "pictures");
} else if (propName == "song") {
play(text);
} else {
println(text);
}
return true;
}
}
function getThingProperty(thingName, thing, propName) {
let text = getNodeText(thingName, thing, propName);
return text;
}
//=========================================================
// Commands
//=========================================================
function getShortcutString(obj) {
let str = "";
for (const key in obj) {
let keyCharIdx = getShortcutIndex(key);
if (keyCharIdx >= 0) {
str +=
" " + key.substring(0, keyCharIdx) +
"[" + key.substring(keyCharIdx, keyCharIdx + 1) + "]" +
key.substring(keyCharIdx + 1);
} else {
str += " " + key;
}
}
return str.substring(1);
// "[q]uit [l]ook [i]nspect [t]ake [c]arrying [u]se [d]rop"
}
//---------------------------------------------------------
// Return the index of a command's shortcut character within the
// command string. If the shortcut key doesn't exist,
// add it to the dictionary.
// Params: commandName: the name of the command
// Return: the index of the shortcut character within the commandName.
//---------------------------------------------------------
function getShortcutIndex(commandName) {
// Check to see if the command is already in the shortcuts dictionary
for (const key in shortcuts) {
if (shortcuts[key] == commandName) {
return commandName.indexOf(key);
}
}
// Find the first character that doesn't collide with an existing shortcut
for (let i = 0; i < commandName.length; i++) {
if (!shortcuts[commandName.charAt(i)]) {
shortcuts[commandName.charAt(i)] = commandName;
return i;
}
}
}
//=========================================================
// Commands
//=========================================================
//---------------------------------------------------------
// Look command handler
//---------------------------------------------------------
async function look() {
if (!(await handle("on", "look at", player.location))) {
printRoomProperty(player.location, "picture");
printRoomDescription(player.location);
println("You look and you see" + getThingsString(player.location) + ".");
}
}
//---------------------------------------------------------
// Inspect command handler
//---------------------------------------------------------
async function inspect() {
let
thingName = await readLine("What do you want to inspect? "),
thing = getThing(thingName);
if (thing && thing.isVisible()) {
if (!(await handle("on", "inspect", thingName, thing))) {
thing.playSong();
thing.printPicture();
if (!thing.printDescription()) {
println("Inspecting the " + thingName + " reveals nothing.");
}
}
} else {
println("There is nothing by that name to inspect.");
}
}
//---------------------------------------------------------
// Take command handler
//---------------------------------------------------------
async function take() {
let
thingName = await readLine("What do you want to take? "),
thing = getThing(thingName);
if (!thing || !thing.isVisible()) {
println("This room does not contain that.");
} else if (!(await handle("on", "take", thingName, thing))) {
if (takeThing(player.location, thingName)) {
println("The " + thingName + " was added to your inventory.");
} else {
println("You cannot take the " + thingName + ".");
}
}
}
//---------------------------------------------------------
// Carrying command handler
//---------------------------------------------------------
async function carrying() {
// TODO: Fix arguments for the "onCarrying" callback
if (!(await handle("on", "carrying", Object.keys(player.inventory)))) {
println("You are carrying the following:");
println(getInventoryString());
}
}
//---------------------------------------------------------
// Use command handler
//---------------------------------------------------------
async function use() {
let
thingName = await readLine("What do you want to use? "),
thing = getThing(thingName);
if (thing && thing.isVisible()) {
if (!thing.isExpired()) {
if (!(await handle("on", "use", thingName, thing))) {
if (thingName == "fists" || thingName == "feet") {
println("Your " + thingName + " appear to have no use here.");
} else {
println("The " + thingName + " appears to have no use.");
}
}
await handle("after", "use", thingName, thing);
} else {
println("You cannot use that anymore.");
}
} else {
println("There is nothing by that name to use.");
}
}
//---------------------------------------------------------
// Drop command handler
//---------------------------------------------------------
async function drop() {
let thingName = await readLine("What do you want to drop? ");
if (thingName == "fists" || thingName == "feet") {
println("You cannot drop that.");
} else {
let thing = getInventory().getThing(thingName);
if (thing && !(await handle("on", "drop", thingName, thing))) {
dropThing(player.location, thingName);
println("The " + thingName + " was dropped.");
} else {
println("You are not carrying anything by that name.");
}
}
}
//---------------------------------------------------------
// Handle attack command
//---------------------------------------------------------
async function attack() {
let livingThings = listLivingThingsInRoom(player.location);
if (!livingThings.length) {
println("There is nothing to attack.");
return;
}
let targetName = (livingThings.length > 1) ?
await readLine("What do you want to attack? ") :
livingThings[0],
target = targetName && getThing(targetName);
if (target && target.isVisible()) {
if (target.isAlive()) {
let
weaponName = await readLine("What do you want to use? "),
weapon = getInventory().contains(weaponName) && getThing(weaponName);
if (weapon) {
if (!weapon.isExpired()) {
if (!(await handle("on", "attack", targetName, weaponName)) &&
!(await handle("on", "attack using", weaponName, targetName))) {
runAttackThing(targetName, target, weaponName, weapon);
weapon.expend();
if (weapon.isExpired()) {
await handle("on", "expired", weaponName);
}
}
} else {
println("You cannot use that anymore.");
}
} else {
println("You are not carrying that.");
}
} else {
println("Attacking the " + targetName + " does nothing.");
}
} else {
println("There is nothing by that name to attack.");
}
}
//---------------------------------------------------------
// List the living things (health > 0) in the room
// Return: array of strings of living things in room
//---------------------------------------------------------
function listLivingThingsInRoom(roomName) {
let
thingsList = getRoom(roomName).listThings(),
livingList = [];
for (const thingName of thingsList) {
let thing = getThing(thingName, roomName);
if (thing && thing.isAlive() && thingName != player.name ) {
livingList.push(thingName);
}
}
return livingList;
}
//---------------------------------------------------------
// Run attack on thing by player
//---------------------------------------------------------
async function runAttackThing(targetName, target, weaponName, weapon) {
let hit = getRandomChance(weapon.getAttribute("accuracy"), 10);
if (hit) {
let damage = Math.ceil(weapon.getAttribute("damage") /
(target.getAttribute("defense") || 1));
if (damage) {
println("You hit the " + targetName + ".");
await handle("after", "hit", targetName, damage);
} else {
println("You missed the " + targetName + ".");
}
target.injure(damage);
if (!target.isAlive()) {
if (!(await handle("on", "killed", targetName, weaponName))) {
killThing(targetName, weaponName);
}
await handle("after", "killed", targetName, weaponName);
}
} else {
println("You attempted to hit the " + targetName + " but missed.");
}
}
//---------------------------------------------------------
// Handle a thing being killed by the player
//---------------------------------------------------------
function killThing(thingName, weaponName) {
println("You managed to kill the " + thingName + " using your " + weaponName + ".");
}
//---------------------------------------------------------
// Handle the player being killed by a thing
//---------------------------------------------------------
async function killPlayer() {
print("Your injuries prove to be life threatening...%w" +
"perhaps next time you will fare better...%w" +
"until then may you rest in peace.%w");
await end();
}
//---------------------------------------------------------
// Run the attack loop for things against the player
//---------------------------------------------------------
async function runAttacks() {
let things = world[player.location]["things"];
for (const key in things) {
let thing = getThing(key);
if (thing.isVisible() && thing.isAlive() &&
getRandomChance(thing.getAttribute("aggression"), 10)) {
if (!(await handle("on", "attacked by", key))) {
await runAttackPlayer(key, thing);
}
}
}
}
//---------------------------------------------------------
// Run an attack by a thing against the player
//---------------------------------------------------------
async function runAttackPlayer(thingName, thing) {
let hit = getRandomChance(thing.getAttribute("accuracy"), 10);
if (hit) {
let p = getThing(player.name);
if (!(await handle("on", "hit by", thingName, thing.getAttribute("damage")))) {
var damage =
Math.ceil(thing.getAttribute("damage") /
(p.getAttribute("defense") || 1));
p.injure(damage);
if (p.isAlive()) {
println("The " + thingName + " attacked and injured you.");
} else {
if (!(await handle("on", "killed by", thingName))) {
killPlayer();
}
}
}
await handle("after", "hit by", thingName, damage);
} else {
println("The " + thingName + " attempted to attack you but missed.");
}
}
//---------------------------------------------------------
// Handle quit command
//---------------------------------------------------------
async function quit(args) {
let sure = await readLine("Are you sure (y/n)? ");
if (sure == "y" || sure == "yes") {
if (!(await runCallback("onQuit"))) {
await end();
}
}
}
//---------------------------------------------------------
// Handle mute and unMute commands
//---------------------------------------------------------
function mute() {
muteSound();
removeCommand(mute, commands["other"]);
addCommand(commands["other"], unmute);
}
function unmute() {
unmuteSound();
removeCommand(unmute, commands["other"]);
addCommand(commands["other"], mute);
}
function credits() {
playSong("credits.txt");
printPicture("credits.txt");
}
async function printHighScores() {
let scores = loadHighScores();
if (!(await handle("on", "print high scores", scores))) {
if (scores.length) {
let rankLen = (scores.length + "").length;
let pointsLen = (scores[scores.length - 1].points + "").length;
let str = "Rank Player";
let spaces = utils.repeatStr(" ", 40 - (str.length + "Points".length));
str += spaces + "Points\n";
str += utils.repeatStr("-", 40) + "\n";
for (let i = 0; i < scores.length; i++) {
let score = scores[i];
let rankSpaces = utils.repeatStr(" ", 5 - (rankLen + ((i + 1) + "").length));
let lineLen = (str.split("\n"))[0].length;
spaces = utils.repeatStr(" ", lineLen - (5 + score.name.length + pointsLen));
str += (i + 1) + "." + rankSpaces + score.name + spaces + score.points + "\n";
}
println("%c%l" + str + "%x", "pictures");
} else {
println("There are no high scores recorded for this game yet.");
}
}
}
//---------------------------------------------------------
// Take an thing from a room.
// Return: thing if the thing could be taken, undefined if not.
//---------------------------------------------------------
function takeThing(roomName, thingName) {
let thing = world[roomName]["things"] && world[roomName]["things"][thingName];
if (thing &&
(thing["attributes"] && !thing["attributes"]["stationary"] ||
!thing["attributes"])) {
player.inventory[thingName] = thing
delete world[roomName]["things"][thingName];
return thing;
}
}
//---------------------------------------------------------
// Drop a thing into a room.
//---------------------------------------------------------
function dropThing(roomName, thingName) {
if (player.inventory[thingName]) {
world[roomName]["things"][thingName] = player.inventory[thingName];
delete player.inventory[thingName];
}
}
//---------------------------------------------------------
// Get the string of player's inventory items.
//---------------------------------------------------------
function getInventoryString() {
let thingsStr = "";
for (const key in player.inventory) {
if (thingsStr == "") {
thingsStr = key;
} else {
thingsStr += ", " + key;
}
}
if (thingsStr == "") {
thingsStr = "nothing";
}
return thingsStr;
}
//---------------------------------------------------------
// Print the description for a given room.
//---------------------------------------------------------
function printRoomDescription(roomName) {
println("You are in the " + roomName + ".");
printRoomProperty(roomName, "description");
let exitsArray = world[roomName]["exits"] &&
Object.keys(world[roomName]["exits"]),
len = exitsArray && exitsArray.length;
if (len == 1) {
println("You can move in the direction: " + exitsArray[0] + ".");
} else if (len) {
let exitsString = exitsArray.slice(0, len - 1).join(", ") +
" and " + exitsArray[len - 1];
println("You can move in the directions: " + exitsString + ".");
} else {
println("There are no obvious exits.");
}
}
//---------------------------------------------------------
// Prints the property of a room.
// Parameters:
// roomName: the name of the room to print the property for.
// propName: the name of the property to print
// (description or picture)
//---------------------------------------------------------
function printRoomProperty(roomName, propName) {
let text = getRoomProperty(roomName, propName);
if (text) {
if (propName == "picture") {
print(text, "pictures");
} else if (propName == "song") {
play(text);
} else {
println(text);
}
return true;
}
}
function printRoomProperties(roomName) {
printRoomProperty(roomName, "song");
printRoomProperty(roomName, "picture");
printRoomProperty(roomName, "description");
}
//---------------------------------------------------------
// Get the property of a room given its name and the property name.
// Parameters:
// roomName: the name of the room to get the property for.
// propName: the name of the property to get (description or picture)
//---------------------------------------------------------
function getRoomProperty(roomName, propName) {
let text = getNodeText(roomName, world[roomName], propName);
return text;
}
//---------------------------------------------------------
// Get the command prompt. Should be called as part of repl or eval.
//---------------------------------------------------------
async function getCommandPrompt(menu) {
let prompt;
if (menu) {
prompt = getShortcutString(menu) + ": ";
} else {
prompt =
"What would you like to do next?\n" +
getShortcutString(commands) + "\n" +
getShortcutString(world[player.location]["exits"]) + ": ";
}
prompt = (await runCallback("onCommandPrompt", menu || commands, prompt)) || prompt;
debug && console.log(prompt);
return prompt;
}
//---------------------------------------------------------
// Gets the text of a node in the world as either the value
// or the file that it points to.
// Parameters:
// nodeName: the name of the node (roomName or thingName)
// node: the node to get the text for (room or thing)
// key: the key of the property (description or picture)
// Return: the property text
//---------------------------------------------------------
function getNodeText(nodeName, node, key) {
let value = node[key];
// Check for suppression case where key exists but is set false
if (Object.keys(node).indexOf(key) > -1 && !value) {
return "";
}
// If the property is a filename attempt to read it first
let text = readFile("./" + key + "s/" + value) || value;
if (!text) {
// Attempt to read the file with the same name as the node
text = readFile("./" + key + "s/" + nodeName + ".txt");
}
return text;
}
//---------------------------------------------------------
// Load the world object from either world.json or example-world.json.
// Return: the loaded world object.
// Side Effects: exit on error.
//---------------------------------------------------------
function loadWorld() {
try {
var contents = fs.readFileSync('./world.json');
} catch (error) {
try {
// Attempt to load the example-world
contents = fs.readFileSync('./example-world.json');
} catch (error) {
println("Error loading world: " + error);
}
}
// World file loaded, attempt to parse the map
try {
return JSON.parse(contents);
} catch (error) {
println("Error creating world: " + error);
println("Please contact your system administrator for help with this error.");
}
println("Please contact your system administrator for help with this error.");
}
//---------------------------------------------------------
// Put the player in the world as a thing. Also sets global player.location.
//---------------------------------------------------------
function initPlayer() {
// Get the initial Room
if (world["welcome"] && world["welcome"]["entrance"]) {
var initialRoom = world["welcome"]["entrance"];
} else {
initialRoom = Object.keys(world)[0];
}
player = {
name: "player" + id,
displayName: "player" + id,
location: initialRoom,
attributes: { health: 10, defense: 5 },
inventory: {
fists: { attributes: { accuracy: 6, damage: 2 } },
feet: { attributes: { accuracy: 4, damage: 3 } }
}
};
world[initialRoom]["things"] = world[initialRoom]["things"] || {};
// Actually add the player to the world in order to treat like a thing
world[initialRoom]["things"][player.name] = player;
return player;
}
//---------------------------------------------------------
// Call an event handler by chaining pre, verb, noun, and argument
//---------------------------------------------------------
async function handle(prep, verb, noun, argument) {
// The function is a registered specific callback
// ie: addCallback(onUseThing);
// that did not point to the default object callback.
let
callbackName = camel(prep + " " + verb + " " + noun),
result = (await runCallback(callbackName, argument || noun, argument));
// Attempt to run a registered generic callback
// ie: addCallback(onUse);
callbackName = camel(prep + " " + verb);
return (await runCallback(callbackName, noun, argument)) || result;
}
//---------------------------------------------------------
// Runs a registered callback, returning the result.
// Return:
// true if callback handles/breaks callback chain
// undefined if chain should continue being called
//---------------------------------------------------------
async function runCallback(callbackName, argument1, argument2) {
if (callbacks[callbackName]) {
try {
return !(await callbacks[callbackName](argument1, argument2));
} catch (error) {
console.error(error.name + ": " + error.message + " when calling function " +
callbackName +
(error.lineNumber ? " in line " + error.lineNuber + "." : "."));
}
}
}
//---------------------------------------------------------
// Check for a callback function for user input, otherwise
// evaluate the command.
//---------------------------------------------------------
async function repl() {
while (playing) {
let str = (await readLine(await getCommandPrompt())).trim();
await evalCommand(commands, str);
// TODO: determine if we should always attack on any and all commands
if (!(await handle("on", "run attacks"))) {
await runAttacks();
}
}
await readLine("Press ENTER to play again...");
await start(); // TODO: better method for restarting game
}
//---------------------------------------------------------
// Process a single command given its name. Command routines
// are responsible for requesting additional input from user.
//---------------------------------------------------------
async function evalCommand(menu, commandName) {
commandName = shortcuts[commandName] || commandName;
// The onCommand callback allows override of global command response
if (!(await runCallback("onCommand", commandName))) {
if (commandName && menu[commandName]) {
// Found the standard command in the dictionary, treat built-in
// and user-added commands the same
if (typeof menu[commandName] === "object") {
let str = (await readLine(await getCommandPrompt(menu[commandName]))).trim();
await evalCommand(menu[commandName], str);
} else {
debug && console.log("Running command: " + menu[commandName]);
await menu[commandName]();
}
} else if (!(await move(commandName))) {
// Command doesn't match and neither does move
println("I don't understand that command.");
}
}
}
//---------------------------------------------------------
// Add the default commands to the command dictionary
//---------------------------------------------------------
function addCommands() {
addCommand(look);
addCommand(inspect);
addCommand(take);
addCommand(carrying);
addCommand(use);
addCommand(drop);
addCommand(attack);
// Add these to make sure they have shortcuts reserved
getShortcutIndex("north");
getShortcutIndex("south");
getShortcutIndex("east");
getShortcutIndex("west");
getShortcutIndex("up");
getShortcutIndex("down");
let other = {};
addCommand("other", other);
addCommand(other, quit);
addCommand(other, printHighScores, "high scores");
if (readFile("./pictures/credits.txt")) {
addCommand(other, credits);
}
addCommand(other, mute);
}
//---------------------------------------------------------
// Replace the tokens in a template with values contained
// in the templateStrings dictionary.
//---------------------------------------------------------
function replaceTemplateStrings(text) {
if (isString(text)) {
text = text.replace(/{{(\w+)}}/g, function (match, p1) {
return templateStrings[p1];
});
return text;
}
}
//---------------------------------------------------------
// Utility for sync reading a file in utf8 format.
//---------------------------------------------------------
function readFile(fileName) {
try {
let contents = fs.readFileSync(fileName, "utf8");
return contents;
} catch (error) {
return "";
}
}
//---------------------------------------------------------
// Utility for camel-casing a string.
//---------------------------------------------------------
function camel(string) {
var str = string.split(" ").map(function (word, i) {
return i ? word.charAt(0).toUpperCase() + word.slice(1) : word;
}).join("");
return str;
}
//---------------------------------------------------------
// Utility for returning the indefinite article (a/an) based on a given noun.
//---------------------------------------------------------
function artFor(noun) {
let
letter = noun && noun.toLowerCase()[0],
isVowel = letter && ["a", "e", "i", "o", "u"].indexOf(letter) > -1;
return isVowel ? "an" : "a";
}
//---------------------------------------------------------
// Utility for determining if a given argument is a function.
//---------------------------------------------------------
function isFunction(argument) {
return (argument &&
({}.toString.call(argument) === "[object Function]" ||
argument.constructor.name === "AsyncFunction"));
}
//---------------------------------------------------------
// Utility for determining if a given argument is a string.
//---------------------------------------------------------
function isString(argument) {
return typeof argument === "string";
}
//---------------------------------------------------------
// Helper to resolve/get a thing based on arguments provided.
// Parameters:
// thingName: the name of the thing to find.
// inRoomName: if provided, restrict search to a specific room, otherwise
// perform global search.
//---------------------------------------------------------
function resolveThing(thingName, inRoomName) {
// Check to see if thing is supposed to be the player
thingName = (thingName == "player") ? getThisPlayerName() : thingName;
// Check to see if the thing is in the room first
let
roomName = inRoomName || player.location,
room = world[roomName],
thing = room && room["things"] && room["things"][thingName];
// Thing is not in the room, check inventory
thing = thing || player.inventory[thingName];
if (!thing && inRoomName != "inventory") {
if (!inRoomName) {
// Perform a global search for the thing
for (const key in world) {
room = world[key];
if (room["things"] && room["things"][thingName]) {
roomName = key;
break;
}
}
thing = world[roomName]["things"][thingName];
} else {
// The search failed, return an empty object
return {};
}
}
// Return an object containing thing, the roomName, and room found in
return {
thing,
roomName,
room: world[roomName]
}
}
//---------------------------------------------------------
// Game API
//---------------------------------------------------------
//---------------------------------------------------------
// Get the public API object for this player's inventory
//---------------------------------------------------------
function getInventory() {
return {
list: function () {
return Object.keys(player.inventory);
},
contains: function (thingName) {
return player.inventory[thingName];
},
removeThing: function (thingName) {
let thing = player.inventory[thingName];
delete player.inventory[thingName];
return thing != undefined;
},
getThing: function (thingName) {
return getThing(thingName, "inventory");
}
}
}
//---------------------------------------------------------
// Get the public API object for a room in the world
//---------------------------------------------------------
function getRoom(roomName) {
let room = world[roomName];
return {
getDescription: function () {
return getRoomProperty(roomName, "description");
},
setDescription: function (description) {
room["description"] = description;
},
printDescription: function () {
return printRoomProperty(roomName, "description");
},
getPicture: function () {
return getRoomProperty(roomName, "picture");
},
setPicture: function (picture) {
room["picture"] = picture;
},
printPicture: function () {
return printRoomProperty(roomName, "picture");
},
getSong: function () {
return getRoomProperty(roomName, "song");
},
setSong: function (song) {
room["song"] = song;
},
playSong: function () {
return printRoomProperty(roomName, "song");
},
listThings: function () {
return room["things"] &&
Object.keys(room["things"]);
},
contains: function (thingName) {
return (
(room["things"] &&
room["things"][thingNa