UNPKG

adventure-engine

Version:

Simple text based adventure game library.

1,363 lines (1,240 loc) 67 kB
//========================================================= // 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