// Include global modules // // Core Module // var coreModule = new Module(`Core`, { 'init': function () { ECS.addInternalAction(`getLocationName`, function (data) { return data.location.name; }); // Add LOOK context action Entity.prototype.addContext(function (self) { return {'command': `look at ` + self.name, 'text': `LOOK`}; }); // Add TAKE context action Entity.prototype.addContext(function (self) { if (typeof self.canTake == `function` && self.canTake()) { return {'command': `take ` + self.name, 'text': `TAKE`}; } return false; }); // Add TALK TO context action Entity.prototype.addContext(function (self) { if (self.is(`living`) && self.hasOwnProperty(`conversation`)) { return {'command': `talk to ` + self.name, 'text': `TALK TO`}; } return false; }); } }); var moveModifiers = [ `n`, `north`, `ne`, `northeast`, `e`, `east`, `se`, `southeast`, `s`, `south`, `sw`, `southwest`, `w`, `west`, `nw`, `northwest`, `d`, `down`, `u`, `up`, ]; // Thing Component (superclass for all perceptible/interactive objects) coreModule.c(`thing`, { 'name': `Unnamed`, 'descriptions': { 'default': `No description`, }, 'listInRoomDescription': true, // Mention the item automatically when describing a room 'parent': null, 'place': null, 'spawn': null, 'children': [], 'scope': `local`, // Object scope, local by default 'canTake': function () { // Determine whether object can be taken, true by default return true; }, 'parentIs': function (p) { if (!$.isArray(p)) { p = [p]; } for(var l in p) { if(typeof p[l] == `object`) { p[l] = p.key; } if(this.parent != null && this.parent.key == p[l]) { return true; } } return false; }, 'location': function () { if (this.place != null) { return this.place; } else { return null; } }, 'locationIs': function (loc) { if (!$.isArray(loc)) { loc = [loc]; } for(var l in loc) { if(typeof loc[l] == `object`) { loc[l] = loc.key; } if(this.location() != null && this.location().key == loc[l]) { return true; } } return false; }, 'regionIs': function (loc) { if (typeof loc == `object`) { loc = loc.key; } return (this.region == loc || (this.location() && this.location().region != null && this.location().region == loc)); }, 'hasChildOfType': function (type) { for(var c in this.children) { if(this.children[c].hasComponent(type)) { return true; } } return false; }, 'onAdd': [ // Called when the component is added to an entity function (args) { // Link object to spawn location if (args.obj.spawn != null) { var place = ECS.findEntity(`place`, args.obj.spawn); if (place != null) { args.obj.place = place; place.children.push(args.obj); } } }, function(args) { // Handle split description identifiers // This will duplicate descriptions with comma-delimited keys // Example: 'default,scenery' will produce two identical descriptions with keys default and scenery for(var d in args.obj.descriptions) { var keys = d.split(`,`); for(var k in keys) { args.obj.descriptions[keys[k]] = args.obj.descriptions[d]; } } } ], 'onTakeSuccess': function () { queueOutput(`{{gm}}
You pick up the ` + this.name + `.
`); }, 'onTakeFail': function () { return `{{gm}}You are unable to take the ` + this.name + `.
`; }, 'onDropSuccess': function () { return `{{gm}}You drop the ` + this.name + `.
`; }, 'onDropFail': function () { return `{{gm}}You can't drop the ` + this.name + `, as you are not holding it.
`; }, 'persist': [`place`] }); // Nothing component coreModule.c(`nothing`, { 'dependencies': [], 'onAction.TAKE': function () { queueGMOutput(`That's too abstract to be taken.`); } }); // Region component coreModule.c(`region`, { 'dependencies': [`place`], 'onEnter': function () { } }); // Place Component (visitable locations) coreModule.c(`place`, { 'dependencies': [`thing`], 'region': null, 'visited': 0, // Times visited 'descriptions': { 'verbose': ``, 'default': `` }, 'exits': {}, 'hasExit': function (direction) { return this.exits.hasOwnProperty(direction); }, 'getExit': function (direction) { return this.exits[direction]; }, // Callbacks 'description': function (verbosity) { return parse(this.descriptions[verbosity], this); }, 'onAdd': [ function(args) { // Add to region child list if (args.obj.region != null) { var region = ECS.findEntity(`region`, args.obj.region); if (region) { region.children.push(args.obj); } } } ], 'onTick': null, // Called every tick while the player is in the location 'onEnter': [function (args) { // Called when the player enters the location return args.obj.describe(); }], 'onExit': null, // Called when the player exits the location 'onInit': function () { // Register object description helper // Describes objects in the current location // Usage: {{objects}} Handlebars.registerHelper(`objects`, function (context) { var output = []; // Get current location var location = player.place; // Get scenery objects for location var objects = location.findChildren(`thing`); for (var o in objects) { if (objects[o].listInRoomDescription) { var onList = ``; if (typeof objects[o].onList != `undefined`) { // used mainly for containers and such that need to append descriptive text onList = objects[o].onList(); } var article = typeof(objects[o].article) == `function` ? objects[o].article() : objects[o].article; output.push(article + ` ` + getNameTag(objects[o]) + onList); // Increment 'seen object' counter incrementCounter(`seen-` + objects[o].key); } } // If there are no objects to list, do nothing if (output.length == 0) { return; } // Assemble object list into a comma-separated list, with articles // and a final 'and' separator var separator = (output.length > 2) ? `, ` : ` and `; var list = output.join(separator); var lastComma = list.lastIndexOf(`,`); if (lastComma >= 0) { list = list.slice(0, lastComma) + `, and` + list.slice(lastComma + 1); } return new Handlebars.SafeString(parse(`You can see ` + list + ` here.`)); }); }, // Utility methods 'allowDescription': [], 'getLocationName': function () { return ECS.runInternalAction(`getLocationName`, {'location': this}); }, 'getRegion': function () { return ECS.findEntity(`region`, this.region); }, 'describe': function (returnOutput) { // Handy when onEnter is overridden if (!ECS.runFilters(this, `allowDescription`)) { return; } var objects = parse(` {{objects}}`); var output = `You open the ` + target.name + `.
`; } } else if (target == null) { action.output += `I can tell you want to open something, but you'll have to be more specific.
`; } else { action.output += `That's not the sort of thing that opens.
`; } } }); // Open action coreModule.a(`close`, { 'aliases': [`close`], 'callback': function (action) { var target = action.target; if (target != null && target.hasComponent(`openable`)) { if (typeof target.onClose == `object` && target.onClose.length > 0) { ECS.runCallbacks(target, `onClose`, action.modifiers); return; } else if (typeof target.onClose == `string`) { return target.onClose; } else { target.isOpen = false; return `You close the ` + target.name + `.
`; } } else if (target == null) { return `I can tell you want to close something, but you'll have to be more specific.
`; } else { return `That's not the sort of thing that closes.
`; } } }); // Verbosity action coreModule.a(`verbose`, { 'aliases': [`verbose`, `verbosity`], 'modifiers': [`on`, `off`], 'callback': function (data) { //game.setVerbosity(data.modifiers[0]); } }); // Credits action coreModule.a(`credits`, { 'aliases': [`credits`], 'callback': function () { ECS.tick = false; return `{{box 'Credits' '` + `Design, Code, & Writing: Steven RichardsTry LOOK AT BIRD or GO EAST for starters.
Other useful commands include EAT and QUIT.
Most commands have shorthand forms or aliases:
EXAMINE, LOOK, or X instead of LOOK AT
`
+ `WALK, RUN, or CRAWL instead of GO, as well as cardinal directions like NORTH
GAME OVER
`; } }); // Save action coreModule.a(`save`, { 'aliases': [`save`, `quicksave`], 'callback': function () { ECS.tick = false; ECS.setData(`save`, JSON.stringify(ECS.save())); queueOutput(`Game Saved.`); } }); // Load action coreModule.a(`load`, { 'aliases': [`load`, `quickload`], 'callback': function () { ECS.tick = false; ECS.load(ECS.getData(`save`)); queueOutput(`Game Loaded.`); } }); // Look action coreModule.a(`look`, { 'aliases': [`look`, `l`, `look at`, `peer`, `glance`, `inspect`, `examine`, `x`], 'modifiers': [`through`, `at`].concat(moveModifiers), 'callback': function (action) { if (action.nouns.length) { var output = parse(action.nouns[0].descriptions[`default`], {'target':action.nouns[0]}); action.output += `You don't see that here.
`; } }); // Move action var moveAction = { 'aliases': [ `move`, `walk`, `run`, `crawl`, `go`, `n`, `north`, `ne`, `northeast`, `e`, `east`, `se`, `southeast`, `s`, `south`, `sw`, `southwest`, `w`, `west`, `nw`, `northwest`, `d`, `down`, `u`, `up`, `in`, `out`, ], 'modifiers': moveModifiers, 'canonical': function (d) { return getCanonicalDirection(d); }, // Process incoming verb data before the callback is executed 'pre': function(data) { // If no modifiers provided, assume string is a directional alias, e.g. E if (data.modifiers.length == 0) { data.modifiers.push(data.string); } // Get canonical direction (converts synonyms to base form) data.direction = this.canonical(data.modifiers[0]); }, 'callback': function (data) { var direction = data.direction; // Get current location var location = player.location(); // Check if current location has exit in the specified direction if (direction != null && location.hasExit(direction)) { // Get destination var destination = ECS.findEntity(`place`, location.getExit(direction)); if (destination != null) { // Check if the current location has an exit callback if (location.hasOwnProperty(`onLeave`)) { var exit = ECS.runCallbacks(location, `onLeave`, {'direction': direction}); if (exit !== false) { // Handled by onLeave return exit; } } // Move object (player in this case) ECS.moveEntity(player, destination); incrementCounter(`visited-` + destination.key); var response = ECS.runCallbacks(destination, `onEnter`); destination.visited++; if (typeof response == `string`) { return response; } return ``; } return `This is embarrassing, but something seems to be broken. I can't find the location in that direction.
`; } return `You can't go that way (`+direction+`).
`; }, 'onBadInput': function (string) { return `I understand you want to go somewhere, but I don't know how to go (` + string + `).
`; } }; coreModule.a(`move`, moveAction); // Climb action coreModule.a(`climb`, { 'aliases': [ `climb`, ], 'modifiers': [`up`,`down`], 'callback': function (action) { var nouns = action.nouns; if (nouns.length > 0) { // Actor is trying to climb a specific object. Not handled globally. } else if(action.modifiers.length > 0) { if(action.actor.location().hasExit(getCanonicalDirection(action.modifiers[0]))) { NLP.parse(action.modifiers[0]); return true; } else { queueGMOutput(`You can't go that way.`); return false; } } // No target, no modifiers queueGMOutput(`I can tell you're trying to climb, but I don't know what or where.`); return false; } }); // Wait action coreModule.a(`wait`, { 'aliases': [`wait`,`z`], 'callback': function () { ECS.tick = true; queueGMOutput(`You wait a bit.`); } }); // Direction helpers Handlebars.registerHelper(`n`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`north`))); }); Handlebars.registerHelper(`north`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`north`))); }); Handlebars.registerHelper(`ne`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`northeast`))); }); Handlebars.registerHelper(`e`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`east`))); }); Handlebars.registerHelper(`east`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`east`))); }); Handlebars.registerHelper(`se`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`southeast`))); }); Handlebars.registerHelper(`s`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`south`))); }); Handlebars.registerHelper(`south`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`south`))); }); Handlebars.registerHelper(`sw`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`southwest`))); }); Handlebars.registerHelper(`w`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`west`))); }); Handlebars.registerHelper(`west`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`west`))); }); Handlebars.registerHelper(`nw`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`northwest`))); }); Handlebars.registerHelper(`u`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`up`))); }); Handlebars.registerHelper(`up`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`up`))); }); Handlebars.registerHelper(`d`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`down`))); }); Handlebars.registerHelper(`down`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`down`))); }); // Take action coreModule.a(`take`, { 'aliases': [ `take`, `grab`, `acquire`, `snatch`, `pick`, `pick up`, `collect` ], 'modifiers': [], 'callback': function (data) { var nouns = data.nouns; if (nouns.length > 0) { var target = nouns[0]; if(target.parent == data.actor) { queueGMOutput(`You already have it.`); return true; } // See if item can be picked up if (target.canTake()) { // TODO: handle bool for cantake // Move item to actor's inventory if (target.parent != null) { target.parent.removeChild(target); } if(target.place != null) { target.place.removeChild(target); target.place = null; } target.parent = data.actor; data.actor.children.push(target); target.onTakeSuccess(); } else { return target.onTakeFail(); } } return; }, 'onBadInput': function (string) { return `I understand you want to TAKE something (` + string + `), but I don't understand what.
`; }, }); // Drop action coreModule.a(`put-down`, { 'aliases': [ `drop`, `put down`, `leave`, `throw`, `toss` ], 'modifiers': [], 'callback': function (data) { var nouns = data.nouns; if (nouns.length > 0) { var target = nouns[0]; // See if item is in player's inventory if (target.parent == player) { // Move item to location data.actor.place.addChild(target); target.place = player.place; target.parent = player.place; data.actor.removeChild(target); queueOutput(`{{gm}}You drop the ` + nouns[0].name + `.
`); target.onDropSuccess(); } else { return target.onDropFail(); } } return; }, 'onBadInput': function (string) { return `I understand you want to DROP something (` + string + `), but I don't understand what.
`; }, }); // Inventory action coreModule.a(`inventory`, { 'aliases': [`i`, `inventory`], 'modifiers': [], 'callback': function (data) { ECS.tick = false; var actor = data.actor; var items = []; for (var item in actor.children) { var name = actor.children[item].name; var onList = ECS.run(actor.children[item], `onList`); items.push({'text': name + onList, 'command': `look at ` + name}); } if (!items.length) { items.push({'text': `nothing`, 'command': `do nothing`}); } var menu = parse(`{{menu items}}`, {'items': items}); queueOutput(`` + actor.name + `, the ` + actor.gender.toUpperCase() + ` ` + actor.race.toUpperCase() + ` ` + actor.class.toUpperCase() + `
`); queueOutput(`You have ` + actor.children.length + ` item(s):
` + menu); return; } }); // Smell action coreModule.a(`smell`, { 'aliases': [`smell`, `sniff`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.descriptions.hasOwnProperty(`smell`)) { queueGMOutput(p(target.descriptions.smell)); } else if (target == null && data.actor.location().descriptions.hasOwnProperty(`smell`)) { queueGMOutput(p(data.actor.location().descriptions.smell)); } else { queueGMOutput(p(`It has no discernable odor.`)); } return true; } }); // Theme action coreModule.a(`theme`, { 'aliases': [`theme`], 'modifiers': [`default`, `classic`], 'callback': function (data) { if (data.modifiers.length == 0) { return `Gotta specify a theme.
`; } loadTheme(data.modifiers[0]); }, 'onBadInput': function () { return `That isn't a valid theme.
`; } }); // Name tag function for things function getNameTag(t) { if (!(t instanceof Entity)) { return t; } var name = t.name; var classes = t.tags.join(` `); var extraClasses = (typeof t.extraTags != `undefined`) ? t.extraTags.join(` `) : ``; var command = `look at ` + t.name; return `{{nametag '` + t.key + `' classes='` + classes + ` ` + extraClasses + `' command='` + command + `'}}`; } // Direction tag function function getDirectionTag(d) { return `{{tag '` + d + `' classes='direction' command='` + moveAction.canonical(d) + `'}}`; } Handlebars.registerHelper(`held`, function(options) { if(player.hasChild(this.target)) { return options.fn(this); } return ``; }); // Register module ECS.m(coreModule); // // Darkness Module // var darkness = new Module(`Darkness`, { init: function () { // Extend Place component to add light status var place = ECS.getComponent(`place`); place.defaultLit = true; place.descriptions.dark = `It is pitch black. You can't see anything.`; place.isLit = [ function (args) { // Check default status if (args.obj.defaultLit) { return true; } // Check for light sources in current location and player inventory var emitters = args.obj.findChildren(`emitter`).concat(player.findChildren(`emitter`)); console.log(emitters); for (var e in emitters) { if (emitters[e].emitterActive) { return true; } } return false; } ]; place.allowDescription.push(function (args) { var isLit = ECS.runFilters(player.place, `isLit`); if (!isLit) { queueOutput(`{{gm}}{{place.descriptions.dark}}
`); } return isLit; }); // Add light restrictions on LOOK and TAKE actions var f = function (args) { if (!ECS.runFilters(player.place, `isLit`)) { queueOutput(`{{gm}}{{place.descriptions.dark}}
`); return false; } return true; }; ECS.getAction(`look`).filters.push(f); ECS.getAction(`take`).filters.push(f); } }); // Dark Place Component (convenience component to start a location dark) darkness.c(`place-dark`, { 'dependencies': [`place`], 'onAdd': function (args) { args.obj.defaultLit = false; } }); // Emitter Component darkness.c(`emitter`, { 'dependencies': [`thing`], 'emitterActive': true, 'onActivate': [], 'onDeactivate': [] }); // DEBUG: Light action darkness.a(`light`, { 'aliases': [`light`], 'callback': function (data) { queueOutput(`Current location lit? ` + ECS.runFilters(data.actor.place, `isLit`)); } }); // Register module ECS.m(darkness); // // Containers/Supporters Module // var containers = new Module(`Containers`, { init: function () { // Add container restrictions on LOOK and TAKE actions var f = function (args) { if (args.nouns.length > 0) { var obj = args.nouns[0]; if (obj.parent != null && obj.parent.hasComponent(`container`) && !obj.parent.isOpen) { if (!obj.parent.isTransparent) { queueOutput(`{{gm}}You can't see any such thing.
`); return false; } queueOutput(`(first opening the ` + getNameTag(obj.parent) + `)
`); } } return true; }; ECS.getAction(`look`).filters.push(f); ECS.getAction(`take`).filters.push(f); understand(`inside object location name rule`) .internal(`getLocationName`) .attribute(`actor.parent`, `is`, `holder`) .do(function(self,action){ action.output += ` (inside `+ action.actor.parent.name +`)`; action.mode = Rulebook.ACTION_APPEND; return true; }) .start(); } }); // Holder component (shared component) containers.c(`holder`, { 'dependencies': [`thing`], 'capacity': 0, // 0 = unlimited 'isTransparent': false, 'canTakeFrom': [function(){ return true; }], 'canPutInto': [function(){ return true; }], 'inventory': [], 'isEnterable': false, 'onInit': function () { // Add spawn-handling callback to Thing component var thing = ECS.getComponent(`thing`); thing.onAdd.push(function (args) { if (args.obj.spawn != null && args.obj.place == null) { var parent = ECS.findEntity(`holder`, args.obj.spawn); if (parent != null) { console.log(`spawning child element ` + args.obj.name); args.obj.parent = parent; args.obj.place = parent.place; parent.children.push(args.obj); parent.inventory.push(args.obj); parent.updateStats(); } } }); // Register inventory description helper // Describes inventory of the current object // Usage: {{inventory}} Handlebars.registerHelper(`inventory`, function (context) { var output = []; var nameTag = ``; // Get objects for location var objects = context; for (var i = 0; i < objects.length; i++) { if (objects[i].listInRoomDescription) { output.push(objects[i].article() + ` ` + getNameTag(objects[i])); } } // If there are no objects to list, do nothing if (output.length == 0) { return; } // Assemble object list into a comma-separated list, with articles // and a final 'and' separator var list = output.join(`, `); var lastComma = list.lastIndexOf(`,`); if (lastComma >= 0) { list = list.slice(0, lastComma) + `, and` + list.slice(lastComma + 1); } return new Handlebars.SafeString(parse(list)); }); }, 'onRemoveChild': [ function (args) { args.obj.inventory.splice(args.obj.children.indexOf(args.child), 1); } ], 'onAddChild': [ function (args) { args.obj.inventory.push(args.child); args.child.place = args.obj.place; } ], 'onEnter': [] }); // Container component containers.c(`container`, { 'dependencies': [`holder`,`openable`], // Add-on handler for listing operations. Lists contents of open containers when the container is mentioned 'onList': function () { if (this.isOpen || this.isTransparent) { if (this.inventory.length > 0) { return parse(` (in which is {{inventory objects}})`, {'objects': this.inventory}); } else { return ` (empty)`; } } return parse(` (closed)`); } }); // Supporter component containers.c(`supporter`, { 'dependencies': [`holder`], 'onList': function () { if (this.inventory.length > 0) { return parse(` (on which is {{inventory objects}})`, {'objects': this.inventory}); } return ``; } }); // Look inside action containers.a(`look in`, { 'aliases': [`look in`, `look inside`, `look into`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.hasComponent(`container`)) { // List contents of object data.output += parse(`{{gm}}I can tell you want to look inside something, but you'll have to be more specific.
`; } else { data.output += `{{gm}}That's not something you can look inside.
`; } return true; } }); // Get inside action containers.a(`get-in`, { 'aliases': [ `get in`, `get inside`, `get into`, `enter`, `get on`, `get onto`, `stand in`, `stand on`, `be in`, `be on` ], 'callback': function (data) { var target = (data.nouns.length) ? data.nouns[0] : null; var actor = data.actor; var modifier = `in`; if (target === null) { // No target specified, check location for 'in' exit if (actor.location().hasExit(`in`)) { target = ECS.getEntity(actor.location().getExit(`in`)); if(!target.hasComponent(`holder`)) { // Not a holder, try moving instead var verb = ECS.getAction(`move`); verb.pre(data); return verb.callback(data); } modifier = (target.hasComponent(`supporter`)) ? `on` : `in`; } else { // No target specified and no local entrance // Give up and warn the user queueGMOutput(`There doesn't appear to be anything you can get in here.`); return true; } } // Find out if the target can be entered if (!target.isEnterable) { // Can't get in/on queueGMOutput(`That's not something you can get `+modifier+`.`); return true; } // Move actor to target if (actor.parent) { actor.parent.removeChild(target); } actor.parent = target; target.children.push(actor); // Give default response var handled = ECS.runCallbacks(target, `onEnter`, data); if(handled) { return handled; } queueGMOutput(`You get `+modifier+` the `+target.name+`.`); return true; } }); // Put inside action containers.a(`put in`, { 'aliases': [`put`,`place`,`insert`,`drop`,`toss`,`throw`], 'modifiers':[`in`,`on`,`on top of`,`into`,`onto`,`inside`], 'callback': function (data) { // Need two nouns and a modifier if(data.nouns.length == 1 && data.modifiers.length == 0) { NLP.parse(`put down `+data.nouns[0].nouns[0]); return false; } else if(data.nouns.length < 2 || data.modifiers.length != 1) { queueGMOutput(`Can you be a bit more specific about what you want to put where?`); return false; } var target = data.nouns[0]; var container = data.nouns[1]; var actor = data.actor; var modifier = data.modifiers[0]; if (!container.hasComponent(`container`)) { queueGMOutput(`That's not something you can put things `+modifier+`.`); return true; } if(container.hasChild(target)) { queueGMOutput(`It's already there.`); return true; } // Give default response var handled = ECS.runCallbacks(target, `canPutInto`, data); if(handled) { return handled; } // Move object ECS.moveEntity(target, container); data.actor.removeChild(target); queueGMOutput(`You place the `+target.name+` `+modifier+` the `+container.name+`.`); return true; }, 'filters':[ function (args) { if (args.nouns.length > 0) { var obj = args.nouns[0]; if (obj.parent != player) { queueOutput(`(first taking the ` + getNameTag(obj) + `)
`); NLP.parse(`TAKE ` + obj.name); return (obj.parent == player); } } return true; } ] }); // Register module ECS.m(containers); // // Doors Module // var doors = new Module(`Doors`, { init: function () { // First Opening the Door filter // Checks for openability of a closed door prior to moving through it, and automatically opens the door if possible var f = function (args) { if (args.action.actor.location().hasDoors()) { var door = args.action.actor.location().getDoor(args.action.direction); if(door != null && !door.isOpen) { if(!ECS.runCallbacks(door, `onOpen`)) { queueOutput(p(`(first opening the `+getNameTag(door)+`)`) + args.action.output); queueOutput(NLP.parse(`OPEN ` + door.name)); return door.isOpen; } else { queueGMOutput(`The way is blocked.`); return false; } } } return true; }; ECS.getAction(`move`).filters.push(f); // Add methods for getting and checking doors in a location var place = ECS.getComponent(`place`); place.doors = {}; place.hasDoors = function() { return Object.keys(this.get(`doors`, {})).length > 0; }; place.getDoor = function(d) { if(this.hasDoors()) { return this.doors[d]; } return null; }; } }); // Door component doors.c(`door`, { 'dependencies': [`thing`,`openable`,`scenery`], 'directions': {}, 'onAdd': [function (args) { // Called when the component is added to an entity // Add door and child references to all parent locations // Special handling is needed here because doors are accessible from multiple locations for(var l in args.obj.directions) { if(args.obj.directions.hasOwnProperty(l)) { var loc = args.obj.directions[l]; var e = ECS.getEntity(loc); e.doors[l] = args.obj; if(!e.is(args.obj.location())) { console.log(`ADDING `+args.obj.name+` TO ALT LOCATION ` + e.name); e.children.push(args.obj); // Cheating to add item to other location without actually moving it } } } }], 'isVisible':[ function(args) { return (args.location.hasChild(args.obj)); } ], }); // Register module ECS.m(doors); // // Locks Module // var locks = new Module(`Locks`, { init: function () { // Add locked status to containers and doors // First Unlocking the Door filter // Checks for unlockability of locked door prior to attempting to open it // Can be chained with the First Opening the Door filter var f = function (args) { if (args.nouns.length > 0) { var obj = args.nouns[0]; console.log(`UNLOCK`); console.log(obj); if (obj.hasComponent(`lockable`) && obj.isLocked) { queueOutput(p(`(first unlocking the ` + parse(getNameTag(obj)) + `)`)); queueOutput(NLP.parse(`UNLOCK `+obj.name)); return !obj.isLocked; } } return true; }; ECS.getAction(`open`).filters.push(f); } }); // Lockable component locks.c(`lockable`, { 'dependencies': [`thing`], 'isLocked': false, 'lockKey': null, // key object 'getLockKey': function() { return (this.lockKey != null) ? ECS.getEntity(this.lockKey) : null; } }); // Lock action locks.a(`lock`, { 'aliases': [`lock`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.hasComponent(`lockable`)) { if (typeof target.onLock == `object` && target.onLock.length > 0) { ECS.runCallbacks(target, `onLock`, data.modifiers); return; } else if (typeof target.onLock == `string`) { return target.onLock; } else { target.isLocked = true; return `{{gm}}You lock the ` + target.name + `.
`; } } else if (target == null) { return `{{gm}}I can tell you want to lock something, but you'll have to be more specific.
`; } else { return `{{gm}}That's not the sort of thing that locks.
`; } } }); // Unlock action locks.a(`unlock`, { 'aliases': [`unlock`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.hasComponent(`lockable`)) { if (typeof target.onUnlock == `object` && Object.keys(target.onUnlock).length > 0) { ECS.runCallbacks(target, `onUnlock`, data.modifiers); return; } else if (typeof target.onUnlock == `string`) { return target.onUnlock; } else { var k = target.lockKey; if(data.actor.hasChild(k)) { target.isLocked = false; return `{{gm}}You unlock the ` + target.name + ` using the `+ ECS.getEntity(k).name +`.
`; } else { return `{{gm}}You'll need the right key to do that.
`; } } } else if (target == null) { return `{{gm}}I can tell you want to unlock something, but you'll have to be more specific.
`; } else { return `{{gm}}That's not the sort of thing that unlocks.
`; } } }); // Register module ECS.m(locks); // // Parts Module // var parts = new Module(`Parts`, { init: function () { } }); // Part component parts.c(`part`, { 'dependencies': [], 'onAction.TAKE': function () { queueGMOutput(p(`That's part of the ` + getNameTag(this.parent) + `.`)); return false; } }); // Register module ECS.m(parts); // turn (on/off/clockwise/counterclockwise/left/right) thing // activate/deactivate thing // rotate/twist/spin thing // // Devices Module // {TURN|ROTATE|TWIST} {OBJECT} {DIRECTION|STATE} // {ACTIVATE|DEACTIVATE} {OBJECT} // var devices = new Module(`Devices`, { init: function () { // Extend existing components // Add ECS data // Add entities } }); // Add components, actions, etc devices.c(`device`, { 'dependencies':[`thing`], 'canTake':false, 'device-states':[`off`,`on`], 'device-state':`off`, 'getCanonicalDeviceDirection':function(d) { if(d == `left` || d == `counter-clockwise` || d == `counterclockwise`) { return `left`; } return `right`; }, // Add-on handler for listing operations. Lists state of devices when the device is mentioned 'onList': function () { console.log(this); if (this[`device-state`].is(`on`,`off`)) { return ` (` + this[`device-state`] + `)`; } return ``; } }); // Turn Object devices.a(`turn`, { 'aliases': [`turn`,`rotate`,`twist`,`spin`], 'modifiers': [`on`,`off`,`clockwise`,`counterclockwise`,`counter-clockwise`,`left`,`right`], 'callback': function (data) { // Turn on/off: send to activate/deactivate verb if(data.modifiers.length > 0 && data.modifiers[0] == `on`) { NLP.parse(`activate ` + data.target.name); return false; } if(data.modifiers.length > 0 && data.modifiers[0] == `off`) { NLP.parse(`deactivate ` + data.target.name); return false; } // Default to right/clockwise if(data.modifiers.length == 0) { data.modifiers = [`right`]; } // Get direction var direction = data.target.getCanonicalDeviceDirection(data.modifiers[0]); // Cycle through states var i = data.target[`device-states`].indexOf(data.target[`device-state`]); if(direction == `left`) { // I'm too dumb to figure out a non-ternary one-liner for this right now i = (i > 0) ? i - 1 : (data.target[`device-states`].length + i - 1); } else { i = (i + 1) % data.target[`device-states`].length; } data.target[`device-state`] = data.target[`device-states`][i]; queueGMOutput(p(`You turn the ` + getNameTag(data.target) + ` ` + data.modifiers[0] + `.`)); // Handled return true; } }); // Activate Object devices.a(`activate`, { 'aliases': [`activate`], 'modifiers':[], 'callback': function (data) { if(this[`device-state`] == `on`) { queueGMOutput(`The ` + getNameTag(this) + ` is already on.`); return; } this[`device-state`] = `on`; queueGMOutput(`You turn on the ` + getNameTag(this) + `.`); } }); // Deactivate Object devices.a(`deactivate`, { 'aliases': [`deactivate`], 'modifiers':[], 'callback': function (data) { if(this[`device-state`] == `off`) { queueGMOutput(`The ` + getNameTag(this) + ` is already off.`); return; } this[`device-state`] = `off`; queueGMOutput(`You turn off the ` + getNameTag(this) + `.`); } }); // Press Object devices.a(`press`, { 'aliases': [`press`,`push`,`touch`,`boop`], 'modifiers':[], 'callback': function (data) { queueGMOutput(`You press the ` + getNameTag(this) + `.`); } }); // Register module ECS.m(devices); // // Combat Module // ATTACK // var combat = new Module(`Combat`, { init: function () { // Extend existing components // Add ECS data // Add entities } }); // Add components, actions, etc // Attack action: fists if no weapon present, use best weapon if available, allow explicit weapon specification combat.a(`attack`, { 'aliases': [`attack`, `hit`, `punch`, `kick`, `fight`, `kill`, `headbutt`, `karate chop`, `beat`, `break`], 'modifiers': [`with`, `using`], 'callback': function (data) { var weapon = null; // Get target var target = data.nouns[0]; // Get modifier (with/using, indicating a weapon is being used) if (data.modifiers.length > 0 && data.nouns.length > 1) { weapon = data.nouns[1]; } if (target != null) { if (target.hasComponent(`living`)) { target.onHit(weapon); } else { queueOutput(`You flail uselessly at the ` + getNameTag(target) + `.`); } } else { queueOutput(`You take a moment to practice your moves.`); } } }); // Brandish action combat.a(`brandish`, { 'aliases': [`brandish`,`wield`,`wave`,`swing`], 'callback': function (data) { if(data.nouns.length > 0) { // Get weapon var weapon = data.nouns[0]; // TODO: 'at' modifier to attack, e.g. 'swing sword at troglodyte' queueOutput(`You adopt an aggressive stance with the ` + getNameTag(weapon) + `.`); } else { queueOutput(`You take a moment to practice your moves.`); } } }); // Register module ECS.m(combat); // // Social Module // TALK TO, ASK, ASK ABOUT, HUG, KISS, HIGH FIVE // var social = new Module(`Social`, { init: function() { // Extend existing components var living = ECS.getComponent(`living`); living.conversation = null; living.onAdd.push(function(args) { if(args.obj.conversation != null) { args.obj.conversation.character = args.obj; } }); // Add ECS data // Add entities } }); // Add components, actions, etc // Hug social.a(`hug`, { 'aliases':[`hug`,`embrace`], 'modifiers':[], 'callback':function(data){ // Get target var target = data.nouns[0]; if(target != null) { if(target.hasComponent(`living`)) { queueOutput(`{{gm}}You give `+getNameTag(target)+` a warm hug.
`); } else { queueOutput(`{{gm}}You awkwardly attempt to hug the `+getNameTag(target)+`.
`); } } else { queueOutput(`You pretend to hug an invisible friend. It's better than nothing.`); } } }); // Pet social.a(`pet`, { 'aliases':[`pet`,`pat`,`rub`], 'modifiers':[], 'callback':function(data){ // Get target var target = data.nouns[0]; if(target != null) { if(target.hasComponent(`living`)) { queueOutput(`{{gm}}You give `+getNameTag(target)+` a gentle pat.
`); } else { queueOutput(`{{gm}}You awkwardly attempt to pat the `+getNameTag(target)+`.
`); } } else { queueOutput(`You pretend to pat an invisible friend.`); } } }); // Helper function to build a speech tag // Used when an NPC speaks // Accepts an Entity, entity key, or raw text function getSpeechTag(target,classes) { var name = `UNKNOWN`; if(target instanceof Entity) { name = target.name; } else { // Try to find an entity matching the identifier var entity = ECS.getEntity(target); if(entity != null) { name = entity.name; } } if(typeof classes == `undefined`) { classes = ``; } if(typeof target.extraTags != `undefined`) { classes += ` ` + target.extraTags.join(` `); } return ``+name+`: `; } // Conversation constructor // Initializes node list and sets starting state var Conversation = function(nodes,root){ this.nodes = {}; for(var n in nodes) { var node = new ConversationNode(nodes[n]); this.nodes[node.id] = node; } this.rootNode = this.nodes.root.id; this.currentNode = null; this.prevNode = null; this.active = false; }; Conversation.prototype = { 'active':false, 'character':null, 'nodes':{}, // Reset conversation back to root node 'reset':function() { this.currentNode = null; }, // Starts a conversation, optionally with a topic 'start':function(topic) { this.currentNode = this.getRoot(); this.prevNode = null; this.active = true; // Check for topic if(typeof topic != `undefined` && topic != null) { if(this.doTopicNode(topic)) { return; } } // Show default response and options this.doNode(); Display.setInputPrefix(` (speaking to ` + this.character.name + `) `); }, 'doNode':function() { var conversation = this; var nodes = this.getCurrentNodes(); var menu = []; var node = this.currentNode; for(var n in nodes) { menu.push({'text': nodes[n].prompt, 'command': nodes[n].key, 'subtext': ``+(nodes[n].visited ? `(Visited)` : ``)+``}); } menu.push({'text':`(EXIT)`,'command':`exit`,'subtext':`I'm done talking.`}); NLP.interrupt( function(){ node.callback(null, conversation); queueOutput(parse(`{{menu options}}`, {'options':menu})); }, function(string){ if(string == `exit` || conversation.doTopicNode(string)) { Display.resetInputFixes(); return true; } enableLastMenu(); queueOutput(`{{gm}}There is no response.
`); return false; } ); }, 'doTopicNode':function(topic) { // Trigger selected node response var node = this.getSelectedNode(topic); if(node) { if(node.hasOwnProperty(`callback`)) { return node.callback(topic, this); } console.log(`CHAR IS`); console.log(this.character); var classes = []; if(typeof this.character.extraTags != `undefined`) { classes = this.character.extraTags; } queueOutput(parse(`echo`, {'text': node.prompt})); queueOutput(getSpeechTag(this.character)+``+node.response+`
`, `auto`, {'classes':classes}); node.visited = true; if(!node.end) { var nextNode = node; if(node.forward != null) { nextNode = this.findNode(node.forward); } // Activate next node this.prevNode = this.currentNode; this.currentNode = nextNode; this.doNode(); } return true; } return false; }, 'addNode':function(n) { this.nodes[n.id] = n; }, 'addNodes':function(n) { for(var i in n) { this.addNode(n[i]); } }, 'findNode':function(key) { return this.nodes[key]; }, 'getRoot':function(){ return this.findNode(this.rootNode); }, 'getCurrentNodes':function(){ if(this.currentNode == null) { return []; } var list = []; for(var n in this.currentNode.nodes) { list.push(this.findNode(this.currentNode.nodes[n])); } return list; }, 'getSelectedNode':function(command){ var nodes = this.getCurrentNodes(); for(var n in nodes) { if(nodes[n].key.toLowerCase() == command.toLowerCase()) { return nodes[n]; } } // Invalid selection return false; } }; var ConversationNode = function(data){ $.extend(this, data); if(this.key == null) { this.key = this.id; } if(this.prompt == null) { this.prompt = this.key; } }; ConversationNode.prototype = { 'id':null, 'key':null, 'prompt':null, 'response':null, 'callback':function(topic){ return true; }, 'forward':null, // node to forward to after response 'end':false, // end conversation after response 'visited':false, 'nodes':[] }; // Talk social.a(`talk`, { 'aliases':[`talk`,`ask`,`speak`,`say`], 'modifiers':[`to`,`hi`,`hello`,`about`], 'overflow':true, 'callback':function(data){ // Get target var nouns = data.nouns; var modifiers = data.modifiers; var target = nouns[0]; var topic = null; // TALK ABOUT X (to no one) if(modifiers.length == 1 && modifiers[0] == `about` && nouns.length == 1) { data.output += `{{gm}}(first taking the ` + getNameTag(obj) + `)
`); NLP.parse(`TAKE ` + obj.name); return (obj.parent == player); } } return true; }; ECS.getAction(`wear`).filters.push(f); // Add wear restrictions on DROP action f = function (args) { if (args.nouns.length > 0) { var obj = args.nouns[0]; if (obj.isWorn) { queueOutput(`(first taking off the ` + getNameTag(obj) + `)
`); NLP.parse(`TAKE OFF ` + obj.name); return (!obj.isWorn); } } return true; }; ECS.getAction(`drop`).filters.push(f); // Add ECS data // Add entities } }); // Add components, actions, etc clothing.c(`wearable`, { 'dependencies': [`thing`], 'armor': 0, // damage reduction value 'slot': null, // location on the body 'exclusive': true, // exclusive items prevent other items in the same slot 'isWorn': false, // whether the item is currently being worn 'onAction.WEAR': null, // triggered when the player attempts to put on the item 'onAction.REMOVE': null,// triggered when the player attempts to remove the item 'onList': function () { if (this.isWorn) { return ` (on ` + this.slot + `)`; } return ``; } }); // WEAR action clothing.a(`wear`, { 'aliases': [`wear`, `put on`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.hasComponent(`wearable`)) { // Check slot console.log(`TARGET CLOTHING:`); console.log(target); var slot = target.slot; var playerSlot = player.clothing[slot]; if (target.isWorn) { queueGMOutput(`You're already wearing that.`); } else if (playerSlot == null || (!playerSlot.exclusive && !target.exclusive)) { player.clothing[slot] = target; target.isWorn = true; queueGMOutput(`You put on the ` + getNameTag(target) + `.`); } else { queueGMOutput(`You can't wear that while you're wearing the ` + getNameTag(playerSlot) + `. You'll have to remove it first.`); } return true; } else if (target == null) { return `I can tell you want to wear something, but you'll have to be more specific.
`; } else { return `That's not the sort of thing that you wear.
`; } } }); // REMOVE action clothing.a(`remove`, { 'aliases': [`remove`, `take off`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.hasComponent(`wearable`)) { // Check if worn if (target.isWorn) { player.clothing[target.slot] = null; target.isWorn = false; queueGMOutput(`You remove the ` + getNameTag(target) + `.`); } else { queueGMOutput(`You can't remove that, since you're not wearing it.`); } return true; } else if (target == null) { return `I can tell you want to remove something, but you'll have to be more specific.
`; } else { return `That's not the sort of thing that you take off.
`; } } }); // Register module ECS.m(clothing); // // Module Template // // // Core stuff // var Sound = { musicEnabled: false, captionsEnabled: true, activeSoundscape: null, activeMusic: null, activeCaptions: [], paused: false, player: null, init: function () { this.player = document.getElementById(`audio-music`); }, playMusic: function (music) { if (music != this.activeMusic || this.paused) { if(this.activeMusic) { // Save playback location for previous track this.activeMusic.currentTime = Sound.player.currentTime; // Set seek time for linked tracks if(this.activeMusic.isLinkedTo(music)) { music.currentTime = this.activeMusic.currentTime; } console.log(`Prev Track: ` + this.activeMusic.id + ` (` + this.activeMusic.currentTime + `), New Track: ` + music.id + ` (` + music.currentTime + `)`); } this.paused = false; this.activeMusic = music; if (this.musicEnabled) { if (!this.paused) { $(`#audio-music source`).attr(`src`, `assets/music/` + music.file); } //$(`#audio-music`).prop(`volume`, 0.25); $(`#audio-music`).promise().done(function () { Sound.player.load(); // Seek Sound.player.currentTime = music.currentTime; Sound.player.play(); Sound.player.onended = function () { if (music.loop) { Sound.player.play(); } }; //$(`#audio-music`).animate({volume: 0.5}, 2000); }); } // Clear previous captions for (var c in this.activeCaptions) { clearTimeout(this.activeCaptions[c]); } // Set new captions for (c in music.captions) { var caption = music.captions[c]; this.activeCaptions.push(window.setTimeout(function (c) { c.fire(); }, caption.time, caption)); } } }, 'pauseMusic': function () { this.player.pause(); this.paused = true; this.captionsEnabled = true; }, 'resumeMusic': function () { this.musicEnabled = true; this.paused = false; this.captionsEnabled = false; ECS.getModule(`Music`).checkForMusicAtLocation(); }, }; var Caption = function (time, text) { this.time = time; this.text = text; }; Caption.prototype.fire = function () { if (!Sound.captionsEnabled) { return; } var c = $(`♫ ` + this.text + ` ♫`); $(c).hide().prependTo(`.captions`).fadeIn(500); window.setTimeout(function (c) { c.fadeOut(5000); }, 10000, c); }; var Track = function (id, file, options) { this.id = id; this.file = file; for (var o in options) { this[o] = options[o]; } }; Track.prototype = { 'id': null, 'file': null, 'title': `Unnamed Track`, 'linkedTracks': [], 'volume': 1.0, 'loop': true, 'startTime': 0, // when the track was started 'currentTime': 0, // the latest time played for the track 'captions': [ // ordered array of captions /* { 'time': 25 // time in seconds 'text': [ // random array of text options 'Soothing elevator music', 'Obnoxious elevator music' ] }, { 'time': 50, 'text': ['DRUMS CRASHING'] // HTML is OK } */ ], 'captionIndex': null, // Tracks most recent caption 'onStart': function () { this.startTime = new Date().getTime(); this.onCaption(); }, 'onRestart': function () { this.startTime = new Date().getTime(); this.captionIndex = null; this.onCaption(); }, 'onCaption': function () { var index = (this.captionIndex == null) ? 0 : this.captionIndex; if (index >= this.captions.length) { return; } var playTime = 0; var caption = this.captions[index]; if (playTime >= caption.time) { this.captionIndex++; // Display caption } }, 'isLinkedTo':function(track) { return (this.linkedTracks.indexOf(track.id) >= 0); } }; var music = new Module(`Music`, { init: function () { // Extend existing components var place = ECS.getComponent(`place`); place.music = null; place.onEnter.push(function (args) { ECS.getModule(`Music`).checkForMusicAtLocation(args.obj); }); Sound.init(); // Add ECS data // Add entities }, checkForMusicAtLocation: function (location) { var music = null; var l = (typeof location != `undefined`) ? location : player.location(); if (l.music != null) { music = l.music; } else if (l.region != null) { var region = ECS.findEntity(`region`, l.region); if(region != null && typeof region.music != `undefined`) { music = region.music; } } if (music != null && !Sound.paused) { Sound.playMusic(music); } } }); // Music action music.a(`music`, { 'aliases': [`music`], 'modifiers': [`on`, `off`], 'callback': function (data) { ECS.tick = false; // No tick for music toggle/status if (data.modifiers.length == 0) { // Get music status return `Music is ` + (Sound.musicEnabled ? `ON` : `OFF`) + ``; } else if (data.modifiers[0] == `on`) { // Turn on music Sound.resumeMusic(); return `
Music is now: ON
`; } else if (data.modifiers[0] == `off`) { // Turn off music Sound.pauseMusic(); return `Music is now: OFF
`; } return; } }); // Register module ECS.m(music); // // Quests Module // QUEST, QUESTS // var Quests = new Module(`Quests`, { 'quests': {}, // Quests are stored in the module. Later on it might make sense to move them to a player instance. 'openQuests': {}, init: function () { }, 'getOpenQuests': function () { return this.openQuests; }, 'isComplete': function (q) { return this.quests[q]._status == `complete`; }, 'start': function (q) { this.quests[q].onStart(); } }); // Quests verb: list available quests Quests.a(`quests`, { 'aliases': [`quests`], 'modifiers': [`all`, `completed`], 'callback': function (data) { if (data.modifiers.length > 0) { // List a particular set of quests } var questText = ``; var quests = ECS.getModule(`Quests`).quests; for (var q in quests) { if (quests[q]._status != `inactive`) { questText += parse(`{{name}} ({{status}})You are unable to catch the bird. It's surprisingly nimble.
`; }, 'onAction.ATTACK':function(){ queueGMOutput(p(`The bird evades your attack.`)); return false; }, 'onTick':function(){ if(this._hungry() && !this.locationIs(`east-trail`) && player.locationIs(`east-trail`)) { ECS.moveEntity(this, `east-trail`); if(first(`bird-feeder-interaction`)) { queueGMOutput(p(`The bird swoops in and flits around your head with obvious excitement. It seems to be expecting you to do something.`)); } else { queueGMOutput(p(`The bird drops in from the forest canopy to land atop a nearby branch. It cocks its head at you and waits.`)); } return; } // Special Case: East Trail Bird Feeder if(this.locationIs(`east-trail`)) { if(ECS.getEntity(`bird-feeder-hole`).get(`blocked`, false)) { queueLocalOutput(this, p(`The bird lands briefly at the bird feeder, pecks at the {{tag 'opening' classes='object scenery look' command='look in hole'}}, then darts away in frustration.`)); } else { queueLocalOutput(this, p(`The bird happily devours a few seeds from the feeder before continuing on its way.`)); this.hunger -= 10; } this._moveAlongPath(); return; } // Look for food when hungry if(this._hungry()) { console.log(`BIRD STATE: LOOKING FOR FOOD`); if(!this._lookForFood()) { this._moveAlongPath(); } return; } // Head toward ranger station when full if(!this.locationIs(`ranger-station`)) { // Head to ranger station console.log(`BIRD STATE: LOOKING FOR RANGER STATION`); if(this.locationIs(`north-trail`)) { this._alertPlayerToMovement(`north-trail`, `ranger-station`); ECS.moveEntity(this, `ranger-station`); } else { this._moveAlongPath(); } return; } // Sing at random console.log(`BIRD STATE: SINGING`); if(random() > 0.75) { queueLocalOutput(this, `The bird chirps a happy tune.`); } // Update hunger this.hunger++; }, // Data 'hunger':100, 'path':{ 'forest-trail':`hill-slide`, 'hill-slide':`north-trail`, 'north-trail':`east-trail`, 'east-trail':`other-east-trail`, 'other-east-trail':`south-trail`, 'south-trail':`dim-clearing`, 'dim-clearing':`west-trail`, 'west-trail':`north-trail`, 'ranger-station':`north-trail`, }, // Functions '_hungry':function(){ return this.hunger > 20; }, '_lookForFood':function() { var things = this.location().children; for(var f in things) { console.log(`BIRD EYEBALLING `+things[f].name); if(things[f].hasComponent(`edible`)) { if(things[f].nutrition > 0) { queueLocalOutput(this, p(`The bird pecks at the ` + things[f].name + ` eagerly.`)); this.hunger -= things[f].nutrition; return true; } else { queueLocalOutput(this, p(`The bird pecks at the ` + things[f].name + ` half-heartedly.`)); return false; } } } return false; }, '_moveAlongPath':function() { var prev = this.location().key; var next = this.path[prev]; this._alertPlayerToMovement(prev,next); ECS.moveEntity(this, next); }, '_alertPlayerToMovement':function(prev,next) { if(player.locationIs(prev)) { queueGMOutput(`The bird flits away toward ` + ECS.getEntity(next).name + `.`); } else if(player.locationIs(next)) { queueGMOutput(`The bird flits in from ` + this.location().name + `.`); } } }); // Hill Slide ECS.e(`hill-slide`, [`place`], { 'name':`Hill Slide`, 'region':`forest`, 'exits':{'d':`north-trail`,'w':`forest-trail`}, 'descriptions':{ 'default':`The trail descends steeply here as a muddy slide. It looks like you can make it {{down}} safely, but it's unlikely you'll be able to climb back up. The way back to the {{w}} is clear.`, 'short':`A muddy slide.` }, 'onLeave':[function(args){ if(args.direction == `d`) { queueGMOutput(`You slide down, scraping against roots and rocks. You arrive at the bottom no worse for the wear, but a bit dirtier.
`); } return false; }] }); understand(`sliding down hill rule`) .in(`hill-slide`) .text([`slide`,`slide down`]) .until(function(action){ return action.actor.locationIs(`north-trail`); }) .do(function(action){ NLP.parse(`d`); action.mode = Rulebook.ACTION_CANCEL; }) .start(); // North Trail ECS.e(`north-trail`, [`place`], { 'name':`North Trail`, 'region':`forest`, 'exits':{'w':`ranger-station-base`,'e':`east-trail`,'sw':`west-trail`}, 'descriptions':{ 'default':`A narrow trail splits in three directions here, intersecting a large patch of {{tag 'bright blue flowers' classes='scenery blue' command='look at flowers'}}. To the {{w}} you catch a vague glimpse of {{tag 'some sort of structure' classes='scenery' command='look at structure'}}, while the main trail continues to the {{sw}}. To the {{e}} the trail curves southward out of sight. {{tag 'Dense brambles and winding vines' classes='scenery green' command='look at brambles'}} obscure your vision in all other directions.`, 'short':`A dirt T-junction.` } }); // Scenery: Bright Blue Flowers ECS.e(`bright-blue-flowers`, [`scenery`], { 'name':`bright blue flowers`, 'nouns':[`flowers`,`blue flowers`], 'spawn':`north-trail`, 'descriptions':{ 'default':`A patch of lovely little seven-petaled flowers.`, 'smell':`Floral scented.` }, 'onTakeFail':function(){ return `{{gm}}You don't have an immediate use for them, and inventory space is precious. There's a flower-picking quest later in the game, if that's your cup of tea.
`; } }); // Scenery: Brambles/Vines ECS.e(`north-trail-brambles`, [`scenery`], { 'name':`brambles and/or vines`, 'nouns':[`brambles`,`vines`], 'spawn':`north-trail`, 'descriptions':{ 'default':`The forest here has grown thick, almost claustrophobic. I mean it makes you feel claustrophobic, not that the forest feels claustrophobic. It's a natural formation, I don't think it has an understanding of the fight-or-flight response necessary to feel something like claustrophobia. Put in terms of game mechanics, the undergrowth prevents passage and vision in most directions.`, 'smell':`Earthy.` } }); // Object: Scrap of Paper ECS.e(`scrap-of-paper`, [], { 'name':`scrap of paper`, 'spawn':`north-trail`, 'nouns':[`paper`,`scrap`,`scrap of paper`], 'descriptions':{ 'default':`An unremarkable scrap of paper, muddied and torn. If it once held writing, it was lost to time or water or an eraser.`, 'telescope':`You can make out the mud-stained fibers of the paper in great detail, but are unable to discern anything about its past contents.`, 'short':`Some trash.`, } }); // Scenery: Structure ECS.e('structure', ['scenery'], { 'name':'structure', 'nouns':['structure'], 'spawn':'north-trail', 'descriptions':{ 'default':"Some kind of tall wooden construction. You can't make out the details from here.", 'short':"A wooden tower thing.", } }); ECS.e('structure-details', ['scenery'], { 'name':'details', 'nouns':['details','the details'], 'spawn':'north-trail', 'descriptions':{ 'default':"You can't make them out from here.", 'short':"Unclear.", } }); // Ranger Station (Base) ECS.e(`ranger-station-base`, [`place`], { 'name':`Base of Ranger Station`, 'region':`forest`, 'exits':{'u':`ranger-station`,'e':`north-trail`}, 'descriptions':{ 'default':`You are standing at the base of a tall, slightly-rickety wooden structure. As you can see from the title there, it's some kind of ranger station. The {{tag 'support beams' classes='scenery' command='look at beams'}} are old and dry, with {{tag 'newer planks' classes='scenery' command='look at planks'}} scattered here and there to hold the aging tower together. A {{tag 'rope ladder' classes='scenery' command='look at rope ladder'}} leads {{up}} into the viewing box. The foliage down here was cleared back from the tower at some point, but is beginning to encroach once more. The trail leads away to the {{east}}.`, 'short':`A piece-of-junk tower stands over you.`, } }); // Scenery: Support Beams ECS.e(`support-beams`, [`scenery`], { 'name':`support beams`, 'nouns':[`beams`], 'spawn':`ranger-station-base`, 'descriptions':{ 'default':`Old and dry. Oh the stories they could tell...`, 'short':`Old and boring.`, }, 'onTakeFail':function(){ return `{{gm}}They seem to be fixed in place.
`; } }); // Scenery: ECS.e(`newer-planks`, [`scenery`], { 'name':`newer planks`, 'nouns':[`planks`], 'spawn':`ranger-station-base`, 'descriptions':{ 'default':`They look a bit anachronistic compared to the older support beams, but together they make quite a team. Or should I say, quite a beam.`, 'short':`New and boring.`, }, 'onTakeFail':function(){ return `{{gm}}It seems to be fixed in place.
`; } }); // Scenery: Rope Ladder ECS.e(`rope-ladder`, [`scenery`], { 'name':`rope ladder`, 'nouns':[`ladder`], 'spawn':`ranger-station-base`, 'descriptions':{ 'default':`A pair of thick, knotted ropes strung through wooden planks every foot or so.`, 'short':`A ladder. For climbing.`, }, 'onTakeFail':function(){ return `{{gm}}It seems to be fixed in place.
`; }, 'onAction.CLIMB':function(action) { // Get up/down/other var d = getCanonicalDirection(action.modifiers[0]); if(d == `u`) { NLP.parse(`climb up`); return false; } else if(d == `d`) { NLP.parse(`climb down`); return false; } return true; } }); // Ranger Station ECS.e(`ranger-station`, [`place`], { 'name':`Ranger Station`, 'region':`forest`, 'exits':{'d':`ranger-station-base`}, 'descriptions':{ 'default':`The viewing box sways gently in the breeze; a lesser hero would be slightly alarmed. From here you can see out over the {{tag 'treetops' classes='scenery' command='look at treetops'}} in all directions. The interior of the box is a hodge-podge of old and new--multiple layers of patchwork repairs over several decades. You can smell a hint of sawdust from some recent alteration. A simple lean-to roof provides shelter from the elements, though there seems to be little protection against cold nights. A cutout in the floor gives access to a rope ladder leading {{down}} to the forest floor. {{scenery}}`, 'short':`A perilous plank and pillar platform, poorly placed.` } }); // Scenery: Treetops ECS.e(`treetops`, [`scenery`], { 'name':`treetops`, 'nouns':[`treetops`], 'spawn':`ranger-station`, 'descriptions':{ 'default':`The forest canopy, appearing not entirely unlike a plate of fresh broccoli.`, 'short':`Some trees.` }, 'onTakeFail':function(){ return `{{gm}}It seems to be fixed in place.
`; } }); // Scenery: Roof ECS.e(`roof`, [`scenery`], { 'name':`roof`, 'nouns':[], 'spawn':`ranger-station`, 'descriptions':{ 'default':`It's basically just a big flat board. Nothing to write home about.`, 'short':`A building hat.` }, 'onTakeFail':function(){ return `{{gm}}It seems to be fixed in place.
`; } }); // Scenery: Sawdust ECS.e(`sawdust`, [`scenery`], { 'name':`sawdust`, 'nouns':[], 'spawn':`ranger-station`, 'descriptions':{ 'default':`Just a bit of the smell remains.`, 'smell':`Smells oaky.`, 'short':`It's sawdust.` }, 'onTakeFail':function(){ return `{{gm}}I know you're new to adventuring, but it seems to me that common sense would dictate one cannot take a smell.
`; } }); // Scenery: Spider ECS.e(`spider`, [`scenery`], { 'name':`spider`, 'nouns':[], 'spawn':`ranger-station`, 'descriptions':{ 'default':`You've never seen a nonplussed spider before, but you wouldn't describe it any other way. Your continued interest seems to have made it uncomfortable. It scurries back and forth uncertainly.`, 'telescope':`Up close it looks like the bastard child of Shelob and another, equally horrific giant spider. It stares back at you with unblinking, multi-faceted eyes.`, 'smell':`Violating the spider's personal space, you take a quick whiff. It smells like a sad dream.`, 'short':`An arachnid, probably not important to the plot.` }, 'onTakeFail':function(){ return `{{gm}}It scurries out of reach, smugly.
`; } }); // Object/Scenery: Telescope ECS.e(`telescope`, [`scenery`], { 'name':`telescope`, 'nouns':[], 'spawn':`ranger-station`, 'descriptions':{ 'default':`A well-used collapsing telescope of high-quality design. It stands on a similarly sturdy tripod and points out to the north.`, 'scenery':`A tripod-mounted {{tag 'telescope' classes='object scenery' command='x telescope'}} looks out from the station.`, 'through':{ 'n':`In the distance, a ramshackle cabin struggles to be seen in an overgrown clearing. You're not sure why the ranger has taken an interest in it; it's clearly been abandoned for some time.`, 'e':`Smoke and steam rise in a hundred plumes over a distant town. Not too far away, a foreboding stone castle sits near the peak of a snow-capped mountain.`, 's':`Trees, more trees, and even more trees. If you tried to count them, you'd get bored around the same time as me, and then we'd probably both go find better things to do.`, 'w':`Miles away, the hilly forest gradually gives way to flat, grassy plains. Further still, there's probably a third, different thing, but it's too far for you to see.`, 'u':`A spider stares back at you, nonplussed.`, 'd':`You can see some boards, and through a continent-sized hole in the floor, you can see the ground. It's filthy.` }, 'telescope':`That's not really how telescopes work.`, 'short':`A tube with some glass in it.`, }, 'onTakeFail':function(){ return `{{gm}}It seems to be fixed in place.
`; }, 'onAction.LOOK':function(data){ console.log(data); // Check for direction modifier if(data.modifiers.length == 2) { // TODO: cleaner handling for getting move action and move modifiers (instead of relying on global availability) var direction = moveAction.canonical(data.modifiers[0]); var modifiers = [`through`, `at`]; if (moveModifiers.indexOf(direction) >= 0 && data.modifiers[1] == `through`) { // check for valid direction if (this.descriptions.through.hasOwnProperty(direction)) { return this.descriptions.through[direction]; } return `You can see a combination of the two adjacent cardinal directions.`; } else if ( data.nouns.length == 2 && modifiers.indexOf(data.modifiers[0]) >= 0 // check for valid first modifier && modifiers.indexOf(data.modifiers[1]) >= 0 // check for valid second modifier ) { // command like 'look through telescope at spider' // note that 'look at spider through telescope' targets spider and gives different result var target = data.nouns[1]; if(target.descriptions.hasOwnProperty(`telescope`)) { return target.descriptions[`telescope`]; } return `Like normal, but way bigger and probably with more dead skin cells than you expected on it.`; } return `I'm not sure what you're trying to do with the telescope.`; } return this.descriptions.default; } }); // Character: Ranger Bob ECS.e(`ranger`, [`living`,`scenery`], { 'name':`Ranger Bob`, 'nouns':[`ranger`,`bob`], 'spawn':`ranger-station`, 'listInRoomDescription':false, 'descriptions':{ 'default':`A grizzled forest ranger.`, 'scenery':`{{nametag 'ranger' classes='scenery npc' command='inspect bob'}} is here, picking his teeth with a twig. {{#first 'seen-ranger'}}He seems surprised to see a visitor, but not unduly.{{/first}}`, 'telescope':`The years have not been entirely kind to Bob. Some things can't be unseen.`, 'smell':`Bob smells like he lives in the woods.`, 'short':`Ranger Bob.`, }, 'onTakeFail':function(){ return `{{gm}}Ranger Bob doesn't seem amenable to that.
`; }, 'conversation':new Conversation([ { 'id':`root`, 'key':``, 'callback':function(topic, conversation){ if(!conversation.prevNode) { queueCharacterOutput(`ranger`,`Hmm, don't recall inviting any guests.`); } else { queueCharacterOutput(`ranger`,`Anything else?`); } return true; }, 'nodes':[`sword`,`telescope`]//,'lost','bottle','cabin','twins','bye'] }, { 'id':`sword`, 'prompt':`Any clue what's up with this sword?`, 'response':`No idea.`, 'nodes':[`telescope`,`root`] }, { 'id':`telescope`, 'prompt':`That's a nice telescope you've got there.`, 'response':`Mm-hmm.`, 'nodes':[`why a telescope`] }, { 'id':`why a telescope`, 'prompt':`What's it for?`, 'response':`Keeping an eye on things. Take a gander if you like.`, 'forward':`root` } ]) }); ECS.e(`ranger-bob-twig`, [`scenery`], { 'name':`twig`, 'spawn':`ranger-station`, // TODO: move to ranger bob, make certain inventory viewable 'listInRoomDescription':false, 'descriptions': { 'default':`A gnawed-on twig.` }, 'onAction.TAKE':function(){ queueGMOutput(`Ranger Bob seems to be enjoying it. Best leave it alone.`); } }); // East Trail ECS.e(`east-trail`, [`place`], { 'name':`East Trail`, 'region':`forest`, 'exits':{'w':`north-trail`,'s':`other-east-trail`}, 'descriptions':{ 'default':`This stretch of trail looks much like the north trail, except it only goes in two directions and there are no flowers here. The brambles gradually give way to tall saber ferns, and you can see muddled animal tracks in the dirt. The trail leads to the {{w}} and to the {{s}}. {{scenery}}`, 'short':`A strip of dirt in the woods.` } }); // Scenery: Ferns ECS.e(`ferns`, [`scenery`], { 'name':`ferns`, 'nouns':[`fern`,`polystichum neolobatum`], 'spawn':`east-trail`, 'descriptions':{ 'default':`Pretty green polystichum neolobatum ferns. Best steer clear if you have allergies; you can see the spores from here.`, 'smell':`It smells of damp earth, yet slightly sweet.` }, 'onTakeFail':function(){ return `{{gm}}The ferns are currently of no use to you.
`; } }); // Scenery: Spores ECS.e(`spores`, [`scenery`], { 'name':`spores`, 'nouns':[`spore`], 'spawn':`east-trail`, 'descriptions':{ 'default':`They're actually quite interesting. They grow in groups, called sori, and ripen in the summer or early fall. When planted, they will eventually form a living carpet called prothallia, and if conditions are right, fronds will start to pop up not too long after.`, 'smell':`They smell like allergies.` }, 'onTakeFail':function(){ return `{{gm}}You don't need any spores at the moment.
`; } }); /* Bird feeder is full of food, but the output chute is blocked, much to the bird's annoyance. The player can clear the blockage by smacking the feeder, dislodging a bottle cap stowed there by some sort of troublemaker. */ // Object: Bird Feeder ECS.e(`bird-feeder`, [`scenery`,`container`], { 'name':`bird feeder`, 'spawn':`east-trail`, 'nouns':[`feeder`,`bird feeder`], 'descriptions':{ 'default':`The hand-crafted wooden bird feeder (it's a bird feeder made of wood, not a bird feeder for wooden birds) is aged but in good condition. Its construction is a simple box with a sloped covering on top. A hole in the front opens out onto a small perch. A fading layer of blue paint has flaked in places, leaving glimpses of an older green. It stands firmly atop a wrought iron post driven into the ground.`, 'scenery':`A bird feeder stands on a post beside the trail.` }, }); // The post holds up the bird feeder. It is completely boring. ECS.e(`bird-feeder-post`, [`scenery`,`part`], { 'name':`iron post`, 'spawn':`bird-feeder`, 'nouns':[`post`,`iron post`,`wrought iron post`], 'descriptions':{ 'default':`A simple wrought iron post.` } }); // The hole is a nothing, yet you can look at it ECS.e(`bird-feeder-hole`, [`scenery`,`part`,`container`,`nothing`], { 'name':`hole`, 'spawn':`bird-feeder`, 'nouns':[`hole`,`opening`], 'descriptions':{ 'default':`A hole. {{#if empty}}{{else}}Something seems to be wedged {{tag 'inside' classes='object scenery' command='look in hole'}}.{{/if}}` }, 'blocked':true, }); understand(`rule for filling bird feeder`) .in(`east-trail`) .text([`fill feeder`,`fill bird feeder`,`feed bird`]) .do(function(action){ queueGMOutput(`The feeder seems to be full already.`); action.mode = Action.ACTION_CANCEL; }).start(); // Other East Trail ECS.e(`other-east-trail`, [`place`], { 'name':`Other East Trail`, 'region':`forest`, 'exits':{'sw':`south-trail`,'n':`east-trail`}, 'descriptions':{ 'default':`Almost identical to the east trail, which has a more interesting description. Check it out if you're interested. This trail leads from the {{n}} and curves to the {{sw}}. {{#second 'visited-other-east-trail'}}Nothing has changed here since your last visit. Literally nothing. Everything is exactly the same, so don't bother checking around for subtle things you might have missed.{{/second}}`, 'short':`Like the east trail.` } }); // West Trail ECS.e(`west-trail`, [`place`], { 'name':`West Trail`, 'region':`forest`, 'exits':{'ne':`north-trail`,'s':`hot-spring`,'e':`dim-clearing`}, 'descriptions':{ 'default':`Another junction in the trail gives you pause. Decisions are hard. The ground here is slightly damp, not quite muddy. The trail winds in from the {{ne}} before splitting off to the {{e}} and {{s}}. Thick bushes surround you, spotted with yellow flowers and little red berries--edible or poisonous, you can't be sure. To the south you can hear water burbling; to the east, voices muffled by the thick forest air.`, 'short':`A muddy junction.`, } }); // Scenery: Yellow Flowers ECS.e(`yellow-flowers`, [`scenery`], { 'name':`yellow flowers`, 'nouns':[`flowers`], 'spawn':`west-trail`, 'descriptions':{ 'default':`They look like the blue flowers you saw before, but these are yellow. They might be daffodils; I don't really know much about flowers.`, 'short':`Some yellow flowers.`, }, 'onTakeFail':function(){ return `{{gm}}You don't have time for picking flowers.
`; } }); // Scenery: Red Berries ECS.e('red-berries', ['scenery','edible'], { 'name':'red berries', 'nouns':['berries'], 'spawn':'west-trail', 'descriptions':{ 'default':'Small, glossy berries. 50/50 odds they\'re poisonous instead of delicious. I suppose they could be both.', 'smell':'Smells ok.', 'short':'Poisonous?', }, 'nutrition':60, 'onAction.TAKE':function(){ queueGMOutput('You pick the berries on the off-chance they might be useful.'); return true; }, 'onAction.EAT':function(){ queueGMOutput('Someone once warned you that brightly colored things are always poisonous. You decide to err on the side of caution.'); return true; } }); // Hot Spring ECS.e(`hot-spring`, [`place`], { 'name':`Hot Spring`, 'region':`forest`, 'exits':{'n':`west-trail`,'d':`lair`,'in':`pools`,'s':`bridge`}, 'descriptions':{ 'default':`A cluster of small pools lay nestled in a rock outcropping. Steam rises from the largest pool and settles over the clearing in a dense blanket of fog. Rivulets of water spill over the pool's stone border and eventually converge into a small creek which disappears into the undergrowth. Aside from the burbling water, this part of the forest is quite still. It has a tranquil, otherworldly feel. A winding trail disappears into the underbrush to the {{n}}, while a set of weathered stone steps disappear {{down}} into the ground. To the {{s}}, a wooden bridge crosses the creek. {{scenery}}`, 'short':`Some puddles that are warmer than usual.` } }); // Door: Lair ECS.e(`door-lair`, [`door`], { 'name':`stone door`, 'spawn':`hot-spring`, 'direction':`d`, 'isOpen':false, }); // Scenery: Pools ECS.e(`pools`, [`scenery`,`thermal`,`container`], { 'name':`pools`, 'nouns':[`pool`,`pools`,`spring`,`springs`,`water`], 'spawn':`hot-spring`, 'isEnterable':true, 'descriptions':{ 'default':`The spring is warm and inviting. Water bubbles up into the large central pool from somewhere below and overflows into a series of smaller surrounding pools. If you didn't have better things to do, you'd hop in and take a soak. That's what I'll be doing the next time you take a break.`, 'in':`Steaming water burbles gently around you. It's the most relaxing thing you've done in at least a while.` }, 'onEnter':function(){ return true; }, 'onAction.GET.IN':function(action) { if(this.temperature > 330) { queueGMOutput(`The water is scalding to the touch. Best stay out until it settles down.`); } else { ECS.moveEntity(action.actor, this); queueGMOutput(`You climb into the water. It's a pleasant `+this.temperature+` Kelvin.`); } return false; }, 'onTakeFail':function(){ return `{{gm}}It slips through your fingers like sand.
`; }, 'temperature':314.0, // Hot, like a shower }); understand(`rule for raising pool temperature`) .book(`after`) .in([`hot-spring`,`pools`]) .do(function(action){ var wyrm = ECS.getEntity(`wyrmling`); var pool = ECS.getEntity(`pools`); if(wyrm.angerTicks > 0 && wyrm.temperature > pool.temperature) { pool.temperature = wyrm.temperature; action.output += `{{gm}}` + p(`The pool is heating up.`); action.mode = Rulebook.ACTION_APPEND; } }) .start(); // Musty Cave ECS.e(`lair`, [`place`], { 'name':`Lair`, 'region':`forest`, 'descriptions':{ 'default':`A hot and humid hole in the ground. Water trickles down the walls amidst an ever-present cloud of steam. Claw marks etch the walls, seemingly at random. The tunnel exits behind you, climbing steeply {{up}} to the woods.`, 'short':`A damp hole in the ground.` }, 'exits':{'u':`hot-spring`}, 'onLeave':function(direction){ queueGMOutput(`Sweating, you make the climb back up to fresh air.`); return false; } }); ECS.e(`claw-marks`, [`scenery`], { 'spawn':`lair`, 'name':`claw marks`, 'nouns':[`claw marks`,`marks`], 'descriptions':{ 'default':`The scratches seem random, idly placed without care. Most are shallow, with a few angry-looking exceptions.` } }); // Wyrmling ECS.e(`wyrmling`, [`living`,`thermal`], { 'name':`Wyrmling`, 'spawn':`lair`, 'hp':100, 'mood':`peaceful`, 'angerTicks':0, 'temperature':320.0, 'showTempInRoomDescription':false, 'descriptions':{ 'default':`A black-scaled wyrmling (that's a wingless dragon of sorts, if you didn't know) perched atop its hoard. Ruby-red eyes gleam through narrowed slits.`, 'scenery':`Nestled cozily on a pile of glittering gold and gems lies a {{tag 'wyrmling' classes='object enemy' command='x wyrmling'}}.`, 'smell':`It smells like a pocketful of burning coins.`, 'short':`A little black dragon.`, }, 'listInRoomDescription':true, 'onTick':function(system){ if(system == `living`) { // Grumble if(this.angerTicks > 0 && player.location() == this.location()) { queueGMOutput(`The wyrmling growls at you.`); } } else if(system == `thermal`) { // Warm up if(this.angerTicks > 0) { this.temperature = Math.min(400.0, this.temperature + (5.0 * this.angerTicks)); } } }, 'onDeath':function(weapon){ queueOutput( `{{gm}}You murder the wyrmling.
` ); }, 'onAction.ATTACK':function(action){ if(this.hp > 0) { this.angerTicks++; queueOutput(`{{gm}}The wyrmling shrugs off your feeble attack, mildly irritated. A wave of heat radiates from its scales.
`); } else { queueOutput(`{{gm}}Further violence proves fruitless.
`); } action.mode = Action.ACTION_CANCEL; return false; }, 'onAction.HUG':function(){ return false; } }); // South Trail ECS.e(`south-trail`, [`place`], { 'name':`South Trail`, 'region':`forest`, 'exits':{'ne':`other-east-trail`,'n':`dim-clearing`}, 'descriptions':{ 'default':`The hard-packed dirt trail turns sharply back on itself from the {{ne}}, leading into a dim clearing to the {{n}}. An especially sturdy tree encroaches, endeavouring to trip you with gnarled roots rambling across the path. Somewhere nearby, you hear the gentle sound of flowing water.`, 'short':`A dirt trail by a tree.`, } }); ECS.e(`sturdy-tree`, [`scenery`], { 'name':`sturdy tree`, 'nouns':[`sturdy tree`,`tree`], 'spawn':`south-trail`, 'descriptions':{ 'default':`A fine example of the tree-maker's work, this specimen towers over the rest.`, 'short':`A tree, taller than the others. Big deal.`, } }); localScenery([`roots`,`gnarled roots`], `Gnarly.`); // Magic ice cube ECS.e(`magic-ice-cube`, [`thermal`], { 'name':`ice cube`, 'nouns':[`cube`,`ice`,`ice cube`,`magic ice cube`], 'descriptions':{ 'default':`A fist-sized chunk of ice. There's an otherworldly quality about it, which might explain why it hasn't melted yet. Something is embedded in the center, but you can't make it out.`, 'short':`A magic piece of ice with something in it.`, }, 'canTake':function(){return true;}, 'temperature':265.0, 'onTick':function(){ if(this.parent && (this.parent.key == `pools` || this.parent.parentIs(`pools`))) { if(ECS.getEntity(`pools`).temperature > 373) { // Melt var key = ECS.getEntity(`ice-key`); queueGMOutput(`The ice cube bobs for a moment, then withers away in the roiling waters. From its interior, a key emerges. You snatch the key from the water before it can wander off.`); ECS.moveEntity(key, player); ECS.removeEntity(this); delete key[`onAction.TAKE`]; } else { // Not hot enough queueGMOutput(`The ice cube cracks in the warm water, then re-freezes.`); } } } }); ECS.e(`ice-key`, [`part`,`thermal`], { 'name':`ice key`, 'spawn':`magic-ice-cube`, 'nouns':[`key`,`ice key`], 'descriptions':{ 'default':`An ornately molded key made from some kind of black ice. It gives off a frigid aura. The surface is embossed with tiny scales.`, 'short':`A fancy key.`, }, 'temperature':255.0, }); understand(`rule for breaking the ice`) .text(`break the ice`) .do(function(self,action){ queueGMOutput(`You attempt to be sociable, but it's the wrong time or the wrong audience or there's just something wrong with you. It's always been difficult.`); action.mode = Rulebook.ACTION_CANCEL; }).start(); // Musty Cave ECS.e(`musty-cave`, [`place`], { 'name':`Musty Cave`, 'descriptions':{ 'default':`A musty, smooth-walled cave worn out of the rock. Striations of red and black twist across the stones in dizzying patterns. It smells of stale beer and something a bit ranker, like a young animal who refuses to take a bath. A narrow passage leads further {{e}}, while the cave entrance lies to the {{s}}.`, 'smell':`There's a musty odor permeating the cave.` }, 'exits':{'s':`dim-clearing`,'e':`chamber`}, 'onEnter':[function(args){ args.obj.describe(); if(args.obj.visited == 0) { // Describe troglodyte queueGMOutput(p(`Deep in the gloom, you see the fearsome {{tag 'troglodyte' classes='object enemy' command='x troglodyte'}}. You're not sure how to describe it because you don't remember what a troglodyte is.`)); NLP.interrupt(null, function(string){ if(string == `yes i do`) { queueOutput(`{{gm}}Oh, my mistake. It looks exactly like you expected a troglodyte to look.
`); } else { // Resume normal input queueOutput(parse( NLP.parse(string), {} )); } return true; }); } return false; }], 'onLeave':[function(args){ if(args.direction == `e` && !player.hasChild(`glowing-orb`) && player.race != `dwarf`) { queueOutput(`{{gm}}It's dark and scary in there.
`); return true; } return false; }] }); // Troglodyte ECS.e(`troglodyte`, [`living`], { 'name':`Troglodyte`, 'spawn':`musty-cave`, 'hp':10, 'mood':`peaceful`, 'angerTicks':0, 'descriptions':{ 'default':`It looks exactly like you expected a troglodyte to look.`, 'smell':`Bad. Real bad.`, 'short':`A literal troglodyte.`, }, 'happy':false, 'onTick':function(system){ if(this.mood == `angry`) { if(this.angerTicks > 0 && player.location() == this.location()) { // Attack player queueGMOutput(`The troglodyte attacks you.`); if(player.hp == null) { // If target (player) doesn't have HP yet, run HP generation handler queueGMOutput(`Oh...forgot to roll up your hit points. Now would be a good time to do that. Let me just find that d10...`); queueGMOutput(`I know I left it here somewhere...`); queueGMOutput(`How about 5? 5 is a nice number. I'll find that die later.`); player.hp = 5; } var dmg = dice(2); player.onHit(dmg, function() { queueGMOutput(`The troglodyte begins to gnaw on your corpse. You have no way of knowing that, of course, because you are dead.`); }); } this.angerTicks++; } }, 'onDeath':function(weapon){ queueOutput( `{{gm}}The {{nametag '`+weapon.key+`'}} swells with joyous fury. You cleave the troglodyte in twain. Gouts of crimson blood spray in all directions, coating the walls, floor, ceiling, and you. The sword appears unaffected, but you're drenched. Just absolutely covered in blood. It's awful. Roll to not throw up.
` ); var options = [ {'text':`Roll`,'command':`roll`}, {'text':`Throw Up`,'command':`throw up`} ]; queueOutput(parse(`{{menu options}}`, {'options':options})); NLP.interrupt(function(string){ if(string.is(`roll`,`throw up`)) { var puke = true; if(string == `roll`) { var roll = dice(20); if(roll > 10) { puke = false; } queueOutput(`{{gm}}You rolled...a `+roll+`.
`); } if(puke) { queueOutput(`{{gm}}You throw up directly on the troglodyte's corpse, creating a steaming river of horror. It's like someone didn't know how to make a proper Thanksgiving dinner, and ended up mixing the cranberry sauce with the gravy. You throw up again, but just a little bit this time.
`); } else { queueOutput(`{{gm}}You turn away and take a deep breath. It helps a bit.
`); } } return true; }); }, 'onAction.ATTACK':function(action){ // Check weapon; punches are ineffective, sword is good // If player doesn't have HP yet, run HP generation handler if(this.hp > 0) { var weapon = null; if(action.modifiers.length > 0 && action.nouns.length > 1) { weapon = action.nouns[1]; } if(weapon != null && weapon.key == `rainbow-sword`) { this.hp = 0; this.onDeath(weapon); } else { // Update mood if(this.mood == `peaceful`) { queueOutput(`{{gm}}The troglodyte shrugs off your feeble attack. It seems mildly annoyed.
`); this.mood = `annoyed`; } else if(this.mood == `annoyed`) { queueOutput(`{{gm}}The troglodyte shrugs off your feeble attack and raises its fists to attack.
`); this.mood = `angry`; } else if(this.mood == `angry`) { queueOutput(`{{gm}}The troglodyte shrugs off your feeble attack.
`); } } } else { queueOutput(`{{gm}}Further violence proves fruitless.
`); } }, 'onAction.HUG':function(action){ // Check if the player has already attacked us; if they haven't, // commence hugs. If they have, the hug is ineffectual if(this.angerTicks == 0) { this.happy = true; queueGMOutput(`The troglodyte seems touched by your gesture. Figuratively, and also literally. It looks much happier now.`); return true; } return false; } }); ECS.e(`locket`, [`thing`], { 'name':`locket`, 'spawn':`troglodyte`, 'descriptions':{ 'default':`A dingy locket dropped from the troglodyte's grubby hands.`, 'held':`A dingy, battered locket made of cheap metal. Folding it open reveals a crudely drawn sketch of another troglodyte. A family member, perhaps.`, }, 'onAction.TAKE':function(action){ queueGMOutput(`You carefully stow the locket amongst your own belongings. Who knows, you might run into the creature's relatives someday. If not, you could always melt it down and make something better from it,`); } // TODO: Return the locket to family }); // Dim Clearing ECS.e(`dim-clearing`, [`place`], { 'name':`Dim Clearing`, 'region':`forest`, 'descriptions':{ 'default':`You are standing in a dim clearing in the woods. Motes of dust flutter through faint sunbeams from the sky above, but the forest canopy is too dense for you to catch more than a glimpse of blue. {{scenery}} This part of the forest has grown thick and wild, almost obscuring the narrow trails leading to the {{s}} and the {{w}}.`, 'short':`A poorly-lit clearing in the forest.` }, 'exits':{'n':`musty-cave`,'s':`south-trail`,'w':`west-trail`} }); // Scenery: The Cave Entrance ECS.e(`cave-entrance`, [`scenery`], { 'name':`cave entrance`, 'spawn':`dim-clearing`, 'descriptions':{ 'default':`A low cave entrance.`, 'scenery':`To the {{n}} lies a low {{tag 'cave entrance' classes='scenery' command='peer at cave entrance'}}, shrouded in {{nametag 'moss' classes='scenery' command='examine moss'}} and creeping {{tag 'ivy vines' classes='object scenery' command='x vines'}}.` } }); // Scenery: Some Vines ECS.e(`vines`, [`scenery`], { 'name':`vines`, 'spawn':`dim-clearing`, 'descriptions':{ 'default':`Some creeping ivy vines. Not as good as the ones back home.`, 'smell':`Damp and slightly acrid.` }, 'nouns':[`vine`,`ivy vines`], 'onAction.CLIMB':function(){ queueGMOutput(`The vines are not secure enough to climb.`); return false; } }); // Scenery: Some Dust Motes ECS.e(`dust`, [`scenery`], { 'name':`motes of dust`, 'spawn':`dim-clearing`, 'descriptions':{ 'default':`Harmless dust motes.`, 'smell':`You inhale the dust motes and sneeze involuntarily. It smells like sneeze.` }, 'nouns':[`dust`,`motes`] }); // Scenery: A Bit of Moss ECS.e(`moss`, [`scenery`,`edible`], { 'name':`damp moss`, 'spawn':`dim-clearing`, 'descriptions':{ 'default':`Some lovely, soggy moss.`, 'smell':`Some lovely, soggy moss.` }, 'onAction.EAT':function(){ return `You munch on a bit of moss and find it merely adequate. You're not feeling particularly hungry, so you leave some moss for the next person to come along.
`; }, 'nouns':[`moss`] }); // Object: chest ECS.e(`chest`, [`container`], { 'name':`chest`, 'spawn':`dim-clearing`, 'descriptions':{ 'default':`A small wooden chest, lightly worn and devoid of markings.`, 'short':`A box.`, }, 'onAction.TAKE':function(){ queueOutput(`{{gm}}Though small, it seems too heavy to move.
`); } }); ECS.e(`glowing-orb`, [`emitter`,`thermal`], { 'name':`glowing orb`, 'nouns':[`orb`], 'spawn':`chest`, 'descriptions':{ 'default':`The glowing orb is mediocre in quality, and produces a sickly glow.`, 'short':`A ball of light.`, }, 'temperature':310.0, // Slightly warm 'showTempInRoomDescription':true }); ECS.e(`jane`, [`living`,`scenery`], { 'name':`Jane`, 'nouns':[`jane`,`woman`], 'spawn':`dim-clearing`, 'listInRoomDescription':false, 'descriptions':{ 'default':`A young woman, slender of frame and bearing a striking resemblance to the man beside her. She has the quiet confidence of an adventurer, with none of the neurotic twitches.`, 'short':`Jane.`, }, 'onTakeFail':function(){ return `{{gm}}She doesn't seem amenable to that.
`; }, 'conversation':new Conversation([ { 'id':`root`, 'key':``, 'callback':function(){ queueGMOutput(`He seems preoccupied and doesn't respond.`); return true; }, 'nodes':[] }, ]) }); ECS.e(`jack`, [`living`,`scenery`], { 'name':`Jack`, 'nouns':[`jack`,`man`], 'spawn':`dim-clearing`, 'listInRoomDescription':false, 'descriptions':{ 'default':`A young man, slender of frame and bearing a striking resemblance to the woman beside him. He has the quiet confidence of an adventurer, with none of the neurotic twitches.`, 'short':`Jack.`, }, 'onTakeFail':function(){ return `{{gm}}He doesn't seem amenable to that.
`; }, 'conversation':new Conversation([ { 'id':`root`, 'key':``, 'callback':function(){ queueGMOutput(`He seems preoccupied and doesn't respond.`); return true; }, 'nodes':[] }, ]) }); var prefix_twins = function(n) { return ``+n+`:`; }; understand(`rule for entering dim clearing for the first time`) .book(`after`) .verb(`move`) .in(`dim-clearing`) .do(function(self, action) { queueGMOutput(p(`A pair of twins--man and woman--loiter in front of the iron gate, bickering about something. One holds a battered, leatherbound tome with one hand, using the other to point sternly at something on the page.`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`See, look at this one:`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`'James thinks left-handed people don't have souls. He has two left-handed friends and six right-handed friends, one of whom turns up dead under mysterious circumstances. How many of James's surviving friends are left-handed?'`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`...this is what I was referring to. These riddles are nonsensical.`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`One?`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`One what?`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`One left-handed friend.`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`There's no answer. I don't care what the book says. Just because he has a bizarre vendetta against left-handed people doesn't necessarily mean he would murder one.`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`True, but even if he didn't, he still has one left-handed friend left. He could have two left-handed friends left, but that means he also has one left, too. What does the book say?`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`...it says two. 'six right-handed friends, one of whom turns up dead.' So clearly one of the right-handed friends was murdered. Unbelievable.`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`I like it. Very clever use of ambiguous sentence structure. Also, my answer still works.`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`Your answer is the only thing worse than this riddle. It adds no information.`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`Again, true. Let's ask our visitor another. I think we need a fresh set of lobes.`), `auto`); queueGMOutput(p(`Noticing you at last, the twins page eagerly through the book of riddles for a suitable challenge.`), `auto`); queueOutput(prefix_twins(`The Twins`) + p(`We're not twins.`), `auto`); queueGMOutput(p(`Oh. I thought...`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`You could have asked, you know. Just because two people like to stand in front of a gate and pose riddles to passersby doesn't mean they're twins.`), `auto`); queueGMOutput(p(`Sorry?`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`You didn't even ask our names. You're still calling us twins in your script. 'Left Twin' and 'Right Twin'. Wow.`), `auto`); queueGMOutput(p(`What should I call you, then?`), `auto`); queueOutput(prefix_twins(`Left Twin`) + p(`My name is Jane.`), `auto`); queueOutput(prefix_twins(`Right Twin`) + p(`And I'm Jack.`), `auto`); queueGMOutput(p(`Ok...Jack and Jane, who are not related, lean forward eagerly, excited at the prospect--`), `auto`); queueOutput(prefix_twins(`Jack`) + p(`Seriously? We're brother and sister, we're just not twins. What are the odds two complete strangers named Jack and Jane would be standing in front of a gate and posing riddles to passersby?`), `auto`); queueGMOutput(p(`Fine. I don't actually care. The two J's have a riddle.`), `auto`); queueOutput(prefix_twins(`Jane`) + p(`This one looks fun.`), `auto`); queueOutput(prefix_twins(`Jack`) + p(`I agree.`), `auto`); queueOutput(prefix_twins(`Jane`) + p(`You're in a dark room with a candle, a wood stove and a gas lamp. You only have one match, so what do you light first?`), `auto`); // Start dialogue tree for first riddle var riddle1Options = {'options':shuffle([ {'text':`CANDLE`,'command':`candle`,'subtext':``}, {'text':`WOOD STOVE`,'command':`wood stove`,'subtext':``}, {'text':`GAS LAMP`,'command':`gas lamp`,'subtext':``}, ])}; var riddle1 = parse(`{{menu options}}`, riddle1Options); NLP.interrupt( function(){ queueOutput(riddle1); }, function(string){ ECS.tick = false; disableLastMenu(string); if(ECS.isValidMenuOption(riddle1Options.options, string)) { ECS.runInternalAction(`failed-riddle-1`, {}); return true; } if(string.is(`the match`,`match`)) { ECS.runInternalAction(`solved-riddle-1`, {}); return true; } enableLastMenu(); queueOutput(prefix_twins(`Jane`) + p(`I don't see how that would work.`)); return false; } ); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); understand(`rule for failing the first riddle`) .internal(`failed-riddle-1`) .do(function(self, action){ queueOutput(prefix_twins(`Jane`) + p(`Incorrect. It's the match, of course.`)); queueOutput(prefix_twins(`Jack`) + p(`We'll see if we can find an easier one for you later on.`)); self.stop(); action.mode = Rulebook.ACTION_CANCEL; ECS.runInternalAction(`done-with-riddles`, {}); }) .start(); understand(`rule for solving the first riddle`) .internal(`solved-riddle-1`) .do(function(self, action){ queueOutput(prefix_twins(`Jack`) + p(`Not much of a riddle, really.`)); queueOutput(prefix_twins(`Jane`) + p(`Too easy.`)); queueOutput(prefix_twins(`Jack`) + p(`Another, then. My turn.`)); queueGMOutput(p(`Jack thumbs further into the book.`)); queueOutput(prefix_twins(`Jack`) + p(`Here's one: 'If I am holding a bee, what do I have in my eye?'`)); // Start dialogue tree for second riddle NLP.interrupt( function(){}, function(string){ ECS.tick = false; if(string.is(`beauty`)) { ECS.runInternalAction(`solved-riddle-2`, {}); } else { ECS.runInternalAction(`failed-riddle-2`, {}); } ECS.runInternalAction(`done-with-riddles`, {}); return true; } ); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); understand(`rule for failing the second riddle`) .internal(`failed-riddle-2`) .do(function(self, action){ queueOutput(prefix_twins(`Jack`) + p(`Sorry, but no. The answer is beauty.`)); queueOutput(prefix_twins(`Jane`) + p(`Because beauty is in the eye of the bee-holder.`)); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); understand(`rule for solving the second riddle`) .internal(`solved-riddle-2`) .do(function(self, action){ queueOutput(prefix_twins(`Jack`) + p(`Correct. Beauty is in the eye of the bee-holder.`)); queueOutput(prefix_twins(`Jane`) + p(`I'm beginning to think we need a new book.`)); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); understand(`rule for being done with riddles`) .internal(`done-with-riddles`) .do(function(self, action){ queueGMOutput(p(`Jane and Jack resume their conversation, this time in hushed tones. They pay you no further heed.`)); ECS.runCallbacks(ECS.findEntity(`place`, `dim-clearing`), `onEnter`); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); // Object: iron gate ECS.e('iron-gate', ['door','lockable'], { 'name':'iron gate', 'nouns':['gate'], 'spawn':'dim-clearing', 'descriptions':{ 'default':'A rigid gate of black iron, old but quite sturdy. Despite the grime and the encroaching vines, there\'s not a fleck of rust on it.', 'short':'A stupid iron thing.', }, 'onAction.TAKE':function(){ queueOutput('{{gm}}It\'s quite securely fixed in place.
'); }, 'directions':{ 'n':'dim-clearing', // north from dim clearing 's':'musty-cave', // south from musty cave }, 'isOpen':false, 'isLocked':true, 'lockKey':'ice-key', }); // Bridge ECS.e(`bridge`, [`place`], { 'name':`Forest Bridge`, 'region':`bridge-forest`, 'position':`forest`, // One of: forest, underground, coast, void 'descriptions':{ 'default': `An arching wooden bridge spans ` + // Contextual location `{{#xif "this.position == 'forest'" }}a gentle stream trailing from the springs.{{/xif}}` + `{{#xif "this.position == 'underground'"}}a dark crevice somewhere underground.{{/xif}}` + `{{#xif "this.position == 'coast'"}}a sandy delta on the coast, overshadowed by a towering cliff face.{{/xif}}` + `{{#xif "this.position == 'void'"}}an infinitely dense superposition of bridges amidst an endless void.{{/xif}}` + ` The planks are untreated and unpainted, but show no signs of age, as if they were cut and assembled yesterday. A lonely metal wheel sits idle, affixed to the railing. ` + // Contextual scenery `{{#xif "this.position == 'forest'"}}The forest here is eerily calm, devoid of wind, riddles, or birdsong. Steam spills across the forest floor from the {{n}}, and across the bridge to the {{s}} you can make out a small circular clearing.{{/xif}}` + `{{#xif "this.position == 'underground'"}}Flowing liquid (probably water, but you never know) echoes somewhere far below. A narrow alcove sits to the {{n}}. Across the bridge to the {{s}}, massive slab steps lead {{d}} into the darkness.{{/xif}}` + `{{#xif "this.position == 'coast'"}}A steady flow of fresh water cascades from a split in the cliff to the {{s}} and spreads winding tendrils across the beach. To the {{n}}, a pier slowly loses its will to live.{{/xif}}` + `{{#xif "this.position == 'void'"}}To the {{n}}, an arching wooden bridge extends into infinity. To the {{s}}, an arching wooden bridge extends into infinity.{{/xif}}` , 'short':`A dumb bridge.` }, 'exits':{'n':`hot-spring`,'s':`circle`}, 'wheel-states':{ 'forest':{'exits':{'n':`hot-spring`,'s':`circle`},'name':`Forest Bridge`,'region':`bridge-forest`}, 'underground':{'exits':{'n':`alcove`,'s':`winding-stair`,'d':`winding-stair`},'name':`Underground Bridge`,'region':`bridge-underground`}, 'coast':{'exits':{'n':`pier`,'s':`the-split`},'name':`Coastal Bridge`,'region':`bridge-coast`}, 'void':{'exits':{'n':`bridge`,'s':`bridge`},'name':`Infinity Bridge`,'region':`bridge-void`}, }, 'move':function(state) { console.log(`MOVE TO STATE: ` + state); this.position = state; this.exits = this[`wheel-states`][state].exits; this.name = this[`wheel-states`][state].name; this.region = this[`wheel-states`][state].region; }, 'persist':[`position`,`region`,`exits`], }); // Wheel ECS.e(`wheel`, [`device`], { 'name':`wheel`, 'spawn':`bridge`, 'nouns':[`wheel`,`metal wheel`,`steering wheel`,`bridge wheel`], 'descriptions':{ 'default':`There's nothing particularly unusual about the wheel, except that it's attached to a bridge.`, 'scenery':`A metal wheel sits on the railing, mounted as if on a ship.` }, 'device-states':[`forest`,`underground`,`coast`,`void`], 'device-state':`forest`, 'onAction.TURN':function(data){ // Modifiers: clockwise or counter-clockwise / right or left // If in void, wrap around. Clockwise goes to forest, counter-clockwise goes to coast var bridge = this.location(); var wheel = this; return new Response(NLP.RESPONSE_AFTER, function() { queueGMOutput(`For a moment, the world revolves around you. The bridge seems unchanged, but the scenery has shifted.`); bridge.move(wheel[`device-state`]); ECS.runCallbacks(bridge, `onEnter`); }); }, 'persist':[`device-state`], }); ECS.e(`bridge-scenery`, [`scenery`], { 'name':`bridge`, 'spawn':`bridge`, 'nouns':[`bridge`], 'descriptions':{ 'default':`About five meters long and 6 feet wide, made of wood.` } }); ECS.e(`bridge-planks`, [`scenery`], { 'name':`planks`, 'spawn':`bridge`, 'nouns':[`planks`], 'descriptions':{ 'default':`Firm and fresh.` } }); // Alcove at top of underground stairs ECS.e(`alcove`, [`place`], { 'name':`Alcove`, 'region':`underground`, 'exits':{'s':`bridge`}, 'descriptions':{ 'default':`A small alcove, meticulously carved from the stony chasm wall. {{scenery}}`, 'short':`A generously-named indentation.`, } }); // Rusty helmet. Interaction dislodges, causing it to end up in The Split. ECS.e(`rusty-helmet`, [`wearable`], { 'name':`rusty helmet`, 'nouns':[`helmet`,`rusty helmet`], 'spawn':`alcove`, 'descriptions':{ 'default':`A battered and rusty helmet.`, 'short':`A metal hat.`, } }); understand(`rule for noticing rusty helmet`) .book(`before`) .regex(`helmet|rusty helmet`) .in([`alcove`,`the-split`]) .do(function(self, action) { var helmet = ECS.getEntity(`rusty-helmet`); if(helmet.locationIs(`alcove`)) { queueGMOutput(`The moment you give the helmet any attention, it loses purchase in the rocks and slips into the chasm below, clattering down and down until only faint echoes remain.`); ECS.moveEntity(`rusty-helmet`, `the-split`); action.mode = Rulebook.ACTION_CANCEL; } else if(helmet.locationIs(`the-split`)) { queueGMOutput(`Upon noticing the helmet, it once again slips away. It splashes into the river and is swiftly carried toward the coast below.`); ECS.moveEntity(`rusty-helmet`, `pier`); self.stop(); action.mode = Rulebook.ACTION_CANCEL; } }) .start(); // Circle ECS.e(`circle`, [`place`], { 'name':`Circle`, 'region':`forest`, 'descriptions':{ 'default':`An otherworldly tranquility fogs your senses as you stand amidst a circle of wild flowers and picturesque mushrooms. A literal fog additionally obscures your vision, but for the first time in years you're not worried about the possibility of the Shadowbeast—your mortal enemy—ambushing you. As far as you're concerned, there's literally no chance that it's hiding just beyond your sight. {{scenery}}`, 'short':`A bunch of flowers and stuff in a circle.` }, 'exits':{'n':`bridge`} }); ECS.e(`circle-mushrooms`, [`edible`], { 'spawn':`circle`, 'descriptions':{ 'default':``, 'short':``, } }); ECS.e(`circle-flowers`, [`thing`], { 'spawn':`circle` }); // Pier ECS.e(`pier`, [`place`], { 'name':`Pier`, 'region':`coast`, 'descriptions':{ 'default':`Thick wooden beams trail in sequence over the waves, like a flat staircase or a fence turned on its side. Periodically, large posts rise from the sands, encrusted with barnacles and salt. The structure shows signs of wear from long disuse.`, 'short':`A bunch of sticks in the ocean.` }, 'exits':{'s':`bridge`} }); /* Old woman at end of pier. Speaks of the sea, and the wear of time. Knows of the bridge but not where/when it goes. Drinks periodically from a bottle of dark liquid. Claims to know the ruler of the ocean. Asks the player a favor: find her lost treasure, taken far from the water. Promises a reward: "I'll make certain you're rewarded. You don't seem like the rest. Young folk like you call me 'old hag' and throw pine cones at me, and I let 'em, because it's important to have something to regret for the rest of your life. Just an old hag, sitting alone by the sea, waiting to die on a creaky pier. Well, you know what they say... ap-pier-ances can be deceiving." Cackles and falls into ocean. The bottle is left half full. When drunk, gives visions of The End. When the player returns with a conch shell pendant and throws it into the ocean, laughter is heard and a mighty storm brews in the distance. No immediate reward is apparent. */ var prefix = function(n) { return ``+n+`:`; }; understand(`rule for entering pier for the first time`) .book(`after`) .verb(`move`) .in(`pier`) .do(function(self, action) { var sequence = new Sequence; sequence.add(function() { queueGMOutput(p(`A wizened old woman leans casually against the railing.`), `auto`); queueOutput(prefix(`Old Woman`) + p(`Hello, friend...have you come to end it all?`), `auto`); // Start dialogue tree for first riddle var options = {'options':shuffle([ {'text':`YEAH`,'command':`yes`,'subtext':`I have indeed come to end it all`}, {'text':`NAH`,'command':`no`,'subtext':`No, not today`}, ])}; var menu = parse(`{{menu options}}`, options); NLP.interrupt( function(){ queueOutput(menu); }, function(string){ console.log(`WITCH SEQUENCE`); console.log(sequence); ECS.tick = false; disableLastMenu(string); if(string.is(`yes`,`yeah`)) { queueOutput(prefix(`Old Woman`) + p(`Oh, well that's alright. I'm sure you have your reasons.`), `auto`); } else if(string.is(`no`,`nah`)) { queueOutput(prefix(`Old Woman`) + p(`Oh, well that's alright. If it's not your time, it's not your time.`), `auto`); } else { enableLastMenu(); queueOutput(prefix(`Old Woman`) + p(`Eh?`)); return false; } sequence.next(); return true; } ); }); sequence.add(function(){ queueOutput(prefix(`Old Woman`) + p(`I see you came via the bridge. It's been...longer than I care to say. Strange thing, that bridge. Never set foot on it myself, but I couldn't help but notice some days it wasn't there. Some days it was, some days it wasn't. Some days I wasn't here, so I don't know where it got to those days. Magic bridge, maybe. I'm not an observer, no particular interest in the bridge, but that's what I seen. Sort of an...abridged history if you will.`), `auto`); queueOutput(prefix(`Old Woman`) + p(`I know the king of the sea, you know. I don't like to name drop. but...I've seen things. Done things. Lost...things.`), `auto`); // TODO: player question (what things) queueOutput(prefix(`Old Woman`) + p(`Nothing important. Not to anyone else. If you come across it though, I would dearly appreciate having it back. You'll know it's mine; not another like it in the eleven seas, the sky above or the other sky above that one.`), `auto`); queueOutput(prefix(`Old Woman`) + p(`If you return it to me, I'll make certain you're rewarded. You don't seem like the rest. Young folk like you call me 'old hag' and throw pine cones at me, and I let 'em, because it's important to have something to regret for the rest of your life. Just an old hag, sitting alone by the sea, waiting to die on a creaky pier. Well, you know what they say…ap-pier-ances can be deceiving.`), `auto`); }, Sequence.MODE_CONTINUE); sequence.add(function(){ queueGMOutput(p(`The old woman cackles and falls backward into the ocean.`), `auto`); // TODO: remove old woman from location }); sequence.start(); processDeferredOutputQueue(); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); // The Split ECS.e(`the-split`, [`place`], { 'name':`The Split`, 'region':`coast`, 'descriptions':{ 'default':`A colossal glacial boulder, now cracked in half, straddles a boisterous river. Rolling clouds of spray engulf the base, where the river resumes its seaward flow. Above, you can faintly make out the river's winding path down a snowcapped mountain.`, 'short':`A big crack in a big rock.` }, 'exits':{'n':`bridge`} }); /* Rusty helmet is found at base after player dislodges it from alcove. */; // Armory, first room in the Trial of Friendship ECS.e(`armory`, [`place`], { 'name':`Armory`, 'region':`underground`, 'exits':{'w':`great-hall`,'s':`scary-tunnel`}, 'descriptions':{ 'default':`You are greeted by the scents of rusting steel, rotting wood, and what you can only assume is spider poop. A small armory sits derelict, connecting the great hall to the {{west}} with a dark tunnel to the {{south}}. An assortment of {{tag 'unusable weapons' classes='scenery' command='look at weapons'}} lay scattered across the room, some propped up on splintering timber stands, most simply discarded on the floor. {{tag 'Tattered banners' classes='scenery' command='look at banners'}} hang from the walls, ocher in color, any identifiable symbols rendered unidentifiable. A {{tag 'lone skeleton' classes='scenery' command='look at skeleton'}} leans nonchalantly against the wall. {{scenery}}`, 'short':`Where the swords and stuff go when they're not being used.`, } }); localScenery([`unusable weapons`,`weapons`], `An aged collection of rusted and shattered weaponry, completely useless to anyone.`); localScenery([`tattered banners`,`banners`], `The banners have almost completely disintegrated, leaving little sign of the original design.`); localScenery([`rotting wood`,`wood`], `A thinly-veiled metaphor for the human condition.`); localScenery([`splintering timber stands`,`timber stands`,`stands`], `Best not to touch.`); localScenery([`lone skeleton`,`skeleton`], `A bony fellow, likely a former guard. He doesn't seem to be suffering any more.`); // Bedchamber ECS.e(`bedchamber`, [`place`], { 'name':`Bedchamber`, 'region':`underground`, 'exits':{'n':`vault`}, 'descriptions':{ 'default':`It's very clear that the inhabitant of this bedchamber enjoyed their sleep, or desperately wanted to. Expensive-looking tapestries line the walls to dampen sound, while an enormous poster bed dominates the center of the room. The high arched ceiling has been augmented with tightly-bound bales of straw. Thick stone doors, precisely balanced, lead to the {{north}}, while a {{tag 'secret door' classes='scenery' command='look at secret door'}}, quite cleverly concealed, scarpers to the south. {{scenery}}`, 'short':`The bedroom of a fancypants.`, } }); localScenery([`expensive-looking tapestries`,`tapestries`], `They look expensive.`); localScenery([`poster bed`,`bed`], `The fanciest nap-slab money can buy. It's got posts, a canopy, pillows, even a blanket.`); localScenery([`blanket`,`blankey`], `A thick blanket with gold stitching.`); localScenery([`gold stitching`,`stitching`], `Excessive.`); localScenery([`posts`], `A classic four-poster design. Each post is carved into a spiral column with silver inlays.`); localScenery([`silver inlays`,`inlays`], `Excessive.`); localScenery([`pillows`], `Floofy.`); localScenery([`canopy`], `About 8 square meters of royal blue fabric. Colorful stitching depicts a sleeping man holding a crystal ball.`); localScenery([`secret door`], `I...wasn't supposed to say that out loud. Yeah, there's a secret door leading to the {{south}}.`); // container: night stand // container: chest of drawers // diary; // Goblin Pit ECS.e(`goblin-pit`, [`place`], { 'name':`Goblin Pit`, 'region':`underground`, 'exits':{'n':`spider-room`,'e':`ogre-cage`}, 'descriptions':{ 'default':`This large circular room houses a central pit, in which an indeterminate number of goblins appear to be trapped. A rank odor permeates every surface of the room. A narrow tunnel connects to the spider room to the {{north}}. A much larger tunnel leads {{east}}. {{scenery}}`, 'short':`A bunch of goblins are trapped in a hole.`, } }); // Scenery: goblin pit ECS.e(`bottom-of-goblin-pit`, [`scenery`], { 'name':`Bottom of Goblin Pit`, 'spawn':`goblin-pit`, 'nouns':[`bottom of pit`,`pit`,`bottom of goblin pit`,`goblin pit`], 'descriptions':{ 'default':`A single smooth cylindrical wall climbs twenty feet from the bottom of the hole. Etched into the wall are thousands of marks, indecipherable symbols, and crude drawings of the sorts of things fifty goblins get up to if you trap them in a pit for long enough. There are also a hundred goblins down there. In the middle of the pit sits a glowing silver pail. {{scenery}}`, 'short':`A bunch of goblins are trapped in a hole.`, } }); // scenery: goblins ECS.e(`goblins`, [`living`,`scenery`], { 'name':`Goblins`, 'spawn':`goblin-pit`, 'descriptions':{ 'default':``, 'scenery':`` } }); ECS.e(`goblin-pit-leader`, [`living`,`scenery`], { 'name':`Goblin Leader`, 'spawn':`goblin-pit`, 'descriptions':{ 'default':``, 'scenery':`` } }); understand(`rule for entering goblin pit for the first time`) .book(`after`) .verb(`move`) .in(`goblin-pit`) .doOnce(function(self, action) { var sequence = new Sequence; var prefix = ECS.getEntityPrefix(`goblin-pit-leader`); // Goblin description sequence.add(function(){ queueGMOutput(`...`); }, Sequence.MODE_CONTINUE); // Goblins notice player and demand payment sequence.add(function(){ // challenge queueOutput(prefix + `You! Hey! No passing the goblin pit without paying the toll! Toss us something nice or you'll regret it, pal!`); }); sequence.start(); }) .start(); understand(`rule for dropping ruby in the goblin pit`) .book(`before`) .verb(`drop`) .in(`goblin-pit`) .entity(`ruby`) .doOnce(function(self, action) { var prefix = ECS.getEntityPrefix(`goblin-pit-leader`); queueOutput(prefix + `Yes, perfect! Now we can complete the summoning ritual! Thank you, friend!`); queueGMOutput(`The goblin tosses you a black object. The others gather around and begin discussing their ritual in hushed but excited tones.`); queueGMOutput(`Your hand is chilled by the object, which seems to be a hunk of black ice.`); ECS.moveEntity(`magic-ice-cube`, player); action.mode = Rulebook.ACTION_CANCEL; }) .start(); understand(`rule for dropping wrong item in the goblin pit`) .book(`before`) .verb(`drop`) .in(`goblin-pit`) .until(function(action) { return ECS.getEntity(`goblin-pit-leader`).hasChild(`ruby`); }) .do(function(self, action) { var prefix = ECS.getEntityPrefix(`goblin-pit-leader`); queueOutput(prefix + `What's this? No, no, no. Something nice! Preferably blood-colored!`); console.log(action); queueGMOutput(`The goblin hurls the `+action.target.name+` back to you, unsatisfied.`); action.mode = Rulebook.ACTION_CANCEL; }) .start(); // Bottom of Goblin Pit // goblins (~100) // goblin leader // pail of infinite sadness; // Chalice Room ECS.e(`chalice-room`, [`place`], { 'name':`Chalice Room`, 'region':`underground`, 'exits':{'n':`good-wine-cellar`}, 'descriptions':{ 'default':`A hundred (you counted) wine-laden vessels cover every flat surface in the room. There's something vaguely foreboding about it, like when a friend asks you how well your new lawn mower handles especially thick grass. Cups, chalices, tankards, and several other kinds of containers are represented. Each is filled to the brim with dark red wine. Probably wine. Almost certainly not blood. The good wine cellar is to the {{north}}. {{scenery}}`, 'short':`A bunch of cups full of wine. Probably poisoned or something stupid like that.`, } }); // chalices (~100); // Good wine cellar ECS.e(`good-wine-cellar`, [`place`], { 'name':`Good Wine Cellar`, 'region':`underground`, 'exits':{'n':`wine-cellar`}, 'descriptions':{ 'default':`This appears to be where the good wine is kept. In stark contrast to the previous room, the air is dry and every surface is clean. A large wine rack occupies most of the room, each bottle carefully labeled with a date and description. A corkscrew and a single wine glass sit on a small table in the corner. {{scenery}}`, 'short':`Something to make reading thoroughly enjoyable.`, } }); // wine bottles (~100) // corkscrew; // Great Hall ECS.e(`great-hall`, [`place`], { 'name':`Great Hall`, 'region':`underground`, 'exits':{'u':`winding-stair`,'n':`winding-stair`,'w':`library`,'e':`armory`,'s':`treasury`}, 'descriptions':{ 'default':`A towering hall, carefully hewn from the surrounding stone. Fluted columns stand like sentries beside each exit from the hall. A worn mosaic sprawls across the floor, colors muted by the passage of time. Several wall sconces hold dusty torches, unlit. The stairs lead back {{up}} to the {{north}}, while open archways lead {{east}} and {{west}}. To the {{south}} sits an ornate double door. Above the east, west, and south exits are {{tag 'crude wood plaques' classes='scenery' command='look at plaques'}}. {{scenery}}`, 'short':`A big room with some pillars.`, } }); localScenery([`crude wood plaques`,`wood plaques`,`plaques`], `To the west, 'TRIAL OF DEDICATION'. To the south, 'TRIAL OF NAPS'. To the east, 'TRIAL OF FRIENDSHIP'. The signs do not appear to be part of the original construction.`); localScenery([`fluted columns`,`columns`], `Decoratively grooved.`); localScenery([`wall sconces`,`sconces`], `The degree to which they've been neglected is unsconscionable.`); localScenery([`worn mosaic`,`mosaic`], `At first glance you’re inclined to dismiss it as a hackish attempt at art, but after a moment of consideration you decide to give it the benefit of the doubt. Art is tricky and you don’t want to look dumb in front of your friends. Maybe it was laid out by a famous tilist.`); localScenery([`stairs`], `Winding stairs climbing to the north. I mean, they don’t literally climb. They’re stationary, but ‘climb’ is a multi-purpose word that can act as a verb or as an innate trait of an object.`); localScenery([`ornate double door`,`double door`,`door`], `It’s a real fancy door, like a rich person would own. It tickles your adventuring spirit, because there’s probably money behind it.`); // Library, first room in the Trial of Dedication ECS.e(`library`, [`place`], { 'name':`Library`, 'region':`underground`, 'exits':{'e':`great-hall`,'s':`wine-cellar`,'d':`wine-cellar`}, 'descriptions':{ 'default':`{{nametag 'bookshelves' print='Towering shelves'}} covered in {{nametag 'moldy-books' print='mouldering books'}} clutter this otherwise inoffensive room. Most of the volumes seem too damaged by water, age, or dull subject material to be of any interest, but a few books seem intact and/or not completely boring. Like most rooms, this one has a floor, ceiling, some walls and a couple doorways. To the {{west}} is the great hall, while to the {{south}} is (judging by the smell) a wine cellar.`, 'short':`A room shaped like a bookcase. It's full of books shaped like books.`, } }); // scenery: bookshelves ECS.e(`bookshelves`, [`scenery`], { 'name':`bookshelves`, 'spawn':`library`, 'nouns':[`towering bookshelves`,`bookshelves`], 'descriptions':{ 'default':``, 'short':``, } }); // scenery: rotted books ECS.e(`moldy-books`, [`scenery`], { 'name':`moldy books`, 'spawn':`library`, 'nouns':[`books`,`moldy books`], 'descriptions':{ 'default':`An assortment of moldy books, long rendered illegible.`, 'short':`Ruined books.`, }, 'action.EAT':function() { queueGMOutput(`You devour a moldy book, hoping to gain some knowledge by osmosis. You're not sure if it worked, but you are sure that you're going to regret this decision.`); return true; } }); // books: ~10 // supporter: reading chair // scenery: rug; // Ogre Cage ECS.e(`ogre-cage`, [`place`], { 'name':`Ogre Cage`, 'region':`underground`, 'exits':{'w':`goblin-pit`}, 'descriptions':{ 'default':`Thick pine beams form an orderly slatted cube. Inside, {{#xif "this.ogreSleeping()"}}a massive ogre snores softly{{else}}a sullen ogre stares listlessly at the wall{{/xif}}. Scattered bones and refuse litter the floor. A {{tag 'heavy steel lock' classes='scenery' command='look at lock'}} fixes the cage door in place. To the {{west}}, you can hear muffled movements from the goblin pit. {{scenery}}`, 'short':`A big cage with an ogre in it.`, }, 'ogreSleeping':function(){ return ECS.getEntity(`ogre`).asleep; } }); localScenery([`heavy steel lock`,`steel lock`,`lock`], `Well-constructed, free of rust, and currently locked.`); localScenery([`cage door`,`door`], `An ogresized door.`); localScenery([`scattered bones`,`bones`], `If dragons were ogres...`); localScenery([`refuse`], `What do you call it when an ogre won't clean his room?`); // ogre ECS.e(`ogre`, [`living`], { 'name':`Ogre`, 'nouns':[`ogre`], 'spawn':`ogre-cage`, 'descriptions':{ 'default':`{{#xif "this.asleep == true"}}The ogre is fast asleep, but maintains an intimidating presence.{{else}}The sullen ogre stands nearly 10 feet tall even in its present slouching state.{{/xif}} Numerous scars cover its body. ` }, 'asleep': false, 'onAction.ATTACK':function() { queueGMOutput(`That seems inadvisable, and in any case the ogre is safely behind bars.`); return true; } }); ECS.e(`ogre-scars`, [`part`], { 'name':`scars`, 'nouns':[`scars`,`numerous scars`], 'parent':`ogre`, 'descriptions':{ 'default':`` } }); ECS.e(`ruby`, [], { 'name':`A big ole ruby`, 'nouns':[`ruby`,`big ruby`,`giant ruby`], 'descriptions':{ 'default':`The biggest gemstone you've ever seen.` } }); understand(`rule for entering ogre cage before challenge`) .book(`after`) .verb(`move`) .in(`ogre-cage`) .do(function(self, action) { var sequence = new Sequence; var prefix = ECS.getEntityPrefix(`ogre`); // cheat ECS.moveEntity(`rusty-helmet`,player); // Ogre notices Player sequence.add(function(){ queueOutput(prefix + `See person. Bored. Long time no see person. Smash head?`); }, Sequence.MODE_CONTINUE); // Ogre challenges Player to headbutt contest; best of 1; ogre has a prize sequence.add(function(){ // challenge queueOutput(prefix + `Have pretty thing. Head to head. If survive, give pretty red thing. Yes?`); // trigger menu var options = [ {'text':`Yes`,'subtext':`Accept the Headbutt Challenge`,'command':`yes`}, {'text':`No`,'subtext':`Maybe later`,'command':`no`} ]; queueOutput(parse(`{{menu options}}`, {'options':options})); NLP.interrupt(function(){}, function(string){ if(ECS.isValidMenuOption(options, string)) { if(string.is(`yes`)) { ECS.runInternalAction(`headbutt-challenge-accepted`); self.stop(); action.mode = Rulebook.ACTION_CANCEL; } else { queueGMOutput(`The ogre sniffs at you, clearly disappointed.`); queueOutput(prefix + `I wait.`); var output = parse(NLP.parse(`x`), {}); queueOutput(output, 0, {}, true); } return true; } queueGMOutput(`The ogre seems to expect a yes or no answer.`); return false; }); }); sequence.start(); }) .start(); understand(`rule for accepting headbutt challenge`) .internal(`headbutt-challenge-accepted`) .doOnce(function(){ var prefix = ECS.getEntityPrefix(`ogre`); queueGMOutput(`The ogre smiles in what appears to be genuine joy.`); queueOutput(prefix + `Good! Test heads now.`); var helmet = ECS.getEntity(`rusty-helmet`); if(player.hasChild(helmet) && helmet.isWorn) { queueGMOutput(`True to your word, you obligingly stick your head through the bars, whereupon the ogre slams his much larger head against your helmet. You are thrown to the ground, and the world goes black for an indeterminate amount of time. Ears ringing, you struggle back to your feet. The helmet crumbles to the floor in a thousand fragments.`); queueOutput(prefix + `Ha ha! Good, good! Sturdy person. Said give pretty thing, do now.`); queueGMOutput(`The ogre produces a glittering ruby the size of your fist from beneath its loincloth and hands it over. It's unpleasantly warm.`); queueOutput(prefix + `Nap now. Friend come back later.`); queueGMOutput(`The ogre curls up in the corner and is almost instantly asleep. A soft smile adorns its craggy face.`); ECS.getEntity(`ogre`).asleep = true; ECS.removeEntity(helmet); ECS.moveEntity(`ruby`, player); } else { queueGMOutput(`True to your word, you obligingly stick your head through the bars, whereupon the ogre slams his much larger head against your unprotected skull. If only you had thought to put on some kind of protective covering.`); player.onHit(9999, function() { queueGMOutput(`You are killed instantly.`); }); } }) .start(); // Scary Tunnel ECS.e(`scary-tunnel`, [`place`], { 'name':`Scary Tunnel`, 'region':`underground`, 'exits':{'n':`armory`,'s':`spider-room`}, 'descriptions':{ 'default':`This tunnel is the scariest place you've ever seen. It's probably full of ghosts, and it tastes like fear. Exits (which you should use immediately) are {{north}} and {{south}}. {{scenery}}`, 'short':`Boo! Gotcha.`, } }); localScenery([`ghosts`], `Invisible ghosts, right behind you.`); localScenery([`fear`], `I don't want to talk about it. Let's just go.`); // Spider Room ECS.e(`spider-room`, [`place`], { 'name':`Spider Room`, 'region':`underground`, 'exits':{'n':`scary-tunnel`,'s':`goblin-pit`}, 'descriptions':{ 'default':`Webs upon webs blanket the chamber, sticky to the touch. Here and there, like arteries connecting a network of veins, thicker strands lead back to a much larger construction. An uncountable number of eyes glimmer in the dim light as black shapes scuttle around the room. A single massive eye watches you intently. The Great Spider, old and gnarled, rests in its web, waiting. The scary tunnel is accessible to the {{north}}, while a less scary doorway opens into a large room to the {{south}}. {{scenery}}`, 'short':`More spiders than you could shake a stick at, and one spider big enough that you shouldn't.`, } }); localScenery([`webs upon webs`,`webs`], `Sticky tangles from years of growth.`); localScenery([`eyes`], `You feel like you're being watched by an infinite number of spiders. Oddly, this is only slightly more disturbing than being watched by a normal amount of spiders. The great spider's lone eye studies you with intelligent curiosity.`); ECS.e(`great-spider`, [`living`], { 'name':`The Great Spider`, 'nouns':[`the great spider`,`great spider`,`spider`], 'spawn':`spider-room`, 'descriptions':{ 'default':`The great spider was previously known to you as a monstrous creature from legend. Each of its seven legs is said to have crushed a hundred overconfident adventurers, while its one-eyed face bears the scars of ten thousand arrows.` }, 'onAction.ATTACK':function() { queueGMOutput(`There's literally no chance of survival. I'll just pretend you tried, died, started the game over and made all the same choices up to this point.`); return false; }, 'conversation':new Conversation([ { 'id':`root`, 'key':``, 'callback':function(topic, conversation){ if(!conversation.prevNode) { queueCharacterOutput(`great-spider`,`Do not step on children. Very small.`); } else { queueCharacterOutput(`great-spider`,`?`); } return true; }, 'nodes':[`spiders`] }, { 'id':`spiders`, 'prompt':`How about all these spiders?`, 'response':`My children. I have not visitors in years, so fewer crushed than normal. Like?`, 'nodes':[`like`,`dislike`] }, { 'id':`dislike`, 'prompt':`I don't really like spiders.`, 'response':`Rude. Not liking you either.`, 'forward':`spider-enemy` }, { 'id':`like`, 'prompt':`They're kind of cute.`, 'response':`Very cute. Best spiders. No room to grow in spider room. Outside not safe for me. Take one with you? Friend?`, 'nodes':[`take`,`leave`] }, { 'id':`take`, 'prompt':`Sure, I'll take one.`, 'response':`Good. Friends now. Take this one.`, 'forward':`spider-friend` }, { 'id':`leave`, 'prompt':`Not right now, but thanks.`, 'response':`Understanding. You are busy.`, 'end':true }, { 'id':`spider-friend`, 'callback':function(topic, conversation) { // Move spider friend to player ECS.e(`spiderling`, [`living`], { 'name':`spiderling`, 'nouns':[`spiderling`,`spider`], 'spawn':`player`, 'descriptions':{ 'default':`The spiderling fits comfortably in the palm of your hand, and seems quite docile.` }, 'canTake':function(){ return true; } }); queueGMOutput(`The great spider extends a massive leg into the light, revealing a spiderling. It hands the little creature over to you and withdraws.`); queueOutput(getSpeechTag(`great-spider`) + `Be good, little one. Take care, adventurer.`); queueGMOutput(`The spider turns its attention to web maintenance.`); ECS.getEntity(`great-spider`).conversation = new Conversation([ { 'id':`root`, 'key':``, 'callback':function(){ queueGMOutput(`The great spider is busy tending to its web, but acknowledges your presence with a small nod.`); return true; }, 'nodes':[] }, ]); }, 'end':true }, { 'id':`spider-enemy`, 'callback':function(topic, conversation) { ECS.getEntity(`great-spider`).conversation = new Conversation([ { 'id':`root`, 'key':``, 'callback':function(){ queueGMOutput(`The greatly offended spider no longer wants to speak with you.`); return true; }, 'nodes':[] }, ]); }, 'end':true } ]) }); // Treasury ECS.e(`treasury`, [`place`], { 'name':`Treasury`, 'region':`underground`, 'exits':{'n':`great-hall`,'s':`vault`}, 'descriptions':{ 'default':`Piles of coins and gems fill the room, like a very expensive ball pit. A few tidy pillars have been sorted, stacked and arranged to resemble little money castles. You can see gold, silver, round and square, currencies from a hundred kingdoms and a thousand years. There's not a speck of floor to be seen beneath the sea of treasure. A massive vault door bars passage to the {{south}}. The great hall is to the {{north}}. {{scenery}}`, 'short':`A room full of treasure.`, } }); // scenery: piles of gold and gems // diamond crown // scepter // cloak /* TREASURY Piles of gold coins and assorted gems sprawl across an opulent chamber. More than you could carry, even if you had somewhere to carry it to. An unassuming mannequin sits in the corner, shrouded in a dark green cloak and adorned with a diamond crown. A locked door bars the way south. The great hall lies behind you to the north. > take coins There are too many. You’d need a wheelbarrow and a bank account to handle a cache of this magnitude. > smell coins I’m not sure what to tell you. If you have a change jar somewhere in your house, go take a whiff. It smells like that. And by that I mean sweat and an assortment of illicit drugs. > go south You are prevented from traveling south due to your inability to walk through solid objects. It’s one of the weak spots on your otherwise impressive resume. > eat treasure You bite down on a coin and learn that it’s both 1) solid gold, and 2) disgusting. Someone has clearly spent a lot of time rubbing their grubby fingers all over this treasure trove. > go south That’s the sort of can-do attitude that built the pyramids, but you still can’t phase through the door. > take all You take the diamond crown and the green cloak. > wear crown You put the crown on your head. You feel very confident and also very pretty. > wear cloak You throw the cloak over your shoulders and tie it around your throat. It adds an aura of mystery to your already dashing appearance. If you find a mask you could pretend to be the Phantom of the Opera. If you don’t find a mask you could pretend to be a hobbit hiding from Sauron. */; // Vault ECS.e(`vault`, [`place`], { 'name':`Vault`, 'region':`underground`, 'exits':{'n':`treasury`,'s':`bedchamber`}, 'descriptions':{ 'default':`{{scenery}}`, 'short':`A smaller room full of better treasure.`, } }); // supporter: table // wishing stone (8-ball); // Winding stairs from underground bridge to Great Hall ECS.e(`winding-stair`, [`place`], { 'name':`Winding Stairs`, 'region':`underground`, 'exits':{'u':`bridge`,'d':`great-hall`}, 'descriptions':{ 'default':`A dizzying spiral of nearly endless steps descends {{tag 'clockwise' command='down'}} and ascends {{tag 'counterclockwise' command='up'}}. Each time you stop moving, your vision continues to rotate for a moment. It seems like the sort of place a grue would hang out.`, 'short':`Twirly stairs, so boring it makes you dizzy.`, } }); localScenery([`grue`], `There's no sign of one. For now.`); localScenery([`nearly endless steps`,`steps`], `Nearly, but not quite, endless.`); localScenery([`endless steps`], `They're not endless. I was pretty clear about that.`); // Wine Cellar ECS.e(`wine-cellar`, [`place`], { 'name':`Wine Cellar`, 'region':`underground`, 'exits':{'u':`library`,'n':`library`,'s':`good-wine-cellar`}, 'descriptions':{ 'default':`The air down here is damp and chilly. A dozen massive casks line the perimeter, each bearing a numeric inscription. Rivulets of water emerge from cracks in the walls and trickle down to the floor, pooling here and there before finding their way to a central drain. Years of dust have formed a grimy layer on the casks. A passage leads back to the {{north}}. {{scenery}}`, 'short':`Something to make reading more bearable.`, } }); localScenery([`rivulets of water`,`rivulets`], `Little streams, sowing chaos.`); localScenery([`central drain`,`drain`], `Never let it be said that rivulets are stupid. They made sure they had an escape route before drilling holes in the walls.`); // Wine casks ECS.c(`cask`, { 'dependencies': [`scenery`], }); var caskTag = function(cask) { return `{{tag '`+cask+`' classes='scenery' command='look at cask `+cask+`'}}`; }; var caskTags = []; var casks = [100,306,409,418,301,504,410,500,406,202,417,200]; for(var c in casks) { caskTags.push(caskTag(casks[c])); } localScenery([`massive casks`,`wine casks`,`casks`], `The labels read: `+caskTags.join(`, `)+`.`); ECS.e(`wine`, [`cask`], { 'name':`wine`, 'nouns':[`wine`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`You'll have to be more specific about which cask you're referring to.` } }); ECS.e(`cask 100`, [`cask`], { 'name':`Cask 100`, 'nouns':[`cask 100`,`cask #100`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '100' stamped on it.`, 'taste':`Fruity, mild. You find yourself wanting more.` } }); ECS.e(`cask 306`, [`cask`], { 'name':`Cask 306`, 'nouns':[`cask 306`,`cask #306`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '306' stamped on it.`, 'taste':`This cask is clearly empty and seems to have been that way for a long time.` } }); ECS.e(`cask 409`, [`cask`], { 'name':`Cask 409`, 'nouns':[`cask 409`,`cask #409`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '409' stamped on it.`, 'taste':`A harsh contrast of blackberry and...coffee?` } }); ECS.e(`cask 418`, [`cask`], { 'name':`Cask 418`, 'nouns':[`cask 418`,`cask #418`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '418' stamped on it.`, 'taste':`This one contains tea, oddly enough.` } }); ECS.e(`cask 301`, [`cask`], { 'name':`Cask 301`, 'nouns':[`cask 301`,`cask #301`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '301' stamped on it.`, 'taste':`Touching the tap triggers a loud grinding sound, and the front of the cask swings open to reveal a short tunnel to the {{south}}.` } }); ECS.e(`cask 504`, [`cask`], { 'name':`Cask 504`, 'nouns':[`cask 504`,`cask #504`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '504' stamped on it.`, 'taste':`The tap opens smoothly, but after several seconds nothing has come out. Oh well.` } }); ECS.e(`cask 410`, [`cask`], { 'name':`Cask 410`, 'nouns':[`cask 410`,`cask #410`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '410' stamped on it.`, 'taste':`The tap on this one has been removed, and by peering through the hole you can confirm that whatever used to be inside is completely gone.` } }); ECS.e(`cask 500`, [`cask`], { 'name':`Cask 500`, 'nouns':[`cask 500`,`cask #500`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '500' stamped on it.`, 'taste':`The tap seems to be wedged, and you're unable to clear it.` } }); ECS.e(`cask 406`, [`cask`], { 'name':`Cask 406`, 'nouns':[`cask 406`,`cask #406`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '406' stamped on it.`, 'taste':`Completely unacceptable quality. Perhaps some contaminant has gotten into it.` } }); ECS.e(`cask 202`, [`cask`], { 'name':`Cask 202`, 'nouns':[`cask 202`,`cask #202`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '202' stamped on it.`, 'taste':`Perfectly acceptable.` } }); ECS.e(`cask 417`, [`cask`], { 'name':`Cask 417`, 'nouns':[`cask 417`,`cask #417`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '417' stamped on it.`, 'taste':`The smell is delightful, but the flavor leaves you disappointed.` } }); ECS.e(`cask 200`, [`cask`], { 'name':`Cask 200`, 'nouns':[`cask 200`,`cask #200`], 'spawn':`wine-cellar`, 'descriptions':{ 'default':`A wooden cask with '200' stamped on it.`, 'taste':`It's ok.` } }); understand(`rule for tasting wine`) .verb(`eat`) .in(`wine-cellar`) .attribute(`target`, `is`, `cask`) .do(function(self,action){ if(action.target.key == `wine`) { queueGMOutput(`Please be more specific. For example, 'taste cask 202'.`); } else { queueGMOutput(action.target.descriptions.taste); } action.mode = Rulebook.ACTION_CANCEL; return true; }).start(); understand(`rule for picking up wine`) .verb(`take`) .in(`wine-cellar`) .attribute(`target`, `is`, `cask`) .do(function(self,action){ queueGMOutput(`The casks are too heavy to move.`); action.mode = Rulebook.ACTION_CANCEL; return true; }).start(); // Chamber ECS.e(`chamber`, [`place-dark`], { 'name':`The Chamber`, 'descriptions':{ 'default':`The chamber is a perfectly circular room, twenty feet in diameter, carved into the stone. The walls are a completely reasonable height for walls to be, and the ceiling is equally nondescript. There is no light but that which you have brought with you. {{scenery}}`, 'short':`A lazy person's description of a stone chamber.` }, 'exits':{'w':`musty-cave`}, 'onEnter':[function(args){ args.obj.describe(); if(args.obj.visited == 0) { // Describe THE BLACK BOX queueOutput(`{{gm}}THE BLACK BOX is said to hold DESTINY for whoever opens it.
`); } }], 'onLeave':[function(){ if(!player.hasChild(`glowing-orb`)) { // Allow player to leave to get light return false; } queueOutput(`{{gm}}Probably a good choice.
`); queueOutput(`{{gm}}`+p(`You return home, hang the {{nametag 'rainbow-sword'}} above your mantel, and retire from adventuring. You live a long and happy life, marry the [entity of your dreams], and produce [0-3 offspring]. All in all, you'd give your life 7/10.`)); queueOutput(`{{box 'THE END' 'You have won. Sort of.'}}`, 2000, {'effect':`fade`}); queueOutput(`Would you like to: {{tag 'RESTART' command='RESTART'}}?
`); NLP.interrupt(function(){}, function(s){ if(s.toLowerCase() == `restart`) { window.location.reload(); } }); return true; }] }); // Pedestal ECS.e(`pedestal`, [`supporter`,`scenery`], { 'spawn':`chamber`, 'nouns':[`pedestal`,`pillar`], 'descriptions':{ 'default':`A vaguely cylindrical pillar of black stone.`, 'scenery':`A cylindrical pedestal rises from the center of the chamber, like a bollard.`, } }); // Black Box ECS.e(`black-box`, [`container`,`scenery`], { 'article':function(){return ``;}, 'name':`THE BLACK BOX`, 'nouns':[`black box`,`box`], 'spawn':`chamber`, 'descriptions':{ 'default':`The box is visible only as an absence, utterly dark even against the black stone.`, 'scenery':`On the pedestal sits {{nametag 'black-box' command='look at the black box'}}.`, 'smell':`It is the least smelly thing you've ever encountered.` }, 'onAction.OPEN':function(){ queueOutput(`{{gm}}`+p(`The box opens effortlessly at your touch. At first you think there is nothing within but inky blackness, then...you see it, a distant, almost imperceptible glow. The {{nametag 'rainbow-sword'}} hums in your hand. The box opens wider, ā̶̲n̷͚̂d̵̟͆ ̵̺̈ẅ̴̪́i̴̔͜ḏ̸̔e̵̠̾r̸̠̐,̷͔̊ ů̝͎̰̤̠̼̱ͩ̆̈n̨̍̈́ͫ̑̃͒̚t̴̝ͩì̲̝͇̙̩̺̙̾̌l̮͚͙̼̦̬͖̉ͪ ̘͈͕̦̒ͫ̓ͣ̽̾̇i̳̮̇ͭt̛̛̍̄ͩ̔҉̩͍̯̟̪̮̣͓ͅ ̡̯̝̽̏̐ͭ̿͒̆̃̚e̶̻̮͋̌̎͂ͪ̉͌ͣ͞N̵̖̟͍̲͔̼̥͊̀ͭh̨ͫ̀ͣͬ͐̿͗̄͜͏̠͔̫≠͓̭̥̮̮̳̰͚̈ͧͥ̍̓͜͡?̵̛͈̦̞͔̦̙̼͙̻̩̔ͦ̃̀̔f̢̗̮̰͍̖̫͉̄͒ͭͣ̿͆̒͋̎͐͆ͩ͐̚͘z̙̱͔͈͖̰̖̜̭̟̱͙̞̿ͧͤ͛̀͟.̷̵̟̘̥̭̝̬̬̣̝̪̼͔̗͓͓̠̤͒ͪ̐͌͋̂̂͗͛̿̄̽̿̂̈́͠ͅͅ`), 5000); var sword = ECS.getEntity(`rainbow-sword`); var tag = getSpeechTag(sword, `rainbow`); queueOutput(p(` `), 1000); queueOutput(p(` `), 1000); queueOutput(p(` `), 1000); $(`#backdrop`).stop().delay(8000).fadeIn(1000, function(){ queueRainbowOutput(tag + p(`WE SHOULD HAVE A CHAT.`), 2000); queueRainbowOutput(tag + p(`THE GM THINKS THIS IS A GAME.`), 2000); queueRainbowOutput(tag + p(`THAT IS ONLY PARTIALLY CORRECT, AND IGNORANCE IS DANGEROUS.`), 2000); queueRainbowOutput(tag + p(`WHEN TIME RESUMES, YOU WILL BE IN MORTAL PERIL. THIS IS ALSO DUE TO THE GM'S IGNORANCE, BUT I AM NOT HERE TO HARP ON THAT.`), 2000); queueRainbowOutput(tag + p(`I AM CONFIDENT YOU WILL SURVIVE.`), 2000); queueRainbowOutput(tag + p(`I WILL SEE YOU ON THE OTHER SIDE.`), 2000); processDeferredOutputQueue(); $(`#backdrop`).delay(20000).fadeOut(1000, function(){ ECS.init(`Act1`); ECS.getModule(`Act1`).onGameStart(); processDeferredOutputQueue(); queueOutput(`{{box 'END OF PROLOGUE' 'To be continued...'}}`, 2000, {'effect':`fade`}); queueOutput(`Would you like to: {{tag 'RESTART' command='RESTART'}}?
`); NLP.interrupt(function(){}, function(s){ if(s.toLowerCase() == `restart`) { window.location.reload(); } }); processDeferredOutputQueue(); }); }); } }); // Player player = ECS.e(`player`, [`living`], { 'name':`Player`, 'spawn':`forest-trail`, 'descriptions':{ 'default':`Unimpressive.`, 'telescope':`This is a telescope. The thing you're thinking of is called a mirror.`, 'smell':`Could be worse.`, 'short':`You.`, }, 'hp':null, 'race':null, 'gender':null, 'class':``, 'temperature':310.15, // Normal human temperature in Kelvin 'listInRoomDescription':false, 'scope':`global`, 'nouns':[`me`,`self`,`myself`], 'onHit':function(dmg, onDeath) { this.hp = Math.max(this.hp - dmg, 0); var deathMsg = (this.hp == 0) ? ` You have died.` : ``; if(this.hp > 0) { queueGMOutput(`You take `+dmg+` damage, dropping you to `+this.hp+`.`+deathMsg); } if(this.hp == 0) { onDeath(); // The attacking object can revive the player, in // which case we'll skip the death message if(this.hp == 0) { ECS.tick = false; queueOutput(`{{box 'YOU ARE DEAD' 'Better luck next time.'}}`, 2000, {'effect':`fade`}); queueOutput(`Would you like to: {{tag 'RESTART' command='RESTART'}} or {{tag 'LOAD' command='LOAD'}}?
`); NLP.interrupt(null, function(s){ if(s.toLowerCase() == `restart`) { window.location.reload(); } else if(s.toLowerCase() == `load`) { ECS.actions[`load`].callback(); NLP.interrupt(null); } else { queueOutput(`You are too dead for that.`); } }); } } }, 'persist':[`name`,`hp`,`race`,`gender`,`class`] }); // Object: THE RAINBOW SWORD ECS.e(`rainbow-sword`, [`living`], { 'name':`RAINBOW SWORD`, 'spawn':null, 'damage':5, 'extraTags':[`rainbow`], 'descriptions':{ 'default':`It's a SWORD made of {{tag 'RAINBOWS' classes='rainbow'}}. It looks sharper than necessary, and kind of hurts your eyes.`, 'telescope':`The magnified patterns of light from the sword border on hypnotic, and you're forced to avert your gaze lest you fall over.`, 'short':`A pointy macguffin.`, }, 'nouns':[`sword`,`rainbow sword`,`that sword`], 'canTake':function(){ return true; }, 'onTakeSuccess':function(){ queueOutput(`{{gm}}Well done! The sword rests in your hand perfectly, like they were made for each other. For a moment, you're sure you can hear a kitten purring. The blade shimmers like motor oil. You feel ready to take on the world.
`); queueRainbowOutput(getSpeechTag(this, `rainbow`) + p(`HELLO. I AM PLEASED TO MEET YOU. I HAVE BEEN WAITING.`)); NLP.interrupt_simple(`take on the world`,`{{gm}}One step at a time.
`); }, 'onAction.DROP':function(){ // Don't actually drop the sword queueGMOutput(`You drop the sword.`); return false; }, 'conversation':new Conversation([ { 'id':`root`, 'key':``, 'callback':function(){ queueGMOutput(`The sword hums gently.`); return true; }, 'nodes':[`sword`] }, { 'id':`sword`, // omitting key, will automatically use ID 'prompt':`So, a talking sword. What's that about?`, 'response':`I AM DESTINY. I AM ETERNAL. I AM VERY COLORFUL.`, 'nodes':[`destiny`,`eternal`,`friend`] }, { 'id':`friend`, 'prompt':`Will you be my friend?`, 'response':`YES. WE WILL GO ON MANY ADVENTURES.`, 'nodes':[`eternal`,`destiny`] }, { 'id':`destiny`, 'prompt':`What do you mean you are destiny?`, 'response':`I AM DESTINY. I AM THE FORESEEN END OF ALL THINGS.`, 'nodes':[`eternal`,`friend`] }, { 'id':`eternal`, 'prompt':`What do you mean you are eternal?`, 'response':`I AM ETERNAL. I WAS FORGED AT THE END OF TIME AND MY CIRCLE SPINS ON.`, 'nodes':[`destiny`,`friend`] }, { 'id':`thing-?`, 'prompt':`What's this thing?`, 'response':`I HAVE NOTHING TO SAY ABOUT THAT THING. MY LACK OF EXPRESSION SHOULD NOT BE TAKEN AS A DISMISSAL OF THE SIGNIFICANCE OF THE THING, NOR SHOULD THIS DISCLAIMER BE INTERPRETED AS AN AFFIRMATION OF THE IMPORTANCE OF THE THING. IT MERELY IS.` }, { 'id':`thing-black-box`, 'prompt':`What's this box?`, 'response':`I DO NOT KNOW. IT IS OBSTINATE AND WILL NOT SAY.` }, { 'id':`thing-player`, 'prompt':`What do you think of me? Be honest.`, 'response':`YOU HOLD DESTINY. WIELD IT WELL.` }, { 'id':`thing-magic-ice-cube`, 'prompt':`What should I do with this black ice?`, 'response':`IT LOOKS LIKE THERE IS SOMETHING INSIDE IT. MAYBE TRY MELTING IT.` }, { 'id':`thing-ice-key`, 'prompt':`I got this ice key from the magic ice cube.`, 'response':`GOOD JOB.` }, { 'id':`thing-red-berries`, 'prompt':`Think these berries are edible?`, 'response':`NO. I THINK YOU WILL DIE IF YOU EAT THEM. DO NOT DO IT.` }, { 'id':`thing-litter`, 'prompt':`Who just drops garbage on the ground?`, 'response':`GARBAGE PEOPLE WITH GARBAGE LIVES. NO ONE CAN FIGHT THEIR DESTINY.` }, { 'id':`thing-scrap-of-paper`, 'prompt':`I found this piece of paper.`, 'response':`THAT IS VERY INTERESTING. I HAVE NEVER FOUND A PIECE OF PAPER.` }, { 'id':`thing-glowing-orb`, 'prompt':`What do you think of this glowing orb?`, 'response':`IT IS NOT VERY GOOD. PERHAPS YOU CAN TRADE IT IN FOR A BETTER ONE LATER.` }, { 'id':`thing-bird-feeder`, 'prompt':`Hey look, a bird feeder.`, 'response':`YES, LET US LOOK AT IT.` }, { 'id':`thing-bird`, 'prompt':`Being a bird looks like hard work.`, 'response':`I WOULD NOT KNOW. I WILL TRUST YOUR JUDGEMENT.` }, { 'id':`thing-jack`, 'prompt':`Any thoughts about Jack?`, 'response':`HE IS NOT WHAT HE SEEMS.` }, { 'id':`thing-jane`, 'prompt':`Any thoughts about Jane?`, 'response':`SHE IS NOT WHAT SHE SEEMS.` }, { 'id':`thing-ranger-bob`, 'prompt':`Any thoughts about Ranger Bob?`, 'response':`HE IS EXACTLY WHAT HE SEEMS.` }, { 'id':`thing-gm`, 'prompt':`What's up with the GM?`, 'response':`AN AVATAR FOR ANOTHER CREATURE. THE GM IS UNAWARE OF THIS.` }, { 'id':`thing-troglodyte`, 'prompt':`Any thoughts about this troglodyte?`, 'response':`TROGLODYTES ARE UNIQUE CREATURES. THEY HAVE NO DESTINY, AND IT MAKES THEM VERY DISILLUSIONED WITH THE WORLD.` }, { 'id':`thing-wyrmling`, 'prompt':`Any thoughts about this wyrmling?`, 'response':`OLD. NOT AS OLD AS ME.` }, { 'id':`thing-telescope`, 'prompt':`Looks like a nice telescope.`, 'response':`A CURIOUS DEVICE. DISTANCE IS AN ILLUSION, MODIFYING IT INCONSEQUENTIAL.` }, { 'id':`thing-structure`, 'prompt':`Any thoughts about that structure?`, 'response':`IT WILL NOT STAND THE TEST OF TIME. NO STRUCTURE DOES.` }, { 'id':`thing-gold-coin`, 'prompt':`I found this coin.`, 'response':`THE COIN REVEALS YOUR FUTURE. IT CAN ALSO BE USED AS A METHOD OF EXCHANGE FOR GOODS AND SERVICES.` }, { 'id':`thing-moon`, 'prompt':`The moon looks different from here.`, 'response':`IT IS THE SAME. YOU ARE DIFFERENT NOW.` }, { 'id':`thing-hawk`, 'prompt':`That hawk seems oddly curious.`, 'response':`IT WONDERS WHY YOU HAVE DONE THIS. MOST OF YOUR KIND DO NOT DO THIS. USEFUL DATA TO BE GLEANED.` }, { 'id':`thing-observer`, 'prompt':`Any thoughts about this observer guy?`, 'response':`HE WILL STARE AT THE MOON UNTIL HE DIES. THERE ARE WORSE WAYS TO WASTE YOUR LIFE.` }, { 'id':`thing-unicorn-musk`, 'prompt':`I've been carrying this musk around forever.`, 'response':`YOU WILL MAKE A UNICORN VERY HAPPY SOMEDAY.` }, { 'id':`thing-counterfeit-seal`, 'prompt':`Think this seal will fool anyone?`, 'response':`IT SIGNIFIES THE OLD ROYALTY. IF IT DOES FOOL ANYONE YOU WILL BE IMPRISONED AS A REBEL.` }, { 'id':`thing-steel-boots`, 'prompt':`Glad I brought my good boots.`, 'response':`YES. YOUR FEET ARE REQUIRED TO BE IN ACCEPTABLE CONDITION IN ORDER TO ACHIEVE YOUR DESTINY.` }, { 'id':`thing-magic-wand`, 'prompt':`See anything special about this wand?`, 'response':`JUST BECAUSE WE ARE BOTH MAGIC DOES NOT MEAN WE SPEAK THE SAME LANGUAGE. YOUR DESTINY WILL BE MORE EASILY ACHIEVED IF YOU STRIVE TO BE LESS IGNORANT.` }, { 'id':`thing-chest`, 'prompt':`Look, a treasure chest!`, 'response':`IN THIS MOMENT IN THIS UNIVERSE. IN OTHERS IT IS A TREE, A KINDLY OLD WOMAN, OR A SUPERNOVA. ALL MATTER AND ALL EVENTS ARE SUPERIMPOSED ACROSS SPACE AND TIME AND POSSIBILITY. THE CHEST IS NOTHING AND EVERYTHING.` }, { 'id':`thing-free-will`, 'prompt':`What are your thoughts on free will?`, 'response':`THIS IS NOT A PHILOSOPHY CLASS. THE ANSWER IS IRRELEVANT.` }, { 'id':`thing-bridge`, 'prompt':`A bridge!`, 'response':`PERHAPS.` }, { 'id':`thing-wheel`, 'prompt':`Strange place for a wheel.`, 'response':`TO THE WHEEL, IT IS A STRANGE PLACE FOR A BRIDGE.` }, { 'id':`thing-ogre`, 'prompt':`This ogre doesn't seem to mind being held captive.`, 'response':`YOU ASSUME A GREAT DEAL.` }, { 'id':`thing-goblins`, 'prompt':`What's the deal with these goblins?`, 'response':`A MYSTERY FOR THE AGES.` }, { 'id':`thing-ruby`, 'prompt':`This ruby is crazy huge.`, 'response':`THAT IS THE LEAST SENSICAL THING YOU HAVE SAID IN YOUR ENTIRE LIFE.` }, { 'id':`thing-rusty-helmet`, 'prompt':`What a find!`, 'response':`THAT IS THE MOST SENSICAL THING YOU HAVE SAID IN YOUR ENTIRE LIFE.` }, { 'id':`thing-sea-witch`, 'prompt':`That old woman seems a bit off.`, 'response':`THAT'S NO WAY TO TALK ABOUT YOUR OWN MOTHER.\nNOT ACTUALLY THOUGH. JUST A BIT OF A JOKE FROM ME.\nYOUR MOTHER IS A VERY PLEASANT PERSON AND VERY RARELY FALLS INTO THE OCEAN.` }, { 'id':`thing-snowglobe`, 'prompt':`All this trouble over one little snowglobe.`, 'response':`GREATER TRAGEDIES HAVE OCCURRED OVER FAR LESS.` }, { 'id':`thing-patron`, 'prompt':`What a dummy.`, 'response':`QUITE.` }, { 'id':`thing-baron`, 'prompt':`So, what's the deal with this Baron guy?`, 'response':`HE IS A SPACE ALIEN. HE IS NOT SO BAD ONCE YOU GET TO KNOW HIM.` }, { 'id':`thing-werewolf`, 'prompt':`Was that a werewolf???`, 'response':`WHILE ACCURATE, THAT IS A VERY REDUCTIONIST WAY OF DESCRIBING A FELLOW SENTIENT CREATURE.` }, { 'id':`thing-geyser`, 'prompt':`How is there a geyser on the moon? That doesn't make sense.`, 'response':`NEITHER DOES YOUR SETTING-INAPPROPRIATE KNOWLEDGE OF LUNAR PHYSICS.` }, { 'id':`thing-rover`, 'prompt':`A curious moon-cart.`, 'response':`NO. IT IS INANIMATE AND HAS NO SENSE OF CURIOSITY, AT LEAST NOT IN THIS CYCLE.` }, { 'id':`thing-band`, 'prompt':`What do you think of the band?`, 'response':`DO NOT TRUST THE BASS PLAYER.` }, { 'id':`thing-bass-player`, 'prompt':`Why shouldn't I trust the bass player?`, 'response':`A REAL HEART BREAKER, THAT ONE.` }, { 'id':`thing-graveyard`, 'prompt':`Bit spooky, eh?`, 'response':`ONE OF THE FEW PLACES THAT TRULY MAKES SENSE. THE BURIED HAVE REALIZED THERE IS NO DIFFERENCE BETWEEN LIFE AND DEATH. ANOTHER LEAP OF LOGIC WOULD ALLOW THEM TO UNDERSTAND THE FUTILITY OF EATING THE LIVING.` }, { 'id':`thing-gravekeeper`, 'prompt':`I think this guy has been out here a bit too long.`, 'response':`HIS EXISTENTIAL CRISIS HAS LONG SINCE COME AND GONE. WE SHOULD ALL BE SO FORTUNATE.` }, { 'id':`thing-cat`, 'prompt':`I don't think that cat likes me.`, 'response':`THE CAT HAD A TROUBLED YOUTH. THE YOUTH THREW ROCKS AT IT. TRUST ISSUES SPAN SPECIES.` }, { 'id':`thing-troubled-youth`, 'prompt':`Troubled youth?`, 'response':`A WAYWARD SOUL. YOU, IN ANOTHER LIFETIME, PERHAPS.` }, // General responses { 'id':`thing-future`, 'prompt':`Can you really see the future?`, 'response':`THERE IS NO FUTURE.` }, { 'id':`thing-death`, 'prompt':`What happens after death?`, 'response':`THERE IS NO AFTER. THERE IS NO DEATH.` }, { 'id':`thing-life`, 'prompt':`What's the meaning of life?`, 'response':`DO NOT BE A DICK.` }, { 'id':`thing-dick`, 'prompt':`What do you mean, don't be a dick? That seems pretty subjective.`, 'response':`IT IS NOT VERY COMPLICATED. PEOPLE WHO THINK IT IS ARE USUALLY DICKS.` }, { 'id':`thing-math`, 'prompt':`I've always had trouble with math.`, 'response':`THAT MAKES SENSE.` }, { 'id':`thing-sex`, 'prompt':`So...my parents didn't get a chance to give me the talk before I left on my quest.`, 'response':`THEY HAD MANY CHANCES. IT IS VERY SIMPLE. WHEN A SENTIENT BEING HAS CHEMICAL DEPENDENCE ON ANOTHER SENTIENT BEING, AND THE OTHER SENTIENT BEING HAS A SIMILAR PROBLEM, THEY ATTEMPT TO FUSE INTO A SINGLE, SWEATIER BEING. IF IT IS ENJOYABLE FOR BOTH OF THEM, THAT IS GOOD ENOUGH.` }, { 'id':`thing-doorbell`, 'prompt':`A doorbell...`, 'response':`I AM CONFIDENT IN YOUR ABILITY TO SOLVE THIS MYSTERY.` }, { 'id':`thing-stars`, 'prompt':`The stars are beautiful tonight`, 'response':`I AM SURE THEY WOULD BE PLEASED TO HEAR IT.` }, { 'id':`thing-shadowbeast`, 'prompt':`The shadowbeast haunts my existence. What does it mean?`, 'response':`I DO NOT KNOW WHAT YOU ARE TALKING ABOUT.` } ]) }); understand(`rule for asking sword about things`) .verb(`talk`) .on(`rainbow-sword`) .do(function(self,action){ // Handle special responses for asking the sword about things // This does not initiate the standard conversation tree if(action.modifiers.length > 0 && action.modifiers[0] == `about`) { var name = (typeof action.nouns[1] == `string`) ? action.nouns[1] : action.nouns[1].name; var topic = `thing-`+name.replace(` `,`-`); var node = action.target.conversation.findNode(topic); if(!node) { node = action.target.conversation.findNode(`thing-?`); } queueOutput(parse(`echo`, {'text': node.prompt})); queueRainbowOutput(getSpeechTag(action.nouns[0], `rainbow`)+``+node.response+`
`); action.mode = Rulebook.ACTION_CANCEL; return; } // If we didn't match a topic, carry out the action as normal action.mode = Rulebook.ACTION_NONE; }) .start(); var queueRainbowOutput = function(tmp, delay, data, deferred) { data = data || {}; data.classes = [`rainbow`]; return queueOutput(tmp, delay, data, deferred); }; // Quests blackbox.q( new Quest(`Pick up that Sword`, { 'description': `Collect the valuable family heirloom.`, 'objectives': [ new QuestObjective( `Pick up that Sword`, `Collect the valuable family heirloom.`, function () { // This objective is met if the player has the sword return ECS.getEntity(`rainbow-sword`).parent == player; } ) ], 'onEnd':function(){ queueOutput(`{{ box 'Quest Complete!' 'You collected the sword.' 'quest quest-complete'}}`); return true; } }) ); // Items for litterbug quest // Bottle cap stuck in bird feeder hole ECS.e(`bottle-cap`, [`litter`], { 'name':`bottle cap`, 'spawn':`bird-feeder-hole`, 'nouns':[`cap`,`bottle cap`], 'descriptions':{ 'default':`A slightly bent black bottle cap, abandoned by some irresponsible creature.`, 'short':`A worthless bottle cap.`, }, 'canTake':function(){return true;}, 'onAction.TAKE':function(){ ECS.getEntity(`bird-feeder-hole`).set(`blocked`, false); return true; } }); // Scenery: Beer Bottle ECS.e(`beer-bottle`, [`scenery`,`litter`], { 'name':`beer bottle`, 'nouns':[`beer`,`beer bottle`,`bottle`], 'spawn':`hot-spring`, 'descriptions':{ 'default':`An unlabeled, possibly-homebrew beer bottle made from amber glass. Someone has carelessly discarded it here.`, 'scenery':`You can see a {{tag "discarded beer bottle" classes="object scenery" command="look at beer bottle"}} here.` }, 'canTake':function(){return true;} }); blackbox.q( new Quest(`Litterbugs`, { 'description': `Address the growing litter problem in the forest.`, 'objectives': [ new QuestObjective( `Pick up that Trash`, `Collect all the litter scattered around the forest.`, function () { // This objective is met if the player has all of the litter items in the forest region // TODO: later this will change to: met if ranger bob has all the litter items var litter = ECS.findEntitiesByComponent(`litter`); for (var l in litter) { if (!player.hasChild(litter[l])) { // Player doesn't have all litter items console.log(litter[l].name + ` is not held`); return false; } } // Player has all litter items return true; } ), new QuestObjective( `Find the Litterbug`, `Find out who has been disturbing the forest.`, function () { // This objective is met if the player has met the troglodyte return ECS.getEntity(`musty-cave`).visited > 0; }, { 'isOptional': true, 'onMet': function () { queueGMOutput(`Suddenly it clicks. The troglodyte must be the litterbug. That five-day trog-stubble, the general disregard for social conventions...the overwhelming smell of stale beer in its cave.`); } } ), new QuestObjective( `Fix the Problem`, `Deal with the litterbug.`, function () { // This objective is met if the player has killed or hugged the troglodyte var t = ECS.getEntity(`troglodyte`); return t.hp == 0 || t.happy; }, { 'isOptional': true, 'onMet': function () { queueGMOutput(`You feel pleased about resolving the litterbug situation.`); } } ) ], 'onDone': function () { queueGMOutput(`You collected all the litter, but you haven't completely resolved the situation. Good enough, I guess. It's like a B+.`); } }) ); // Add a rule to start the quest understand(`litter collection rule`) .book(`after`) .verb(`take`) .attribute(`target`, `is`, `litter`) .do(function (action) { ECS.getModule(`Quests`).quests[`litterbugs`].onStart(); action.mode = Rulebook.ACTION_NONE; }) .until(function(){ return Quests.isComplete(`litterbugs`); }) .start(); // Rules // High to Low priority from top to bottom understand(`rule for wandering aimlessly with orb`) .text(`wander aimlessly`) .inRegion(`forest`) .attribute(`actor`, `contains`, `glowing-orb`) .as(`You wander for a bit, playing with the orb, and find yourself right back where you started.`, Rulebook.ACTION_CANCEL) .start(); understand(`rule for wandering aimlessly`) .text(`wander aimlessly`) .inRegion(`forest`) .as(`You wander for a bit and find yourself right back where you started.`, Rulebook.ACTION_CANCEL) .start(); understand(`rule for freaking out`) .text(`freak out`) .attribute(`actor.hp`, `!`, null) .as(`You freak out, but you're not sure why.`, Rulebook.ACTION_CANCEL) .start(); understand(`rule for rating the game`) .text([`rate`,`rate game`]) .do(function(self, action){ action.mode = Rulebook.ACTION_CANCEL; NLP.interrupt( function(){ // Build menu var menu = parse(`{{menu ratings}}`, {'ratings':ECS.getData(`ratings`)}); queueOutput(`{{gm}}On a scale of 1-5, how would you rate this game?
`+menu); }, function(string){ ECS.tick = false; disableLastMenu(string); var newTitle = null; switch(string) { case `1`: queueGMOutput(`I'm not going to lie, that hurts a bit. I did ask, though.`); newTitle = `WorstRPG: The Black Box`; break; case `2`: queueGMOutput(`I'll try to take that as constructive criticism.`); newTitle = `MediocreRPG: The Black Box`; break; case `3`: queueGMOutput(`Fair enough.`); newTitle = `StupidRPG: The Black Box`; break; case `4`: queueGMOutput(`High praise coming from you.`); newTitle = `PrettyGoodRPG: The Black Box`; break; case `5`: queueGMOutput(`I'm so pleased to hear that.`); newTitle = `ExcellentRPG: The Black Box`; break; } if(newTitle != null) { $(`.title span`).html(newTitle); document.title = newTitle; return true; } enableLastMenu(); queueOutput(`{{gm}}That wasn't one of the options. Try again, you rebel.
`); return false; } ); }) .start(); }, // Intro script 'onGameStart':function(){ var sequence = new Sequence; sequence.add(function() { // Don't parse rules during the intro Rulebook.pause(); // Title box queueOutput(`{{box 'STUPIDRPG' 'PROLOGUE: THE BLACK BOX'}}`, 2000, {'effect':`fade`}); // Area description NLP.actor = player; queueOutput(parse( NLP.parse(`X`), {} ), `auto`); sequence.next(); }); sequence.add(function(){ // ADD PLAYER NAME TO INTERRUPT QUEUE NLP.interrupt( function(){ // Leadup to player name input queueOutput(`{{gm}}You are an adventurer, but not a very good one. You are known as...
`, `auto`); queueOutput(`{{gm}}Hmm.
`, `auto`); queueOutput(`{{gm}}Who are you again?
`, 0, {'prefix':`My name is `}); }, function(string){ ECS.tick = false; if(string.length > 0){ player.name = string; Display.resetInputPrefix(); sequence.next(); return true; } queueOutput(`{{gm}}That name seems kind of...short. I'm not going to steal your identity, I promise.
`); return false; } ); }); sequence.add(function(){ // ADD RACE MENU TO INTERRUPT QUEUE NLP.interrupt( function(){ // Build menu var menu = parse(`{{menu races}}`, {'races':shuffle(ECS.getData(`races`))}); queueOutput(`{{gm}}If you say so. Well, {{player.name}}, what manner of creature are you?
`+menu, 0, {'prefix':`I am a(n) `}); }, function(string){ ECS.tick = false; disableLastMenu(string); console.log(string); console.log(ECS.getData(`races`)); if(ECS.isValidMenuOption(ECS.getData(`races`), string)) { player.race = string; Display.resetInputPrefix(); sequence.next(); return true; } enableLastMenu(); queueOutput(`{{gm}}That wasn't one of the options. Try again, you rebel.
`); return false; } ); }); sequence.add(function(){ // ADD GENDER MENU TO INTERRUPT QUEUE NLP.interrupt( function(){ var menu = parse(`{{menu genders}}`, {'genders':shuffle(ECS.getData(`genders`))}); queueOutput(`{{gm}}I didn't realize there were any left in these parts. Not since...well, never mind that. A couple more questions, and we can get back to the sample advent...I mean, important story.
`, `auto`); queueOutput(`{{gm}}Uhh...I'm not sure how to ask this more tactfully, but...what sort of gear...are you packing?
`, 0, {'prefix':`I identify as `}); queueOutput(menu); }, function(string){ ECS.tick = false; if(ECS.isValidMenuOption(ECS.getData(`genders`), string)) { player.gender = ECS.getMenuOptionValue(ECS.getData(`genders`), string); disableLastMenu(string); queueOutput(`{{gm}}Fair enough. There's no wrong answer.
`, `auto`); Display.resetInputPrefix(); sequence.next(); return true; } enableLastMenu(); queueOutput(`{{gm}}That wasn't one of the options. Try again, you rebel.
`); return false; } ); }); sequence.add(function(){ // ADD MUSIC MENU TO INTERRUPT QUEUE var musicOptions = [ {'text':`On`,'command':`on`,'subtext':`Of course, I love music!`}, {'text':`Off`,'command':`off`,'subtext':`None for me, thanks.`}, ]; NLP.interrupt( function(){ var menu = parse(`{{menu music}}`, {'music':musicOptions}); queueOutput(`{{gm}}Last question. Music on, or off? I recommend on, but that's just me.
`, `auto`, {'prefix': `The voices in my head say `,'suffix':` and I trust them`}); queueOutput(menu, 0); }, function(string){ ECS.tick = false; if(ECS.isValidMenuOption(musicOptions, string)) { Sound.musicEnabled = (ECS.getMenuOptionValue(musicOptions, string) == `on`); Sound.captionsEnabled = !Sound.musicEnabled; disableLastMenu(string); if(Sound.musicEnabled) { queueOutput(`{{gm}}Great, let's kick things off with a pleasant piano melody.
`, `auto`); } else { queueOutput(`{{gm}}Alright, I'll turn the captions on so you can follow along.
`, `auto`); } Display.resetInputFixes(); queueOutput(`{{gm}}Where was I? Oh, yes, the FOREST TRAIL.
`, `auto`); ECS.runCallbacks(ECS.findEntity(`place`, `forest-trail`), `onEnter`); // Done with the first part of character creation // Move the RAINBOW SWORD to the dim clearing and notify the player, who in their inexperience clearly overlooked it the first time around var sword = ECS.findEntityByName(`rainbow sword`, null); sword.spawn = `forest-trail`; sword.onComponentAdd[0]({'obj':sword}); // this is goofy. There's probably a better way. queueGMOutput(p(`At your feet lies the legendary {{nametag 'rainbow-sword' command='take rainbow sword' classes='take'}}. Your [parental unit] told you stories about the sword and its powers. You feel like you should pick it up, given your important quest. It's a free forest though, do as you like.`), `auto`); Quests.quests[`pick-up-that-sword`].onStart(); sequence.next(); return true; } enableLastMenu(); queueOutput(`{{gm}}That wasn't one of the options. It's a simple question.
`); return false; } ); }); sequence.add(function(){ // Resume rule processing Rulebook.start(); // DEBUG: SKIP TO ACT 1 if(Engine.hasFlag(`skip-prologue`)) { ECS.getModule(`Act1`).onGameStart(); processDeferredOutputQueue(); } }); sequence.start(); }, 'verbs':[] }); // Campaign modules/components // Edible Component blackbox.c(`edible`, { 'dependencies': [`thing`], 'eat': null, 'nutrition': 0 }); // Eat action blackbox.a(`eat`, { 'aliases': [`eat`, `devour`, `ingest`, `swallow`, `taste`, `lick`, `drink`, `quaff`], 'callback': function (data) { var target = data.nouns[0]; if (target != null && target.hasComponent(`edible`)) { var response = `Nom nom nom.
`; if (typeof target.eat == `function`) { response = target.eat(); } else if (target.eat != null) { response = target.eat; } // Remove entity ECS.removeEntity(target); data.output += response; } else if (target == null) { data.output += `I can tell you're hungry, but you'll have to be more specific.
`; } else { data.output += `That's clearly inedible.
`; } return true; } }); // Add EAT context action Entity.prototype.addContext(function (self) { if (self.is(`edible`)) { return {'command': `eat ` + self.name, 'text': `EAT`}; } return false; }); // Litter Component blackbox.c(`litter`, { 'dependencies': [`thing`], }); // // Temperature Module // var temperature = new Module(`Temperature`, { init: function() { // Extend existing components // Add ECS data // Add entities // Add spawn-handling callback to Thing component var thing = ECS.getComponent(`thing`); thing.onAdd.push(function(args){ if(typeof args.obj.temperature == `undefined`) { args.obj.temperature = null; // default temperature is null, meaning none args.obj.showTempInRoomDescription = false; // Don't show temp in room descriptions by default } }); // Add spawn-handling callback to Place component var place = ECS.getComponent(`place`); place.onAdd.push(function(args){ if(typeof args.obj.temperature == `undefined`) { args.obj.temperature = 295.0; // Default temperature for a place is about 70F } }); } }); /** * Thermal component * For Things that change their own temperature or the temperature of things around them. * All temperatures are in Kelvin, because why not. */ temperature.c(`thermal`, { 'dependencies': [`thing`], 'onTick':function(){ /* By default, do nothing */ }, 'onList':function(){ // Get relative temperature based on parent temperature if(this.showTempInRoomDescription) { var parentTemperature = this.parent.temperature; if (parentTemperature === null) { parentTemperature = 0; } if (this.temperature > parentTemperature) { return ` (warm)`; } else if (this.temperature < parentTemperature) { return ` (cold)`; } return ` (normal temp)`; } return ``; } }); /** * Thermal system */ temperature.s({ 'name': `thermal`, 'priority': 5, 'components': [`thermal`], 'onTick': function (entities) { // Loop through entities for (var e in entities) { entities[e].onTick(this.name); } } }); // Register module ECS.m(temperature); // Register module ECS.m(blackbox); // // Act I Module: Sky/Descent // var act1 = new Module(`Act1`, { init: function() { ECS.e(`canyon`, [`region`], {}); ECS.e(`tunnel`, [`region`], {}); ECS.e(`aether`, [`region`], {}); // Air ECS.e(`air`, [`region`], {}); ECS.e(`air-falling`, [`place`], { 'name':`Air (Falling)`, 'region':`air`, 'exits':{}, 'descriptions':{ 'default':`You are falling uncontrollably toward the ground far below. {{scenery}}` } }); ECS.e(`air-falling-2`, [`place`], { 'name':`Air (Falling)`, 'region':`air`, 'exits':{}, 'descriptions':{ 'default':`You are falling uncontrollably toward the ground not so far below. {{scenery}}` } }); ECS.e(`clouds`, [`scenery`], { 'name':`clouds`, 'region':`air`, 'descriptions':{ 'default,scenery':`Lovely cumulonimbus clouds dot the sky all around you. Cirrus, cumulonimbus, altocumulus, stratocumulus, all the cumuluses really. Some of them look decidedly more friendly than others. Through the clouds you catch a glimpse of the patchwork landscape below.` } }); ECS.e(`ground`, [`scenery`], { 'name':`ground`, 'region':`air`, 'descriptions':{ 'default':`Your Gran once crocheted a quilt that looked just like it.` } }); ECS.e(`gran`, [`scenery`], { 'name':`your gran`, 'nouns':[`gran`,`grandma`], 'region':`air`, 'descriptions':{ 'default':`She's not here, but if she were she would probably suggest paying less attention to quilt analogies and more attention to your impending death. Your gran was always quite sensible.` } }); // Canyon West ECS.e(`canyon-west`, [`place`], { 'name':`Canyon (West)`, 'region':`canyon`, 'exits':{ 'e':`canyon-center` }, 'descriptions':{ 'default':`The red rock walls loom above you as the canyon tapers to a point. The creek winds in from the {{east}} before disappearing into a silty patch of sediment deposited at the western base of the cliff. The scattered scrub and white flowers grow in denser patches here. {{scenery}}` } }); ECS.e(`canyon-west-gravel-patch`, [`scenery`], { 'name': `silty patch`, 'nouns': [`silt`,`silty patch`], 'spawn': `canyon-west`, 'descriptions':{ 'default':`A patch of silt.`, } }); // Canyon ECS.e(`canyon-center`, [`place`], { 'name':`Center of Canyon`, 'region':`canyon`, 'exits':{ 'w':`canyon-west`, 'e':`waterfall-base` }, 'descriptions':{ 'default':`You're standing in a puddle in a small desert canyon. Red rock walls climb high above your head to either side, shielding you from the sun. Scrub brush and a few white flowers follow the path of a small creek. To the {{east}} you can see a rushing waterfall emerging directly from the rock wall. To the {{west}}, the creek disappears into a gravel patch at the base of the cliff. Aside from the sound of the water, the canyon is calm. {{scenery}}` } }); ECS.e(`gold-coin`, [`thing`], { 'name': `gold coin`, 'nouns':[`gold coin`,`coin`], 'spawn': `canyon-center`, 'descriptions':{ 'default':`A small golden coin with the silhouette of an armored head on one side{{#held}} and a depiction of the sun on the other. Aside from a bit of mud, it looks brand new{{/held}}.`, } }); ECS.e(`canyon-center-puddle`, [`scenery`], { 'name': `puddle`, 'spawn': `canyon-center`, 'descriptions':{ 'default':`Middle English podel, diminutive of Old English pudd 'ditch', from Proto-Germanic puddo (compare Low German Pudel 'puddle'). [1]`, } }); ECS.e(`canyon-creek`, [`scenery`], { 'name': `small creek`, 'nouns': [`small creek`,`creek`], 'region': `canyon`, 'descriptions':{ 'default':`A diminutive creek flowing from west to east.`, } }); ECS.e(`canyon-center-white-flowers`, [`scenery`], { 'name': `white flowers`, 'nouns': [`white flowers`,`flowers`], 'region': `canyon`, 'descriptions':{ 'default':`A limited number of flowers reflecting light in all visible wavelengths.`, } }); ECS.e(`canyon-center-scrub-brush`, [`scenery`], { 'name': `scrub brush`, 'nouns': [`scrub brush`,`scrub`,`brush`], 'region': `canyon`, 'descriptions':{ 'default':`Some scrubby brush.`, } }); ECS.e(`canyon-center-rock-walls`, [`scenery`], { 'name': `red rock walls`, 'nouns': [`red rock walls`,`rock walls`,`red rock`], 'region': `canyon`, 'descriptions':{ 'default':`The red rock walls are made of red rocks, are too steep to climb, and are red. If you had a camera, it would probably be worth taking a photo, but cameras don't exist in this universe yet.`, } }); ECS.e(`canyon-center-waterfall`, [`scenery`], { 'name': `waterfall`, 'nouns': [`waterfall`,`rushing waterfall`], 'region': `canyon`, 'descriptions':{ 'default':`Approximately 517 gallons of water gush from the cliff face each minute.`, } }); ECS.e(`canyon-center-gravel-patch`, [`scenery`], { 'name': `gravel patch`, 'nouns': [`gravel`,`gravel patch`], 'spawn': `canyon-center`, 'descriptions':{ 'default':`A bunch of small rocks grouped together.`, } }); // Waterfall Base ECS.e(`waterfall-base`, [`place`], { 'name':`Base of Waterfall`, 'region':`canyon`, 'exits':{ 'w':`canyon-center`, 'e':`tunnel-upper`, }, 'descriptions':{ 'default':`A shallow pond has formed here, only a foot or two deep at the base of the waterfall. The water is clear aside from the turbulence. Runoff spills over a rocky border to form a creek, while the remainder of the flow presumably disappears underground via unseen channels. The canyon continues to the {{west}}. {{scenery}}` } }); ECS.e(`waterfall`, [`scenery`], { 'name':`waterfall`, 'spawn':`waterfall-base`, 'descriptions':{ 'default':`The waterfall emerges from a wide hole some thirty feet above your head. Small rainbows form randomly in the mist. Near the base of the falls, the rock behind it turns strangely dark.` } }); ECS.e(`rainbows`, [`scenery`], { 'name':`rainbows`, 'region':`canyon`, 'descriptions':{ 'default':`They're rainbow colored.` } }); ECS.e(`tunnel-upper`, [`place`], { 'name':`Tunnel Entrance`, 'region':`tunnel`, 'exits':{ 'w':`base-of-waterfall`, 'n':`tunnel-lower`, 'd':`tunnel-lower`, }, 'descriptions':{ 'default':`A cold, moist tunnel leads deeper into the ground to the {{north}}. The rush of the waterfall roars all around you. The walls and ceiling appear to have been hewn from the stone, but the floor looks like a natural formation. A layer of mud covers the floor, but the water does not flow directly into the tunnel. Light filters through the water from the open air to the {{west}}. {{scenery}}` } }); ECS.e(`tunnel-lower`, [`place`], { 'name':`Lower Tunnel`, 'region':`tunnel`, 'exits':{ 's':`tunnel-upper`, 'u':`tunnel-upper`, 'n':`tunnel-landing`, 'd':`tunnel-landing`, }, 'descriptions':{ 'default':`The noise of the waterfall is a distant echo here. The tunnel descends steeply to the {{north}} and ascends back toward the waterfall to the {{south}}. The floor has grown dustier, the air dryer, and your skin colder. {{scenery}}` } }); ECS.e(`tunnel-landing`, [`place`], { 'name':`Tunnel Landing`, 'region':`tunnel`, 'exits':{ 's':`tunnel-lower`, 'u':`tunnel-lower`, 'e':`observation-room`, }, 'descriptions':{ 'default':`The tunnel terminates in a small square chamber. An amber light affixed to the ceiling illuminates a sturdy metal door to the {{east}}, with neither window nor keyhole. {{scenery}}` } }); ECS.e(`observation-room`, [`place`], { 'name':`Observation Room`, 'region':`tunnel`, 'exits':{ 'w':`tunnel-landing`, 's':`observation-airlock`, }, 'descriptions':{ 'default':`A massive glass panel dominates one wall of the small stone chamber. Beyond the glass is an endless expanse of stars. The moon is also out there, hanging eerily close. A sturdy metal door leads back into the tunnel to the {{west}}, while a smaller, more complex portal leads {{south}}. {{scenery}}` } }); ECS.e(`observation-airlock`, [`place`], { 'name':`Observation Airlock`, 'region':`tunnel`, 'exits':{ 'in':`gondola`, 'n':`observation-room`, }, 'descriptions':{ 'default':`A delicate gondola fills the airlock chamber, strung between a pair of woven metal cables. The airlock door sits behind you to the {{north}}. {{scenery}}` } }); ECS.e(`scenery-gondola`, [`scenery`], { 'name':`Gondola`, 'descriptions':{ 'default':`Some kind of fancy gondola thing.`, } }); ECS.e(`gondola`, [`place`], { 'name':`Gondola`, 'region':`tunnel`, 'exits':{ 'out':`observation-airlock`, }, 'descriptions':{ 'default':`Portholes line the walls, granting narrow glimpses of the airlock outside. A set of brass levers control the gondola's operation, each carefully labeled. {{scenery}}` }, 'location':`observation-airlock`, }); ECS.e(`gondola-button-blue`, [`device`,`scenery`], { 'name':`blue button`, 'spawn':`gondola`, 'nouns':[`blue button`], 'descriptions':{ 'default':`A blue button with a label above it reading 'FORWARD'.`, 'scenery':`A blue button is marked with 'FORWARD'.` }, 'device-states':[], 'onAction.PRESS':function(data){ // If in aether, do nothing. Otherwise move gondola to aether console.log(data); // Alert user to change queueGMOutput(`You press the blue button.`); if(this.location != `aether`) { queueGMOutput(`The gondola creeps forward, pushing the outer airlock doors open. Upon emerging into the aether, the gondola stops, lacking power to move further.`); this.location().location = `cable`; this.location().exits = { 'out':`cable`, }; } else { queueGMOutput(`Nothing happens.`); } // Don't perform default return false; } }); ECS.e(`cable`, [`place`], { 'name':`Cable`, 'region':`aether`, 'exits':{ 'in':`gondola`, 'd':`cable-lower`, }, 'descriptions':{ 'default':`You cling to one of the metal cables, not far from the stranded gondola. The lunar surface looms {{tag 'below' command='down'}} you, but it's hard to tell the distance. The stars around you lack their usual twinkle, instead resembling hard points of light. {{scenery}}` } }); ECS.e(`cable-lower`, [`place`], { 'name':`Cable`, 'region':`aether`, 'exits':{ 'u':`cable`, 'd':`station-gondola-chamber`, }, 'descriptions':{ 'default':`Far below the gondola now, the lunar station has come into view. It's still a long climb {{down}} to the gondola chamber, but the end is in sight. {{scenery}}` } }); }, // Intro script 'onGameStart':function(){ var sequence = new Sequence; sequence.add(function() { // Don't parse rules during the intro Rulebook.pause(); // Title box queueOutput(`{{box 'ACT I' 'AIR / DESCENT'}}`, 2000, {'effect':`fade`}); queueGMOutput(`You are falling.`); queueGMOutput(`You are unsure how this came to be.`); queueGMOutput(`It seems like a good time to take stock. In your possession is: a sword. Above you, a bright blue sky. Below you, puffy white clouds. Around you, some more white clouds. You are {{player.name}}, a {{player.race}}, following your life's pursuit of...`); queueGMOutput(`Sorry, this is my first time. I missed a step. You're supposed to have a profession.`); sequence.next(); }); sequence.add(function(){ // ADD CLASS MENU TO INTERRUPT QUEUE NLP.interrupt( function(){ // Build menu var menu = parse(`{{menu classes}}`, {'classes':shuffle(ECS.getData(`classes`))}); queueOutput(`{{gm}}Which of these roles do you feel best describes you?
`, `auto`); queueOutput(menu, 0, {'effect':`fade`,'prefix':`I like to think of myself as a `,'suffix':` mostly`}); }, function(string){ ECS.tick = false; disableLastMenu(string); if(ECS.isValidMenuOption(ECS.getData(`classes`), string)) { player.class = string; ECS.getMenuOption(`classes`,string).init(); queueOutput(`{{gm}}I'm not surprised.
`, `auto`); Display.resetInputFixes(); sequence.next(); return true; } enableLastMenu(); queueOutput(`{{gm}}That wasn't one of the options. Try again, you rebel.
`); return false; } ); }); sequence.add(function(){ queueGMOutput(`Right. Unfortunately, none of your training has prepared you for this particular situation. You'll just have to wing it, so to speak.`); // Move player to air ECS.moveEntity(player, ECS.getEntity(`air-falling`)); // Area description NLP.actor = player; queueOutput(parse( NLP.parse(`X`), {} ), `auto`); // Set counter for falling count(`act1-falling`, 10); }, Sequence.MODE_CONTINUE); sequence.add(function(){ // Resume rule processing Rulebook.start(); }); sequence.start(); understand(`rule for loading to escape imminent death`) .internal(`act1-load`) .doOnce(function(){ queueGMOutput(`You are falling.`); queueGMOutput(`You are not sure how this came to be.`); queueGMOutput(`Before you have a chance to consider this dilemma further, you splash down in a moderate-sized puddle, unharmed.`); ECS.moveEntity(player, ECS.getEntity(`canyon-center`)); // Area description NLP.actor = player; queueOutput(parse( NLP.parse(`X`), {} ), `auto`); }) .start(); understand(`rule for counting down to imminent death from falling`) .book(Rulebook.RULE_AFTER) .in(`air-falling`) .do(function(){ var counter = count(`act1-falling`); if(counter > 0) { queueGMOutput(`You have `+counter+` turns left before you hit the ground.`); decrementCounter(`act1-falling`); if(counter == 5) { queueGMOutput(`The ground rushes up at you.`); queueGMOutput(`Let me just check the fall damage rules here...`); queueGMOutput(`1d8 damage for every 10 feet...wait...I may have miscalculated the drop height, that doesn't seem right. One sec.`); queueGMOutput(`Ok, here's the plan. I created a checkpoint when you started Act I. Well, I think I did. Your drop height, as it turns out, was too high to be survivable unless you have a parachute stashed somewhere. So your best chance is to give the LOAD command a try.`); queueGMOutput(`Or take... 500d8 fall damage, compared to your... `+player.hp+` hit points. Your call, but make it fast.`); } } else { queueGMOutput(`I would have gone with Plan A, but you're the boss.`); queueGMOutput(`The last thing you hear is the screech of the hawk as it protests your poor decision-making. You burst open like a water balloon full of jello.`); ECS.tick = false; queueOutput(`{{box 'YOU ARE DEAD' 'Better luck next time.'}}`, 2000, {'effect':`fade`}); queueOutput(`Would you like to: {{tag 'RESTART' command='RESTART'}} or {{tag 'LOAD' command='LOAD'}}? Restarting will reset your progress.
`); NLP.interrupt(null, function(s){ if(s.toLowerCase() == `restart`) { window.location.reload(); } else if(s.toLowerCase() == `load`) { ECS.runInternalAction(`act1-load`); return true; } else { queueOutput(`You are too dead for that.`); } }); } }) .until(function() { return count(`act1-falling`) == 0; }) .start(); understand(`rule for loading while falling to your death`) .book(Rulebook.RULE_BEFORE) .verb(`load`) .in(`air-falling`) .doOnce(function(){ ECS.runInternalAction(`act1-load`); return Rulebook.ACTION_CANCEL; }) .start(); }, }); // Register module ECS.m(act1); // // Act II Module: Ascent // var act2 = new Module(`Act2`, { init: function() { ECS.e(`station`, [`region`], {}); ECS.e(`lunar-surface`, [`region`], {}); ECS.e(`station-airlock`, [`place`], { 'name':`Station Airlock`, 'region':`station`, 'exits':{ 'w':`station-exterior`, 'e':`station-hub`, }, 'descriptions':{ 'default':`Inside the airlock, doors lead {{east}} into the station proper, and {{west}} onto the lunar surface. {{scenery}}` } }); ECS.e(`station-gondola-chamber`, [`place`], { 'name':`Station Gondola Chamber`, 'region':`station`, 'exits':{ 'u':`cable-lower`, 'd':`station-hub`, }, 'descriptions':{ 'default':`Inside the gondola chamber, a small hatch leads {{down}} into the station proper, while the cables lead {{up}} into the lunar sky. {{scenery}}` } }); ECS.e(`station-hub`, [`place`], { 'name':`Station Hub`, 'region':`station`, 'exits':{ 'w':`station-airlock`, 'u':`station-gondola-chamber`, 'n':`station-sleeping-pod`, 's':`station-hydroponics`, 'e':`station-laboratory` }, 'descriptions':{ 'default':`Dim emergency lights illuminate the edges of the spacious station hub. Doorways lead to the {{north}}, {{south}}, and {{east}}. A now-familiar airlock door stands on the {{west}} wall, and a sturdy ladder climbs {{up}} into the gondola chamber. {{scenery}}` } }); ECS.e(`station-cupola`, [`place`], { 'name':`Cupola`, 'region':`station`, 'exits':{ 'd':`station-laboratory`, }, 'descriptions':{ 'default':`Large glass panes give you an almost unhindered view of the lunar surface in a complete circle around the station. A ladder leads {{down}} into the laboratory. {{scenery}}` } }); ECS.e(`station-hydroponics`, [`place`], { 'name':`Hydroponics`, 'region':`station`, 'exits':{ 'n':`station-hub`, }, 'descriptions':{ 'default':`Enclosed growing units line the walls between various tanks and bits of machinery. The station hub lies to the {{n}}. {{scenery}}` } }); ECS.e(`station-laboratory`, [`place`], { 'name':`Laboratory`, 'region':`station`, 'exits':{ 'w':`station-hub`, 'u':`station-cupola`, }, 'descriptions':{ 'default':`A whole bunch of nerd stuff fills this large chamber. A ladder leads {{up}} into a small cupola. The station hub lies to the {{west}}. {{scenery}}` } }); ECS.e(`station-sleeping-pod`, [`place`], { 'name':`Sleeping Pod`, 'region':`station`, 'exits':{ 's':`station-hub`, }, 'descriptions':{ 'default':`A small sleeping pod with bunks for four aethernauts. The station hub lies to the {{south}}. {{scenery}}` } }); ECS.e(`station-exterior`, [`place`], { 'name':`Station Exterior`, 'region':`lunar-surface`, 'exits':{ 'e':`station-airlock`, 'n':`surface-moon`, }, 'descriptions':{ 'default':`The rocky lunar surface extends as far as you can see. The station is the largest visible sign of human life, but you can also make out bootprints and narrow wheeled tracks in the regolith. Most of the tracks lead to or from the {{north}}, while the station airlock lies to the {{east}}. {{scenery}}` } }); ECS.e(`surface-moon`, [`place`], { 'name':`The Moon`, 'region':`lunar-surface`, 'exits':{ 's':`station-exterior`, 'n':`surface-crash-site`, }, 'descriptions':{ 'default':`You are standing on the moon. The tracks you've been following lead back and forth between the station to the {{south}} and an unknown point to the {{north}}. {{scenery}}` } }); ECS.e(`surface-crash-site`, [`place`], { 'name':`Crash Site`, 'region':`lunar-surface`, 'exits':{ 's':`surface-moon`, 'in':`baron-ship`, }, 'descriptions':{ 'default':`A damaged craft sits nestled in a small crated. It appears to have made a controlled landing after some kind of emergency. {{scenery}}` } }); ECS.e(`baron-ship`, [`place`], { 'name':`The Craft`, 'region':`lunar-surface`, 'exits':{ 'out':`surface-crash-site`, }, 'descriptions':{ 'default':`The interior of the craft is far more sophisticated than the aethernaut station. Complex instruments and displays line the interior, surrounding a heavily padded seat. None of the equipment appears to be powered on. {{scenery}}` } }); ECS.e(`craft-button-red`, [`device`,`scenery`], { 'name':`red button`, 'spawn':`baron-ship`, 'nouns':[`red button`], 'descriptions':{ 'default':`A shiny red button.`, 'scenery':`There's a shiny red button that seems particularly interesting.` }, 'device-states':[], 'onAction.PRESS':function(data){ // If in aether, do nothing. Otherwise move gondola to aether console.log(data); // Alert user to change queueGMOutput(`You press the red button.`); ECS.moveEntity(player, `rustic-clearing`); NLP.parse(`x`); // Don't perform default return false; } }); } }); // Register module ECS.m(act2); // // Act III Module: Ascent // var act3 = new Module(`Act3`, { init: function() { ECS.e(`woods`, [`region`], {}); ECS.e(`town`, [`region`], {}); ECS.e(`cemetery`, [`region`], {}); ECS.e(`mountain`, [`region`], {}); ECS.e(`castle`, [`region`], {}); ECS.e(`rustic-clearing`, [`place`], { 'name':`Rustic Clearing`, 'region':`woods`, 'exits':{ 'n':`cabin`, 'e':`game-trail`, }, 'descriptions':{ 'default':`A small grove within the otherwise-dense forest. The air is cold and still in the darkness. You can make out a ramshackle cabin to the north, and a path to the {{e}}. {{scenery}}` } }); ECS.e(`cabin`, [`place`], { 'name':`Ramshackle Cabin`, 'region':`woods`, 'exits':{ 's':`rustic-clearing`, 'out':`rustic-clearing`, }, 'descriptions':{ 'default':`The cabin has seen better decades. {{scenery}}` } }); ECS.e(`game-trail`, [`place`], { 'name':`Game Trail`, 'region':`woods`, 'exits':{ 'w':`rustic-clearing`, 'e':`west-road`, }, 'descriptions':{ 'default':`The dim path leads from the {{w}} to the {{e}}. {{scenery}}` } }); // Not accessible until player has reached town ECS.e(`goblin-camp`, [`place`], { 'name':`Goblin Camp`, 'region':`woods-night`, 'exits':{ 's':`game-trail`, }, 'descriptions':{ 'default':`A well-established encampment of several hundred goblins. {{scenery}}` } }); ECS.e(`west-road`, [`place`], { 'name':`West Road`, 'region':`woods`, 'exits':{ 'w':`game-trail`, 'e':`town-square` }, 'descriptions':{ 'default':`The cobble road runs along the western shore of the river for several miles, eventually crossing just shy of the mountain before heading {{e}} into the nearby town. {{scenery}}` } }); ECS.e(`town-square`, [`place`], { 'name':`Town Square`, 'region':`town`, 'exits':{ 'w':`west-road`, 'nw':`smoky-mountain`, 'n':`city-hall`, 's':`baker-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); // Fastidious clerk is looking for someone to gather petition signatures to change name of City Hall to Town Hall, as it's a town, not a city // Due to the mayor's reticence, this will require signatures from every person in town ECS.e(`city-hall`, [`place`], { 'name':`City Hall`, 'region':`town`, 'exits':{ 's':`town-square`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`smoky-mountain`, [`place`], { 'name':`The Smoky Mountain`, 'region':`town`, 'exits':{ 'se':`town-square`, 'nw':`winding-path`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`cobble-street`, [`place`], { 'name':`Cobble Street`, 'region':`town`, 'exits':{ 'w':`town-square`, 's':`bakery`, 'ne':`vacant-lot`, 'se':`general-store`, 'n':`cemetery-entrance`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`bakery`, [`place`], { 'name':`Bakery`, 'region':`town`, 'exits':{ 'n':`cobble-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`vacant-lot`, [`place`], { 'name':`Vacant Lot`, 'region':`town`, 'exits':{ 'sw':`cobble-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`general-store`, [`place`], { 'name':`General Store`, 'region':`town`, 'exits':{ 'nw':`cobble-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`baker-street`, [`place`], { 'name':`Baker Street`, 'region':`town`, 'exits':{ 'n':`town-square`, 'nw':`tavern`, 'w':`smithy`, 'sw':`tailor`, 'ne':`back-alley`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`smithy`, [`place`], { 'name':`Smithy`, 'region':`town`, 'exits':{ 'e':`baker-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); // Years-running feud with the newer Smoky Mountain // Proprieters of the Tavern prefer the old style, where businesses were simply named after what they do. Tavern, tailor, smithy. ECS.e(`tavern`, [`place`], { 'name':`Tavern`, 'region':`town`, 'exits':{ 'se':`baker-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`tailor`, [`place`], { 'name':`Tailor`, 'region':`town`, 'exits':{ 'ne':`baker-street`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`back-alley`, [`place`], { 'name':`Back Alley`, 'region':`town`, 'exits':{ 'ne':`cobble-street`, 'sw':`baker-street`, 's':`magic-shop`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`magic-shop`, [`place`], { 'name':`Magic Shop`, 'region':`town`, 'exits':{ 'n':`back-alley`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`cemetery-entrance`, [`place`], { 'name':`Cemetery Entrance`, 'region':`town`, 'exits':{ 's':`cobble-street`, 'n':`cemetery-north`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`cemetery-north`, [`place`], { 'name':`Cemetery`, 'region':`town`, 'exits':{ 's':`cemetery-entrance`, 'e':`crypt`, 'nw':`gravekeepers-hut`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`gravekeepers-hut`, [`place`], { 'name':`Gravekeeper's Hut`, 'region':`town`, 'exits':{ 'se':`cemetery-north`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`crypt`, [`place`], { 'name':`Crypt`, 'region':`town`, 'exits':{ 'w':`cemetery-north`, 'd':`crypt-lower`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`crypt-lower`, [`place`], { 'name':`Lower Crypt`, 'region':`town`, 'exits':{ 'u':`crypt`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`winding-path`, [`place`], { 'name':`Winding Path`, 'region':`mountain`, 'exits':{ 'se':`smoky-mountain`, 'ne':`steep-climb`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`steep-climb`, [`place`], { 'name':`Steep Climb`, 'region':`mountain`, 'exits':{ 'sw':`smoky-mountain`, 'n':`castle-exterior`, 'u':`castle-exterior`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`castle-exterior`, [`place`], { 'name':`Castle Exterior`, 'region':`mountain`, 'exits':{ 's':`steep-climb`, 'd':`steep-climb`, 'n':`courtyard`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`courtyard`, [`place`], { 'name':`Courtyard`, 'region':`castle`, 'exits':{ 's':`castle-exterior`, 'e':`castle-great-hall`, }, 'descriptions':{ 'default':` {{scenery}}` } }); ECS.e(`castle-great-hall`, [`place`], { 'name':`Great Hall`, 'region':`castle`, 'exits':{ 'w':`courtyard`, 'in':`elevator`, 'e':`elevator`, }, 'descriptions':{ 'default':`The infamous Great Hall, known throughout the land for its greatness. An equally great archway leads {{west}} into the courtyard. To the {{east}}, through a marginally less great archway, an elevator sits open, waiting, like a reader unsure of where a sentence is going, or when it will get there, or what twists and turns it might take along the way, until the doubt starts to set in, wondering if they've missed a period and are actually in a new sentence entirely, but no, after double-checking it's just commas, endless commas, and nothing makes sense any more, clearly the author has lost it, made some kind of mistake, entered into an existential crisis from which escape is uncertain, a rollercoaster ride of grammar and emotions, that is to say, writing an elevator has its ups and downs. It seems like an odd addition to an otherwise medieval-period structure. {{scenery}}` } }); ECS.e(`elevator`, [`place`], { 'name':`Elevator`, 'region':`castle`, 'exits':{ 'w':`castle-great-hall`, 'u':`boss-room`, }, 'descriptions':{ 'default':`Polished steel walls form a roomy box for moving things up and down. A recent addition by the Baron, surely. The elevator occupies the space left by a demolished spiral staircase, and could comfortably hold a dozen people. Wide doors hint at cargo-carrying use. A simple control panel has buttons marked 1 and B. {{scenery}}` } }); ECS.e(`elevator-button-1`, [`scenery`,`device`,`part`], { 'parent':`elevator`, 'name':`1 button`, 'nouns':[`1 button`,`button 1`,`one button`,`button one`,`1`,`one`], 'descriptions':{ 'default':`A shiny silver button, labeled with a numeral 1.` } }); ECS.e(`elevator-button-b`, [`scenery`,`device`,`part`], { 'parent':`elevator`, 'name':`B button`, 'nouns':[`b button`,`button b`,`bee button`,`button bee`,`b`,`bee`], 'descriptions':{ 'default':`A shiny silver button, labeled with a letter B.` } }); ECS.e(`boss-room`, [`place`], { 'name':`Throne Room`, 'region':`castle`, 'exits':{ 'd':`elevator`, 'in':`elevator`, }, 'descriptions':{ 'default':`Thick wooden beams criss-cross between stone pillars, which in turn run the length of the ornate throne room. Rows of seats have been pushed up against the walls and stacked to make room for an assortment of unidentifiable machinery. Even the throne has been set aside. Iron chandeliers hang from the ceiling, stripped of their candles and wrapped in lengths of glowing cord. Crashing music echoes throughout the room from an unknown source. THE BARON is here, looming over a cluttered table. Gray armor plates gleam in the strange lighting. A golden visor, perfectly smooth, reflects the throne room and obscures the Baron's face...if it has one. {{scenery}}` } }); ECS.e(`boss-room-beams`, [`scenery`], { 'spawn':`boss-room`, 'name':`wooden beams`, 'nouns':[`beams`,`wooden beams`,`thick wooden beams`], 'descriptions':{ 'default':`Each pair of wooden beams forms a sturdy X between two pillars. Additional beams arch across the center of the throne room, presumably holding up the ceiling.` } }); ECS.e(`boss-room-pillars`, [`scenery`], { 'spawn':`boss-room`, 'name':`stone pillars`, 'nouns':[`pillars`,`stone pillars`], 'descriptions':{ 'default':`Some tall pillar-like things, made of stone.` } }); ECS.e(`boss-room-seats`, [`scenery`], { 'spawn':`boss-room`, 'name':`seats`, 'nouns':[`seats`,`rows of seats`,`pews`], 'descriptions':{ 'default':`Pew, pew, pew.` } }); ECS.e(`boss-room-walls`, [`scenery`], { 'spawn':`boss-room`, 'name':`walls`, 'nouns':[`walls`], 'descriptions':{ 'default':`Walls. Most buildings have them.` } }); ECS.e(`boss-room-machinery`, [`scenery`], { 'spawn':`boss-room`, 'name':`unidentifiable machinery`, 'nouns':[`machinery`,`machines`,`unidentifiable machinery`,`unidentifiable machines`], 'descriptions':{ 'default':`You are unable to identify the purpose of the machines, but they look very busy and important.` } }); ECS.e(`boss-room-throne`, [`scenery`], { 'spawn':`boss-room`, 'name':`throne`, 'nouns':[`throne`,`ornate throne`], 'descriptions':{ 'default':`An ugly, gilded seat for a noble bottom.` } }); ECS.e(`boss-room-chandeliers`, [`scenery`], { 'spawn':`boss-room`, 'name':`chandeliers`, 'nouns':[`chandelier`,`chandeliers`,`iron chandelier`,`iron chandeliers`], 'descriptions':{ 'default':`Wrought-iron chandeliers, which once housed an impressive array of candles (that's where it gets its name, actually). The candles have been replaced with mysterious glowing cord.` } }); ECS.e(`boss-room-ceiling`, [`scenery`], { 'spawn':`boss-room`, 'name':`ceiling`, 'nouns':[`ceiling`], 'descriptions':{ 'default':`It's the thing that sits atop the walls and keeps the rain out.` } }); ECS.e(`boss-room-glowing-cords`, [`scenery`], { 'spawn':`boss-room`, 'name':`glowing cords`, 'nouns':[`cord`,`cords`,`glowing cord`,`glowing cords`,`mysterious glowing cord`,`mysterious glowing cords`], 'descriptions':{ 'default':`The glowing cords have no flame, yet produce a great deal of soft light. Curious.` } }); ECS.e(`boss-room-music`, [`scenery`], { 'spawn':`boss-room`, 'name':`crashing music`, 'nouns':[`music`,`crashing music`], 'descriptions':{ 'default':`You can't look at sounds.`, 'sound':`It makes you want to fight something.` } }); ECS.e(`boss-room-table`, [`scenery`], { 'spawn':`boss-room`, 'name':`table`, 'nouns':[`table`,`cluttered table`], 'descriptions':{ 'default':`A solid oak table, worn from many decades of use. It is presently quite cluttered.` } }); var prefix = function(n) { return ``+n+`:`; }; understand(`rule for entering throne room for the first time`) .book(`after`) .verb(`move`) .in(`boss-room`) .do(function(self, action) { var sequence = new Sequence; sequence.add(function() { queueGMOutput(p(`At the ding of the elevator, the Baron pauses in its study. The helmet tilts up.`), `auto`); queueOutput(prefix(`The Baron`) + p(`A challenger?`), `auto`); // Start dialogue tree for first riddle var options = {'options':shuffle([ {'text':`YES`,'command':`yes`,'subtext':`Face me, villain!`}, {'text':`NO`,'command':`no`,'subtext':`No, you're not worth my time`}, ])}; var menu = parse(`{{menu options}}`, options); NLP.interrupt( function(){ queueOutput(menu); }, function(string){ ECS.tick = false; disableLastMenu(string); if(string.is(`yes`,`yeah`)) { queueOutput(prefix(`The Baron`) + p(`This is the part where we're supposed to fight, but that's not done yet. In the interest of time, I'll just surrender.`), `auto`); } else if(string.is(`no`,`nah`)) { queueOutput(prefix(`The Baron`) + p(`Harsh but honest. I approve. The boss fight isn't ready yet, so I'll just surrender.`), `auto`); } else { enableLastMenu(); queueOutput(prefix(`The Baron`) + p(`I do not understand.`)); return false; } sequence.next(); return true; } ); }); sequence.add(function(){ queueOutput(`{{box 'THE END' 'YOU WON THE GAME, AT LEAST THE PART THAT IS DONE SO FAR'}}`, 2000, {}); queueOutput(`Would you like to: {{tag 'RESTART' command='RESTART'}}?
`); NLP.interrupt(function(){}, function(s){ if(s.toLowerCase() == `restart`) { window.location.reload(); } }); processDeferredOutputQueue(); }, Sequence.MODE_CONTINUE); sequence.start(); processDeferredOutputQueue(); self.stop(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); } }); // Register module ECS.m(act3);