// 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[l].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 args.obj.setBackground(); return args.obj.describe(); }], 'onExit': null, // Called when the player exits the location 'setBackground':function(){ if(typeof this.background != `undefined` && this.background != null) { $(`body`).css({'background-color':this.background}); return; } var region = this.getRegion(); if(region != null && typeof region.background != `undefined` && region.background != null) { $(`body`).css({'background-color':region.background}); return; } $(`body`).css({'background-color':`#202020`}); }, '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 Richards`+action.nouns[0].descriptions.help+`
`; } return `No help available for '`+action.nouns[0].name+`'.`; } return `Try 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 () { queueGMOutput(`No need for that. I'm sure everything will go fine.`); /* ECS.tick = false; ECS.setData(`save`, JSON.stringify(ECS.save())); queueOutput(`Game Saved.`); */ } }); // Load action coreModule.a(`load`, { 'aliases': [`load`, `quickload`], 'callback': function () { if(ECS.data.hasOwnProperty(`save`)) { ECS.tick = false; ECS.load(ECS.getData(`save`)); queueOutput(`Game Loaded.`); } else { queueGMOutput(`Nothing to load right now.`); } } }); // Look action coreModule.a(`look`, { 'aliases': [`look`, `l`, `look at`, `peer`, `glance`, `inspect`, `examine`, `investigate`, `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`, `enter`, `out`, `exit` ], '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`))); }); Handlebars.registerHelper(`in`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`in`))); }); Handlebars.registerHelper(`enter`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`enter`))); }); Handlebars.registerHelper(`out`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`out`))); }); Handlebars.registerHelper(`exit`, function () { return new Handlebars.SafeString(parse(getDirectionTag(`exit`))); }); // Take action coreModule.a(`take`, { 'aliases': [ `take`, `grab`, `acquire`, `snatch`, `pick`, `pick up`, `collect`, `get`, ], '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`, `hop in`, `hop inside`, `hop into`, `hop on`, `hop 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`); data.string = data.text = `in`; data.modifiers = [`in`]; verb.pre(data); return verb.callback(data); } else { // Re-run action with noun provided return NLP.parse(`get in ` + target.nouns[0]); } //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; } }); // Get outside action containers.a(`get-out`, { 'aliases': [ `get out`, `get outside`, `exit`, `get off`, `get out of`, `get off of`, `hop out`, `hop outside`, `hop off`, `hop out of`, `hop off of`, `be out`, `be off` ], 'callback': function (data) { var actor = data.actor; var target = (data.nouns.length) ? data.nouns[0] : null; var modifier = `out`; if (target === null) { // No target specified, check location for 'out' exit target = actor.parent; if (target.hasExit(`out`)) { target = ECS.getEntity(target.getExit(`out`)); if(!target.hasComponent(`holder`)) { // Not a holder, try moving instead var verb = ECS.getAction(`move`); data.string = data.text = `out`; data.modifiers = [`out`]; verb.pre(data); return verb.callback(data); } modifier = (target.hasComponent(`supporter`)) ? `off` : `out`; } 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 out of here.`); return true; } } // Find out if the target can be entered if (!target.isEnterable) { // Can't get out/off queueGMOutput(`That's not something you can get `+modifier+` of.`); return true; } // Move actor out of target ECS.moveEntity(actor, actor.location()); // Give default response var handled = ECS.runCallbacks(target, `onExit`, data); if(handled) { return handled; } queueGMOutput(`You get `+modifier+` of 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 if(!ECS.runCallbacks(container, `canPutInto`, data)) { queueGMOutput(`That doesn't seem to fit.`); return true; } // 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); 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)) + `)`)); 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`) { queueGMOutput(target.onLock); } else { target.isLocked = true; queueGMOutput(`You lock the ` + target.name + `.
`); } } else if (target == null) { return queueGMOutput(`I can tell you want to lock something, but you'll have to be more specific.
`); } else { return queueGMOutput(`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`) { queueGMOutput(target.onUnlock); } else { var k = target.lockKey; if(data.actor.hasChild(k)) { target.isLocked = false; queueGMOutput(`You unlock the ` + target.name + ` using the `+ ECS.getEntity(k).name +`.
`); } else { queueGMOutput(`You'll need the right key to do that.
`); } } } else if (target == null) { return queueGMOutput(`I can tell you want to unlock something, but you'll have to be more specific.
`); } else { return queueGMOutput(`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': [`thing`], '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.getEntity(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.updateStats(); } } }); }, 'onAction.TAKE': function () { queueGMOutput(p(`That's part of the ` + getNameTag(this.parent) + `.`)); return false; }, 'onAdd': function (args) { args.obj.isVisible.push(function(args){ // Entity parent is in location return args.obj.parent.locationIs(args.location); }); } }); // 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) { if(!data.target.is(`device`)) { queueGMOutput(`That's not something you can turn.`); return; } // 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`,`use`], 'modifiers':[], 'callback': function (data) { if(!data.target.is(`device`)) { queueGMOutput(`That's not something you can use.`); return; } if(this[`device-state`] == `on`) { queueGMOutput(p(`The ` + getNameTag(data.target) + ` is already on.`)); return; } this[`device-state`] = `on`; queueGMOutput(p(`You turn on the ` + getNameTag(data.target) + `.`)); } }); // Deactivate Object devices.a(`deactivate`, { 'aliases': [`deactivate`], 'modifiers':[], 'callback': function (data) { if(!data.target.is(`device`)) { queueGMOutput(`That's not something you can deactivate.`); return; } if(this[`device-state`] == `off`) { queueGMOutput(p(`The ` + getNameTag(data.target) + ` is already off.`)); return; } this[`device-state`] = `off`; queueGMOutput(p(`You turn off the ` + getNameTag(data.target) + `.`)); } }); // Press Object devices.a(`press`, { 'aliases': [`press`,`push`,`touch`,`boop`], 'modifiers':[], 'callback': function (data) { queueGMOutput(p(`You press the ` + getNameTag(data.target) + `.`)); } }); // Register module ECS.m(devices); // // Reading Module // var reading = new Module(`Reading`, { init: function () { // Add READ context action Entity.prototype.addContext(function (self) { if (self.is(`readable`)) { return {'command': `read ` + self.name, 'text': `READ`}; } return false; }); } }); // Readable component reading.c(`readable`, { 'dependencies': [`thing`], 'onInit': function() {}, 'onAdd': function (args) {} }); // Read verb reading.a(`read`, { 'aliases':[`read`], 'modifiers':[], 'callback':function(data){ if(!data.target) { queueGMOutput(`I can tell you're trying to read something, but I'm not sure what.`); return; } else if(!data.target.is(`readable`)) { queueGMOutput(`That's not something you can read.`); return; } queueGMOutput(`You read the ` + getNameTag(data.target) + `, and find it utterly forgettable.`); } }); // Register module ECS.m(reading); // // 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`)) { queueGMOutput(p(`You give `+getNameTag(target)+` a warm hug.`)); } else { queueGMOutput(p(`You awkwardly attempt to hug the `+getNameTag(target)+`.`)); } } else { queueGMOutput(`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`)) { queueGMOutput(p(`You give `+getNameTag(target)+` a gentle pat.`)); } else { queueGMOutput(p(`You awkwardly attempt to pat the `+getNameTag(target)+`.`)); } } else { queueGMOutput(p(`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) { console.log(nodes[n]); 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 && node.enabled) { if(node.hasOwnProperty(`callback`)) { var handled = node.callback(topic, this); if(!node.hasOwnProperty(`continue`) || !node.continue) { return handled; } } var classes = []; if(typeof this.character.extraTags != `undefined`) { classes = this.character.extraTags; } if(node.response) { queueCharacterOutput(player, p(node.prompt)); queueOutput(p(getSpeechTag(this.character)+node.response), ((node.end) ? 100 : `auto`), {'classes':classes}); } node.visited = true; if(node.hasOwnProperty(`after`)) { node.after(topic, this); } 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]; }, 'enableNode':function(key) { this.findNode(key).enabled = true; }, 'disableNode':function(key) { this.findNode(key).enabled = false; }, 'getRoot':function(){ return this.findNode(this.rootNode); }, 'getCurrentNodes':function(){ if(this.currentNode == null) { return []; } var list = []; for(var n in this.currentNode.nodes) { var node = this.findNode(this.currentNode.nodes[n]); if(node.enabled) { list.push(node); } } return list; }, 'getSelectedNode':function(command){ var nodes = this.getCurrentNodes(); for(var n in nodes) { var prompt = nodes[n].prompt.toLowerCase().replace(`?`,``); if(nodes[n].key.toLowerCase() == command.toLowerCase() || prompt == 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, 'enabled':true, '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}}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); // // Magic Module // var magic = new Module(`Magic`, { init: function () { } }); // Spell component magic.c(`spell`, { 'dependencies': [`thing`], 'scope': `global`, 'cast': function(action) { } }); // Cast verb magic.a(`cast`, { 'aliases': [`cast`], 'modifiers': [`spell`], 'overflow': true, 'callback': function (data) { var target = data.nouns[0]; if (player.class != `wizard`) { queueGMOutput(`What do you think you are, some kind of wizard?`); } else if (target instanceof Entity && target.hasComponent(`spell`)) { return target.cast(data); } else { return queueGMOutput(`You don't know that spell.
`); } } }); magic.a(`undo`, { 'aliases': [`undo`], 'callback': function (data) { queueGMOutput(`Even the mightiest wizards would have difficulty turning back time.`); return true; } }); // Register module ECS.m(magic); // // Module Template // // // Core stuff // var Sound = { baseUrl: `http://stupidrpg.com/1.0.0/`, musicEnabled: false, captionsEnabled: true, activeSoundscape: null, activeMusic: null, activeCaptions: [], paused: false, player: null, tracks: {}, init: function () { this.player = document.getElementById(`audio-music`); }, playMusic: function (music) { if(typeof music == `string`) { music = this.tracks[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(music.file == null) { this.stopMusic(); return; } if (!this.paused) { $(`#audio-music source`).attr(`src`, this.baseUrl + `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.currentTime = music.loopSeek; Sound.player.play(); } else { Sound.activeMusic = null; Sound.activeCaptions = []; } }; //$(`#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)); } } }, 'stopMusic': function () { this.player.pause(); this.activeMusic = null; this.activeCaptions = []; }, '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(); this.player.play(); }, 'registerTrack': function (track) { this.tracks[track.id] = track; } }; 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]; } Sound.registerTrack(this); }; Track.prototype = { 'id': null, 'file': null, 'title': `Unnamed Track`, 'linkedTracks': [], 'volume': 1.0, 'loop': true, 'loopSeek': 0.0, '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(self,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. {{scenery}}`, '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.` } }); // 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}}. {{scenery}}`, '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) { if(!action.modifiers.length && action.target == this && player.locationIs(`ranger-station-base`)) { action.modifiers.push(`u`); } // 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`,`device`], { 'name':`telescope`, 'nouns':[], 'spawn':`ranger-station`, 'descriptions':{ 'default':`A well-used collapsing {{tag 'telescope' classes='object scenery' command='use 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 I did, 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.`, 'help':`Try 'LOOK NORTH THROUGH TELESCOPE' or 'LOOK THROUGH TELESCOPE AT RANGER BOB'.`, }, 'canTake':function() { return false; }, 'onTakeFail':function(){ return `{{gm}}It seems to be fixed in place.
`; }, 'onAction.ACTIVATE':function(){ return new Response(NLP.RESPONSE_INSTEAD, this.descriptions.help); }, 'onAction.LOOK':function(data){ console.log(`TELESCOPE`); console.log(data); if(data.modifiers.length == 2) { var moveAction = ECS.getAction(`move`); var direction = moveAction.canonical(data.modifiers[0]); var modifiers = [`through`, `at`]; // Check for direction modifier if (moveAction.modifiers.indexOf(direction) >= 0 && data.modifiers[1] == `through`) { // check for valid direction if (this.descriptions.through.hasOwnProperty(direction)) { queueGMOutput(this.descriptions.through[direction]); return; } queueGMOutput(`You can see a combination of the two adjacent cardinal directions.`); return; } 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`]; } queueGMOutput(`Like normal, but way bigger and probably with more dead skin cells than you expected on it.`); return; } queueGMOutput(`I'm not sure what you're trying to do with the telescope.`); return; } queueGMOutput(p(this.descriptions.default)); return; } }); ECS.e(`telescope-cabin`, [`scenery`], { 'name':`ramshackle cabin`, 'nouns':[`cabin`,`ramshackle cabin`], 'spawn':`ranger-station`, 'descriptions':{ 'default':`Hard to make out with the naked eye.`, 'telescope':`A ramshackle cabin. It looks vaguely ominous, as if it holds plot significance not yet revealed.` }, 'listInRoomDescription':false }); // 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. Been on the hunt for some litterbugs. Take a gander if you like.`, 'nodes':[`litterbugs`,`root`] }, { 'id':`litterbugs`, 'prompt':`Litterbugs?`, 'callback':function(topic, conversation) { queueCharacterOutput(`ranger`, `Local kids, I reckon. Leavin' trash all around the woods. I can't leave my post up here often enough to collect it all. You look like an adventuring type; if you were to do the cleanup for me and bring the evidence back, mayhaps I have something that would interest you. They like messing with the birds and foolin around at the hot springs.`); ECS.getModule(`Quests`).quests[`litterbugs`].onStart(); return true; }, '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.`); } }); understand(`rule for giving trash to ranger bob`) .in(`ranger-station`) .verb(`give`) .attribute(`target`,`is`,`litter`) .attribute(`nouns`,`containsEntity`,`ranger`) .do(function(self,action){ queueCharacterOutput(`ranger`, `You just hang on to that for now. There's more out there; I can feel it.`); action.mode = Rulebook.ACTION_CANCEL; }) .start(); ECS.e(`gate-key`, [], { 'name':`gate key`, 'spawn':`ranger-bob`, 'nouns':[`key`,`gate key`], 'descriptions':{ 'default':`A simple key, most likely to a gate.`, 'short':`A plain key.`, }, }); // 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.` }, 'onAction.LOOK.IN':function(){ return NLP.parse(`look in hole`); } }); // 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(self,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{{#xif "ECS.getEntity('jane-and-jack').locationIs('dim-clearing')"}}; to the east, voices muffled by the thick forest air{{/xif}}.`, 'short':`A muddy junction.`, } }); localScenery([`thick bushes`,`bushes`], `Bushy.`); // 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`}, '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. {{#xif "ECS.getData('stage')!='prologue'"}}To the {{s}}, a wooden bridge crosses the creek. You're not sure how you didn't notice it earlier.{{/xif}} {{scenery}}`, 'short':`Some puddles that are warmer than usual.` }, 'onLeave':[function(args){ if(args.direction == `d` && !player.checkProgress(`busybody`)) { queueGMOutput(`Some supernatural force prevents you from walking down the steps.`); return true; } return false; }] }); localScenery([`dense blanket of fog`,`blanket of fog`,`fog`], `Attempting to use the fog as an actual blanket would likely result in hypothermia. Freezing to death next to a hot spring, while ironic, doesn't look good on a tombstone.`); localScenery([`steam`], `Sort of an anti-fog, when you really think about it.`); localScenery([`small creek`,`creek`], `That water has places to be and people to see.`); localScenery([`weathered stone steps`,`stone steps`,`steps`], `The edges of the steps look a bit...clawed upon.`); // 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. Just eyeballing it, you think the temperature is approximately {{target.temperature}} Kelvin.`, '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 { action.actor.parent = this; queueGMOutput(`You climb into the water. It's a pleasant `+this.temperature+` Kelvin.`); } return false; }, 'onAction.GET.OUT':function(action) { if(action.actor.parent == this) { ECS.moveEntity(action.actor, this.place); action.actor.parent = action.actor.location(); queueGMOutput(`You climb out of the water.`); } else { queueGMOutput(`You're not inside the pools at the moment.`); } return false; }, 'onTakeFail':function(){ return `{{gm}}It slips through your fingers like sand.
`; }, 'temperature':314.0, // Hot, like a shower }); understand(`rule for getting out of pools`) .book(`before`) .in([`hot-spring`,`pools`]) .text([`get out`,`out`,`exit`]) .do(function(self,action){ action.mode = Rulebook.ACTION_CANCEL; NLP.parse(`get out of pools`); }) .start(); understand(`rule for raising pool temperature`) .book(`after`) .in([`hot-spring`,`pools`]) .do(function(self,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(); understand(`rule for saying the magic password`) .book(`before`) .in([`hot-spring`,`pools`]) .text([`ranger bob is a busybody`,`say ranger bob is a busybody`,`say "ranger bob is a busybody"`]) .do(function(self,action){ action.mode = Rulebook.ACTION_CANCEL; if(player.checkProgress(`busybody`)) { queueGMOutput(`You've already spoken the magic password.`); } else { queueGMOutput(`You speak the magic phrase. The fog near the steps stirs suddenly, disturbed by a warm rush of air.`); player.setProgress(`busybody`); } }) .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`, 'article':`the`, 'nouns':[`wyrmling`,`the wyrmling`,`wyrm`,`dragon`], '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. It's clearly too large to make it up the stairs, meaning it was most likely brought here at a younger age.`, '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)); if(player.locationIs(`lair`)) { if(this.temperature < 400) { queueGMOutput(`The wyrmling increases noticeably in temperature. It's at least a solid `+this.temperature+`K now.`); } else { queueGMOutput(`The wyrmling seems to have reached a peak temperature of `+this.temperature+`K. It's quite uncomfortable to stand near.`); } } } } }, '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(action){ if(this.hp > 0){ this.angerTicks += 2; queueOutput(`{{gm}}The wyrmling grumbles softly, annoyed at your display of affection, but not quite enough to get up and kill you. A wave of heat radiates from its scales.
`); } else { queueOutput(`{{gm}}Further hugging proves fruitless.
`); } action.mode = Action.ACTION_CANCEL; 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. Melting the ice cube might be a good course of action.`, 'short':`A magic piece of ice with something in it.`, }, 'canTake':function(){return true;}, 'temperature':265.0, 'onTick':function(){ if((player.locationIs(`hot-spring`) || player.locationIs(`pools`)) && 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. The key has a small crescent moon on it. There's probably a similarly-marked door somewhere.`); 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. Maybe if the water were warmer something would happen.`); } } } }); 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 a small crescent moon.`, '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.`)); } 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() && (player.hp > 0 || player.hp == null)) { // 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. Attacking with a weapon might be useful.
`); } } } 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 false; } return true; }, 'onAction.TALK': function(action) { queueGMOutput(p(`You chat with the troglodyte for a bit, and while it doesn't seem to understand a thing you say, you think this might be the beginning of a beautiful friendship.`)); } }); 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,`); return true; } }); // 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'}} behind an {{tag 'iron gate' command='look at iron gate'}}, 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`,`ivy`], '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`, `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.
`; }, 'onAction.TALK':function(){ queueGMOutput(`She seems preoccupied and doesn't respond.`); } }); 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.
`; }, 'onAction.TALK':function(){ queueGMOutput(`He seems preoccupied and doesn't respond.`); } }); ECS.e(`jane-and-jack`, [`living`,`scenery`], { 'name':`Jane and Jack`, 'nouns':[`jane and jack`,`jack and jane`,`twins`,`siblings`], 'spawn':`dim-clearing`, 'descriptions':{ 'default':`A pair of siblings who seem unable to act their age.` }, 'listInRoomDescription':false, 'move':function(location){ ECS.moveEntity(`jack`, location); ECS.moveEntity(`jane`, location); ECS.moveEntity(this, location); }, 'onAction.TALK':function(){ queueGMOutput(`They seem preoccupied and don't respond.`); } }); 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':`gate-key`, }); // Bridge ECS.e(`bridge`, [`place`], { 'name':`Underground Bridge`, 'region':`bridge-underground`, 'position':`underground`, // 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 {{tag 'metal wheel' command='look at 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. You catch a whiff of salt spray. 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 == 'coast' && ECS.getEntity('rusty-helmet').locationIs('the-split')"}} Something glints in a helmety way by the split.{{/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':`alcove`,'s':`winding-stair`}, '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`], }); var bridgeScenery = function(state, nouns, description) { ECS.e(`bridge-scenery-` + nouns[0], [`scenery`], { 'name':nouns[0], 'nouns':nouns, 'spawn':`bridge`, 'descriptions':{ 'default':description }, 'visibleFrom':function(location) { return location.key == `bridge` && ECS.getEntity(`wheel`)[`device-state`] == state; } }); }; bridgeScenery(`forest`, [`gentle stream`,`stream`], `A gentle forest stream originating in the nearby hot spring.`); bridgeScenery(`forest`, [`steam`], `Some spring stream steam.`); bridgeScenery(`forest`, [`clearing`,`circular clearing`], `A round clearing, somewhere around here.`); bridgeScenery(`coast`, [`delta`,`sandy delta`], `A river delta, like the one the Nile has but orders of magnitude smaller.`); bridgeScenery(`coast`, [`cliff`,`cliff face`,`towering cliff face`], `A big cliff, like the one you had to climb last week when you were trying to impress someone.`); bridgeScenery(`coast`, [`split`], `The water has worn the cliff down through eons of peer pressure.`); bridgeScenery(`coast`, [`pier`], `Looks like a good spot to meet merfolk.`); bridgeScenery(`underground`, [`liquid`,`flowing liquid`], `It's too dark to see, but it smells like the ocean.`); bridgeScenery(`void`, [`bridges`,`superposition of bridges`,`infinitely dense superposition of bridges`], `Suppose you superimposed...never mind. It's just a lot of overlapping bridges. Probably a quantum something-or-other.`); bridgeScenery(`void`, [`infinity`], `It's a lot to take in.`); // 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. It can be turned {{tag 'left' command='turn wheel left'}} or {{tag 'right' command='turn wheel right'}}.`, 'scenery':`A metal wheel sits on the railing, mounted as if on a ship.` }, 'device-states':[`forest`,`underground`,`coast`,`void`], 'device-state':`underground`, '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() { incrementCounter(`moved-bridge-wheel`); queueGMOutput(`For a moment, the world revolves around you{{#first 'moved-bridge-wheel'}} just like you've always wanted{{/first}}. The bridge seems unchanged, but the scenery has shifted.`); bridge.move(wheel[`device-state`]); ECS.runCallbacks(bridge, `onEnter`); }); }, 'persist':[`device-state`], 'listInRoomDescription':false, 'canTake':function() { return false; }, }); 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`,'e':`tunnel-landing`}, 'descriptions':{ 'default':`A small alcove, meticulously carved from a stony chasm wall. The chasm to the {{s}} is spanned by a gently arched wooden bridge. The tunnel lies behind you to the {{e}}. {{scenery}}`, 'short':`A generously-named indentation.`, } }); // 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`} }); localScenery([`otherworldly tranquility`,`tranquility`,`metaphorical fog`], `Feels nice.`); localScenery([`literal fog`,`fog`], `Airborne water vapor.`); localScenery([`the shadowbeast`,`shadowbeast`], `You check, just to be sure. It's not here, not right now at least.`); ECS.e(`mushroom`, [`edible`], { 'name':`brown mushroom`, 'nouns':[`mushroom`,`a mushroom`,`brown mushroom`,`a brown mushroom`], 'spawn':`circle`, 'descriptions':{ 'default':`A small brown mushroom. Might be edible.` }, 'listInRoomDescription':false, 'onAction.TAKE':function(){ queueGMOutput(`You pluck a mushroom from the loamy forest floor. Another immediately grows in its place.`); if(this.parent == player) { queueGMOutput(`The mushroom already in your possession disintegrates.`); } ECS.moveEntity(this, player); return false; }, 'onAction.EAT':function(){ if(this.parent != player) { queueOutput(`(first taking a mushroom)`); NLP.parse(`take brown mushroom`); } queueGMOutput(`Soft and earthy. It's not bad, and you suffer no immediate ill effects. Mild drowsiness, maybe.`); ECS.moveEntity(this, ECS.getEntity(`circle`)); player.setProgress(`digesting-mushroom`); }, 'onAction.DROP':function(){ queueGMOutput(`You drop the mushroom, which disintegrates in mid-air.`); ECS.moveEntity(this, ECS.getEntity(`circle`)); } }); ECS.e(`circle-mushrooms`, [`scenery`], { 'name':`circle of mushrooms`, 'nouns':[`picturesque mushrooms`,`mushrooms`], 'spawn':`circle`, 'descriptions':{ 'default':`A circle of mushrooms about two meters across, centered in a larger circle of flowers.`, 'short':`Mushrooms in flowers.`, }, 'listInRoomDescription':true, 'onAction.EAT':function(action) { if(!ECS.getEntity(`mushroom`).parent == player) { queueOutput(`(first taking a mushroom)`); NLP.parse(`take brown mushroom`); } return NLP.parse(`eat brown mushroom`); }, 'onAction.TAKE':function(action) { return NLP.parse(`take mushroom`); } }); ECS.e(`circle-flowers`, [`thing`], { 'name':`circle of flowers`, 'nouns':[`circle of flowers`,`flowers`,`flower`], 'spawn':`circle`, 'descriptions':{ 'default':`A collection of bright pink flowers encircling a circle of brown mushrooms.`, 'short':`Flowers around mushrooms.` }, 'onAction.TAKE':function(){ queueGMOutput(`The flowers slip through your grasp. Odd.`); return false; } }); // 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 throw plastic in the ocean?`), `auto`); // Start dialogue tree for first riddle var options = {'options':shuffle([ {'text':`YEAH`,'command':`yes`,'subtext':`I have indeed come to throw plastic in the ocean`}, {'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. Maybe some other 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`} }); // 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. His helmet is conspicuously missing.`); // Bedchamber ECS.e(`bedchamber`, [`place`], { 'name':`Bedchamber`, 'region':`underground`, 'exits':{'s':`treasury`,'n':`great-hall`}, '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 back {{north}} into the great hall, 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([`thick stone doors`,`stone doors`,`doors`], `Thicc.`); 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, or maybe a snowglobe.`); localScenery([`secret door`], `I...wasn't supposed to say that out loud. Yeah, there's a secret door leading to the {{south}}. It's not even locked. You can just {{tag 'walk right through' classes='direction' command='s'}}`); // container: night stand ECS.e(`nightstand`, [`supporter`,`scenery`], { 'name':`nightstand`, 'nouns':[`nightstand`,`night stand`], 'spawn':`bedchamber`, 'listInRoomDescription':true, 'descriptions':{ 'default':`A quality piece of furniture, built to last.` } }); // container: chest of drawers // diary ECS.e(`diary`, [`readable`], { 'name':`diary`, 'nouns':[`diary`], 'spawn':`nightstand`, 'descriptions':{ 'default':`A leatherbound diary marked 'PRIVATE'.` }, 'onAction.READ':function(){ queueGMOutput(`He's quite prolific. Let's just cover the highlights. The first few hundred entries mostly seem to focus on the unnamed author boasting about his accomplishments in life. A self-described financial titan and benefactor to those less fortunate.`, `auto`); queueGMOutput(`Things start to take a turn later on. Hiring guards to protect his treasury, something about a falling-out with a longtime friend over a snowglobe. Mysteriously disappearing treasure, firing the guards, etc, etc.`, `auto`); queueOutput(`{{box '11.28.774' "Not sleeping much these days. More is gone every day. Tried moving some into my bedchamber, but something moves it right back. I'm beginning to think I was too hasty in accusing and firing the guards. This smells of something more insidious." 'diary' }}`, `auto`); queueOutput(`{{box '12.17.774' "A breakthrough! In my dreams, in deepest sleep. I saw the little people in their little village. They must be the thieves! I woke up before I could catch them." 'diary' }}`, `auto`); queueOutput(`{{box '12.30.774' "I understand now. It was the snowglobe all along. Not an insult, oh no. A cursed artifact, meant to drive me mad." 'diary' }}`, `auto`); queueOutput(`{{box '12.31.774' "I've permanently redirected my efforts into making my strongest wine yet for the ultimate sleeping draught." 'diary' }}`, `auto`); queueOutput(`{{box '3.12.775' "I've made five hundred attempts now, but the answer lay behind me. I wasted nearly two hundred batches since the optimal product. Meaning, if you were at number 500, and counted backwards almost-but-not-quite 200 times, you'd reach the number of the successful batch. A swig of that, a bite of one of the local mushrooms, and it's checkmate for you, Mr. Snowglobe." 'diary' }}`, `auto`); queueGMOutput(`That's the final entry, but you notice a few pages missing here and there. The binding is in very poor condition.`); } }); // 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`,`goblins`], '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.`, } }); ECS.e(`goblin-pit-leader`, [`living`,`scenery`], { 'name':`Goblin Leader`, 'nouns':[`goblin leader`,`leader`], 'spawn':`goblin-pit`, 'descriptions':{ 'default':`The leader is shorter but stockier than the other goblins in the pit. She waves a gnarled stick when she speaks.`, 'scenery':`` }, 'conversation':new Conversation([ { 'id':`root`, 'key':``, 'callback':function(topic, conversation){ if(!conversation.prevNode) { queueCharacterOutput(`goblin-pit-leader`,`You!`); } else { queueCharacterOutput(`goblin-pit-leader`,`What now?!`); } return true; }, 'nodes':[`toll`] }, { 'id':`toll`, 'prompt':`What's the toll?`, 'response':`Toll! Goblin pit toll! Give us something nice, or when we get out of here you'll be sorry!`, 'nodes':[`nice`,`root`] }, { 'id':`nice`, 'prompt':`Something nice?`, 'response':`Nice thing! Shiny! Red maybe!`, 'forward':`root` } ]) }); 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(`A shrill voice calls out to you from the pit.`); }, 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(`ruby`, `goblin-pit-leader`); ECS.moveEntity(`magic-ice-cube`, player); ECS.getEntity(`goblin-pit-leader`)[`onAction.TALK`] = function() { queueGMOutput(`The goblin leader is too preoccupied with planning what sounds like a vaguely apocalyptic event to talk to you.`); return false; }; 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(); understand(`rule for giving item to the goblin leader`) .verb(`give`) .in(`goblin-pit`) .attribute(`nouns`,`containsEntity`,`goblin-pit-leader`) .do(function(self,action){ action.mode = Rulebook.ACTION_CANCEL; NLP.parse(`drop ` + action.target.name); }) .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, filled with identical bottles. You can head back to the wine cellar to the {{north}}. {{scenery}}`, 'short':`Something to make reading thoroughly enjoyable.`, } }); ECS.e(`good-wine`, [`edible`], { 'name':`good wine bottle`, 'nouns':[`wine`,`wine bottle`,`good wine bottle`,`good wine`], 'spawn':`good-wine-cellar`, 'descriptions':{ 'default':`A bottle labeled 'Good Wine', dated 1.12.775. The 'O's in 'Good Wine' are drawn as sleepy eyelids.` }, 'onAction.EAT':function(){ queueGMOutput(`You take a sip of the wine. It's perfectly chilled and well-balanced. You feel yourself grow pleasantly drowsy.`); player.setProgress(`digesting-wine`); } }); // Great Hall ECS.e(`great-hall`, [`place`], { 'name':`Great Hall`, 'region':`underground`, 'exits':{'u':`winding-stair`,'n':`winding-stair`,'w':`library`,'e':`armory`,'s':`bedchamber`}, '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. {{#xif "ECS.getEntity('storybook').locationIs('library')"}}One {{nametag 'storybook' print='story book'}} in particular catches your attention.{{/xif}} Like most rooms, this one has a floor, ceiling, some walls and a couple doorways. To the {{e}} 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.`, } }); localScenery([`volumes`,`damaged volumes`,`damaged books`,`books`], `Not worth the time or the possible diseases.`); // scenery: bookshelves ECS.e(`bookshelves`, [`scenery`], { 'name':`bookshelves`, 'spawn':`library`, 'nouns':[`towering bookshelves`,`bookshelves`], 'descriptions':{ 'default':`High shelves packed end to end with damaged books. It was probably a mistake to build a library in a damp cavern system.`, 'short':`Shelves.`, } }); // 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; } }); ECS.e(`storybook`, [], { 'name':`story book`, 'spawn':`library`, 'nouns':[`book`,`storybook`,`story book`,`the space creature`,`space creature`], 'descriptions':{ 'default':`A beautifully illustrated children's book about a mysterious space creature. It's entitled 'THE SPACE CREATURE'.` }, 'listInRoomDescription':false, }); understand(`rule for reading storybook`) .regex(/read (book|storybook|story book|space creature|the space creature)/i) .do(function(self,action) { read_storybook(); action.mode = Rulebook.ACTION_CANCEL; }) .start(); function read_storybook() { var f = $(`