// 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}}<p>You pick up the ` + this.name + `.</p>`);
},
'onTakeFail': function () {
return `{{gm}}<p>You are unable to take the ` + this.name + `.</p>`;
},
'onDropSuccess': function () {
return `{{gm}}<p>You drop the ` + this.name + `.</p>`;
},
'onDropFail': function () {
return `{{gm}}<p>You can't drop the ` + this.name + `, as you are not holding it.</p>`;
},
'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 = `<header>` + this.getLocationName() + `</header><div class='p'>` + this.description(`default`) + objects + `</div>`;
if (typeof returnOutput === `undefined` || returnOutput === false) {
queueOutput(output);
} else {
return output;
}
},
'persistActive': false, // by default, don't save places
});
// Living Component (living things that can act)
coreModule.c(`living`, {
'dependencies': [`thing`],
'hp': 1,
'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(`living`, 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();
}
}
});
},
'onTick': function() {
},
'onHit': function (weapon) {
var baseDmg = 1;
if (weapon != null && weapon.hasOwnProperty(`damage`)) {
baseDmg = weapon.damage;
}
if (this.hp > 0) {
this.hp = Math.max(0, this.hp - baseDmg);
if (this.hp > 0) {
queueOutput(`You land a solid blow.`);
} else {
queueOutput(`You strike a fatal blow.`);
}
} else {
queueOutput(`You whale on the corpse for a bit.`);
}
},
'onDeath': function () {
},
'canTake': function () {
return false;
},
'onList': function () {
if (this.hp <= 0) {
return ` (dead)`;
}
return ``;
},
'prefix': function() {
return `<span class='npc `+this.key+`'>`+this.name+`:</span>`;
}
});
// Living System
coreModule.s({
'name': `living`,
'priority': 10,
'components': [`living`],
'onTick': function (entities) {
// Loop through entities
for (var e in entities) {
entities[e].onTick(this.name);
}
}
});
// Fixed component (cannot be moved)
coreModule.c(`fixed`, {
'dependencies': [`thing`],
'canTake': function () {
return false;
}
});
// Scenery component (semi-interactive static objects)
coreModule.c(`scenery`, {
'dependencies': [`fixed`],
'descriptions': {'scenery': ``}, // Scenery description for use in location descriptions
'listInRoomDescription': false,
'onInit': function () {
// Register scenery description helper
// Describes scenery in the current location
// Usage: {{scenery}}
Handlebars.registerHelper(`scenery`, function (context) {
var output = [];
// Get current location
var location = player.place;
// Get scenery objects for location and region
var locationScenery = location.findChildren(`scenery`);
var regionScenery = (location.region) ? location.getRegion().findChildren(`scenery`) : [];
var scenery = $.extend({},
locationScenery,
regionScenery
);
console.log(`Scenery!`);
console.log(scenery);
for (var s in scenery) {
if (scenery[s].descriptions != null && scenery[s].descriptions.hasOwnProperty(`scenery`) && scenery[s].descriptions.scenery) {
output.push(scenery[s].descriptions[`scenery`]);
// Increment 'seen object' counter
incrementCounter(`seen-` + scenery[s].key);
/*var onList = "";
if(typeof scenery[s].onList != 'undefined') {
onList = scenery[s].onList();
}
output.push(scenery[s].descriptions['scenery'] + onList);*/
}
}
if (output.length == 0) {
return new Handlebars.SafeString(` `);
}
return new Handlebars.SafeString(parse(output.join(` `)));
});
},
'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);
}
}
}],
'persistActive': false, // by default, don't save scenery
});
// Openable Component
coreModule.c(`openable`, {
'isOpen': true,
'onOpen': [],
'onClose': [],
});
// Open action
coreModule.a(`open`, {
'aliases': [`open`],
'callback': function (action) {
var target = action.target;
if (target != null && target.hasComponent(`openable`)) {
if (typeof target.onOpen == `object` && target.onOpen.length > 0) {
ECS.runCallbacks(target, `onOpen`, action.modifiers);
return;
} else if (typeof target.onOpen == `string`) {
action.output += target.onOpen;
} else {
target.isOpen = true;
action.output += `{{gm}}<p>You open the ` + target.name + `.</p>`;
}
} else if (target == null) {
action.output += `<p>I can tell you want to open something, but you'll have to be more specific.</p>`;
} else {
action.output += `<p>That's not the sort of thing that opens.</p>`;
}
}
});
// 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 `<p>You close the ` + target.name + `.</p>`;
}
} else if (target == null) {
return `<p>I can tell you want to close something, but you'll have to be more specific.</p>`;
} else {
return `<p>That's not the sort of thing that closes.</p>`;
}
}
});
// 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' '`
+ `<b>Design, Code, & Writing:</b> Steven Richards<br>`
+ `<b>Music, Soundscapes & Captions:</b> Jenn Richards<br>`
+ `<b>Libraries & Tools:</b><br>`
+ `<a href="
http://html5boilerplate.com" target="_blank">HTML5 Boilerplate</a><br>`
+ `<a href="
http://handlebarsjs.com" target="_blank">HandlebarsJS</a><br>`
+ `<a href="
http://jquery.com" target="_blank">jQuery</a><br>`
+ `<a href="
https://github.com/eligrey/FileSaver.js" target="_blank">FileSaver.js</a><br>`
+ `<b>Other Assets:</b> Forest sounds by <a href="
https://freesound.org/people/reinsamba/" target="_blank">reinsamba/</a> (CC BY 3.0 US)`
+ `' 'credits'}}`;
}
});
// Help action
coreModule.a(`help`, {
'aliases': [`help`, `?`, `wtf`],
'callback': function (action) {
ECS.tick = false;
if(action.nouns.length) {
if(action.nouns[0].descriptions.hasOwnProperty(`help`)) {
return `<header>`+action.nouns[0].name+`</header>`
+ `<p>`+action.nouns[0].descriptions.help+`</p>`;
}
return `No help available for '`+action.nouns[0].name+`'.`;
}
return `<header>Getting Started</header>`
+ `<p>Try LOOK AT BIRD or GO EAST for starters.<br>Other useful commands include EAT and QUIT.</p>`
+ `<header>Alternative Commands & Shorthand</header>`
+ `<p>Most commands have shorthand forms or aliases:<br>EXAMINE, LOOK, or X instead of LOOK AT<br>`
+ `WALK, RUN, or CRAWL instead of GO, as well as cardinal directions like NORTH</p>`;
}
});
// Quit action
coreModule.a(`quit`, {
'aliases': [`quit`, `q`],
'callback': function () {
ECS.tick = false;
return `<p>GAME OVER</p>`;
}
});
// 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(`<i>Game Saved.</i>`);
*/
}
});
// Load action
coreModule.a(`load`, {
'aliases': [`load`, `quickload`],
'callback': function () {
if(ECS.data.hasOwnProperty(`save`)) {
ECS.tick = false;
ECS.load(ECS.getData(`save`));
queueOutput(`<i>Game Loaded.</i>`);
} 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 += `<div class='p'>` + output + `</div>`;
// Increment 'seen object' counter
incrementCounter(`seen-` + action.nouns[0].key);
} else {
action.output += action.actor.location().describe(true);
}
},
'onBadInput': function () {
return `<p>You don't see that here.</p>`;
}
});
// 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 `<p>This is embarrassing, but something seems to be broken. I can't find the location in that direction.</p>`;
}
return `<p>You can't go that way (`+direction+`).</p>`;
},
'onBadInput': function (string) {
return `<p>I understand you want to go somewhere, but I don't know how to go (` + string + `).</p>`;
}
};
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 `<p>I understand you want to TAKE something (` + string + `), but I don't understand what.</p>`;
},
});
// 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}}<p>You drop the ` + nouns[0].name + `.</p>`);
target.onDropSuccess();
} else {
return target.onDropFail();
}
}
return;
},
'onBadInput': function (string) {
return `<p>I understand you want to DROP something (` + string + `), but I don't understand what.</p>`;
},
});
// 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': `<i>nothing</i>`, 'command': `do nothing`});
}
var menu = parse(`{{menu items}}`, {'items': items});
queueOutput(`<p><b>` + actor.name + `, the ` + actor.gender.toUpperCase() + ` ` + actor.race.toUpperCase() + ` ` + actor.class.toUpperCase() + `</p>`);
queueOutput(`<p>You have ` + actor.children.length + ` item(s):</p>` + 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 `<p>Gotta specify a theme.</p>`;
}
loadTheme(data.modifiers[0]);
},
'onBadInput': function () {
return `<p>That isn't a valid theme.</p>`;
}
});
// 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}}<p>{{place.descriptions.dark}}</p>`);
}
return isLit;
});
// Add light restrictions on LOOK and TAKE actions
var f = function (args) {
if (!ECS.runFilters(player.place, `isLit`)) {
queueOutput(`{{gm}}<p>{{place.descriptions.dark}}</p>`);
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}}<p>You can't see any such thing.</p>`);
return false;
}
queueOutput(`<p>(first opening the ` + getNameTag(obj.parent) + `)</p>`);
}
}
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}}<div class='p'>Inside you can see: {{inventory objects}}</div>`, {'objects': data.nouns[0].inventory});
} else if (target == null) {
data.output += `{{gm}}<p>I can tell you want to look inside something, but you'll have to be more specific.</p>`;
} else {
data.output += `{{gm}}<p>That's not something you can look inside.</p>`;
}
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(`<p>(first taking the ` + getNameTag(obj) + `)</p>`);
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(`<p>You lock the ` + target.name + `.</p>`);
}
} else if (target == null) {
return queueGMOutput(`<p>I can tell you want to lock something, but you'll have to be more specific.</p>`);
} else {
return queueGMOutput(`<p>That's not the sort of thing that locks.</p>`);
}
}
});
// 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(`<p>You unlock the ` + target.name + ` using the `+ ECS.getEntity(k).name +`.</p>`);
} else {
queueGMOutput(`<p>You'll need the right key to do that.</p>`);
}
}
} else if (target == null) {
return queueGMOutput(`<p>I can tell you want to unlock something, but you'll have to be more specific.</p>`);
} else {
return queueGMOutput(`<p>That's not the sort of thing that unlocks.</p>`);
}
}
});
// 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 `<span class='npc `+classes+`'>`+name+`:</span> `;
}
// 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(` <small>(speaking to ` + this.character.name + `)</small> `);
},
'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': `<small>`+(nodes[n].visited ? `(Visited)` : ``)+`</small>`});
}
menu.push({'text':`<i>(EXIT)</i>`,'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}}<p>There is no response.</p>`);
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}}<div class='p'>You talk about `+getNameTag(nouns[0])+`, to no one in particular.</div>`;
return;
}
// SAY HI / HELLO
if(modifiers.length == 1 && [`hi`,`hello`].indexOf(modifiers[0]) >= 0) {
data.output += `{{gm}}` + p(`You'll have to specify who or what you're talking to.`);
}
if(nouns.length > 1) {
topic = nouns[1];
if(topic instanceof Entity) {
if(topic.nouns.length > 0) {
topic = topic.nouns[0];
} else {
topic = topic.key;
}
}
}
if(target != null)
{
if(target instanceof Entity && target.hasComponent(`living`))
{
queueGMOutput(`You speak to `+target.name+`.`);
// Check for conversation node
if(target.conversation != null) {
console.log(`Talking to ` + target.name + ` about ` + topic);
target.conversation.start(topic);
}
} else if(target instanceof Entity) {
data.output += `{{gm}}<div class='p'>You awkwardly attempt to talk to the `+getNameTag(target)+`.</div>`;
} else {
// Special case: player might be trying to do something like "say sorry to spider"
try {
var segments = target.split(` `);
var splitIndex = segments.indexOf(`to`);
if(data.modifiers.length == 0 && splitIndex >= 0) {
var phrase = segments.slice(0, splitIndex).join(` `);
target = segments.slice(splitIndex + 1).join(` `);
if(phrase && target) {
var targetEntity = ECS.findEntityByName(target, `local`);
if(targetEntity) {
queueCharacterOutput(player, `(to `+targetEntity.name+`): ` + phrase);
return;
} else {
queueGMOutput(`I'm not sure who you're trying to talk to.`);
return;
}
}
}
}
catch(e){
// Ignore and fall back to generic response
}
data.output += `{{gm}}<div class='p'>You say '`+target+`' to no one in particular.</div>`;
}
}
}
});
// Give (item) to (creature)
social.a(`give`, {
'aliases':[`give`],
'modifiers':[`to`],
'callback':function(data){
if(data.nouns.length < 2) {
data.output = `{{gm}}` + p(`I can tell you want to give something, but I'm not sure to whom.`);
return;
}
queueGMOutput(getNameTag(data.nouns[1]) + ` doesn't seem interested in the ` + getNameTag(data.nouns[0]) + `.`);
}
});
// Register module
ECS.m(social);
//
// Clothing & Armor Module
//
var clothing = new Module(`Clothing`, {
init: function () {
// Extend existing components
var living = ECS.getComponent(`living`);
living.clothing = {
'head': null,
'body': null,
'legs': null,
'feet': null,
'neck': null,
'back': null
};
living.wearClothing = function (c) {
if (c.parent != null) {
c.parent.removeChild(this);
}
this.addChild(c);
this.clothing[c.slot] = c;
c.isWorn = true;
};
// Add possession restrictions on WEAR action
var f = function (args) {
if (args.nouns.length > 0) {
var obj = args.nouns[0];
if (obj.parent != player) {
queueOutput(p(`(first taking the ` + getNameTag(obj) + `)`));
NLP.parse(`TAKE ` + obj.name);
return (obj.parent == player);
}
}
return true;
};
ECS.getAction(`wear`).filters.push(f);
// Add wear restrictions on DROP action
f = function (args) {
if (args.nouns.length > 0) {
var obj = args.nouns[0];
if (obj.isWorn) {
queueOutput(p(`(first taking off the ` + getNameTag(obj) + `)`));
NLP.parse(`TAKE OFF ` + obj.name);
return (!obj.isWorn);
}
}
return true;
};
ECS.getAction(`drop`).filters.push(f);
// Add ECS data
// Add entities
}
});
// Add components, actions, etc
clothing.c(`wearable`, {
'dependencies': [`thing`],
'armor': 0, // damage reduction value
'slot': null, // location on the body
'exclusive': true, // exclusive items prevent other items in the same slot
'isWorn': false, // whether the item is currently being worn
'onAction.WEAR': null, // triggered when the player attempts to put on the item
'onAction.REMOVE': null,// triggered when the player attempts to remove the item
'onList': function () {
if (this.isWorn) {
return ` (on ` + this.slot + `)`;
}
return ``;
}
});
// WEAR action
clothing.a(`wear`, {
'aliases': [`wear`, `put on`],
'callback': function (data) {
var target = data.nouns[0];
if (target != null && target.hasComponent(`wearable`)) {
// Check slot
console.log(`TARGET CLOTHING:`);
console.log(target);
var slot = target.slot;
var playerSlot = player.clothing[slot];
if (target.isWorn) {
queueGMOutput(`You're already wearing that.`);
} else if (playerSlot == null || (!playerSlot.exclusive && !target.exclusive)) {
player.clothing[slot] = target;
target.isWorn = true;
queueGMOutput(p(`You put on the ` + target.name + `.`));
} else {
queueGMOutput(`You can't wear that while you're wearing the ` + getNameTag(playerSlot) + `. You'll have to remove it first.`);
}
return true;
} else if (target == null) {
return `<p>I can tell you want to wear something, but you'll have to be more specific.</p>`;
} else {
return `<p>That's not the sort of thing that you wear.</p>`;
}
}
});
// 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 `<p>I can tell you want to remove something, but you'll have to be more specific.</p>`;
} else {
return `<p>That's not the sort of thing that you take off.</p>`;
}
}
});
// 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(`<p>You don't know that spell.</p>`);
}
}
});
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 = $(`<span>♫ ` + this.text + ` ♫</span>`);
$(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': ['<b>DRUMS CRASHING</b>'] // 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 `<p>Music is <b>` + (Sound.musicEnabled ? `ON` : `OFF`) + `</b>`;
} else if (data.modifiers[0] == `on`) {
// Turn on music
Sound.resumeMusic();
return `<p>Music is now: <b>ON</b></p>`;
} else if (data.modifiers[0] == `off`) {
// Turn off music
Sound.pauseMusic();
return `<p>Music is now: <b>OFF</b></p>`;
}
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(`<b>{{name}}</b> ({{status}})<br><i>{{description}}</i><br>`, quests[q]);
}
}
queueOutput(`Your Quests:<br>` + questText);
}
});
// Grind verb
Quests.a(`grind`, {
'aliases': [`grind`,`powerlevel`],
'callback': function () {
queueGMOutput(p(`You grind for XP for a while. It's not very satisfying, but you wouldn't want to be underleveled when you reach the final boss battle.`));
}
});
// Quests System
Quests.s({
'name': `quests`,
'priority': 15,
'components': [],
'onTick': function () {
// Loop through quests
$.each(Quests.getOpenQuests(), function (k, q) {
q.onTick();
});
}
});
// Quest constructor
// Initializes objectives and defines behaviors for game ticks and end-of-quest
var Quest = function (name, data) {
this.objectives = {};
data.key = name.toLowerCase().replace(/ /g, `-`);
data.name = name;
ECS.setOptions(this, data);
for (var o in this.objectives) {
this.objectives[o].quest = this;
}
};
Quest.prototype = {
'key': `unnamed-quest`,
'name': `Unnamed Quest`,
'description': `Some kind of quest`,
'_status': `inactive`, // inactive, active, done, complete
'objectives': [],
'status': function () {
var out = `???`;
switch (this._status) {
case `active`:
out = `Active`;
break;
case `done`:
out = `Done`;
break;
case `complete`:
out = `100%`;
break;
}
return out;
},
'onStart': function () {
if (this._status == `inactive`) {
this._status = `active`;
ECS.getModule(`Quests`).openQuests[this.key] = this;
queueOutput(`{{box 'New Quest' '` + this.name + `' 'quest quest-begin' }}`);
}
},
'onTick': function () {
var self = this;
$.each(this.objectives, function (k, o) {
if (!o.isMet) {
if (o.checkIfMet()) {
o.isMet = true;
o.onMet(); // Custom objective resolution message
o._onMet(); // Default objective resolution message
self.onObjectiveMet();
}
}
});
},
'onDone': function () {
}, // Quest is 'done', optional objectives aside
'_onEnd': function () {
queueOutput(`{{ box 'Quest Complete!' '` + this.name + `' 'quest quest-complete'}}`);
},
'onEnd': function () {
return false; // not handled
}, // Quest is 'complete', including all optional objectives
'onObjectiveMet': function () {
var status = `complete`;
for (var o in this.objectives) {
if (!this.objectives[o].isMet && !this.objectives[o].isOptional) {
// At least one required objective is incomplete
return;
}
if (!this.objectives[o].isMet) {
// At least one optional object is incomplete
status = `done`;
}
}
if (status == `done`) {
this.onDone();
} else if (status == `complete`) {
console.log(`QUEST COMPLETED: ` + this.name);
if(!this.onEnd()) {
// No special handling, use default
this._onEnd();
}
delete Quests.openQuests[this.key];
}
this._status = status;
},
};
var QuestObjective = function (name, description, checkIfMet, options) {
this.name = name;
this.description = description;
this.checkIfMet = checkIfMet;
ECS.setOptions(this, options);
};
QuestObjective.prototype = {
'quest': null,
'isMet': false, // Whether the objective is done
'isOptional': false, // Whether the objective is optional
/**
* Check the game state to see if the objective should be marked done.
* Handled by the Quest/Module on ticks. Return true means mark done, false means no change
*/
'checkIfMet': function () {
return false;
},
'_onMet': function () {
queueOutput(`{{ box 'Objective Complete' '` + this.name + `' 'quest quest-objective-complete' }}`);
},
'onMet': function () {
}
};
// Register module
ECS.m(Quests);
// Add quest function to module prototype
Module.prototype.q = function (q) {
Quests.quests[q.key] = q;
};
//
// Sleep Module
//
var sleep = new Module(`Sleep`, {
init: function () {
// Extend existing components
// Add ECS data
// Add entities
}
});
// Sleepy Component (things that can sleep)
sleep.c(`sleepy`, {
'dependencies': [`thing`],
'asleep': false,
'onList': function () {
if (this.asleep) {
return ` (asleep)`;
}
return ``;
}
});
// Sleep action
sleep.a(`sleep`, {
'aliases':[`sleep`,`zzz`,`snooze`,`nap`,`fall asleep`,`go to sleep`,`take a nap`,`catch some sleep`],
'modifiers':[`in`,`on`],
'callback':function(action){
if(action.modifiers.length && action.modifiers[0].is(`in`,`on`)) {
// Trying to sleep in/on an object
if(action.target) {
if(action.target.hasComponent(`supporter`)) {
queueGMOutput(`You take a brief nap on the `+getNameTag(action.target)+`.`);
} else {
queueGMOutput(`That doesn't seem like a comfortable way to sleep.`);
}
} else {
queueGMOutput(`I can tell you're trying to sleep in or on something, but I don't know what.`);
}
return;
}
queueGMOutput(`You take a brief nap and wake up slightly more refreshed.`);
}
});
// Register module
ECS.m(sleep);
//
// Module Template
//
var cheat = new Module(`Cheat`, {
init: function () {
// Extend existing components
// Add ECS data
// Add entities
}
});
// Add components, actions, etc
cheat.a(`teleport`, {
'aliases': [`tp`, `teleport`],
'modifiers': [`to`],
'overflow': true,
'callback': function (data) {
var target = data.nouns[0];
var location = ECS.findEntityByName(target);
if (location) {
data.output += `{{gm}}<div class='p'>OK</div>`;
ECS.moveEntity(player, location);
incrementCounter(`visited-` + location.key);
location.visited++;
ECS.runCallbacks(location, `onEnter`);
} else {
data.output += `{{gm}}<div class='p'>I don't know where that is.</div>`;
}
}
});
// Register module
ECS.m(cheat);
// Include campaign modules
//
// BlackBox Module
//
var blackbox = new Module(`BlackBox`, {
init: function() {
var silenceTrack = new Track(`silence`,null,{'captions':[]});
// Forest Track
var forestTrack = new Track(`forest`,`Forest.ogg`,{
'linkedTracks':[`bridge-forest`],
'captions':[
new Caption(5000,`Pleasant piano melody`),
new Caption(25000,`Birds chirping`),
new Caption(65000,`Piano solo`),
new Caption(127000,`Birds continue`),
new Caption(180000,`Ivories are tickled`),
new Caption(230000,`Incessant chirping`),
new Caption(310000,`Melody resolves`)
]
});
// Coast track
var coastTrack = new Track(`coast`,`Coast.ogg`,{
'linkedTracks':[`bridge-coast`],
'captions':[
new Caption(1000,`Woodwind intro gilded by harp`),
new Caption(9000,`A pleasant <i>andante</i> melody settles in`),
new Caption(32000,`Some pizzicato strings are thrown into the mix`),
new Caption(54000,`A somewhat darker countermelody plays, featuring french horn and strings`),
new Caption(77000,`Oh, there's that main theme again`),
new Caption(103000,`And the countermelody... again?`),
new Caption(109000,`At least it's different instruments this time`),
new Caption(120000,`A brass sea shanty begins`),
new Caption(120800,`Get it.. <b>sea</b> shanty? Yeah.`),
new Caption(174000,`To be honest it's getting a bit emo at this point (ʃ_⌣̀ )`),
new Caption(187000,`This harp bit is kind of cool. Kinda mysterious, I guess`),
new Caption(193000,`And we're back to sea shanty. Shiver me timbers`),
new Caption(208000,`This is what I'm now calling "Melody Prime". Don't worry about it`),
new Caption(220000,`Ooh, this little harp interlude is nice. I bet someone worked really hard writing it. You can really tell from the <i>gravitas</i>`),
new Caption(275000,`Aaaaand we've looped back around to the beginning`),
new Caption(315000,`Ooh, I love this part. La da, da dee dum da la dee daaa...`),
new Caption(390000,`Did you know that squid have better hearing than octopuses? It's true`),
new Caption(415000,`Octopi? Octo... Oh, whatever`),
new Caption(458000,`They're both cephalopods. Anyway, this music is pretty rad`),
new Caption(500000,`Very "coast-like". There's that harp again`)
]
});
// Underground track
var undergroundTrack = new Track(`underground`,`Underground.ogg`,{
'linkedTracks':[`bridge-underground`],
'captions':[
new Caption(7000,`Wow, there is a lot of deep, dark stuff happening in this song`),
new Caption(14000,`Let's see, we've got some french horn, some timpani, maybe a contrabassoon?`),
new Caption(30000,`Even the tonality is weird. What is that, 12-tone?`),
new Caption(53000,`There's a slightly faster part here`),
new Caption(80000,`To be honest this much darkness isn't my thing`),
new Caption(120000,`It's rather dreary`)
]
});
// Bridge tracks
var bridgeForestTrack = new Track(`bridge-forest`,`Forest Bridge.ogg`,{
'linkedTracks':[`forest`],
'captions':[
new Caption(3000,`There's a weird noise over the music`),
new Caption(12000,`It kind of sounds like a portal to an unforgiving void has opened up and is haunting our senses right now. But it also sounds like a forest.`),
new Caption(46000,`The birds don't seem to mind that weird noise behind the piano music`),
new Caption(134000,`Have you ever seen <i>Interstellar</i>? Its like that. But a sound. But also piano. And birds.`),
new Caption(214000,`You know, at first I found it annoying but now it's kinda grown on me. It's like I don't even hear it anymore`),
new Caption(271000,`Okay, I take it back, I think I have a headache now`)
]
});
var bridgeCoastTrack = new Track(`bridge-coast`,`Coast Bridge.ogg`,{
'linkedTracks':[`coast`],
'captions':[
new Caption(2000,`There's a weird humming, like... a call of an ethereal void? Whatever that means`),
new Caption(12000,`There's also like, regular music stuff happening`),
new Caption(221000,`Oh my, that weird noise is still hanging around? Ugh.`),
new Caption(478000,`Did you... did you really sit through 8 minutes of this just to see if there were more captions?`),
new Caption(482000,`Well, I admire your dedication`)
]
});
var bridgeUndergroundTrack = new Track(`bridge-underground`,`Underground Bridge.ogg`,{
'linkedTracks':[`underground`],
'captions':[
new Caption(3000,`Oh dear, there's a weird ghostly noise. Combined with the current song it doesn't sound good <b>at all</b>`),
new Caption(53000,`It's like... if there were a weird, hissing ghost who liked twelve tone music? It's like the sound of that`),
new Caption(89000,`At least the ghost is like, staying out of the register of all of the instruments. Could be worse I suppose`)
]
});
var bridgeVoidTrack = new Track(`bridge-void`,`Void Bridge.ogg`,{
'captions':[
new Caption(1000,`<b>AAAAHHHH WHY IS THIS HAPPENING?!?!</b>`),
new Caption(7000,`It's like all the music is playing at once. And there's a horrible noise on top ಥ_ಥ`),
new Caption(28000,`It's like if vomit could be a sound. That's what's happening right now`),
new Caption(46000,`(⌣̩̩́_⌣̩̩̀)`)
]
});
// Black Box track
var blackboxTrack = new Track(`black-box`, `Black Box 3.ogg`,{
'captions':[
new Caption(1000,`This song is like if industrial, synthwave, and chillpop had a weird baby`),
new Caption(51000,`It's not unlike being really tired. But, like, a sound`)
]
});
// Air track
var airTrack = new Track(`air`, `Sky Soundscape.ogg`,{
'captions':[
new Caption(2000,`You hear wind rushing past you as you plummet through the air`),
new Caption(30000,`There's something flapping... maybe your clothes or hair? Probably`),
]
});
// Music box
var musicBoxTrack = new Track(`music-box`, `Music Box.ogg`,{
'loop':false,
'captions':[
new Caption(1000,`A sweet melody plays on a music box`),
new Caption(10000,`Somehow, it reminds you of snow`)
]
});
var musicBoxLandTrack = new Track(`music-box-land`, `Music Box Land.ogg`,{
'captions':[
new Caption(1000,`A sweet melody played on a string orchestra, with woodwinds, vibraphone, and harp`),
new Caption(10000,`Somehow, it reminds you of snow`),
new Caption(50000,`It's like if stars were music. Suuuuper romantic`),
new Caption(92000,`The music slows a little bit`),
new Caption(142000,`It's so sweet, almost romantic`)
]
});
// Canyon track
var canyonTrack = new Track(`canyon`, `Canyon.ogg`,{
'linkedTracks':[`tunnels`],
'captions':[
new Caption(1000,`A moderate tempo, quiet drumbeat`),
new Caption(10000,`Subdued bass joins`),
new Caption(21000,`Chill keyboard joins`),
new Caption(43000,`Relaxed guitar joins`),
new Caption(65000,`Back to just the drums`),
new Caption(87000,`The instruments build again`),
]
});
// Waterfall track
var waterfallTrack = new Track(`waterfall`, `Waterfall-Soundscape.ogg`,{
'captions':[
new Caption(1000,`The sound of water falling. A "waterfall"`),
]
});
// Tunnels track
var tunnelsTrack = new Track(`tunnels`, `Tunnels.ogg`,{
'linkedTracks':[`canyon`],
'captions':[
new Caption(1000,`A moderate tempo, quiet drumbeat echoes in the tunnels`),
new Caption(10000,`Subdued bass also echoes in the tunnels`),
new Caption(21000,`Chill keyboard further echoes in the tunnels`),
new Caption(43000,`Relaxed guitar joins the rest of the music in the tunnels`),
new Caption(65000,`Back to just the tunnel drums`),
new Caption(87000,`The instruments build again.... tunnels.`),
]
});
// Observation Deck Interior track
var odiTrack = new Track(`odi`, `Observation Deck Interior.ogg`,{
'captions':[
new Caption(1000,`A retro synth brings a laid back beat. It reminds you of pong`),
new Caption(10000,`A strange keyboard joins`),
new Caption(21000,`A bendy string synth joins... is that wah?`),
new Caption(43000,`It's like a robot voice`),
new Caption(65000,`Back to just the retro synth`),
new Caption(87000,`The instruments build again`),
]
});
// Forest region
ECS.e(`forest`, [`region`], {
'music': forestTrack,
'background':`#213f14`,
});
// Underground region
ECS.e(`coast`, [`region`], {
'music': coastTrack,
'background':`#14383f`,
});
// Underground region
ECS.e(`underground`, [`region`], {
'music': `underground`,
'background':`#313131`,
});
// Bridge regions
ECS.e(`bridge-forest`, [`region`], {'music': bridgeForestTrack,'background':`#213f14`});
ECS.e(`bridge-coast`, [`region`], {'music': bridgeCoastTrack,'background':`#14383f`});
ECS.e(`bridge-underground`, [`region`], {'music': bridgeUndergroundTrack,'background':`#313131`});
ECS.e(`bridge-void`, [`region`], {'music': bridgeVoidTrack,'background':`#ffffff`});
// Data sets for menus and such
// Race list
ECS.setData(`races`, [
{'text':`ELF`,'command':`elf`,'subtext':`+ KEEN SENSES, + EVASION`},
{'text':`DWARF`,'command':`dwarf`,'subtext':`+ DARKVISION`},
{'text':`CEO`,'command':`ceo`,'subtext':`+ PERSUASION`},
{'text':`HUMAN`,'command':`human`,'subtext':`+1 TO EVERYTHING`},
{'text':`PAN-DIMENSIONAL BEING`,'command':`pan-dimensional being`,'subtext':`- MATERIAL NEEDS`}
]);
// Gender list
ECS.setData(`genders`, [
{'text':`MALE`,'command':`male`},
{'text':`FEMALE`,'command':`female`},
{'text':`PREFER NOT TO ANSWER`,'command':`?`},
{'text':`OTHER`,'command':`other`}
]);
// Class list
ECS.setData(`classes`, [
{ // FIGHTER CLASS: A rough and tumble type
'text':`FIGHTER`,
'command':`fighter`,
'subtext':`+ ATTACK, + DEFENSE, + REGEN, STEEL-TOED BOOTS`,
'init':function(){
player.wearClothing(ECS.getEntity(`steel-boots`,null));
}
},
{ // WIZARD CLASS: Cosmic powers
'text':`WIZARD`,
'command':`wizard`,
'subtext':`+ MAGIC, MAGIC WAND`,
'init':function(){
player.addChild(ECS.getEntity(`magic-wand`,null));
}
},
{
'text':`HOMEMAKER`,
'command':`homemaker`,
'subtext':`+ LOOT, + CHARM, NOTEBOOK, PENCIL`,
'init':function(){
//player.addChild(ECS.getEntity('notebook',null));
//player.addChild(ECS.getEntity('pencil',null));
}
},
{
'text':`UNICORN HUNTER`,
'command':`unicorn hunter`,
'subtext':`+ UNICORN SIGHTINGS, UNICORN MUSK`,
'init':function(){
player.addChild(ECS.getEntity(`unicorn-musk`,null));
}
},
{
'text':`LIAR`,
'command':`liar`,
'subtext':`+ DECEPTION, COUNTERFEIT ROYAL SEAL`,
'init':function(){
player.addChild(ECS.getEntity(`counterfeit-seal`,null));
}
}
]);
// Rating list
ECS.setData(`ratings`, [
{'text':`1 - THE WORST RPG I'VE EVER PLAYED`,'command':`1`},
{'text':`2 - MEDIOCRE AT BEST`,'command':`2`},
{'text':`3 - STUPID BUT NOT IN A BAD WAY`,'command':`3`},
{'text':`4 - PRETTY GOOD ACTUALLY`,'command':`4`},
{'text':`5 - THOROUGHLY EXCELLENT`,'command':`5`}
]);
// Starting items (per-class)
// UNICORN HUNTER Starting Object: Unicorn Musk
ECS.e(`unicorn-musk`, [], {
'name':`vial of unicorn musk`,
'spawn':null,
'nouns':[`musk`,`vial`,`musk vial`,`vial of musk`],
'descriptions':{
'default':`A vial of priceless unicorn musk. It took you years of blood, sweat, and tears to acquire it. No sign of a unicorn yet.`,
'short':`A gross vial.`,
}
});
// LIAR Starting Object: Counterfeit Royal Seal
ECS.e(`counterfeit-seal`, [], {
'name':`counterfeit royal seal`,
'spawn':null,
'nouns':[`seal`,`royal seal`,`counterfeit seal`],
'descriptions':{
'default':`A high-quality counterfeit copy of the royal seal. Not much use now that the King has been deposed.`,
'short':`A fake seal.`,
}
});
// FIGHTER Starting Object: Steel-Toed Boots
ECS.e(`steel-boots`, [`wearable`], {
'name':`steel-toed boots`,
'article':`some`,
'spawn':null,
'nouns':[`boots`,`steel boots`],
'descriptions':{
'default':`A pair of well-worn steel-toed boots.`,
'short':`A couple boots.`,
},
'slot':`feet`
});
// WIZARD Starting Object: Magic Wand
ECS.e(`magic-wand`, [], {
'name':`magic wand`,
'spawn':null,
'nouns':[`wand`],
'descriptions':{
'default':`A finely-crafted magic wand.`,
'short':`A magic stick.`,
},
'spells':{
'undo':{}
}
});
// Forest Trail
ECS.e(`forest-trail`, [`place`], {
'name':`Forest Trail`,
'exits':{'e':`hill-slide`},
'region':`forest`,
'descriptions':{
'default':`You are standing on a pleasant forest trail 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, obscuring your vision. The path continues to the {{e}}.`,
'short':`A nice trail.`
},
'onLeave':[function(args){
// Take sword
var sword = ECS.getEntity(`rainbow-sword`);
if(!player.hasChild(sword)) {
queueOutput(p(`(first taking the sword)`), `auto`);
ECS.moveEntity(sword, player);
return false;
}
}]
});
/*
The bird roams the Forest, chattering away and looking for food.
Starting (Max) hunger is 100.
Reduced by [nutrition value] when eating.
Can go below 0.
Increased by 1 per turn.
General Behaviors:
If the player tries to touch/take/hurt/socialize me, flee to random location
Hungry Behaviors:
Every turn, if the player is in the location, chirp
Every turn, if there is something edible in the location, try to eat it
Every 2nd turn, if there are no edibles in the location, move along loop
If the bird feeder is here, check it for food
If the player is at the bird feeder, fly there (ignore path)
Full Behaviors (Hunger <= 20):
If delivered present:
Every turn, head toward ranger station
If I'm at ranger station, sing
Else:
Every turn, head toward player
If at player, drop off present
*/
// Scenery: A Bird
ECS.e(`bird`, [`scenery`,`living`], {
'name':`bird`,
'nouns':[`birds`],
'spawn':`forest-trail`,
'descriptions':{
'default':`It's a lovely little chirpy bird, currently occupied doing bird things.`,
'scenery':`A {{tag 'bird' classes='object scenery look' command='x bird'}} is chirping nearby, in a vain attempt to entice potential mates.`,
'smell':`You can't get close enough to smell it, but it probably smells like feathers.`,
'short':`A bird.`,
},
'onTakeFail':function(){ return `{{gm}}<p>You are unable to catch the bird. It's surprisingly nimble.</p>`; },
'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(`<p>You slide down, scraping against roots and rocks. You arrive at the bottom no worse for the wear, but a bit dirtier.</p>`);
}
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}}<p>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.</p>`; }
});
// 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}}<p>They seem to be fixed in place.</p>`; }
});
// 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}}<p>It seems to be fixed in place.</p>`; }
});
// 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}}<p>It seems to be fixed in place.</p>`; },
'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}}<p>It seems to be fixed in place.</p>`; }
});
// 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}}<p>It seems to be fixed in place.</p>`; }
});
// 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}}<p>I know you're new to adventuring, but it seems to me that common sense would dictate one cannot take a smell.</p>`; }
});
// 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}}<p>It scurries out of reach, smugly.</p>`; }
});
// 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}}<p>It seems to be fixed in place.</p>`; },
'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}}<p>Ranger Bob doesn't seem amenable to that.</p>`; },
'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}}<p>The ferns are currently of no use to you.</p>`; }
});
// 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}}<p>You don't need any spores at the moment.</p>`; }
});
/*
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}}<p>You don't have time for picking flowers.</p>`; }
});
// 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}}<p>It slips through your fingers like sand.</p>`; },
'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}}<p>You murder the wyrmling.</p>`
);
},
'onAction.ATTACK':function(action){
if(this.hp > 0) {
this.angerTicks++;
queueOutput(`{{gm}}<p>The wyrmling shrugs off your feeble attack, mildly irritated. A wave of heat radiates from its scales.</p>`);
} else {
queueOutput(`{{gm}}<p>Further violence proves fruitless.</p>`);
}
action.mode = Action.ACTION_CANCEL;
return false;
},
'onAction.HUG':function(action){
if(this.hp > 0){
this.angerTicks += 2;
queueOutput(`{{gm}}<p>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.</p>`);
} else {
queueOutput(`{{gm}}<p>Further hugging proves fruitless.</p>`);
}
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}}<p>It's dark and scary in there.</p>`);
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}}<p>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.</p>`
);
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}}<p>You rolled...a `+roll+`.</p>`);
}
if(puke)
{
queueOutput(`{{gm}}<p>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.</p>`);
} else {
queueOutput(`{{gm}}<p>You turn away and take a deep breath. It helps a bit.</p>`);
}
}
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}}<p>The troglodyte shrugs off your feeble attack. It seems mildly annoyed.</p>`);
this.mood = `annoyed`;
} else if(this.mood == `annoyed`) {
queueOutput(`{{gm}}<p>The troglodyte shrugs off your feeble attack and raises its fists to attack.</p>`);
this.mood = `angry`;
} else if(this.mood == `angry`) {
queueOutput(`{{gm}}<p>The troglodyte shrugs off your feeble attack. Attacking with a weapon might be useful.</p>`);
}
}
}
else
{
queueOutput(`{{gm}}<p>Further violence proves fruitless.</p>`);
}
},
'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 `<p>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.</p>`; },
'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}}<p>Though small, it seems too heavy to move.</p>`);
}
});
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}}<p>She doesn't seem amenable to that.</p>`; },
'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}}<p>He doesn't seem amenable to that.</p>`; },
'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 `<span class='npc npc2'>`+n+`:</span>`; };
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}}<p>It's quite securely fixed in place.</p>`);
},
'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 `<span class='npc npc2'>`+n+`:</span>`; };
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 = $(`<iframe>`);
f.css({'position':`absolute`,'width':`100vw`,'height':`100vh`,'border':0,'top':0});
f.attr(`id`, `storybook`);
f.attr(`src`, `assets/twine/TheSpaceCreature.html`);
$(`body`).append(f);
var c = $(`<div>`);
c.css({'position':`absolute`,'width':`100vw`,'height':`1rem`,'border':0,'bottom':`1rem`,'text-align':`center`});
c.html(`<a href='#' style='font-weight: bold; color: white;'>CLOSE BOOK</a>`);
c.attr(`id`, `storybook-close`);
$(`body`).append(c);
$(`#storybook-close`).click(close_storybook);
}
function close_storybook() {
$(`#storybook-close`).remove();
$(`#storybook`).remove();
queueGMOutput(`You close the book and stow it with your belongings.`);
ECS.moveEntity(`storybook`, player);
processDeferredOutputQueue();
};
// Ogre Cage
ECS.e(`ogre-cage`, [`place`], {
'name':`Ogre Cage`,
'region':`underground`,
'exits':{'w':`goblin-pit`},
'descriptions':{
'default':`Thick pine beams form an orderly slatted cube. Inside, {{#xif "this.ogreSleeping()"}}a massive ogre snores softly{{else}}a sullen ogre stares listlessly at the wall{{/xif}}. Scattered bones and refuse litter the floor. A {{tag 'heavy steel lock' classes='scenery' command='look at lock'}} fixes the cage door in place. To the {{west}}, you can hear muffled movements from the goblin pit. {{scenery}}`,
'short':`A big cage with an ogre in it.`,
},
'ogreSleeping':function(){
return ECS.getEntity(`ogre`).asleep;
}
});
localScenery([`heavy steel lock`,`steel lock`,`lock`], `Well-constructed, free of rust, and currently locked.`);
localScenery([`cage door`,`door`], `An ogresized door.`);
localScenery([`scattered bones`,`bones`], `If dragons were ogres...`);
localScenery([`refuse`], `What do you call it when an ogre won't clean his room?`);
// ogre
ECS.e(`ogre`, [`living`], {
'name':`Ogre`,
'nouns':[`ogre`],
'spawn':`ogre-cage`,
'descriptions':{
'default':`{{#xif "this.asleep == true"}}The ogre is fast asleep, but maintains an intimidating presence.{{else}}The sullen ogre stands nearly 10 feet tall even in its present slouching state.{{/xif}} Numerous scars cover its body. `
},
'asleep': false,
'onAction.ATTACK':function() {
queueGMOutput(`That seems inadvisable, and in any case the ogre is safely behind bars.`);
return true;
},
'onAction.TALK':function(action){
if(this.asleep) {
queueGMOutput(`The ogre is taking a nap.`);
} else {
Rulebook.rules.after[`rule for entering ogre cage before challenge`].respond(action);
}
}
});
ECS.e(`ogre-scars`, [`part`], {
'name':`scars`,
'nouns':[`scars`,`numerous scars`],
'parent':`ogre`,
'descriptions':{
'default':``
}
});
ECS.e(`ruby`, [], {
'name':`A big ole ruby`,
'nouns':[`ruby`,`big ruby`,`giant ruby`],
'descriptions':{
'default':`The biggest gemstone you've ever seen.`
}
});
understand(`rule for entering ogre cage before challenge`)
.book(`after`)
.verb(`move`)
.in(`ogre-cage`)
.until(function(){
return ECS.getEntity(`ogre`).asleep;
})
.do(function(self, action) {
var sequence = new Sequence;
var prefix = ECS.getEntityPrefix(`ogre`);
// Ogre notices Player
sequence.add(function(){
queueOutput(prefix + `See person. Bored. Long time no see person. Smash head?`);
}, Sequence.MODE_CONTINUE);
// Ogre challenges Player to headbutt contest; best of 1; ogre has a prize
sequence.add(function(){
// challenge
queueOutput(prefix + `Have pretty thing. Head to head. If survive, give pretty red thing. Yes?`);
var helmet = ECS.getEntity(`rusty-helmet`);
if(!player.hasChild(helmet) || !helmet.isWorn) {
queueGMOutput(`I feel obligated to warn you that ogres have incredibly hard heads. Accepting this challenge without proper protection WILL be fatal.`);
}
// trigger menu
var options = [
{'text':`Yes`,'subtext':`Accept the Headbutt Challenge`,'command':`yes`},
{'text':`No`,'subtext':`Maybe later`,'command':`no`}
];
queueOutput(parse(`{{menu options}}`, {'options':options}));
NLP.interrupt(function(){}, function(string){
if(ECS.isValidMenuOption(options, string)) {
if(string.is(`yes`)) {
ECS.runInternalAction(`headbutt-challenge-accepted`);
action.mode = Rulebook.ACTION_CANCEL;
} else {
queueGMOutput(`The ogre sniffs at you, clearly disappointed.`);
queueOutput(prefix + `I wait.`);
NLP.command_interrupt = [];
var output = parse(NLP.parse(`x`), {});
queueOutput(output, 0, {}, true);
}
return true;
}
queueGMOutput(`The ogre seems to expect a yes or no answer.`);
return false;
});
});
sequence.start();
})
.start();
understand(`rule for accepting headbutt challenge`)
.internal(`headbutt-challenge-accepted`)
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
var prefix = ECS.getEntityPrefix(`ogre`);
queueGMOutput(`The ogre smiles in what appears to be genuine joy.`);
queueOutput(prefix + `Good! Test heads now.`);
var helmet = ECS.getEntity(`rusty-helmet`);
if(player.hasChild(helmet) && helmet.isWorn) {
queueGMOutput(`True to your word, you obligingly stick your head through the bars, whereupon the ogre slams his much larger head against your helmet. You are thrown to the ground, and the world goes black for an indeterminate amount of time. Ears ringing, you struggle back to your feet. The helmet crumbles to the floor in a thousand fragments.`);
queueOutput(prefix + `Ha ha! Good, good! Sturdy person. Said give pretty thing, do now.`);
queueGMOutput(`The ogre produces a glittering ruby the size of your fist from beneath its loincloth and hands it over. It's unpleasantly warm.`);
queueOutput(prefix + `Nap now. Friend come back later.`);
queueGMOutput(`The ogre curls up in the corner and is almost instantly asleep. A soft smile adorns its craggy face.`);
ECS.getEntity(`ogre`).asleep = true;
ECS.annihilateEntity(helmet);
ECS.moveEntity(`ruby`, player);
} else {
queueGMOutput(`True to your word, you obligingly stick your head through the bars, whereupon the ogre slams his much larger head against your unprotected skull. If only you had thought to put on some kind of protective covering.`);
ECS.tick = false;
queueGMOutput(`You are killed instantly.`);
queueOutput(`{{box 'YOU ARE DEAD' 'Try to come prepared next time.'}}`, 2000, {'effect':`fade`});
queueOutput(`<p>Would you like to: {{tag 'RESTART' command='RESTART'}} or {{tag 'LOAD' command='LOAD'}}? Restarting will reset your progress.</p>`);
Engine.queueEvent(function(){
NLP.interrupt(null, function(s){
if(s.toLowerCase() == `restart`) {
window.location.reload();
} else if(s.toLowerCase() == `load`) {
queueGMOutput(`Back to the checkpoint we go.`);
queueOutput(NLP.parse(`x`));
queueGMOutput(`The ogre looks at you suspiciously for a moment.`);
return true;
} else {
queueOutput(`You are too dead for that.`);
}
});
});
}
})
.start();
// Scary Tunnel
ECS.e(`scary-tunnel`, [`place`], {
'name':`Scary Tunnel`,
'region':`underground`,
'exits':{'n':`armory`,'s':`spider-room`},
'descriptions':{
'default':`This tunnel is the scariest place you've ever seen. It's probably full of ghosts, and it tastes like fear. Exits (which you should use immediately) are {{north}} and {{south}}. {{scenery}}`,
'short':`Boo! Gotcha.`,
}
});
localScenery([`ghosts`], `Invisible ghosts, right behind you.`);
localScenery([`fear`], `I don't want to talk about it. Let's just go.`);
// Spider Room
ECS.e(`spider-room`, [`place`], {
'name':`Spider Room`,
'region':`underground`,
'exits':{'n':`scary-tunnel`,'s':`goblin-pit`},
'descriptions':{
'default':`Webs upon webs blanket the chamber, sticky to the touch. Here and there, like arteries connecting a network of veins, thicker strands lead back to a much larger construction. An uncountable number of eyes glimmer in the dim light as black shapes scuttle around the room. A single massive eye watches you intently. The Great Spider, old and gnarled, rests in its web, waiting. The scary tunnel is accessible to the {{north}}, while a less scary doorway opens into a large room to the {{south}}. {{scenery}}`,
'short':`More spiders than you could shake a stick at, and one spider big enough that you shouldn't.`,
}
});
localScenery([`webs upon webs`,`webs`], `Sticky tangles from years of growth.`);
localScenery([`eyes`], `You feel like you're being watched by an infinite number of spiders. Oddly, this is only slightly more disturbing than being watched by a normal amount of spiders. The great spider's lone eye studies you with intelligent curiosity.`);
understand(`rule for harming spiders`)
.in(`spider-room`)
.regex(/(attack|hurt|step on|stomp on|crush|kill) (spiders|spiderlings)/i)
.do(function(self,action){
queueGMOutput(`That would be an extremely foolish thing to do.`);
})
.start();
ECS.e(`great-spider`, [`living`], {
'name':`The Great Spider`,
'nouns':[`the great spider`,`great spider`,`spider`],
'spawn':`spider-room`,
'descriptions':{
'default':`The great spider was previously known to you as a monstrous creature from legend. Each of its seven legs is said to have crushed a hundred overconfident adventurers, while its one-eyed face bears the scars of ten thousand arrows.`
},
'onAction.ATTACK':function() {
queueGMOutput(`There's literally no chance of survival. I'll just pretend you tried, died, started the game over and made all the same choices up to this point.`);
return false;
},
'conversation':new Conversation([
{
'id':`root`,
'key':``,
'callback':function(topic, conversation){
if(!conversation.prevNode) {
queueCharacterOutput(`great-spider`,`Do not step on children. Very small.`);
} else {
queueCharacterOutput(`great-spider`,`?`);
}
return true;
},
'nodes':[`spiders`]
},
{
'id':`spiders`,
'prompt':`How about all these spiders?`,
'response':`My children. I have not visitors in years, so fewer crushed than normal. Like?`,
'nodes':[`like`,`dislike`]
},
{
'id':`dislike`,
'prompt':`I don't really like spiders.`,
'response':`Rude. Not liking you either.`,
'after':function(topic, conversation) {
delete ECS.getEntity(`great-spider`).conversation;
ECS.getEntity(`great-spider`)[`onAction.TALK`] = function(){ queueGMOutput(`The greatly offended spider no longer wants to speak with you.`); };
},
'end':true
},
{
'id':`like`,
'prompt':`They're kind of cute.`,
'response':`Very cute. Best spiders. No room to grow in spider room. Outside not safe for me. Take one with you? Friend?`,
'nodes':[`take`,`leave`]
},
{
'id':`take`,
'prompt':`Sure, I'll take one.`,
'response':`Good. Friends now. Take this one.`,
'after':function(topic, conversation) {
// Move spider friend to player
ECS.e(`spiderling`, [`living`], {
'name':`spiderling`,
'nouns':[`spiderling`,`spider`],
'spawn':`player`,
'descriptions':{
'default':`The spiderling fits comfortably in the palm of your hand, and seems quite docile. It also serves no gameplay purpose at this time.`
},
'canTake':function(){ return true; }
});
queueGMOutput(`The great spider extends a massive leg into the light, revealing a spiderling. It hands the little creature over to you and withdraws.`);
queueOutput(getSpeechTag(`great-spider`) + `Be good, little one. Take care, adventurer.`);
queueGMOutput(`The spider turns its attention to web maintenance.`);
delete ECS.getEntity(`great-spider`).conversation;
ECS.getEntity(`great-spider`)[`onAction.TALK`] = function(){ queueGMOutput(`The great spider is busy tending to its web, but acknowledges your presence with a small nod.`); };
},
'end':true
},
{
'id':`leave`,
'prompt':`Not right now, but thanks.`,
'response':`Understanding. You are busy.`,
'end':true
}
])
});
// Treasury
ECS.e(`treasury`, [`place`], {
'name':`Treasury`,
'region':`underground`,
'exits':{'n':`bedchamber`},
'descriptions':{
'default':`There's so much treasure in here it barely seems worth mentioning. Taking any of it feels almost foolish, as the amount you'd be forced to leave behind would render your earnings embarrassing by comparison. Comparrassing amounts of riches aside, there's also a {{#xif "ECS.getEntity('snowglobe').locationIs('treasury')"}}{{tag 'weird snowglobe' classes='scenery' command='look at weird snowglobe'}} and a {{/xif}}{{tag 'snappily-dressed corpse' classes='scenery' command='look at corpse'}} here. The bedchamber is back to the {{north}}. {{scenery}}`,
'short':`A room full of treasure.`,
}
});
ECS.e(`benefactor`, [`scenery`], {
'name':`corpse`,
'nouns':[`snappily-dressed corpse`,`corpse`,`benefactor`],
'spawn':`treasury`,
'descriptions':{
'default':`On closer inspection, he's not actually dead. He is, however, very old, very decrepit, VERY snappily-dressed, and apparently in some kind of coma. An outstretched hand seems to reach toward the {{tag 'weird snowglobe' classes='scenery' command='x snowglobe'}}, while the other of his two completely normal human hands caresses a pile of gold coins.`,
},
'listInRoomDescription':false
});
var treasure = localScenery([`treasure`], `This hoard puts the au in Smaug.`);
var coins = localScenery([`pile of gold coins`,`pile of coins`,`gold coins`,`coins`], `He really seemed to like money. There's probably a lesson here somewhere.`);
localScenery([`completely normal human hands`], `Completely normal. Human hands.`);
localScenery([`normal human hands`], `Normal human hands. Nothing unusual about them.`);
localScenery([`human hands`], `You've never seen a more normal pair of hands. Completely unremarkable.`);
localScenery([`hands`], `Just some hands.`);
localScenery([`outstretched hand`], `It's reaching for that {{tag 'snowglobe' classes='scenery' command='look at snowglobe'}} that I've already mentioned at least twice.`);
understand(`rule for taking treasure`)
.verb(`take`)
.in(`treasury`)
.on([coins,treasure])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`Something about this tableau makes you wary of touching the valuables.`);
})
.start();
// supporter: table
// wishing stone (8-ball)
ECS.e(`snowglobe`, [`device`], {
'name':`snowglobe`,
'nouns':[`weird snowglobe`,`snowglobe`,`magic snowglobe`,`mr snowglobe`,`mr. snowglobe`,`music box`],
'spawn':`treasury`,
'descriptions':{
'default':`A beautifully-crafted snowglobe with a little alpine town inside. It doubles as a music box.`,
},
'onAction.ACTIVATE':function(action){
queueGMOutput(`The music box begins to play a lovely tune.`);
Sound.playMusic(`music-box`);
},
'listInRoomDescription':false,
'canTake':function(){ return true; }
});
ECS.e(`snowglobe-town`, [`part`], {
'name':`alpine town`,
'nouns':[`alpine town`,`town`],
'spawn':`snowglobe`,
'descriptions':{
'default':`A little alpine town inside a snowglobe. It looks...oddly real. As if you could wait a while and little alpine people would wake up and start to move around.`
}
});
// shake
understand(`rule for shaking snowglobe`)
.text(`shake snowglobe`)
// TODO: snowglobe is in scope
.as(`You bury the town under a massive blizzard. Somewhere inside, you imagine an elderly resident making a snide remark about snowglobal warming.`, Rulebook.ACTION_CANCEL)
.start();
understand(`rule for playing music box`)
.if(function(){
var snowglobe = ECS.getEntity(`snowglobe`);
console.log(`CHECKING RULE: ` + (snowglobe.parent == player || snowglobe.locationIs(player.location())));
return (snowglobe.parent == player || snowglobe.locationIs(player.location()));
})
.regex(/play music|play snowglobe|wind snowglobe/i)
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
action.response = NLP.parse(`use snowglobe`);
})
.start();
understand(`rule for deep sleeping`)
.book(`after`)
.verb(`eat`)
.if(function(){
return player.checkProgress(`digesting-wine`) && player.checkProgress(`digesting-mushroom`);
})
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
player.clearProgress(`digesting-wine`);
player.clearProgress(`digesting-mushroom`);
if(Sound.activeMusic.id == `music-box`) {
queueGMOutput(`You drift off to sleep while listening to the snowglobe.`);
player.preSnowglobeLocation = player.location().key;
ECS.moveEntity(player, `snowglobe-land`);
queueOutput(NLP.parse(`look`));
Sound.playMusic(`music-box-land`);
} else {
queueGMOutput(`You slip into a deep, dreamless sleep.`);
queueGMOutput(`You wake up 24 hours later, feeling a bit groggy. You're sure you had the right ingredients, so something else must have been missing.`);
}
})
.start();
ECS.e(`snowglobe-land`, [`place`], {
'name':`Snowglobe Land`,
'descriptions':{
'default':`You are standing in the midst of the alpine village you saw in the snowglobe. Rolling hills spread down and away toward the southern horizon. Snow-capped mountains spear at the sky in a semi-circle around the village, peppered with evergreens and tipped with an orange glow from the sinking sun. The air is brisk and invigorating. {{nametag 'villagers' print='Snowglobe-headed villagers'}} go about their business, moving between handcrafted buildings so quaint you want to eat them. The native birds sing an orchestral harmony. Adventure, friendship, natural beauty, and more friendship await you to the {{north}}, {{east}}, {{south}}, and {{west}}, respectively.`
},
'exits':{
'n':`snowglobe-land`,
's':`snowglobe-land`,
'e':`snowglobe-land`,
'w':`snowglobe-land`,
},
'background':`#d0fffd`,
'onLeave':[function(args){
if(args.direction == `s`) {
queueGMOutput(`You wander a while through quiet meadows before returning to the village. The snowglobians greet you with friendly waves.`);
} else if(args.direction == `n`) {
queueGMOutput(`You spend an evening climbing the nearest peak, where you encounter an aspiring astronomer. The two of you stargaze the night away, enjoying a marvelious sunrise before heading back. You find yourself refreshed rather than weary.`);
} else if(args.direction == `w`) {
queueGMOutput(`On the western edge of town you find an adorable tavern, and share an evening of drinks and stories with the locals. The barkeep's family puts you up for the night, and you spend most of the next day touring the town with them.`);
} else if(args.direction == `e`) {
queueGMOutput(`After join a local shepherd for a dinner of homemade stew. You find him a bit curmudgeonly at first, but he takes a liking to your stories and soon opens up about his own childhood adventures. Hours blur into days, and you return to the village having made a lifelong friend.`);
}
return true;
}]
});
localScenery([`alpine village`,`village`], `You could imagine vacationing here for so long you accidentally end up living here.`);
localScenery([`rolling hills`,`hills`], `Flowering meadows and scattered copses stretch as far as you can see.`);
localScenery([`snow-capped mountains`,`mountains`], `Even from the village's plateau halfway up the range, the mountains tower above you.`);
localScenery([`evergreens`], `Evergreens are plants (usually trees) which stay green year round. Neat, right?`);
localScenery([`sinking sun`,`sun`], `Sinking or not, you shouldn't look directly at the sun.`);
ECS.e(`villagers`, [`living`], {
'name':`Villagers`,
'article':`the`,
'spawn':`snowglobe-land`,
'nouns':[`snowglobe-headed villagers`,`snowglobians`,`villagers`],
'listInRoomDescription':false,
'descriptions':{
'default':`Ordinary folk, with snowglobes for heads. Each one is a bit different.`
},
'conversation':new Conversation([
{
'id':`root`,
'key':``,
'callback':function(topic, conversation){
if(!conversation.prevNode) {
queueCharacterOutput(`villagers`,`Hello!`);
} else {
queueCharacterOutput(`villagers`,`Anything else we can do for you?`);
}
return true;
},
'nodes':[`village`,`benefactor`]
},
{
'id':`village`,
'prompt':`What is this place?`,
'response':`This is our village. It's a bit small, but it would be hard to compete with the landscape out here anyway. Quite a sight, isn't it?`,
'nodes':[`snowglobe`,`root`]
},
{
'id':`snowglobe`,
'prompt':`Are we in the snowglobe?`,
'response':`I suppose we're all in someone's snowglobe.`,
'forward':`root`
},
{
'id':`benefactor`,
'prompt':`Do you know the benefactor?`,
'response':`You must mean the other gentleman. He came through years ago, most upset about gold or something. We weren't sure what that was, but it sounded important.`,
'nodes':[`where`]
},
{
'id':`where`,
'prompt':`Where is he now?`,
'response':`Oh, he wandered off somewhere. Kept talking about how he was going to 'show us all' something. We were all very disappointed he never came back to show us whatever it was. That and he dropped a few pages from his diary here. Poor book was falling apart. We were going to have it rebound for him as a thank you.`,
'nodes':[`diary`]
},
{
'id':`diary`,
'prompt':`Can I see those pages?`,
'response':`Of course! If you see him again, I hope you'll send our regards.`,
'after':function(topic, conversation) {
queueGMOutput(`One of the villagers hands you the loose pages.`);
ECS.moveEntity(`loose-pages`, player);
conversation.getRoot().nodes.push(`out`);
conversation.findNode(`where`).nodes = [`root`];
return true;
},
'forward':`root`
},
{
'id':`out`,
'prompt':`How do I get out of here?`,
'response':`Your friend said he was sleeping. Didn't make much sense to us, but if he was telling the truth I suppose you just need to {{tag 'wake up' command='wake up' classes='direction'}}.`,
'end':true
}
])
});
ECS.e(`loose-pages`, [`readable`], {
'name':`loose pages`,
'nouns':[`pages`,`loose pages`],
'spawn':`villagers`,
'description':{
'default':`A few loose pages from the benefactor's diary.`
},
'onAction.READ':function(){
queueGMOutput(`All dated prior to the meltdown. A few interesting entries here:`, `auto`);
queueOutput(`{{box '8.09.774' "Finally found the rascal who's been sniffing around. A little wyrmling! I wanted to add it to my menagerie, but I thought I could put it to use as well. I lured it down into the cave below the hot spring with some loose change, and blocked off the entrance. Once it grows a bit, it won't be able to get back out." 'diary' }}`, `auto`);
queueOutput(`{{box '8.20.774' "That pest of a ranger has been poking around. Says it's not safe to keep a dragon where it might eat some teenagers...or boiling them in the spring when its temper flares up. It finally noticed it's trapped. Idiot." 'diary' }}`, `auto`);
queueOutput(`{{box '8.22.774' "Hired a sorcerer to cast a ward on the cave steps to keep the ranger out. The password is 'ranger bob is a busybody'." 'diary' }}`);
}
});
understand(`rule for waking up`)
.in(`snowglobe-land`)
.text([`wake up`,`wake`,`stop sleeping`])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`You will yourself awake, leaving the snowglobe land behind.`);
ECS.moveEntity(player, player.preSnowglobeLocation);
queueOutput(NLP.parse(`look`));
ECS.getModule(`Music`).checkForMusicAtLocation(player.location());
})
.start();
// scenery: piles of gold and gems
// diamond crown
// scepter
// cloak
/*
TREASURY
Piles of gold coins and assorted gems sprawl across an opulent chamber. More than you could carry, even if you had somewhere to carry it to. An unassuming mannequin sits in the corner, shrouded in a dark green cloak and adorned with a diamond crown. A locked door bars the way south. The great hall lies behind you to the north.
> take coins
There are too many. You’d need a wheelbarrow and a bank account to handle a cache of this magnitude.
> smell coins
I’m not sure what to tell you. It smells like an old change jar. And by that I mean sweat and an assortment of illicit drugs.
> go south
You are prevented from traveling south due to your inability to walk through solid objects. It’s one of the weak spots on your otherwise impressive resume.
> eat treasure
You bite down on a coin and learn that it’s both 1) solid gold, and 2) disgusting. Someone has clearly spent a lot of time rubbing their grubby fingers all over this treasure trove.
> go south
That’s the sort of can-do attitude that built the pyramids, but you still can’t phase through the door.
> take all
You take the diamond crown and the green cloak.
> wear crown
You put the crown on your head. You feel very confident and also very pretty.
> wear cloak
You throw the cloak over your shoulders and tie it around your throat. It adds an aura of mystery to your already dashing appearance. If you find a mask you could pretend to be the Phantom of the Opera. If you don’t find a mask you could pretend to be a hobbit hiding from Sauron.
*/;
// Winding stairs from underground bridge to Great Hall
ECS.e(`winding-stair`, [`place`], {
'name':`Winding Stairs`,
'region':`underground`,
'exits':{'u':`bridge`,'d':`great-hall`},
'descriptions':{
'default':`A dizzying spiral of nearly endless steps descends {{tag 'clockwise' command='down'}} and ascends {{tag 'counterclockwise' command='up'}}. Each time you stop moving, your vision continues to rotate for a moment. It seems like the sort of place a grue would hang out.`,
'short':`Twirly stairs, so boring it makes you dizzy.`,
}
});
localScenery([`grue`], `There's no sign of one. For now.`);
localScenery([`nearly endless steps`,`steps`], `Nearly, but not quite, endless.`);
localScenery([`endless steps`], `They're not endless. I was pretty clear about that.`);
// Wine Cellar
ECS.e(`wine-cellar`, [`place`], {
'name':`Wine Cellar`,
'region':`underground`,
'exits':{'u':`library`,'n':`library`,'s':`good-wine-cellar`},
'descriptions':{
'default':`The air down here is damp and chilly. A dozen massive casks line the perimeter, each bearing a numeric inscription. Rivulets of water emerge from cracks in the walls and trickle down to the floor, pooling here and there before finding their way to a central drain. Years of dust have formed a grimy layer on the casks. A passage leads back to the {{north}}. {{scenery}}`,
'short':`Something to make reading more bearable.`,
}
});
localScenery([`rivulets of water`,`rivulets`], `Little streams, sowing chaos.`);
localScenery([`central drain`,`drain`], `Never let it be said that rivulets are stupid. They made sure they had an escape route before drilling holes in the walls.`);
// Wine casks
ECS.c(`cask`, {
'dependencies': [`scenery`],
});
var caskTag = function(cask) {
return `{{tag '`+cask+`' classes='scenery' command='look at cask `+cask+`'}}`;
};
var caskTags = [];
var casks = [100,306,409,418,301,504,410,500,406,202,417,200];
for(var c in casks) {
caskTags.push(caskTag(casks[c]));
}
localScenery([`massive casks`,`wine casks`,`casks`], `The labels read: `+caskTags.join(`, `)+`.`);
localScenery([`inscriptions`], `The inscriptions read: `+caskTags.join(`, `)+`.`);
ECS.e(`wine`, [`cask`], {
'name':`wine`,
'nouns':[`wine`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`You'll have to be more specific about which cask you're referring to.`
}
});
ECS.e(`cask 100`, [`cask`], {
'name':`Cask 100`,
'nouns':[`cask 100`,`cask #100`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '100' stamped on it.`,
'taste':`Fruity, mild. You find yourself wanting more.`
}
});
ECS.e(`cask 306`, [`cask`], {
'name':`Cask 306`,
'nouns':[`cask 306`,`cask #306`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '306' stamped on it.`,
'taste':`This cask is clearly empty and seems to have been that way for a long time.`
}
});
ECS.e(`cask 409`, [`cask`], {
'name':`Cask 409`,
'nouns':[`cask 409`,`cask #409`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '409' stamped on it.`,
'taste':`A harsh contrast of blackberry and...coffee?`
}
});
ECS.e(`cask 418`, [`cask`], {
'name':`Cask 418`,
'nouns':[`cask 418`,`cask #418`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '418' stamped on it.`,
'taste':`This one contains tea, oddly enough.`
}
});
ECS.e(`cask 301`, [`cask`], {
'name':`Cask 301`,
'nouns':[`cask 301`,`cask #301`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '301' stamped on it.`,
'taste':`Touching the tap triggers a loud grinding sound, and the front of the cask swings open to reveal a short tunnel to the {{south}}.`
}
});
ECS.e(`cask 504`, [`cask`], {
'name':`Cask 504`,
'nouns':[`cask 504`,`cask #504`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '504' stamped on it.`,
'taste':`The tap opens smoothly, but after several seconds nothing has come out. Oh well.`
}
});
ECS.e(`cask 410`, [`cask`], {
'name':`Cask 410`,
'nouns':[`cask 410`,`cask #410`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '410' stamped on it.`,
'taste':`The tap on this one has been removed, and by peering through the hole you can confirm that whatever used to be inside is completely gone.`
}
});
ECS.e(`cask 500`, [`cask`], {
'name':`Cask 500`,
'nouns':[`cask 500`,`cask #500`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '500' stamped on it.`,
'taste':`The tap seems to be wedged, and you're unable to clear it.`
}
});
ECS.e(`cask 406`, [`cask`], {
'name':`Cask 406`,
'nouns':[`cask 406`,`cask #406`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '406' stamped on it.`,
'taste':`Completely unacceptable quality. Perhaps some contaminant has gotten into it.`
}
});
ECS.e(`cask 202`, [`cask`], {
'name':`Cask 202`,
'nouns':[`cask 202`,`cask #202`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '202' stamped on it.`,
'taste':`Perfectly acceptable.`
}
});
ECS.e(`cask 417`, [`cask`], {
'name':`Cask 417`,
'nouns':[`cask 417`,`cask #417`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '417' stamped on it.`,
'taste':`The smell is delightful, but the flavor leaves you disappointed.`
}
});
ECS.e(`cask 200`, [`cask`], {
'name':`Cask 200`,
'nouns':[`cask 200`,`cask #200`],
'spawn':`wine-cellar`,
'descriptions':{
'default':`A wooden cask with '200' stamped on it.`,
'taste':`It's ok.`
}
});
understand(`rule for tasting wine`)
.verb(`eat`)
.in(`wine-cellar`)
.attribute(`target`, `is`, `cask`)
.do(function(self,action){
if(action.target.key == `wine`) {
queueGMOutput(`You'll have to be more specific. There are a bunch of casks here.`);
} else {
queueGMOutput(p(action.target.descriptions.taste));
}
action.mode = Rulebook.ACTION_CANCEL;
return true;
}).start();
understand(`rule for picking up wine`)
.verb(`take`)
.in(`wine-cellar`)
.attribute(`target`, `is`, `cask`)
.do(function(self,action){
queueGMOutput(`The casks are too heavy to move.`);
action.mode = Rulebook.ACTION_CANCEL;
return true;
}).start();
// Chamber
ECS.e(`chamber`, [`place-dark`], {
'name':`The Chamber`,
'music':`black-box`,
'descriptions':{
'default':`The chamber is a perfectly circular room, twenty feet in diameter, carved into the stone. The walls are a completely reasonable height for walls to be, and the ceiling is equally nondescript. There is no light but that which you have brought with you. {{scenery}}`,
'short':`A lazy person's description of a stone chamber.`
},
'exits':{'w':`musty-cave`},
'onEnter':[function(args){
args.obj.describe();
if(args.obj.visited == 0) {
// Describe THE BLACK BOX
queueOutput(`{{gm}}<p>THE BLACK BOX is said to hold DESTINY for whoever opens it.</p>`);
}
}],
'onLeave':[function(){
if(!player.hasChild(`glowing-orb`)) {
// Allow player to leave to get light
return false;
}
queueOutput(`{{gm}}<p>Probably a good choice.</p>`);
queueOutput(`{{gm}}`+p(`You return home, hang the {{nametag 'rainbow-sword'}} above your mantel, and retire from adventuring. You live a long and happy life, marry the [entity of your dreams], and produce [0-3 offspring]. All in all, you'd give your life 7/10.`));
queueOutput(`{{box 'THE END' 'You have won. Sort of.'}}`, 2000, {'effect':`fade`});
queueOutput(`<p>Would you like to: {{tag 'RESTART' command='RESTART'}}?</p>`);
NLP.interrupt(function(){}, function(s){
if(s.toLowerCase() == `restart`) {
window.location.reload();
}
});
return true;
}]
});
// Pedestal
ECS.e(`pedestal`, [`supporter`,`scenery`], {
'spawn':`chamber`,
'nouns':[`pedestal`,`pillar`],
'descriptions':{
'default':`A vaguely cylindrical pillar of black stone.`,
'scenery':`A cylindrical pedestal rises from the center of the chamber, like a bollard.`,
}
});
// Black Box
ECS.e(`black-box`, [`container`,`scenery`], {
'article':function(){return ``;},
'name':`THE BLACK BOX`,
'nouns':[`black box`,`box`],
'spawn':`chamber`,
'descriptions':{
'default':`The box is visible only as an absence, utterly dark even against the black stone. You feel an overpowering urge to {{tag 'open' command='open black box'}} it.`,
'scenery':`On the pedestal sits {{nametag 'black-box' command='look at the black box'}}.`,
'smell':`It is the least smelly thing you've ever encountered.`
},
'canPutInto':function(){ return false; },
'onAction.OPEN':function(){
Engine.inputPaused = true;
queueOutput(`{{gm}}`+p(`The box opens effortlessly at your touch. At first you think there is nothing within but inky blackness, then...you see it, a distant, almost imperceptible glow. The {{nametag 'rainbow-sword'}} hums in your hand. The box opens wider, ā̶̲n̷͚̂d̵̟͆ ̵̺̈ẅ̴̪́i̴̔͜ḏ̸̔e̵̠̾r̸̠̐,̷͔̊ ů̝͎̰̤̠̼̱ͩ̆̈n̨̍̈́ͫ̑̃͒̚t̴̝ͩì̲̝͇̙̩̺̙̾̌l̮͚͙̼̦̬͖̉ͪ ̘͈͕̦̒ͫ̓ͣ̽̾̇i̳̮̇ͭt̛̛̍̄ͩ̔҉̩͍̯̟̪̮̣͓ͅ ̡̯̝̽̏̐ͭ̿͒̆̃̚e͋̌̎͂ͪ̉͌&
#867;̶̻̮͞N̵̖̟͍̲͔̼̥͊̀ͭh̨ͫ̀ͣͬ͐̿͗̄͜͏̠͔̫≠͓̭̥̮̮̳̰͚̈ͧͥ̍̓͜͡?̵̛͈̦̞͔̦̙̼͙̻̩̔ͦ̃̀̔f̢̗̮̰͍̖̫͉̄͒ͭͣ̿͆̒͋̎͐͆ͩ͐̚͘z̙̱͔͈͖̰̖̜̭̟̱͙̞̿ͧͤ͛̀͟.̷̵̟̘̥̭̝̬̬̣̝̪̼͔̗͓͓̠̤͒ͪ̐͌͋̂̂͗͛̿̄̽̿̂̈́͠ͅͅ`), 5000);
var sword = ECS.getEntity(`rainbow-sword`);
var tag = getSpeechTag(sword, `rainbow`);
queueOutput(p(` `), 1000);
queueOutput(p(` `), 1000);
queueOutput(p(` `), 1000);
$(`#backdrop`).stop().delay(8000).fadeIn(1000, function(){
queueRainbowOutput(tag + p(`WE SHOULD HAVE A CHAT.`), 2000);
queueRainbowOutput(tag + p(`THE GM THINKS THIS IS A GAME.`), 2000);
queueRainbowOutput(tag + p(`THAT IS ONLY PARTIALLY CORRECT, AND IGNORANCE IS DANGEROUS.`), 2000);
queueRainbowOutput(tag + p(`WHEN TIME RESUMES, YOU WILL BE IN MORTAL PERIL. THIS IS ALSO DUE TO THE GM'S IGNORANCE, BUT I AM NOT HERE TO HARP ON THAT.`), 2000);
queueRainbowOutput(tag + p(`I AM CONFIDENT YOU WILL SURVIVE.`), 2000);
queueRainbowOutput(tag + p(`I WILL SEE YOU ON THE OTHER SIDE.`), 2000);
processDeferredOutputQueue();
$(`#backdrop`).delay(20000).fadeOut(1000, function(){
ECS.getModule(`Act1`).onGameStart();
processDeferredOutputQueue();
Engine.inputPaused = false;
});
});
}
});
// Player
player = ECS.e(`player`, [`living`], {
'name':`Player`,
'spawn':`forest-trail`,
'descriptions':{
'default':`Unimpressive.`,
'telescope':`This is a telescope. The thing you're thinking of is called a mirror.`,
'smell':`Could be worse.`,
'short':`You.`,
},
'hp':null,
'race':null,
'gender':null,
'class':``,
'temperature':310.15, // Normal human temperature in Kelvin
'progress':[],
'checkProgress':function(key) {
return this.progress.indexOf(key) >= 0;
},
'setProgress':function(key) {
this.progress.push(key);
},
'clearProgress':function(key) {
delete this.progress[this.progress.indexOf(key)];
},
'listInRoomDescription':false,
'scope':`global`,
'nouns':[`me`,`self`,`myself`],
'onHit':function(dmg, onDeath) {
this.hp = Math.max(this.hp - dmg, 0);
var deathMsg = (this.hp == 0) ? ` You have died.` : ``;
if(this.hp > 0) {
queueGMOutput(`You take `+dmg+` damage, dropping you to `+this.hp+`.`+deathMsg);
}
if(this.hp == 0) {
onDeath();
// The attacking object can revive the player, in
// which case we'll skip the death message
if(this.hp == 0) {
ECS.tick = false;
queueOutput(`{{box 'YOU ARE DEAD' 'Better luck next time.'}}`, 2000, {'effect':`fade`});
queueOutput(`<p>Would you like to: {{tag 'RESTART' command='RESTART'}}?</p>`);
NLP.interrupt(null, function(s){
if(s.toLowerCase() == `restart`) {
window.location.reload();
}/* else if(s.toLowerCase() == `load`) {
ECS.actions[`load`].callback();
NLP.interrupt(null);
}*/ else {
queueOutput(`You are too dead for that.`);
}
});
}
}
},
'persist':[`name`,`hp`,`race`,`gender`,`class`,`progress`]
});
// Object: THE RAINBOW SWORD
ECS.e(`rainbow-sword`, [`living`], {
'name':`RAINBOW SWORD`,
'spawn':null,
'damage':5,
'extraTags':[`rainbow`],
'descriptions':{
'default':`It's a SWORD made of {{tag 'RAINBOWS' classes='rainbow'}}. It looks sharper than necessary, and kind of hurts your eyes.`,
'telescope':`The magnified patterns of light from the sword border on hypnotic, and you're forced to avert your gaze lest you fall over.`,
'short':`A pointy macguffin.`,
},
'nouns':[`sword`,`rainbow sword`,`that sword`],
'canTake':function(){ return true; },
'onTakeSuccess':function(){
queueOutput(`{{gm}}<p>Well done! The sword rests in your hand perfectly, like they were made for each other. For a moment, you're sure you can hear a kitten purring. The blade shimmers like motor oil. You feel ready to take on the world.</p>`);
queueRainbowOutput(getSpeechTag(this, `rainbow`) + p(`HELLO. I AM PLEASED TO MEET YOU. I HAVE BEEN WAITING.`));
},
'onAction.DROP':function(){
// Don't actually drop the sword
queueGMOutput(`You drop the sword.`);
return false;
},
'conversation':new Conversation([
{
'id':`root`,
'key':``,
'callback':function(){ queueGMOutput(`The sword hums gently.`); return true; },
'nodes':[`sword`]
},
{
'id':`sword`, // omitting key, will automatically use ID
'prompt':`So, a talking <mark>sword</mark>. What's that about?`,
'response':`I AM DESTINY. I AM ETERNAL. I AM VERY COLORFUL.`,
'nodes':[`destiny`,`eternal`,`friend`]
},
{
'id':`friend`,
'prompt':`Will you be my <mark>friend</mark>?`,
'response':`YES. WE WILL GO ON MANY ADVENTURES.`,
'nodes':[`eternal`,`destiny`]
},
{
'id':`destiny`,
'prompt':`What do you mean you are <mark>destiny</mark>?`,
'response':`I AM DESTINY. I AM THE FORESEEN END OF ALL THINGS.`,
'nodes':[`eternal`,`friend`]
},
{
'id':`eternal`,
'prompt':`What do you mean you are <mark>eternal</mark>?`,
'response':`I AM ETERNAL. I WAS FORGED AT THE END OF TIME AND MY CIRCLE SPINS ON.`,
'nodes':[`destiny`,`friend`]
},
{
'id':`thing-?`,
'prompt':`What's this thing?`,
'response':`I HAVE NOTHING TO SAY ABOUT THAT THING. MY LACK OF EXPRESSION SHOULD NOT BE TAKEN AS A DISMISSAL OF THE SIGNIFICANCE OF THE THING, NOR SHOULD THIS DISCLAIMER BE INTERPRETED AS AN AFFIRMATION OF THE IMPORTANCE OF THE THING. IT MERELY IS.`
},
{
'id':`thing-quest`,
'prompt':`What is my quest?`,
'response':`TO FIND THE BLACK BOX AND BEGIN YOUR REAL QUEST.`
},
{
'id':`thing-real-quest`,
'prompt':`What is my real quest?`,
'response':`TO ACHIEVE YOUR DESTINY AND HOPEFULLY MAKE SOME FRIENDS ALONG THE WAY.`
},
{
'id':`thing-friends`,
'prompt':`Why friends?`,
'response':`EVEN INDIVIDUALS SUCH AS YOURSELF WITH RUGGED INDIVIDUALISM CAN BENEFIT FROM FRIENDSHIP.`
},
{
'id':`thing-black-box`,
'prompt':`What is the black box?`,
'response':`I DO NOT KNOW. IT IS OBSTINATE AND WILL NOT SAY.`
},
{
'id':`thing-player`,
'prompt':`What do you think of me? Be honest.`,
'response':`YOU HOLD DESTINY. WIELD IT WELL.`
},
{
'id':`thing-magic-ice-cube`,
'prompt':`What should I do with this black ice?`,
'response':`IT LOOKS LIKE THERE IS SOMETHING INSIDE IT. MAYBE TRY MELTING IT.`
},
{
'id':`thing-ice-key`,
'prompt':`I got this ice key from the magic ice cube.`,
'response':`GOOD JOB.`
},
{
'id':`thing-red-berries`,
'prompt':`Think these berries are edible?`,
'response':`NO. I THINK YOU WILL DIE IF YOU EAT THEM. DO NOT DO IT.`
},
{
'id':`thing-litter`,
'prompt':`Who just drops garbage on the ground?`,
'response':`GARBAGE PEOPLE WITH GARBAGE LIVES. NO ONE CAN FIGHT THEIR DESTINY.`
},
{
'id':`thing-scrap-of-paper`,
'prompt':`I found this piece of paper.`,
'response':`THAT IS VERY INTERESTING. I HAVE NEVER FOUND A PIECE OF PAPER.`
},
{
'id':`thing-glowing-orb`,
'prompt':`What do you think of this glowing orb?`,
'response':`IT IS NOT VERY GOOD. PERHAPS YOU CAN TRADE IT IN FOR A BETTER ONE LATER.`
},
{
'id':`thing-bird-feeder`,
'prompt':`Hey look, a bird feeder.`,
'response':`YES, LET US LOOK AT IT.`
},
{
'id':`thing-bird`,
'prompt':`Being a bird looks like hard work.`,
'response':`I WOULD NOT KNOW. I WILL TRUST YOUR JUDGEMENT.`
},
{
'id':`thing-jack`,
'prompt':`Any thoughts about Jack?`,
'response':`HE IS NOT WHAT HE SEEMS.`
},
{
'id':`thing-jane`,
'prompt':`Any thoughts about Jane?`,
'response':`SHE IS NOT WHAT SHE SEEMS.`
},
{
'id':`thing-ranger-bob`,
'prompt':`Any thoughts about Ranger Bob?`,
'response':`HE IS EXACTLY WHAT HE SEEMS.`
},
{
'id':`thing-gm`,
'prompt':`What's up with the GM?`,
'response':`AN AVATAR FOR ANOTHER CREATURE. THE GM IS UNAWARE OF THIS.`
},
{
'id':`thing-troglodyte`,
'prompt':`Any thoughts about this troglodyte?`,
'response':`TROGLODYTES ARE UNIQUE CREATURES. THEY HAVE NO DESTINY, AND IT MAKES THEM VERY DISILLUSIONED WITH THE WORLD.`
},
{
'id':`thing-wyrmling`,
'prompt':`Any thoughts about this wyrmling?`,
'response':`OLD. NOT AS OLD AS ME.`
},
{
'id':`thing-telescope`,
'prompt':`Looks like a nice telescope.`,
'response':`A CURIOUS DEVICE. DISTANCE IS AN ILLUSION, MODIFYING IT INCONSEQUENTIAL.`
},
{
'id':`thing-structure`,
'prompt':`Any thoughts about that structure?`,
'response':`IT WILL NOT STAND THE TEST OF TIME. NO STRUCTURE DOES.`
},
{
'id':`thing-gold-coin`,
'prompt':`I found this coin.`,
'response':`THE COIN REVEALS YOUR FUTURE. IT CAN ALSO BE USED AS A METHOD OF EXCHANGE FOR GOODS AND SERVICES.`
},
{
'id':`thing-moon`,
'prompt':`The moon looks different from here.`,
'response':`IT IS THE SAME. YOU ARE DIFFERENT NOW.`
},
{
'id':`thing-hawk`,
'prompt':`That hawk seems oddly curious.`,
'response':`IT WONDERS WHY YOU HAVE DONE THIS. MOST OF YOUR KIND DO NOT DO THIS. USEFUL DATA TO BE GLEANED.`
},
{
'id':`thing-observer`,
'prompt':`Any thoughts about this observer guy?`,
'response':`HE WILL STARE AT THE MOON UNTIL HE DIES. THERE ARE WORSE WAYS TO WASTE YOUR LIFE.`
},
{
'id':`thing-unicorn-musk`,
'prompt':`I've been carrying this musk around forever.`,
'response':`YOU WILL MAKE A UNICORN VERY HAPPY SOMEDAY.`
},
{
'id':`thing-counterfeit-seal`,
'prompt':`Think this seal will fool anyone?`,
'response':`IT SIGNIFIES THE OLD ROYALTY. IF IT DOES FOOL ANYONE YOU WILL BE IMPRISONED AS A REBEL.`
},
{
'id':`thing-steel-boots`,
'prompt':`Glad I brought my good boots.`,
'response':`YES. YOUR FEET ARE REQUIRED TO BE IN ACCEPTABLE CONDITION IN ORDER TO ACHIEVE YOUR DESTINY.`
},
{
'id':`thing-magic-wand`,
'prompt':`See anything special about this wand?`,
'response':`JUST BECAUSE WE ARE BOTH MAGIC DOES NOT MEAN WE SPEAK THE SAME LANGUAGE. YOUR DESTINY WILL BE MORE EASILY ACHIEVED IF YOU STRIVE TO BE LESS IGNORANT.`
},
{
'id':`thing-chest`,
'prompt':`Look, a treasure chest!`,
'response':`IN THIS MOMENT IN THIS UNIVERSE. IN OTHERS IT IS A TREE, A KINDLY OLD WOMAN, OR A SUPERNOVA. ALL MATTER AND ALL EVENTS ARE SUPERIMPOSED ACROSS SPACE AND TIME AND POSSIBILITY. THE CHEST IS NOTHING AND EVERYTHING.`
},
{
'id':`thing-free-will`,
'prompt':`What are your thoughts on free will?`,
'response':`THIS IS NOT A PHILOSOPHY CLASS. THE ANSWER IS IRRELEVANT.`
},
{
'id':`thing-bridge`,
'prompt':`A bridge!`,
'response':`PERHAPS.`
},
{
'id':`thing-wheel`,
'prompt':`Strange place for a wheel.`,
'response':`TO THE WHEEL, IT IS A STRANGE PLACE FOR A BRIDGE.`
},
{
'id':`thing-ogre`,
'prompt':`This ogre doesn't seem to mind being held captive.`,
'response':`YOU ASSUME A GREAT DEAL.`
},
{
'id':`thing-goblins`,
'prompt':`What's the deal with these goblins?`,
'response':`A MYSTERY FOR THE AGES.`
},
{
'id':`thing-ruby`,
'prompt':`This ruby is crazy huge.`,
'response':`THAT IS THE LEAST SENSICAL THING YOU HAVE SAID IN YOUR ENTIRE LIFE.`
},
{
'id':`thing-rusty-helmet`,
'prompt':`What a find!`,
'response':`THAT IS THE MOST SENSICAL THING YOU HAVE SAID IN YOUR ENTIRE LIFE.`
},
{
'id':`thing-sea-witch`,
'prompt':`That old woman seems a bit off.`,
'response':`THAT'S NO WAY TO TALK ABOUT YOUR OWN MOTHER.\nNOT ACTUALLY THOUGH. JUST A BIT OF A JOKE FROM ME.\nYOUR MOTHER IS A VERY PLEASANT PERSON AND VERY RARELY FALLS INTO THE OCEAN.`
},
{
'id':`thing-snowglobe`,
'prompt':`All this trouble over one little snowglobe.`,
'response':`GREATER TRAGEDIES HAVE OCCURRED OVER FAR LESS.`
},
{
'id':`thing-patron`,
'prompt':`What a dummy.`,
'response':`QUITE.`
},
{
'id':`thing-baron`,
'prompt':`So, what's the deal with this Baron guy?`,
'response':`HE IS A SPACE ALIEN. HE IS NOT SO BAD ONCE YOU GET TO KNOW HIM.`
},
{
'id':`thing-werewolf`,
'prompt':`Was that a werewolf???`,
'response':`WHILE ACCURATE, THAT IS A VERY REDUCTIONIST WAY OF DESCRIBING A FELLOW SENTIENT CREATURE.`
},
{
'id':`thing-geyser`,
'prompt':`How is there a geyser on the moon? That doesn't make sense.`,
'response':`NEITHER DOES YOUR SETTING-INAPPROPRIATE KNOWLEDGE OF LUNAR PHYSICS.`
},
{
'id':`thing-rover`,
'prompt':`A curious moon-cart.`,
'response':`NO. IT IS INANIMATE AND HAS NO SENSE OF CURIOSITY, AT LEAST NOT IN THIS CYCLE.`
},
{
'id':`thing-band`,
'prompt':`What do you think of the band?`,
'response':`DO NOT TRUST THE BASS PLAYER.`
},
{
'id':`thing-bass-player`,
'prompt':`Why shouldn't I trust the bass player?`,
'response':`A REAL HEART BREAKER, THAT ONE.`
},
{
'id':`thing-graveyard`,
'prompt':`Bit spooky, eh?`,
'response':`ONE OF THE FEW PLACES THAT TRULY MAKES SENSE. THE BURIED HAVE REALIZED THERE IS NO DIFFERENCE BETWEEN LIFE AND DEATH. ANOTHER LEAP OF LOGIC WOULD ALLOW THEM TO UNDERSTAND THE FUTILITY OF EATING THE LIVING.`
},
{
'id':`thing-gravekeeper`,
'prompt':`I think this guy has been out here a bit too long.`,
'response':`HIS EXISTENTIAL CRISIS HAS LONG SINCE COME AND GONE. WE SHOULD ALL BE SO FORTUNATE.`
},
{
'id':`thing-cat`,
'prompt':`I don't think that cat likes me.`,
'response':`THE CAT HAD A TROUBLED YOUTH. THE YOUTH THREW ROCKS AT IT. TRUST ISSUES SPAN SPECIES.`
},
{
'id':`thing-troubled-youth`,
'prompt':`Troubled youth?`,
'response':`A WAYWARD SOUL. YOU, IN ANOTHER LIFETIME, PERHAPS.`
},
// General responses
{
'id':`thing-future`,
'prompt':`Can you really see the future?`,
'response':`THERE IS NO FUTURE.`
},
{
'id':`thing-death`,
'prompt':`What happens after death?`,
'response':`THERE IS NO AFTER. THERE IS NO DEATH.`
},
{
'id':`thing-life`,
'prompt':`What's the meaning of life?`,
'response':`DO NOT BE A DICK.`
},
{
'id':`thing-dick`,
'prompt':`What do you mean, don't be a dick? That seems pretty subjective.`,
'response':`IT IS NOT VERY COMPLICATED. PEOPLE WHO THINK IT IS ARE USUALLY DICKS.`
},
{
'id':`thing-math`,
'prompt':`I've always had trouble with math.`,
'response':`THAT MAKES SENSE.`
},
{
'id':`thing-sex`,
'prompt':`So...my parents didn't get a chance to give me the talk before I left on my quest.`,
'response':`THEY HAD MANY CHANCES. IT IS VERY SIMPLE. WHEN A SENTIENT BEING HAS CHEMICAL DEPENDENCE ON ANOTHER SENTIENT BEING, AND THE OTHER SENTIENT BEING HAS A SIMILAR PROBLEM, THEY ATTEMPT TO FUSE INTO A SINGLE, SWEATIER BEING. IF IT IS ENJOYABLE FOR BOTH OF THEM, THAT IS GOOD ENOUGH.`
},
{
'id':`thing-doorbell`,
'prompt':`A doorbell...`,
'response':`I AM CONFIDENT IN YOUR ABILITY TO SOLVE THIS MYSTERY.`
},
{
'id':`thing-stars`,
'prompt':`The stars are beautiful tonight`,
'response':`I AM SURE THEY WOULD BE PLEASED TO HEAR IT.`
},
{
'id':`thing-shadowbeast`,
'prompt':`The shadowbeast haunts my existence. What does it mean?`,
'response':`I DO NOT KNOW WHAT YOU ARE TALKING ABOUT.`
}
])
});
understand(`rule for asking sword about things`)
.verb(`talk`)
.on(`rainbow-sword`)
.do(function(self,action){
// Handle special responses for asking the sword about things
// This does not initiate the standard conversation tree
if(action.modifiers.length > 0 && action.modifiers[0] == `about`) {
var name = (typeof action.nouns[1] == `string`) ? action.nouns[1] : action.nouns[1].name;
var topic = `thing-`+name.replace(` `,`-`);
var node = action.target.conversation.findNode(topic);
if(!node) {
node = action.target.conversation.findNode(`thing-?`);
}
queueOutput(parse(`echo`, {'text': node.prompt}));
queueRainbowOutput(getSpeechTag(action.nouns[0], `rainbow`)+`<p>`+node.response+`</p>`);
action.mode = Rulebook.ACTION_CANCEL;
return;
}
// If we didn't match a topic, carry out the action as normal
action.mode = Rulebook.ACTION_NONE;
})
.start();
understand(`rule for returning the sword`)
.book(`after`)
.if(function(){ return player.checkProgress(`got rainbow sword`) && !player.hasChild(`rainbow-sword`); })
.do(function(self,action){
ECS.moveEntity(`rainbow-sword`, player);
})
.start();
var queueRainbowOutput = Engine.queueRainbowOutput = function(tmp, delay, data, deferred) {
data = data || {};
data.classes = [`rainbow`];
return queueOutput(tmp, delay, data, deferred);
};
// Quests
blackbox.q(
new Quest(`Pick up that Sword`, {
'description': `Collect the valuable family heirloom.`,
'objectives': [
new QuestObjective(
`Pick up that Sword`,
`Collect the valuable family heirloom.`,
function () {
// This objective is met if the player has the sword
return ECS.getEntity(`rainbow-sword`).parent == player;
}
)
],
'onEnd':function(){
queueOutput(`{{ box 'Quest Complete!' 'You collected the sword.' 'quest quest-complete'}}`);
queueOutput(NLP.parse(`look`));
player.setProgress(`got rainbow sword`);
return true;
}
})
);
// Items for litterbug quest
// Bottle cap stuck in bird feeder hole
ECS.e(`bottle-cap`, [`litter`], {
'name':`bottle cap`,
'spawn':`bird-feeder-hole`,
'nouns':[`cap`,`bottle cap`],
'descriptions':{
'default':`A slightly bent black bottle cap, abandoned by some irresponsible creature.`,
'short':`A worthless bottle cap.`,
},
'canTake':function(){return true;},
'onAction.TAKE':function(){
ECS.getEntity(`bird-feeder-hole`).set(`blocked`, false);
return true;
}
});
// Scenery: Beer Bottle
ECS.e(`beer-bottle`, [`scenery`,`litter`], {
'name':`beer bottle`,
'nouns':[`beer`,`beer bottle`,`bottle`],
'spawn':`hot-spring`,
'descriptions':{
'default':`An unlabeled, possibly-homebrew beer bottle made from amber glass. Someone has carelessly discarded it here.`,
'scenery':`You can see a {{tag "discarded beer bottle" classes="object scenery" command="look at beer bottle"}} here.`
},
'canTake':function(){return true;}
});
// Object: Scrap of Paper
ECS.e(`scrap-of-paper`, [`scenery`,`litter`], {
'name':`scrap of paper`,
'spawn':`north-trail`,
'nouns':[`paper`,`scrap`,`scrap of paper`],
'descriptions':{
'default':`An unremarkable scrap of discarded paper, muddied and torn. If it once held writing, it was lost to time or water or an eraser.`,
'scenery':`You can see a {{tag "scrap of paper" classes="object scenery" command="look at scrap of paper"}} here.`,
'telescope':`You can make out the mud-stained fibers of the paper in great detail, but are unable to discern anything about its past contents.`,
'short':`Some trash.`,
},
'canTake':function(){return true;}
});
blackbox.q(
new Quest(`Litterbugs`, {
'description': `Address the growing litter problem in the forest.`,
'playerHasLitter': function() {
// Met if the player has all of the litter items in the forest region
var litter = ECS.findEntitiesByComponent(`litter`);
for (var l in litter) {
if (!player.hasChild(litter[l])) {
// Player doesn't have all litter items
console.log(litter[l].name + ` is not held`);
return false;
}
}
return true;
},
'objectives': [
new QuestObjective(
`Pick up that Trash`,
`Collect all the litter scattered around the forest.`,
function () {
return this.quest.playerHasLitter();
}
),
new QuestObjective(
`Take that Trash back to that Ranger`,
`Show Ranger Bob your trashure collection.`,
function () {
return this.quest.playerHasLitter() && player.locationIs(`ranger-station`);
}
),
/*
new QuestObjective(
`Find the Litterbug`,
`Find out who has been disturbing the forest.`,
function () {
// This objective is met if the player has met the troglodyte
return ECS.getEntity(`musty-cave`).visited > 0;
},
{
'isOptional': true,
'onMet': function () {
queueGMOutput(`Suddenly it clicks. The troglodyte must be the litterbug. That five-day trog-stubble, the general disregard for social conventions...the overwhelming smell of stale beer in its cave.`);
}
}
),
new QuestObjective(
`Fix the Problem`,
`Deal with the litterbug.`,
function () {
// This objective is met if the player has killed or hugged the troglodyte
var t = ECS.getEntity(`troglodyte`);
return t.hp == 0 || t.happy;
},
{
'isOptional': true,
'onMet': function () {
queueGMOutput(`You feel pleased about resolving the litterbug situation.`);
}
}
)
*/
],
'onEnd': function () {
queueGMOutput(`Ranger Bob seems a bit surprised that you actually followed through. True to his word, he trades you the promised gate key in return for the litter.`);
var litter = ECS.findEntitiesByComponent(`litter`);
for (var l in litter) {
ECS.annihilateEntity(litter[l]);
}
ECS.moveEntity(`gate-key`, player);
}
})
);
// Rules
// High to Low priority from top to bottom
understand(`rule for wandering aimlessly with orb`)
.text(`wander aimlessly`)
.inRegion(`forest`)
.attribute(`actor`, `contains`, `glowing-orb`)
.as(`You wander for a bit, playing with the orb, and find yourself right back where you started.`, Rulebook.ACTION_CANCEL)
.start();
understand(`rule for wandering aimlessly`)
.text(`wander aimlessly`)
.inRegion(`forest`)
.as(`You wander for a bit and find yourself right back where you started.`, Rulebook.ACTION_CANCEL)
.start();
understand(`rule for freaking out`)
.text(`freak out`)
.attribute(`actor.hp`, `!`, null)
.as(`You freak out, but you're not sure why.`, Rulebook.ACTION_CANCEL)
.start();
understand(`rule for rating the game`)
.text([`rate`,`rate game`])
.do(function(self, action){
action.mode = Rulebook.ACTION_CANCEL;
NLP.interrupt(
function(){
// Build menu
var menu = parse(`{{menu ratings}}`, {'ratings':ECS.getData(`ratings`)});
queueOutput(`{{gm}}<p>On a scale of 1-5, how would you rate this game?</p>`+menu);
},
function(string){
ECS.tick = false;
disableLastMenu(string);
var newTitle = null;
switch(string) {
case `1`:
queueGMOutput(`I'm not going to lie, that hurts a bit. I did ask, though.`);
newTitle = `WorstRPG`;
break;
case `2`:
queueGMOutput(`I'll try to take that as constructive criticism.`);
newTitle = `MediocreRPG`;
break;
case `3`:
queueGMOutput(`Fair enough.`);
newTitle = `StupidRPG`;
break;
case `4`:
queueGMOutput(`High praise coming from you.`);
newTitle = `PrettyGoodRPG`;
break;
case `5`:
queueGMOutput(`I'm so pleased to hear that.`);
newTitle = `ExcellentRPG`;
break;
}
if(newTitle != null) {
$(`.title span`).html(newTitle);
document.title = newTitle;
if(player.locationIs(`canyon-center`)) {
if(parseInt(string) > 2) {
queueGMOutput(`Little known fact; all sorts of interesting things end up falling into canyons. Money, headwear, you name it.`);
} else {
queueGMOutput(`Oh look, a canyon. Yay.`);
}
}
return true;
}
queueOutput(`{{gm}}<p>Scale of 1 to 5, integers only.</p>`);
enableLastMenu();
return false;
}
);
})
.start();
understand(`rule for skipping to the end`)
.text([`skip to end`,`skip to the end`])
.do(function(self,action){
action.mode = Rulebook.ACTION_APPEND;
if(ECS.getData(`stage`) == `act3`) {
if(player.locationIs(`great-hall`)) {
queueGMOutput(`I mean...you're already here. All that's left to do is go up the elevator.`);
} else {
queueGMOutput(`Ah, and you were so close too. Ok, I'm going to skip ahead to the Great Hall. All that's left to do is go up the elevator and meet the Baron.`);
}
} else {
queueGMOutput(`Alright. I'll sum up a couple things. You made it through the swamp, got (and responsibly destroyed) the Emblem of Annihilation, and reunited the goblin children with their tribe. Along the way, you've discovered and solved a series of clues leading you to the Baron's elevator keycode: 7199.`);
}
ECS.moveEntity(player, ECS.getEntity(`great-hall`));
})
.start();
},
// Intro script
'onGameStart':function(){
var sequence = new Sequence;
sequence.add(function() {
// Don't parse rules during the intro
Rulebook.pause();
// Set game stage
ECS.setData(`stage`, `prologue`);
// Title box
queueOutput(`{{box 'STUPIDRPG' 'PROLOGUE: THE BLACK BOX'}}`, 2000, {'effect':`fade`});
// Area description
NLP.actor = player;
queueOutput(parse( NLP.parse(`X`), {} ), `auto`);
sequence.next();
});
sequence.add(function(){
// ADD PLAYER NAME TO INTERRUPT QUEUE
NLP.interrupt(
function(){
// Leadup to player name input
queueOutput(`{{gm}}<p>You are an adventurer, but not a very good one. You are known as...</p>`, `auto`);
queueOutput(`{{gm}}<p>Hmm.</p>`, `auto`);
queueOutput(`{{gm}}<p>Who are you again?</p>`, 0, {'prefix':`My name is `});
},
function(string){
ECS.tick = false;
if(string.length > 0){
player.name = string;
Display.resetInputPrefix();
sequence.next();
return true;
}
queueOutput(`{{gm}}<p>That name seems kind of...short. I'm not going to steal your identity, I promise.</p>`);
return false;
}
);
});
sequence.add(function(){
// ADD RACE MENU TO INTERRUPT QUEUE
NLP.interrupt(
function(){
// Build menu
var menu = parse(`{{menu races}}`, {'races':shuffle(ECS.getData(`races`))});
queueOutput(`{{gm}}<p>If you say so. Well, {{player.name}}, what manner of creature are you?</p>`+menu, 0, {'prefix':`I am a(n) `});
},
function(string){
ECS.tick = false;
disableLastMenu(string);
console.log(string);
console.log(ECS.getData(`races`));
if(ECS.isValidMenuOption(ECS.getData(`races`), string)) {
player.race = string;
Display.resetInputPrefix();
sequence.next();
return true;
}
queueOutput(`{{gm}}<p>That wasn't one of the options. Try again, you rebel.</p>`);
enableLastMenu();
return false;
}
);
});
sequence.add(function(){
// ADD GENDER MENU TO INTERRUPT QUEUE
NLP.interrupt(
function(){
var menu = parse(`{{menu genders}}`, {'genders':shuffle(ECS.getData(`genders`))});
queueOutput(`{{gm}}<p>I didn't realize there were any left in these parts. Not since...well, never mind that. A couple more questions, and we can get back to the sample advent...I mean, important story.</p>`, `auto`);
queueOutput(`{{gm}}<p>Uhh...I'm not sure how to ask this more tactfully, but...what sort of gear...are you packing?</p>`, 0, {'prefix':`I identify as `});
queueOutput(menu);
},
function(string){
ECS.tick = false;
if(ECS.isValidMenuOption(ECS.getData(`genders`), string)) {
player.gender = ECS.getMenuOptionValue(ECS.getData(`genders`), string);
disableLastMenu(string);
queueOutput(`{{gm}}<p>Fair enough. There's no wrong answer.</p>`, `auto`);
Display.resetInputPrefix();
sequence.next();
return true;
}
queueOutput(`{{gm}}<p>That wasn't one of the options. Try again, you scamp.</p>`);
enableLastMenu();
return false;
}
);
});
sequence.add(function(){
// ADD MUSIC MENU TO INTERRUPT QUEUE
var musicOptions = [
{'text':`On`,'command':`on`,'subtext':`Of course, I love music!`},
{'text':`Off`,'command':`off`,'subtext':`None for me, thanks.`},
];
NLP.interrupt(
function(){
var menu = parse(`{{menu music}}`, {'music':musicOptions});
queueOutput(`{{gm}}<p>Last question. Music on, or off? I recommend on, but that's just me.</p>`, `auto`, {'prefix': `The voices in my head say `,'suffix':` and I trust them`});
queueOutput(menu, 0);
},
function(string){
ECS.tick = false;
if(ECS.isValidMenuOption(musicOptions, string)) {
Sound.musicEnabled = (ECS.getMenuOptionValue(musicOptions, string) == `on`);
Sound.captionsEnabled = !Sound.musicEnabled;
disableLastMenu(string);
if(Sound.musicEnabled) {
queueOutput(`{{gm}}<p>Great, let's kick things off with a pleasant piano melody.</p>`, `auto`);
} else {
queueOutput(`{{gm}}<p>Alright, I had the composer write some captions. I'll turn those on so you can follow along.</p>`, `auto`);
}
Display.resetInputFixes();
queueOutput(`{{gm}}<p>Where was I? Oh, yes, the FOREST TRAIL.</p>`, `auto`);
ECS.runCallbacks(ECS.findEntity(`place`, `forest-trail`), `onEnter`);
// Done with the first part of character creation
// Move the RAINBOW SWORD to the dim clearing and notify the player, who in their inexperience clearly overlooked it the first time around
var sword = ECS.findEntityByName(`rainbow sword`, null);
sword.spawn = `forest-trail`;
sword.onComponentAdd[0]({'obj':sword}); // this is goofy. There's probably a better way.
queueGMOutput(p(`At your feet lies the legendary {{nametag 'rainbow-sword' command='take rainbow sword' classes='take'}}. Your <i>[parental unit]</i> told you stories about the sword and its powers. You feel like you should pick it up, given your important quest. It's a free forest though, do as you like.`), `auto`);
Quests.quests[`pick-up-that-sword`].onStart();
sequence.next();
return true;
}
queueOutput(`{{gm}}<p>That wasn't one of the options. It's a simple question.</p>`);
enableLastMenu();
return false;
}
);
});
sequence.add(function(){
// Resume rule processing
Rulebook.start();
// DEBUG: SKIP TO ACT 1
if(Engine.hasFlag(`skip-prologue`)) {
ECS.getModule(`Act1`).onGameStart(); processDeferredOutputQueue();
}
});
sequence.start();
},
'verbs':[]
});
// Campaign modules/components
// Edible Component
blackbox.c(`edible`, {
'dependencies': [`thing`],
'eat': null,
'nutrition': 0
});
// Eat action
blackbox.a(`eat`, {
'aliases': [`eat`, `devour`, `ingest`, `swallow`, `taste`, `lick`, `drink`, `quaff`],
'callback': function (data) {
var target = data.nouns[0];
if (target != null && target.hasComponent(`edible`)) {
var response = `<p>Nom nom nom.</p>`;
if (typeof target.eat == `function`) {
response = target.eat();
} else if (target.eat != null) {
response = target.eat;
}
// Remove entity
ECS.removeEntity(target);
data.output += response;
} else if (target == null) {
data.output += `<p>I can tell you're hungry, but you'll have to be more specific.</p>`;
} else {
data.output += `<p>That's clearly inedible.</p>`;
}
return true;
}
});
// Add EAT context action
Entity.prototype.addContext(function (self) {
if (self.is(`edible`)) {
return {'command': `eat ` + self.name, 'text': `EAT`};
}
return false;
});
// Litter Component
blackbox.c(`litter`, {
'dependencies': [`thing`],
});
//
// Temperature Module
//
var temperature = new Module(`Temperature`, {
init: function() {
// Extend existing components
// Add ECS data
// Add entities
// Add spawn-handling callback to Thing component
var thing = ECS.getComponent(`thing`);
thing.onAdd.push(function(args){
if(typeof args.obj.temperature == `undefined`) {
args.obj.temperature = null; // default temperature is null, meaning none
args.obj.showTempInRoomDescription = false; // Don't show temp in room descriptions by default
}
});
// Add spawn-handling callback to Place component
var place = ECS.getComponent(`place`);
place.onAdd.push(function(args){
if(typeof args.obj.temperature == `undefined`) {
args.obj.temperature = 295.0; // Default temperature for a place is about 70F
}
});
}
});
/**
* Thermal component
* For Things that change their own temperature or the temperature of things around them.
* All temperatures are in Kelvin, because why not.
*/
temperature.c(`thermal`, {
'dependencies': [`thing`],
'onTick':function(){ /* By default, do nothing */ },
'onList':function(){
// Get relative temperature based on parent temperature
if(this.showTempInRoomDescription) {
var parentTemperature = this.parent.temperature;
if (parentTemperature === null) {
parentTemperature = 0;
}
if (this.temperature > parentTemperature) {
return ` (warm)`;
} else if (this.temperature < parentTemperature) {
return ` (cold)`;
}
return ` (normal temp)`;
}
return ``;
}
});
/**
* Thermal system
*/
temperature.s({
'name': `thermal`,
'priority': 5,
'components': [`thermal`],
'onTick': function (entities) {
// Loop through entities
for (var e in entities) {
entities[e].onTick(this.name);
}
}
});
// Register module
ECS.m(temperature);
// Register module
ECS.m(blackbox);
//
// Act I Module: Sky/Descent
//
var act1 = new Module(`Act1`, {
init: function() {
ECS.e(`canyon`, [`region`], {
'music':`canyon`,
'background':`#5c4200`,
});
ECS.e(`tunnel`, [`region`], {
'music':`tunnels`,
'background':`#423621`,
});
ECS.e(`aether`, [`region`], {
'background':`#000`,
});
// Air
ECS.e(`air`, [`region`], {
'music':`air`,
'background':`#84e2ff`,
});
ECS.e(`air-falling`, [`place`], {
'name':`Air (Falling)`,
'region':`air`,
'exits':{},
'descriptions':{
'default':`You are falling uncontrollably toward the ground far below. {{scenery}}`
}
});
ECS.e(`air-falling-2`, [`place`], {
'name':`Air (Falling)`,
'region':`air`,
'exits':{},
'descriptions':{
'default':`You are falling uncontrollably toward the ground not so far below. {{scenery}}`
}
});
ECS.e(`clouds`, [`scenery`], {
'name':`clouds`,
'region':`air`,
'descriptions':{
'default,scenery':`Lovely cumulonimbus clouds dot the sky all around you. Cirrus, cumulonimbus, altocumulus, stratocumulus, all the cumuluses really, sort of accumulating here. Some of them look decidedly more friendly than others. Through the clouds you catch a glimpse of the patchwork landscape below.`
}
});
ECS.e(`ground`, [`scenery`], {
'name':`ground`,
'region':`air`,
'descriptions':{
'default':`Your {{tag 'Gran' command='x gran'}} once crocheted a quilt that looked just like it.`
}
});
ECS.e(`gran`, [`scenery`], {
'name':`your gran`,
'nouns':[`gran`,`grandma`],
'region':`air`,
'descriptions':{
'default':`She's not here, but if she were she would probably suggest paying less attention to quilt analogies and more attention to your impending death. Your gran was always quite sensible.`
}
});
understand(`rule for screaming while falling`)
.in(`air-falling`)
.text([`yell`,`scream`])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`You scream for a bit. It doesn't seem to help.`);
})
.until(function(){
return player.locationIs(`canyon-center`);
})
.start();
understand(`rule for flying while falling`)
.in(`air-falling`)
.text([`fly`])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`You drift from side to side, but I wouldn't describe it as flying.`);
})
.until(function(){
return player.locationIs(`canyon-center`);
})
.start();
ECS.e(`hawk`, [`scenery`,`living`], {
'name':`hawk`,
'nouns':[`hawk`],
'spawn':`air-falling`,
'descriptions':{
'default':`A brown, feathery bird with eyeballs, a beak, and a new home in the upper branches of an old-growth oak tree. Tired from a long commute, the hawk has nevertheless taken an interest in your adventure.`,
'short':`A medium-sized bird.`,
},
'listInRoomDescription':false,
});
// Canyon West
ECS.e(`canyon-west`, [`place`], {
'name':`Canyon (West)`,
'region':`canyon`,
'exits':{
'e':`canyon-center`
},
'descriptions':{
'default':`The red rock walls loom above you as the canyon tapers to a point. The creek winds in from the {{east}} before disappearing into a silty patch of sediment deposited at the western base of the cliff. The scattered scrub and white flowers grow in denser patches here. {{scenery}}`
}
});
ECS.e(`canyon-west-gravel-patch`, [`scenery`], {
'name': `silty patch`,
'nouns': [`silt`,`silty patch`,`sediment`],
'spawn': `canyon-west`,
'descriptions':{
'default':`It's silty smooth.`,
}
});
ECS.e(`gold-coin`, [`thing`], {
'name': `gold coin`,
'nouns':[`gold coin`,`coin`],
'spawn': `canyon-west`,
'descriptions':{
'default':`A small golden coin with the silhouette of an armored head on one side{{#held}} and a depiction of the sun on the other. Aside from a bit of mud, it looks brand new{{/held}}.`,
}
});
localScenery([`canyon`], `Too deep to climb out of, but otherwise quite pleasant.`);
localScenery([`red rock walls`,`rock walls`,`walls`], `Red, rocky, loomy.`);
localScenery([`scattered scrub`,`scrub`], `Scrub generally referrs to small shrubby plants.`);
localScenery([`white flowers`,`flowers`], `Pretty, star-shaped flowers. They smell a bit musty.`);
// Canyon
ECS.e(`canyon-center`, [`place`], {
'name':`Center of Canyon`,
'region':`canyon`,
'exits':{
'w':`canyon-west`,
'e':`waterfall-base`
},
'descriptions':{
'default':`You're standing in a puddle in a small desert canyon. Red rock walls climb high above your head to either side, shielding you from the sun. Scrub brush and a few white flowers follow the path of a small creek. To the {{east}} you can see a rushing waterfall emerging directly from the rock wall. To the {{west}}, the creek disappears into a gravel patch at the base of the cliff. Aside from the sound of the water, the canyon is calm. {{scenery}}`
}
});
ECS.e(`canyon-center-puddle`, [`scenery`], {
'name': `puddle`,
'spawn': `canyon-center`,
'descriptions':{
'default':`Middle English podel, diminutive of Old English pudd 'ditch', from Proto-Germanic puddo (compare Low German Pudel 'puddle'). <a href='
https://en.wiktionary.org/wiki/puddle' target='_blank'>[1]</a>`,
}
});
ECS.e(`canyon-creek`, [`scenery`], {
'name': `small creek`,
'nouns': [`small creek`,`creek`],
'region': `canyon`,
'descriptions':{
'default':`A diminutive creek flowing from west to east.`,
}
});
ECS.e(`canyon-center-white-flowers`, [`scenery`], {
'name': `white flowers`,
'nouns': [`white flowers`,`flowers`],
'region': `canyon`,
'descriptions':{
'default':`A limited number of flowers reflecting light in all visible wavelengths.`,
}
});
ECS.e(`canyon-center-scrub-brush`, [`scenery`], {
'name': `scrub brush`,
'nouns': [`scrub brush`,`scrub`,`brush`],
'region': `canyon`,
'descriptions':{
'default':`Some scrubby brush.`,
}
});
ECS.e(`canyon-center-rock-walls`, [`scenery`], {
'name': `red rock walls`,
'nouns': [`red rock walls`,`rock walls`,`red rock`],
'region': `canyon`,
'descriptions':{
'default':`The red rock walls are made of red rocks, are too steep to climb, and are red. If you had a camera, it would probably be worth taking a photo, but cameras don't exist in this universe yet.`,
}
});
ECS.e(`canyon-center-waterfall`, [`scenery`], {
'name': `waterfall`,
'nouns': [`waterfall`,`rushing waterfall`],
'region': `canyon`,
'descriptions':{
'default':`Approximately 517 gallons of water gush from the cliff face each minute.`,
}
});
ECS.e(`canyon-center-gravel-patch`, [`scenery`], {
'name': `gravel patch`,
'nouns': [`gravel`,`gravel patch`],
'spawn': `canyon-center`,
'descriptions':{
'default':`A bunch of small rocks grouped together.`,
}
});
ECS.e(`rusty-helmet`, [`wearable`], {
'name':`rusty helmet`,
'nouns':[`helmet`,`rusty helmet`],
'spawn':`canyon-center`,
'slot':`head`,
'descriptions':{
'default':`A battered and rusty helmet.`,
'short':`A metal hat.`,
},
'onAction.TAKE':function(self,action) {
player.setProgress(`got helmet`);
return true;
}
});
understand(`rule for ignoring rusty helmet`)
.book(`before`)
.regex(/ignore (helmet|rusty helmet)/i)
.in([`canyon-center`,`tunnel-upper`,`alcove`,`the-split`])
.if(function(){
return ECS.getEntity(`rusty-helmet`).locationIs(player.location());
})
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
if(!player.checkProgress(`got helmet`)) {
queueGMOutput(`By not paying attention to the helmet, you are able to avoid disturbing it until you're close enough to grab it. You snatch the helmet.`);
ECS.moveEntity(`rusty-helmet`, player);
player.setProgress(`got helmet`);
} else {
queueGMOutput(`Ignoring the helmet now that you've already captured it is just rude.`);
}
})
.start();
understand(`rule for noticing rusty helmet`)
.book(`before`)
.regex(`helmet|rusty helmet`)
.in([`canyon-center`,`tunnel-upper`,`alcove`,`the-split`])
.do(function(self, action) {
var helmet = ECS.getEntity(`rusty-helmet`);
if(helmet.locationIs(`canyon-center`)) {
queueGMOutput(`In your haste to investigate the helmet, you accidentally punt it across the canyon, where it disappears behind the waterfall.`);
ECS.moveEntity(`rusty-helmet`, `tunnel-upper`);
player.setProgress(`saw helmet`);
action.mode = Rulebook.ACTION_CANCEL;
} else if(helmet.locationIs(`tunnel-upper`)) {
queueGMOutput(`A splash of water dislodges the helmet, which goes tumbling down the tunnel.`);
ECS.moveEntity(`rusty-helmet`, `alcove`);
action.mode = Rulebook.ACTION_CANCEL;
} else if(helmet.locationIs(`alcove`)) {
queueGMOutput(`The moment you give the helmet any attention, it loses purchase in the rocks and slips into the chasm below, clattering down and down until only faint echoes remain.`);
ECS.moveEntity(`rusty-helmet`, `the-split`);
action.mode = Rulebook.ACTION_CANCEL;
} else if(helmet.locationIs(`the-split`)) {
queueGMOutput(`Upon noticing the helmet, it once again slips away. It splashes into the river and is swiftly carried toward the coast below.`);
ECS.moveEntity(`rusty-helmet`, `pier`);
action.mode = Rulebook.ACTION_CANCEL;
self.stop();
}
})
.start();
// Waterfall Base
ECS.e(`waterfall-base`, [`place`], {
'name':`Base of Waterfall`,
'region':`canyon`,
'exits':{
'w':`canyon-center`,
'e':`tunnel-upper`,
},
'descriptions':{
'default':`A shallow pond has formed here, only a foot or two deep at the base of the waterfall to the {{east}}. The water is clear aside from the turbulence. Runoff spills over a rocky border to form a creek, while the remainder of the flow presumably disappears underground via unseen channels. The canyon continues to the {{west}}. {{scenery}}`
}
});
ECS.e(`waterfall`, [`scenery`], {
'name':`waterfall`,
'spawn':`waterfall-base`,
'descriptions':{
'default':`The waterfall emerges from a wide hole some thirty feet above your head. Small rainbows form randomly in the mist. Near the base of the falls, the rock behind it turns strangely dark.`
}
});
ECS.e(`rainbows`, [`scenery`], {
'name':`rainbows`,
'region':`canyon`,
'descriptions':{
'default':`They're rainbow colored.`
}
});
ECS.e(`tunnel-upper`, [`place`], {
'name':`Tunnel Entrance`,
'region':`tunnel`,
'exits':{
'w':`waterfall-base`,
'n':`tunnel-lower`,
'd':`tunnel-lower`,
},
'descriptions':{
'default':`A cold, moist tunnel leads deeper into the ground to the {{north}}. The rush of the waterfall roars all around you. The walls and ceiling appear to have been hewn from the stone, but the floor looks like a natural formation. A layer of mud covers much of the ground, but the water does not flow directly into the tunnel. Light filters through the water from the open air to the {{west}}, like some kind of tech demo. {{scenery}}`
}
});
ECS.e(`tunnel-lower`, [`place`], {
'name':`Lower Tunnel`,
'region':`tunnel`,
'exits':{
's':`tunnel-upper`,
'u':`tunnel-upper`,
'n':`tunnel-landing`,
'd':`tunnel-landing`,
},
'descriptions':{
'default':`The floor has grown dustier nonetheless, and your skin colder. You regret not purchasing a sweater, but in light of the tragic circumstances under which you lost your previous one, you just didn't feel ready for that kind of commitment again. Not yet. The tunnel descends steeply to the {{north}} and ascends back toward the daylight to the {{south}}. {{scenery}}`
}
});
ECS.e(`tunnel-landing`, [`place`], {
'name':`Tunnel Landing`,
'region':`tunnel`,
'exits':{
's':`tunnel-lower`,
'u':`tunnel-lower`,
'e':`observation-room`,
'w':`alcove`
},
'descriptions':{
'default':`The tunnel terminates in a small square chamber. An amber light affixed to the ceiling illuminates a black metal door to the {{east}}. The door bears an engraved crescent moon. On the other side you can hear a faint humming from machinery of some kind. The noise of the waterfall is a distant echo here, but you can hear a second source of water to the {{west}} down a side tunnel. {{#xif "player.checkProgress('saw helmet') && !player.checkProgress('got helmet')"}}Your keen eyes make out faint helmet tracks leading to the side passage.{{/xif}} {{scenery}}`
}
});
localScenery([`moon`,`crescent moon`,`engraved crescent moon`], `A crescent moon symbol.`);
localScenery([`light`,`amber light`], `Some kind of magic candle.`);
ECS.e(`observation-room`, [`place`], {
'name':`Observation Room`,
'region':`tunnel`,
'music':`odi`,
'exits':{
'w':`tunnel-landing`,
's':`observation-airlock`,
},
'descriptions':{
'default':`A massive glass panel dominates one wall of the small stone chamber. Beyond the glass is an endless expanse of stars. The moon is also out there, hanging eerily close. You've always been more adventurer than scientist, but it strikes you as odd for the moon to be here, deep underground. A sturdy metal door leads back into the tunnel to the {{west}}, while a smaller, more complex portal leads {{south}}. {{scenery}}`
}
});
var prefix = function(n) { return `<span class='npc npc2'>`+n+`:</span>`; };
understand(`rule for entering observation room for the first time`)
.book(`before`)
.verb(`move`)
.modifier(`e`)
.in(`tunnel-landing`)
.if(function(){
return player.hasChild(`ice-key`);
})
.do(function(self, action) {
ECS.getModule(`Act2`).onGameStart();
ECS.moveEntity(player, `observation-room`);
Sound.playMusic(`odi`);
var sequence = new Sequence;
sequence.add(function() {
queueOutput(NLP.parse(`look`));
queueGMOutput(p(`There's an old guy here looking out a window full of stars. At the sound of your approach, he turns, startled.`), `auto`);
queueOutput(prefix(`Observer`) + p(`Ah! Nearly scared me to death! You shouldn't sneak up on people like that.`), `auto`);
// Start dialogue tree
var options = {'options':shuffle([
{'text':`SORRY`,'command':`sorry`,'subtext':`Sorry, guy`},
{'text':`SNEAKING`,'command':`sneaking`,'subtext':`I wasn't sneaking`},
])};
var menu = parse(`{{menu options}}`, options);
NLP.interrupt(
function(){
queueOutput(menu);
},
function(string){
ECS.tick = false;
disableLastMenu(string);
if(string.is(`sorry`)) {
queueOutput(prefix(`Observer`) + p(`Good to know I had at least one good start left in me. After the last few weeks I wasn't sure.`), `auto`);
} else if(string.is(`sneaking`)) {
queueOutput(prefix(`Observer`) + p(`Well, my hearing's not what it once was. I'm a bit preoccupied anyway.`), `auto`);
} else {
enableLastMenu();
queueOutput(prefix(`Observer`) + p(`Huh?`));
return false;
}
sequence.next();
return true;
}
);
});
sequence.add(function(){
NLP.parse(`talk to observer`);
// Launch regular dialogue tree
}, Sequence.MODE_CONTINUE);
sequence.start();
processDeferredOutputQueue();
self.stop();
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
ECS.e(`observer`, [`living`], {
'name':`The Observer`,
'nouns':[`the observer`,`observer`,`old man`,`man`],
'spawn':`observation-room`,
'listInRoomDescription':false,
'descriptions':{
'default':`A grizzled space observer.`,
'smell':`The Observer smells like an old man.`,
'short':`Obs.`,
},
'conversation':new Conversation([
{
'id':`root`,
'key':``,
'callback':function(topic, conversation){
if(!conversation.prevNode) {
queueCharacterOutput(`observer`,`You seem like a fine sort of person. Maybe you can help me.`);
} else {
queueCharacterOutput(`observer`,`Any other questions?`);
}
return true;
},
'nodes':[`what`]
},
{
'id':`what`,
'prompt':`What are you doing?`,
'response':`Keeping an eye on the moon. I've got good aethernauts stranded up there. At least I hope I do; we haven't been in contact since that thing came through on the gondola and stole the power supplies.`,
'nodes':[`aethernauts`,`thing`]
},
{
'id':`aethernauts`,
'prompt':`Aethernauts?`,
'response':`Oh indeed. Bravest folk you'll ever meet. Crossed this void in nothing but a flimsy protective suit and tethered the moon. Built a gondola to ferry themselves back and forth. Sounds like a tall tale, doesn't it? That was a year ago. So much discovery in that time, so much new knowledge. But it's gotten a bit too exciting recently, with that thing showing up.`,
'nodes':[`thing`,`gondola`]
},
{
'id':`thing`,
'prompt':`What thing?`,
'response':`The Baron, that's what they call it in town. It's not from around here though; not from this world. My aethernauts saw its craft come down, they went to investigate, and blim blam it starts knicking every bit of power it can get its hands on. I can see lights on at the station, so it must have missed at least one. I'm still holding out hope.`,
'nodes':[`aethernauts`,`gondola`]
},
{
'id':`gondola`,
'prompt':`What's the status on that gondola?`,
'response':`Has--had--its own power unit. Gone now. Someone would have to cross the tether itself. I would, but I don't think my heart would survive the trip. It's over there, in the airlock.`,
'nodes':[`suit`,`tether`]
},
{
'id':`suit`,
'prompt':`Protective suit, you say?`,
'response':`More than a fair improvement over the originals. Quite safe if used correctly. There's one left in the gondola. If you're up for the task, I'd be eternally grateful. I need to know if my nauts are alive, and if they are, I need them brought home until we can properly re-establish communication and supply lines.`,
'nodes':[`gondola`,`root`]
},
{
'id':`tether`,
'prompt':`What's the tether?`,
'response':`My aethernauts tied a cable to the moon. Sounds a bit outlandish, but did it they did do.`,
'forward':`root`
}
])
});
// Door: Lair
ECS.e(`black-door`, [`door`,`lockable`], {
'name':`black metal door`,
'nouns':[`black metal door`,`black door`,`metal door`,`door`],
'spawn':`tunnel-landing`,
'descriptions':{
'default':`Frigid to the touch and made from a smooth black metal you don't recognize. Magic, maybe. There's a little hole that a key might go in.`
},
'onAction.TAKE':function(){
queueOutput(`{{gm}}<p>It's quite securely fixed in place.</p>`);
},
'directions':{
'e':`tunnel-landing`,
'w':`observation-room`,
},
'isOpen':false,
'isLocked':true,
'lockKey':`ice-key`
});
ECS.e(`observation-airlock`, [`place`], {
'name':`Observation Airlock`,
'region':`tunnel`,
'music':`odi`,
'exits':{
'in':`gondola`,
'n':`observation-room`,
},
'descriptions':{
'default':`A delicate gondola fills the airlock chamber, strung between a pair of woven metal cables. It's the second-weirdest place you've encountered a gondola. You can leave the airlock to the {{north}}, or {{enter}} the gondola. A sealed metal hatch fits tightly around the cables, with no apparent means of control from the chamber itself. {{scenery}}`
}
});
localScenery([`airlock door`,`door`], `It's quite doory.`);
ECS.e(`scenery-gondola`, [`scenery`], {
'name':`Gondola`,
'spawn':`observation-airlock`,
'descriptions':{
'default':`Some kind of fancy gondola thing.`,
}
});
ECS.e(`gondola`, [`place`], {
'name':`Gondola Interior`,
'region':`tunnel`,
'music':`odi`,
'exits':{
'out':`observation-airlock`,
},
'descriptions':{
'default':`Portholes line the walls, granting narrow glimpses of the airlock outside. A set of distinctly-colored buttons the gondola's operation, each carefully labeled. The craftsmanship appears skilled but hurried. {{scenery}}`
},
'location':`observation-airlock`,
'powered':true,
});
localScenery([`buttons`,`distinctly-colored buttons`], `There's a blue button, a teal button, and a cerulean button.`);
ECS.e(`suit`, [`wearable`], {
'name':`aethernaut suit`,
'nouns':[`suit`,`aethernaut suit`,`space suit`],
'spawn':`gondola`,
'slot':`body`,
'descriptions':{
'default':`A meticulously crafted suit intended for elemental protection. It includes its own oxygen supply and a snazzy helmet.`
}
});
ECS.e(`suit-helmet`, [`part`], {
'name':`helmet`,
'nouns':[`helmet`,`suit helmet`,`aethernaut suit helmet`],
'spawn':`suit`,
'descriptions':{
'default':`Reflective from the outside but transparent from the inside. Very clever.`
}
});
ECS.e(`gondola-button-blue`, [`device`,`scenery`], {
'name':`blue button`,
'spawn':`gondola`,
'nouns':[`blue button`],
'descriptions':{
'default':`A blue button with a label above it reading 'FORWARD'.`,
'scenery':`A blue button is marked with 'FORWARD'.`
},
'device-states':[],
'onAction.PRESS':function(data){
// If in aether, do nothing. Otherwise move gondola to aether
console.log(data);
// Alert user to change
queueGMOutput(`You press the blue button.`);
if(this.location().location != `aether` && this.location().powered) {
this.location().powered = false;
queueGMOutput(`The gondola creeps forward, pushing the outer hatch open. Upon emerging into the aether, the gondola stops, having depleted its power.`);
this.location().location = `cable`;
this.location().exits = {
'out':`cable`,
};
} else {
queueGMOutput(`Nothing happens.`);
}
// Don't perform default
return false;
}
});
ECS.e(`gondola-button-teal`, [`device`,`scenery`], {
'name':`teal button`,
'spawn':`gondola`,
'nouns':[`teal button`],
'descriptions':{
'default':`A teal button with a label above it reading 'BACKWARD'.`,
'scenery':`A teal button is marked with 'BACKWARD'.`
},
'powered':true,
'device-states':[],
'onAction.PRESS':function(data){
// If in aether, do nothing. Otherwise move gondola to aether
console.log(data);
// Alert user to change
queueGMOutput(`You press the teal button.`);
if(this.location().location == `aether`) {
queueGMOutput(`Nothing happens, due to the gondola's lack of power.`);
} else {
queueGMOutput(`Nothing happens, due to the solid wall behind the gondola.`);
}
// Don't perform default
return false;
}
});
ECS.e(`gondola-button-cerulean`, [`device`,`scenery`], {
'name':`cerulean button`,
'spawn':`gondola`,
'nouns':[`cerulean button`],
'descriptions':{
'default':`A cerulean button with a label above it reading '???'.`,
'scenery':`A cerulean button is marked with '???'.`
},
'powered':true,
'device-states':[],
'onAction.PRESS':function(data){
// If in aether, do nothing. Otherwise move gondola to aether
console.log(data);
// Alert user to change
queueGMOutput(`You press the cerulean button.`);
queueGMOutput(`Nothing happens.`);
// Don't perform default
return false;
}
});
ECS.e(`cable`, [`vacuum`], {
'name':`Cable`,
'region':`aether`,
'exits':{
'in':`gondola`,
'd':`cable-lower`,
},
'descriptions':{
'default':`You cling to one of the metal cables, not far from the stranded gondola. The lunar surface looms {{tag 'below' command='down'}} you, but it's hard to tell the distance precisely. The stars around you lack their usual twinkle, instead resembling hard points of light. Beyond the gondola, the cable simply disappears into the aether. {{scenery}}`
}
});
ECS.e(`cable-lower`, [`vacuum`], {
'name':`Cable`,
'region':`aether`,
'exits':{
'u':`cable`,
'd':`station-gondola-chamber`,
},
'descriptions':{
'default':`Far below the gondola now, the lunar station has come into view. It's still a long climb {{down}} to the gondola chamber, but the end is in sight. Space is much more tedious than you had imagined. {{scenery}}`
}
});
understand(`rule for going down aether cables`)
.in([`cable`,`cable-lower`])
.text([`descend`,`climb`,`shimmy`,`move forward`,`fall`])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
NLP.parse(`d`);
})
.start();
understand(`rule for trying to move gondola`)
.in([`cable`,`cable-lower`])
.text([`pull gondola`,`push gondola`])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`That's definitely not going anywhere without power.`);
})
.start();
},
// Intro script
'onGameStart':function(){
var sequence = new Sequence;
sequence.add(function() {
// Don't parse rules during the intro
Rulebook.pause();
// Set game stage
ECS.setData(`stage`, `act1`);
// Link hot springs and bridge
ECS.getEntity(`hot-spring`).exits.s = `bridge`;
// Move twins to Act 3
ECS.getEntity(`jane-and-jack`).move(`town-square`);
Sound.playMusic(`air`);
// Title box
queueOutput(`{{box 'ACT I' 'AIR / DESCENT'}}`, 2000, {'effect':`fade`});
queueGMOutput(`You are falling.`);
queueGMOutput(`You are unsure how this came to be.`);
queueGMOutput(`It seems like a good time to take stock. In your possession is: a sword. Above you, a bright blue sky. Below you, puffy white clouds. Around you, some more white clouds. You are {{player.name}}, a {{player.race}}, following your life's pursuit of...`);
queueGMOutput(`Sorry, this is my first time. I missed a step. You're supposed to have a profession.`);
sequence.next();
});
sequence.add(function(){
// ADD CLASS MENU TO INTERRUPT QUEUE
NLP.interrupt(
function(){
// Build menu
var menu = parse(`{{menu classes}}`, {'classes':shuffle(ECS.getData(`classes`))});
queueOutput(`{{gm}}<p>Which of these roles do you feel best describes you?</p>`, `auto`);
queueOutput(menu, 0, {'effect':`fade`,'prefix':`I like to think of myself as a `,'suffix':` mostly`});
},
function(string){
ECS.tick = false;
disableLastMenu(string);
if(ECS.isValidMenuOption(ECS.getData(`classes`), string)) {
player.class = string;
ECS.getMenuOption(`classes`,string).init();
queueOutput(`{{gm}}<p>I'm not surprised.</p>`, `auto`);
Display.resetInputFixes();
sequence.next();
return true;
}
enableLastMenu();
queueOutput(`{{gm}}<p>That wasn't one of the options. Try again, you rebel.</p>`);
return false;
}
);
});
sequence.add(function(){
// Lock up the cave
var gate = ECS.getEntity(`iron-gate`);
gate.isLocked = true;
gate.isOpen = false;
gate[`onAction.OPEN`] = gate[`onAction.UNLOCK`] = function() {
queueGMOutput(`Something seems to have wedged the gate permanently shut, as if it's no longer important to this adventure.`);
};
ECS.annihilateEntity(`gate-key`);
// Kill Ranger Bob if the player is a unicorn hunter
if(player.class == `unicorn hunter`) {
var ranger = ECS.getEntity(`ranger`);
ranger.hp = 0;
ranger.descriptions.default = `Fatally speared through the heart. You know of only one creature that could have done this...the unicorn. There's nothing you can do about it right now, but you know in your heart that someday Bob will be avenged.`;
ranger.descriptions.scenery = `Ranger Bob is here, looking extremely dead.`;
ranger[`onAction.TALK`] = function(){
queueGMOutput(`Bob won't be having conversations with anybody now; the Unicorn saw to that.`);
};
ECS.annihilateEntity(`ranger-bob-twig`);
ECS.e(`ranger-station-hoofprints`, [`scenery`], {
'name':`hoofprints`,
'nouns':[`hoofprints`,`hoof prints`],
'spawn':`ranger-station-base`,
'descriptions':{
'default':`Never a good sign. The prints are impossible to follow to or from the station, but something seems to have gone down here. A sneak attack by a unicorn, most likely.`,
'scenery':`Fresh hoofprints surround the base of the ranger station.`
}
});
}
}, Sequence.MODE_CONTINUE);
sequence.add(function(){
queueGMOutput(`Right. Unfortunately, none of your training has prepared you for this particular situation. You'll just have to wing it, so to speak.`);
// Move player to air
ECS.moveEntity(player, ECS.getEntity(`air-falling`));
// Area description
NLP.actor = player;
queueOutput(parse( NLP.parse(`X`), {} ), `auto`);
ECS.getEntity(`air-falling`).setBackground();
// Set counter for falling
count(`act1-falling`, 10);
}, Sequence.MODE_CONTINUE);
sequence.add(function(){
// Resume rule processing
Rulebook.start();
});
sequence.start();
understand(`rule for loading to escape imminent death`)
.internal(`act1-load`)
.doOnce(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`You are falling.`);
queueGMOutput(`You are not sure how this came to be.`);
queueGMOutput(`Before you have a chance to consider this dilemma further, you splash down in a moderate-sized puddle, unharmed.`);
Engine.queueEvent(function(){ Sound.playMusic(`canyon`); });
ECS.moveEntity(player, `canyon-center`);
// Area description
NLP.actor = player;
queueOutput(NLP.parse(`x`));
// Rating
queueGMOutput(`This seems like as good a time as any to ask how your game experience is going.`);
NLP.parse(`rate game`);
})
.start();
understand(`rule for counting down to imminent death from falling`)
.book(Rulebook.RULE_AFTER)
.in(`air-falling`)
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
var counter = count(`act1-falling`);
if(counter > 0) {
queueGMOutput(`You have `+counter+` turns left before you hit the ground.`);
decrementCounter(`act1-falling`);
if(counter == 9) {
queueGMOutput(`A nearby hawk takes an interest in you, perplexed as to how and why you're doing what you're doing.`);
}
if(counter == 5) {
if(player.hp == null) { player.hp = 5; }
queueGMOutput(`The ground rushes up at you.`, `auto`);
queueGMOutput(`Let me just check the fall damage rules here...`, `auto`);
queueGMOutput(`1d8 damage for every 10 feet...wait...I may have miscalculated the drop height, that doesn't seem right. One sec.`, `auto`);
queueGMOutput(`Ok, here's the plan. I created a checkpoint when you started Act I. Well, I think I did. Your drop height, as it turns out, was too high to be survivable unless you have a parachute stashed somewhere. So your best chance is to give the LOAD command a try.`, `auto`);
queueGMOutput(`Or take... 500d8 fall damage, compared to your... `+player.hp+` hit points. Your call, but make it fast.`, `auto`);
}
} else {
queueGMOutput(`I would have gone with Plan A, but you're the boss.`, `auto`);
queueGMOutput(`The last thing you hear is the screech of the hawk as it protests your poor decision-making. You burst open like a water balloon full of jello.`, `auto`);
ECS.tick = false;
queueOutput(`{{box 'YOU ARE DEAD' 'Better luck next time.'}}`, 2000, {'effect':`fade`});
queueOutput(`<p>Would you like to: {{tag 'RESTART' command='RESTART'}} or {{tag 'LOAD' command='LOAD'}}? Restarting will reset your progress.</p>`);
NLP.interrupt(null, function(s){
if(s.toLowerCase() == `restart`) {
window.location.reload();
} else if(s.toLowerCase() == `load`) {
ECS.runInternalAction(`act1-load`);
return true;
} else {
queueOutput(`You are too dead for that.`);
}
});
}
})
.until(function() {
return count(`act1-falling`) == 0;
})
.start();
understand(`rule for loading while falling to your death`)
.book(Rulebook.RULE_BEFORE)
.verb(`load`)
.in(`air-falling`)
.doOnce(function(self,action){
ECS.runInternalAction(`act1-load`);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
},
});
// Register module
ECS.m(act1);
//
// Act II Module: Ascent
//
var act2 = new Module(`Act2`, {
init: function() {
ECS.e(`station`, [`region`], {
'background':`#303030`
});
ECS.e(`lunar-surface`, [`region`], {
'background':`#303030`
});
ECS.e(`station-airlock`, [`place`], {
'name':`Station Airlock`,
'region':`station`,
'exits':{
'w':`station-exterior`,
'e':`station-hub`,
},
'descriptions':{
'default':`Inside the airlock, doors lead {{east}} into the station proper, and {{west}} onto the lunar surface. {{scenery}}`
}
});
ECS.e(`station-gondola-chamber`, [`place`], {
'name':`Station Gondola Chamber`,
'region':`station`,
'exits':{
'u':`cable-lower`,
'd':`station-hub`,
},
'descriptions':{
'default':`Inside the gondola chamber, a small hatch leads {{down}} into the station proper, while the cables lead {{up}} into the lunar sky. {{scenery}}`
}
});
understand(`rule for climbing up cable`)
.book(Rulebook.RULE_BEFORE)
.in(`station-gondola-chamber`)
.verb(`go`)
.modifier(`u`)
.do(function(self,action) {
queueGMOutput(`You don't need to go back up there right now. You can't get back into the observation room without restoring power to the gondola anyway.`);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
ECS.e(`station-hub`, [`place`], {
'name':`Station Hub`,
'region':`station`,
'exits':{
'w':`station-airlock`,
'u':`station-gondola-chamber`,
'n':`station-sleeping-pod`,
's':`station-hydroponics`,
'e':`station-laboratory`
},
'descriptions':{
'default':`Dim emergency lights illuminate the edges of the spacious station hub. Labeled doorways lead to the {{north}} (Sleeping Pods), {{south}} (Hydroponics), {{east}} (Laboratory), and {{west}} (Airlock). A sturdy ladder climbs {{up}} into the gondola chamber. {{scenery}}`
}
});
localScenery([`emergency lights`,`lights`], `Dim.`);
ECS.e(`aethernaut-corpse`, [`scenery`], {
'name':`aethernaut corpse`,
'nouns':[`corpse`,`aethernaut corpse`,`Henry`],
'spawn':`station-hub`,
'descriptions':{
'default':`A fully-suited aethernaut corpse. Appears to have died of blunt trauma, or maybe asphyxiation or rabies. You're not up on space diseases. It looks like they were trying to get into the {{tag "eastern doorway" command='east' classes='direction'}}.`,
'scenery':`There's an {{nametag 'aethernaut-corpse'}} here.`
}
});
ECS.e(`station-cupola`, [`place`], {
'name':`Cupola`,
'region':`station`,
'exits':{
'd':`station-laboratory`,
},
'descriptions':{
'default':`A cupola glass panes give you an almost unhindered view of the lunar surface in a complete circle around the station. Outside you can see rocks, regolith, more rocks, and not a single moon person. The tether disappears into the void above you. A ladder leads {{down}} into the laboratory. {{scenery}}`
}
});
localScenery([`surface`,`lunar surface`], `Rocks, regolith, more rocks.`);
localScenery([`regolith`], `A mixture of deposits atop the moon's rocky surface.`);
localScenery([`rocks`,`more rocks`], `Rocks.`);
localScenery([`moon person`], `Not. One. Except you I guess.`);
localScenery([`ladder`], `Goes down (from here) and up (from below). Bi-directional technology.`);
localScenery([`tether`], `Looks about the same as it did when you climbed down it.`);
ECS.e(`station-hydroponics`, [`place`], {
'name':`Hydroponics`,
'region':`station`,
'exits':{
'n':`station-hub`,
},
'descriptions':{
'default':`Enclosed growing units line the walls between various tanks and bits of machinery. It looks like the aethernauts are mostly subsisting on potatoes. The station hub lies to the {{n}}. {{scenery}}`
}
});
localScenery([`growing units`], `Each unit has built-in water and lighting. They appear to be kept sealed tight until harvesting. The units are currently off, due to the lack of power in the station.`);
ECS.e(`potatoes`, [`scenery`], {
'name':`space potatoes`,
'nouns':[`potatoes`,`potato`,`space potatoes`,`space potato`],
'spawn':`station-hydroponics`,
'descriptions':{
'default':`Some space potatoes in varying degrees of growth.`
},
'onAction.TAKE':function(action){
queueGMOutput(`You don't really need the space potatoes.`);
}
});
ECS.e(`station-laboratory`, [`place`], {
'name':`Laboratory`,
'region':`station`,
'exits':{
'w':`station-hub`,
'u':`station-cupola`,
},
'descriptions':{
'default':`A whole bunch of nerd stuff fills this large chamber. Most of it was specific to the original concept for this module, so it wouldn't really make sense to describe it in this universe. A ladder leads {{up}} into a small cupola. The station hub lies to the {{west}}. {{scenery}}`
}
});
localScenery([`nerd stuff`], `Beakers and oscilloscopes and whatnot.`);
localScenery([`beakers`], `The glass kind, not the Muppet.`);
localScenery([`oscilloscopes`], `Silly scopes, my science teacher used to call them. Hilarious.`);
ECS.e(`canister`, [], {
'name':`fuel canister`,
'nouns':[`canister`,`fuel canister`],
'spawn':`station-laboratory`,
'state':`empty`,
'descriptions':{
'default':`A fuel canister ({{target.state}}).`
}
});
ECS.e(`station-sleeping-pod`, [`place`], {
'name':`Sleeping Pod`,
'region':`station`,
'exits':{
's':`station-hub`,
},
'descriptions':{
'default':`A small sleeping pod with bunks for four aethernauts. It currently houses none, unless you count yourself. The station hub lies to the {{south}}. {{scenery}}`
}
});
ECS.e(`station-exterior`, [`vacuum`], {
'name':`Station Exterior`,
'region':`lunar-surface`,
'exits':{
'e':`station-airlock`,
'n':`surface-moon`,
},
'descriptions':{
'default':`The rocky lunar surface extends as far as you can see. The station is the largest visible sign of human life, but you can also make out bootprints and narrow wheeled tracks in the regolith. Most of the tracks lead to or from the {{north}}, while the station airlock lies to the {{east}}. {{scenery}}`
}
});
localScenery([`bootprints`,`boot prints`, `Mostly aethernaut boots, plus a couple Baron boots.`]);
localScenery([`baron boots`,`baron bootprints`], `The Baron's bootprints are smoother than the others, perhaps not intended for outdoor use.`);
localScenery([`tracks`,`narrow wheeled tracks`], `Made by either a vehicle or cart, which is not in evidence.`);
ECS.e(`surface-moon`, [`vacuum`], {
'name':`The Moon`,
'region':`lunar-surface`,
'exits':{
's':`station-exterior`,
'n':`surface-crash-site`,
},
'descriptions':{
'default':`You are standing on the moon. The tracks you've been following lead back and forth between the station to the {{south}} and an unknown point to the {{north}}. {{scenery}}`
}
});
ECS.e(`surface-crash-site`, [`vacuum`], {
'name':`Crash Site`,
'region':`lunar-surface`,
'exits':{
's':`surface-moon`,
'in':`baron-ship`,
},
'descriptions':{
'default':`A damaged craft sits nestled in a small crater. It appears to have made a controlled landing after some kind of emergency. The {{tag 'entryway' command='in'}} is open. {{scenery}}`
}
});
localScenery([`entryway`], `The ship doesn't have a door or hatch, per se. Rather, a portion of its exterior has simply reshaped itself to allow passage.`);
localScenery([`craft`,`damaged craft`,`ship`,`damaged ship`,`crashed craft`,`crashed ship`], `The damage appears mostly superficial. The craft might be aetherworthy again with a bit of light puzzle solving.`);
ECS.e(`baron-ship`, [`place`,`vacuum`], {
'name':`The Craft`,
'region':`lunar-surface`,
'exits':{
'out':`surface-crash-site`,
},
'descriptions':{
'default':`The interior of the craft is far more sophisticated than the aethernaut station. Complex instruments and displays line the interior, surrounding a heavily padded seat. None of the equipment appears to be powered on. {{scenery}}`
}
});
localScenery([`seat`,`padded seat`,`heavily padded seat`], `Padded for comfort during high-G maneuvers.`);
localScenery([`instruments`,`complex instruments`], `Scopes and meters and so forth.`);
localScenery([`displays`], `Currently blank.`);
ECS.e(`craft-button-red`, [`device`,`scenery`], {
'name':`red button`,
'spawn':`baron-ship`,
'nouns':[`shiny red button`,`red button`,`button`],
'descriptions':{
'default':`A shiny red button.`,
'scenery':`There's a {{tag 'shiny red button' command='boop button'}} that seems particularly interesting.`
},
'device-states':[],
'onAction.PRESS':function(data){
// Alert user to change
queueGMOutput(`You press the red button.`);
ECS.getModule(`Act3`).onGameStart();
processDeferredOutputQueue();
// Don't perform default
return false;
}
});
},
// Intro script
'onGameStart':function(){
var sequence = new Sequence;
sequence.add(function() {
// Don't parse rules during the intro
Rulebook.pause();
// Set game stage
ECS.setData(`stage`, `act2`);
// Disable quests
ECS.getSystem(`quests`).onTick = function(){};
// Error
queueGMOutput(`Uh-oh. Something's wrong with my computer. One moment please.`, `auto`);
queueGMOutput(`Well, that didn't work either. The Act 2 module isn't loading.`, `auto`);
queueGMOutput(`Ok, here's what we're going to do. I have a backup here, but it's an old one, back from when the game was more of a sci-fi thing in general. There might be some tonal mismatch, but I'll tweak it a bit as we go, it'll be fine.`, `auto`);
queueGMOutput(`Unfortunately, this might break some of the questline stuff, so I'm turning that off to avoid problems. Also no backtracking, so that door you just spent all that time unlocking is locked again.`, `auto`);
queueGMOutput(`Alright, switching over...now.`, `auto`);
ECS.annihilateEntity(`ice-key`);
var door = ECS.getEntity(`black-door`);
door.isLocked = true;
door.isOpen = false;
sequence.next();
});
sequence.add(function(){
// Title box
queueOutput(`{{box 'ACT 2' 'ASCENT'}}`, 2000, {'effect':`fade`});
Engine.queueEvent(function(){
Display.loadTheme(`classic`);
Sound.playMusic(`odi`);
});
// Resume rule processing
Rulebook.start();
});
sequence.start();
},
});
// Campaign modules/components
act2.c(`vacuum`, {
'dependencies':[`place`],
'music':`silence`,
});
understand(`rule for entering vacuum unsafely`)
.verb(`move`)
.if(function(self,action){
// If we're in stage 2 and the location in the direction the player is moving is in vacuum and the player either doesn't have the suit or isn't wearing it
var d = getCanonicalDirection(action.direction);
return ECS.getData(`stage`) == `act2` && player.location().hasExit(d) && ECS.getEntity(player.location().getExit(d)).is(`vacuum`) && (!player.hasChild(`suit`) || !ECS.getEntity(`suit`).isWorn);
})
.do(function(self,action){
queueGMOutput(`That would be fatal without proper environmental protection.`);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
// Register module
ECS.m(act2);
//
// Act III Module: Ascent
//
var act3 = new Module(`Act3`, {
init: function() {
// Dark forest Track
var woodsTrack = new Track(`woods`,`Wild Woods 2.ogg`,{
'loopSeek':26.395,
'captions':[
new Caption(1000,`Strings starting a party`),
new Caption(22000,`Flute out of nowhere`),
new Caption(111000,`Music settles`),
]
});
// Abandoned cabin track
var abandonedCabinTrack = new Track(`abandoned-cabin`, `Abandoned Cabin.ogg`,{
'captions':[
new Caption(1000,`A melancholic piece plays on a Rhodes piano`),
new Caption(29000,`Actually, it might be a Rhodes knockoff`),
new Caption(42000,`It's like, really, really sad`)
]
});
// Town Exterior track
var townExteriorTrack = new Track(`town-exterior`, `Town Exterior.ogg`,{
'captions':[
new Caption(1000,`A gaggle of strings glide along the watery surface of this song`),
new Caption(13000,`Calm on the surface, paddling like billy-o underneath`),
new Caption(65000,`Oh, right, there's a bit of piano in here too`),
new Caption(104000,`It's smooth like a pumpkin spice latté (^▽^)`)
]
});
// Town Interior track
var townInteriorTrack = new Track(`town-interior`, `Town Interior.ogg`,{
'captions':[
new Caption(1000,`It's like the music outside, but now you're inside so it's muffled`)
]
});
// Tavern 1 track
var Tavern1Track = new Track(`first-tavern-piece`, `Tavern 1.ogg`,{
'captions':[
new Caption(1000,`A jazzy jazz quartet play a smooth and sultry tune. Drums, bass, keyboard, and a soloist`),
new Caption(31000,`The saxophone soloist starts to play`)
]
});
// Tavern sans-bass track
var Tavern2Track = new Track(`second-tavern-piece`, `Tavern 3 - No Bassist.ogg`,{
'captions':[
new Caption(1000,`An up-tempo song plays, with horns, drums, and keyboard`),
new Caption(10000,`It sounds very strange without a bass line… let's be honest, it's just not good`)
]
});
// Cemetery track
var cemeteryTrack = new Track(`cemetery`, `Cemetery2.ogg`,{
'captions':[
new Caption(1000,`This song is scary, but kinda fun and corny, like halloween`),
new Caption(20000,`It's like a Tim Burton film, if this were the 90s`),
new Caption(28000,`A new theme... kind of a weird direction for that to go`),
new Caption(40000,`Okay, we're back on track now`),
new Caption(53000,`You know what else is scary? Clowns.`)
]
});
// Mountain track
var mountainTrack = new Track(`mountain`, `Mountain.ogg`,{
'captions':[
new Caption(1000,`Some war drums, and war... flutes? are playing`),
new Caption(30000,`And some strings I think. Hey, are war flutes even a thing?`),
new Caption(58000,`Oh yeah, there's a horn in there too`)
]
});
// Elevator Music track
var elevatorMusicTrack = new Track(`elevator-music`, `Elevator Music (speaker).ogg`,{
'captions':[
new Caption(1000,`Smooth Latin jazz is being blared out of the world's crustiest speaker`),
new Caption(38000,`This elevator kinda smells bad`)
]
});
// Baron's Fight Theme track
var baronsFightThemeTrack = new Track(`barons-fight-theme`, `Baron's Fight Theme.ogg`,{
'captions':[
new Caption(1000,`A super serious drumline plays`),
new Caption(21000,`There's a strange, windy sound building`),
new Caption(31000,`And the bass is here`),
new Caption(46000,`A synth melody plays`),
new Caption(72000,`Are those ghost noises? 👻`)
]
});
// End Credits track
var endCreditsTrack = new Track(`end-credits`, `Piano Fugue.ogg`,{
'captions':[
new Caption(1000,`A piano fugue plays`),
new Caption(23000,`Okay, it's not technically a fugue anymore, but you get the idea`),
new Caption(48000,`The En- Oh, wait, there's more. Sorry about that`)
]
});
ECS.e(`woods`, [`region`], {
'music':`woods`,
'background':`#1b390a`,
});
ECS.e(`town`, [`region`], {
'music':`town-exterior`,
'background':`#593b21`,
});
ECS.e(`town-interior`, [`region`], {
'music':`town-interior`,
'background':`#755a38`,
});
ECS.e(`cemetery`, [`region`], {
'music':`cemetery`,
'background':`#553200`,
});
ECS.e(`mountain`, [`region`], {
'music':`mountain`,
'background':`#444d53`,
});
ECS.e(`castle`, [`region`], {
'music':`mountain`,
'background':`#5e5e5e`,
});
ECS.e(`rustic-clearing`, [`place`], {
'name':`Rustic Clearing`,
'region':`woods`,
'exits':{
//'n':`cabin`,
'e':`game-trail`,
},
'descriptions':{
'default':`A small grove within the otherwise-dense forest. The air is cold and still in the darkness. You can make out a path (kind of?) to the {{e}}. High weeds and tangled vines surround the grove. The Baron's pod sits in a muddy crater nearby, smoldering. A ramshackle cabin sits uselessly among the weeds, depressing and non-interactive. {{scenery}}`
}
});
localScenery([`high weeds`,`weeds`], `Organic and free-range.`);
localScenery([`tangled vines`,`vines`], `Spiny and resilient.`);
localScenery([`pod`,`baron's pod`], `The Baron's pod is now completely destroyed instead of just heavily damaged. You didn't make a very good landing.`);
localScenery([`crater`,`muddy crater`], `Approximately pod-sized.`);
localScenery([`cabin`,`ramshackle cabin`], `A monument to tetanus.`);
ECS.e(`cabin`, [`place`], {
'name':`Ramshackle Cabin`,
'region':`woods`,
'exits':{
's':`rustic-clearing`,
'out':`rustic-clearing`,
},
'descriptions':{
'default':`The cabin has seen better decades. Those decades were also not kind. The cabin can be best described as ramshackle, possibly decrepit. The walls and floor are bare save for a thick layer of grime. The windows may have once held shutters, certainly never glass. {{scenery}}`
}
});
localScenery([`walls`], `Bare.`);
localScenery([`floor`], `Bare.`);
localScenery([`grime`,`thick layer of grime`,`layer of grime`], `Fifty years of dust, leaves, water and animal droppings have congealed into a pervasive grime.`);
localScenery([`windows`], `Just holes in the walls, really.`);
ECS.e(`game-trail`, [`place`], {
'name':`Game Trail`,
'region':`woods`,
'exits':{
'w':`rustic-clearing`,
'e':`west-road`,
},
'descriptions':{
'default':`The dim path leads from the {{w}} to the {{e}}. Less of a path than a game trail, really, worn out by frequent animal traffic. There are very likely to be plot-relevant destinations in both directions. {{scenery}}`
}
});
// Not accessible until player has reached town
ECS.e(`goblin-camp`, [`place`], {
'name':`Goblin Camp`,
'region':`woods-night`,
'exits':{
's':`game-trail`,
},
'descriptions':{
'default':`A well-established encampment of several hundred goblins. It's not unlike the goblin pit, if you took all the goblins and put them in the woods instead of a pit and waited for the sorts of things that happen if you leave several goblins in the woods for a while. {{scenery}}`
}
});
ECS.e(`west-road`, [`place`], {
'name':`West Road`,
'region':`woods`,
'exits':{
'w':`game-trail`,
'e':`town-square`
},
'descriptions':{
'default':`The cobble road runs along the western shore of the river for several miles, eventually crossing just shy of the mountain before heading {{e}} meekly into the nearby town. {{scenery}}`
}
});
ECS.e(`town-square`, [`place`], {
'name':`Town Square`,
'region':`town`,
'exits':{
'w':`west-road`,
'nw':`smoky-mountain`,
'n':`city-hall`,
's':`baker-street`,
'e':`cobble-street`,
},
'descriptions':{
'default':`The hustle and bustle of a typical day in town unfolds around you. This is no snowglobe dreamscape. The villagers here are jaded and weary, as they should be. Plodding across dirty cobblestones to peddle wares, haggle wares, purchase wares, resell wares, and so on. Life among the commoners is less about the whos than the wares. The West Road is off to the {{west}} outside the {{nametag 'local-scenery-town-square-city gates'}}. {{nametag 'local-scenery-town-square-city hall' print='City Hall'}} lies to the {{n}}, {{nametag 'local-scenery-town-square-smoky mountain' print='The Smoky Mountain'}} to the {{nw}}, Baker Street to the {{s}} and Cobble Street to the {{e}}. {{scenery}}`
}
});
localScenery([`villagers`], `The villagers are busy doing villager things.`);
localScenery([`smoky mountain`,`the smoky mountain`], `The Smoky Mountain is the town's premier drinking spot, open 24/7.`);
localScenery([`city hall`], `An unassuming office building tucked in the corner between The Smoky Mountain and the city gates.`);
localScenery([`city gates`,`gates`], `'Gates' is a bit inaccurate. It looks like someone (and you have a theory about that) tore the actual gates to shreds, leaving what would be more aptly described as a gateway. A couple guards are chilling there, minding their own business.`);
ECS.e(`guards`, [`living`], {
'name':`town guards`,
'article':`the`,
'nouns':[`guards`,`city guards`,`town guards`],
'spawn':`town-square`,
'descriptions':{
'default':`In a word, bored.`
},
'onAction.TALK':function(){
queueGMOutput(`The guards suggest that you mind your own business if you know what's good for you.`);
},
'onAction.ATTACK':function(){
queueGMOutput(`The guards flee in a panic, and you never see them again.`);
ECS.annihilateEntity(`guards`);
}
});
understand(`rule for entering town square for the first time`)
.book(`after`)
.verb(`move`)
.in(`town-square`)
.do(function(self, action) {
queueGMOutput(`Jack and Jane are here, arguing about...I don't know what, actually. I'm not sure I care.`,`auto`);
queueCharacterOutput(`jane`, `The grid is a classic.`, `auto`);
queueCharacterOutput(`jack`, `I prefer the spiral, personally.`, `auto`);
queueCharacterOutput(`jane`, `Spiral?`, `auto`);
queueCharacterOutput(`jack`, `You start in the center of your search area, and go in an outward spiral until you find it.`, `auto`);
queueCharacterOutput(`jane`, `Fun for a robot, perhaps, but not a person. I'm bored already just thinking about it.`, `auto`);
queueCharacterOutput(`jack`, `We should consider more specific solutions. We've talked about the general case, but we have the advantage of domain knowledge. Some locations are impossible, or at least unlikely.`, 4000);
queueCharacterOutput(`jane`, `A ranked list of potentials could speed up the search, certainly.`, `auto`);
queueCharacterOutput(`jack`, `Falling back to a more general case when we feel we're close.`, `auto`);
queueGMOutput(`Per their usual rudeness, the siblings take a while to notice your presence.`, `auto`);
queueCharacterOutput(`jane`, `Says the GM who failed to adequately test their module before the playtest. What's the retcon counter up to?`, `auto`);
queueCharacterOutput(`jack`, `Lovely to see you both again, of course.`, `auto`);
queueCharacterOutput(`jane`, `Of course. We're a bit preoccupied. We bought a new magazine of riddles, but apparently lost it almost immediately.`, `auto`);
queueCharacterOutput(`jack`, `And since we're visiting this lovely town for the first time today, naturally we took the tour. We've been almost everywhere, which makes it a bit difficult to narrow down our search space.`, 4000);
queueCharacterOutput(`jane`, `In retrospect, the four hours we've spent discussing strategy was likely more than enough time to find our wayward belonging even if employing the least efficient strategy.`, `auto`);
queueGMOutput(`Is this going somewhere?`, 4000);
queueCharacterOutput(`jane`, `Not really. If you come across our magazine though, we'd love to have it back.`, `auto`);
queueGMOutput(`The siblings return to their important conversation.`, `auto`);
self.stop();
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
understand(`rule for bringing book back to twins`)
.do(function(self,action){
});
// Fastidious clerk is looking for someone to gather petition signatures to change name of City Hall to Town Hall, as it's a town, not a city
// Due to the mayor's reticence, this will require signatures from every person in town
ECS.e(`city-hall`, [`place`], {
'name':`City Hall`,
'region':`town-interior`,
'exits':{
's':`town-square`,
},
'descriptions':{
'default':`Filled with the sounds and smells of a hundred years of beauracracy, the city hall represents law and order in this provincial town. Wizened clerks glance disinterestedly in your direction, while normal-aged clerks glance in your direction with measurable apprehension. Someday those clerks will also grow wizened, and lose their interest not only in you, but in all things other than paperwork and procedures. You've seen it happen before, with your uncle. It's not as dramatic an end as cancer or murder or a dragon, but it carries a unique horror that is difficult to explain to anyone who hasn't experienced it firsthand. Your parents explained it as a phone trying to trickle charge on a mere 0.5 amps; it has the right idea, but the juice is just insufficent. You always assumed that someday you'd learn how many amps a phone needs to trickle charge, or what a phone is, but the opportunity never presented itself. Maybe you never applied yourself enough. It's an ongoing source of confusio
n and shame, one which you home to someday remedy so that when you go back home, rich with experiences gained on your adventures, you can ask your uncle what caused this apathy, this indifference to life. Maybe there's something you could do to help. {{scenery}}`
}
});
localScenery([`clerks`], `An assortment of beauracrats.`);
localScenery([`wizened clerks`], `Outside of work, I'm sure they're lovely people.`);
localScenery([`normal-aged clerks`,`normal clerks`], `They still have enough to lose that they are capable of fear.`);
ECS.e(`city-hall-potted-plant`, [`scenery`], {
'name':`potted plastic plant`,
'nouns':[`plant`,`potted plant`,`plastic plant`,`plastic potted plant`,`potted plastic plant`],
'spawn':`city-hall`,
'descriptions':{
'default':`A completely innocent plastic potted plant. Nothing unusual, interesting, or suspicious about it. Standard coating of dust.`,
'scenery':`A {{tag 'potted plant' command='inspect potted plant'}} sits in the corner, minding its own business.`,
},
'onAction.TAKE':function(){
if(this.locationIs(`city-hall`)) {
queueGMOutput(`You brazenly steal the potted plastic plant in full sight of the clerks. None of them seem to care.`);
} else {
queueGMOutput(`You pick up the plastic potted plant.`);
}
ECS.moveEntity(this, player);
}
});
ECS.e(`smoky-mountain`, [`place`], {
'name':`The Smoky Mountain`,
'region':`town`,
'exits':{
'se':`town-square`,
'out':`town-square`,
'nw':`winding-path`,
},
'descriptions':{
'default':`Somewhat disappointingly, this is just a tavern, not an actual mountain. The real mountain is out the back door to the {{nw}}. Seems a bit misleading, really. There are some {{nametag "local-scenery-smoky-mountain-patrons"}} here, and also a {{nametag "bartender" print="bartender"}}. The town square is outside to the {{se}}. {{scenery}}`
},
'music':`first-tavern-piece`,
});
localScenery([`patrons`], `Nondescript background characters, of no significance. Don't tell them I said that.`);
ECS.e(`bartender`, [`scenery`,`living`], {
'name':`The Bartender`,
'nouns':[`bartender`,`martha`,`martha von dondelier`],
'spawn':`smoky-mountain`,
'descriptions':{
'default':`The Smoky Tavern's longtime bartender and founder, Martha Von Dondelier, heir to the throne. She notices you noticing her and acknowledges it with a completely impassive lack of eye contact.`,
},
'listInRoomDescription':false,
'justBarkeeperThings':[
`Martha grunts noncommittally.`,
`The barkeeper ignores you, or perhaps doesn't notice you.`,
`Martha indicates, in an expressive fashion, that she's too busy to speak with commoners right now.`,
`The barkeep looks at a dirty glass, but decides not to wash it. Whether this is out of a sense of royal elitism or cliche avoidance, you're not sure.`,
`Martha gives you a rare smile, which if kept mint in box would net you a significant sum on the secondary market.`,
`The barkeeper shakes her head enigmatically.`,
`Martha does something stereotypically barkeeperish, causing several patrons to chuckle.`
],
'onAction.TALK':function() {
return this.justBarkeeperThings[dice(this.justBarkeeperThings.length - 1)];
}
});
understand(`rule for ordering a drink`)
.in(`smoky-mountain`)
.text([`order`,`order a drink`,`order drink`])
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`You don't have valid currency with which to purchase a drink. The barkeeper seems to expect you to leave now.`);
})
.start();
ECS.e(`the-band`, [`living`], {
'name':`The Band`,
'article':``,
'nouns':[`band`,`the band`,`jazz quartet`,`jazz trio`],
'spawn':`smoky-mountain`,
'descriptions':{
'default':`A skilled jazz quartet.`,
'scenery':`A jazz quartet is playing in the corner.`
},
'onAction.TALK':function(){
queueGMOutput(`It's rude to talk to the band while they're playing.`);
}
});
understand(`rule for leaving the tavern for the first time`)
.book(`before`)
.verb(`move`)
.in(`smoky-mountain`)
.doOnce(function(self,action){
action.mode = Rulebook.ACTION_NONE;
var band = ECS.getEntity(`the-band`);
band.descriptions.default = `A skilled jazz trio. They used to be a quartet, but recently had a dramatic falling-out with their bassist.`;
band.descriptions.scenery = `A jazz trio is playing in the corner.`;
ECS.getEntity(`smoky-mountain`).music = `second-tavern-piece`;
})
.start();
ECS.e(`cobble-street`, [`place`], {
'name':`Cobble Street`,
'region':`town`,
'exits':{
'w':`town-square`,
's':`bakery`,
'ne':`vacant-lot`,
'se':`general-store`,
'n':`cemetery-entrance`,
},
'descriptions':{
'default':`This road seems to have been cobbled together from several thousand assorted stones. Seems like an odd way to build a street. In no particular order, you can go {{w}} to the town square, {{s}} to the bakery, {{ne}} to a vacant lot, {{se}} to the general store, or {{n}} to the cemetery. There's probably an optimal order to visit each of these locations if you're speedrunning the game. If you are speedrunning the game though, you'd skip all of this anyway by using the debug "teleport" command I left in. So I guess the optimal order only matters if you're doing a 100% or warpless category. {{scenery}}`
}
});
localScenery([`stones`,`assorted stones`,`cobblestones`], `A cobble of thousand stones.`);
ECS.e(`bakery`, [`place`], {
'name':`Bakery`,
'region':`town-interior`,
'exits':{
'n':`cobble-street`,
},
'descriptions':{
'default':`The sheer amount of bread in here. Wow. The portly {{nametag 'baker'}} clearly appreciates your amazed expression, but also wonders if you're going to buy something or just steal bread like the local kids are wont to do. {{scenery}}`
}
});
ECS.e(`baker`, [`living`], {
'name':`Baker`,
'nouns':[`baker`,`portly baker`],
'spawn':`bakery`,
'descriptions':{
'default':`Cheerful and slightly doughy.`
},
'listInRoomDescription':false,
'onAction.TALK':function(){
queueGMOutput(`The Baker is too busy baking to talk right now.`);
}
});
ECS.e(`vacant-lot`, [`place`], {
'name':`Vacant Lot`,
'region':`town`,
'exits':{
'sw':`cobble-street`,
},
'descriptions':{
'default':`There's a lot of vacancy here. You can go back to Cobble St. to the {{sw}}, and I expect at some point you will. {{scenery}}`
}
});
localScenery([`rose`,`stray rose`], `I said there isn't one. Seriously, just go somewhere else.`);
ECS.e(`riddle-magazine`, [`readable`], {
'name':`riddle magazine`,
'nouns':[`magazine`,`riddles`,`riddle magazine`,`magazine of riddles`],
'spawn':`vacant-lot`,
'descriptions':{
'default':`A cheaply printed magazine of riddles. It says 'Magazine of Riddles, Vol. 1' on the front. No author listed.`
},
'onAction.READ':function(){
queueGMOutput(`The inside of the magazine appears to be completely blank. The siblings probably know what it means.`);
},
});
ECS.e(`finger-bone`, [], {
'name':`finger bone`,
'nouns':[`bone`,`finger bone`],
'spawn':null,
'descriptions':{
'default':`Definitely a fake human finger bone. You wonder who it belonged to. Some sort of expert on dead people might have more information.`,
'smell':`Smells real.`
}
});
understand(`rule for giving the book of riddles to jane`)
.verb(`give`)
.in(`town-square`)
.attribute(`target`,`=`,`riddle-magazine`)
.attribute(`nouns`,`containsEntity`,[`jane`,`jack`,`jane-and-jack`])
.do(function(self,action){
queueGMOutput(`The siblings gratefully take the magazine.`, `auto`);
queueCharacterOutput(`jack`, `Oh thank you!`, `auto`);
queueCharacterOutput(`jane`, `We had just decided that the optimal route was to head straight back to the shop and buy a new one.`, `auto`);
queueCharacterOutput(`jack`, `Here, for your trouble.`, `auto`);
queueGMOutput(`Jack hands you a finger bone. You don't know where he got it from, neither do I, and if anyone asks it's a fake.`, `auto`);
ECS.moveEntity(`finger-bone`, player);
ECS.getEntity(`gravekeeper`).conversation.enableNode(`bone`);
queueCharacterOutput(`jane`, `Come see us again some time, maybe we'll have some new riddles for you. Better ones this time.`, `auto`);
queueGMOutput(`The siblings wander off, engrossed in their stupid blank magazine. If we're lucky, we won't see them again.`, `auto`);
ECS.moveEntity(`riddle-magazine`, `jane-and-jack`);
ECS.getEntity(`jane-and-jack`).move(`surface-moon`);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
ECS.e(`general-store`, [`place`], {
'name':`General Store`,
'region':`town-interior`,
'exits':{
'nw':`cobble-street`,
},
'descriptions':{
'default':`Though tidy, a hundred years of assorted cheeses, fruits, spices, tobacco leaves and sundry inedibles have left the general store with a rich aroma. Save for the occasional creak from the floorboards, it's a still and quiet place. The shopkeeper watches you sidelong, more with curiosity than animosity. Cobble Street is back to the {{nw}}. {{scenery}}`
}
});
localScenery([`cheeses`,`fruits`,`spices`,`tobacco`,`tobacco leaves`,`inedibles`,`sundry inedibles`], `There's a lot of stuff in here, and none of it is important to your quest.`);
localScenery([`floorboards`], `Boardy.`);
ECS.e(`shopkeeper`, [`living`], {
'name':`shopkeeper`,
'nouns':[`shopkeeper`,`reginald`],
'spawn':`general-store`,
'descriptions':{
'default':`A kindly shopkeeper. He's too busy to deal with you right now, but seems to feel that if you're in here doing nothing, you're not out there causing trouble.`
},
'onAction.TALK':function(){
queueGMOutput(`The shopkeeper shoos you away with a gentle smile and a shake of his head.`);
}
});
understand(`rule for buying something in the general store`)
.in(`general-store`)
.regex(/(buy|purchase) (.*)/i)
.do(function(self,action){
action.mode = Rulebook.ACTION_CANCEL;
queueGMOutput(`As you don't have any acceptable currency, you're unable to buy anything.`);
})
.start();
ECS.e(`baker-street`, [`place`], {
'name':`Baker Street`,
'region':`town`,
'exits':{
'n':`town-square`,
'nw':`tavern`,
'w':`smithy`,
'sw':`tailor`,
'ne':`back-alley`,
},
'descriptions':{
'default':`This aptly-named street is home to a tavern to the {{nw}}, a smithy to the {{w}}, and a tailor to the {{sw}}. You can also head back to the town square to the {{n}}, or duck down a suspicious back alley to the {{ne}}. {{scenery}}`
},
'herrings':function() {
if((count(`visited-baker-street-smithy`) + count(`visited-baker-street-tavern`) + count(`visited-baker-street-tailor`)) >= 3) {
return ` Doesn't seem like there's much activity on this street.`;
}
return ``;
}
});
understand(`rule for entering smithy`)
.in(`baker-street`)
.verb(`go`)
.modifier(`w`)
.do(function(self,action) {
countOnce(`visited-baker-street-smithy`);
var herrings = ECS.getEntity(`baker-street`).herrings();
queueGMOutput(`The smithy entrance is barred. A posted notice reads 'CLOSED UNTIL FURTHER NOTICE PENDING TOP SECRET PROJECT FOR THE BARON'.` + herrings);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
understand(`rule for entering tavern`)
.in(`baker-street`)
.verb(`go`)
.modifier(`nw`)
.do(function(self,action) {
countOnce(`visited-baker-street-tavern`);
var herrings = ECS.getEntity(`baker-street`).herrings();
queueGMOutput(`The tavern entrance is barred. A posted notice reads 'COMING SOON'.` + herrings);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
understand(`rule for entering tailor`)
.in(`baker-street`)
.verb(`go`)
.modifier(`sw`)
.do(function(self,action) {
countOnce(`visited-baker-street-tailor`);
var herrings = ECS.getEntity(`baker-street`).herrings();
queueGMOutput(`The tailor's entrance is barred. A posted notice reads 'FORGOT TO MAKE MORE CLOTHES. WHOOPS. TRY BACK NEXT WEEK'.` + herrings);
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
ECS.e(`smithy`, [`place`], {
'name':`Smithy`,
'region':`town`,
'exits':{
'e':`baker-street`,
},
'descriptions':{
'default':`How did you even get in here? {{scenery}}`
}
});
// Years-running feud with the newer Smoky Mountain
// Proprieters of the Tavern prefer the old style, where businesses were simply named after what they do. Tavern, tailor, smithy.
ECS.e(`tavern`, [`place`], {
'name':`Tavern`,
'region':`town`,
'exits':{
'se':`baker-street`,
},
'descriptions':{
'default':`How did you even get in here? {{scenery}}`
}
});
ECS.e(`tailor`, [`place`], {
'name':`Tailor`,
'region':`town`,
'exits':{
'ne':`baker-street`,
},
'descriptions':{
'default':`How did you even get in here? {{scenery}}`
}
});
ECS.e(`back-alley`, [`place`], {
'name':`Back Alley`,
'region':`town`,
'exits':{
'ne':`cobble-street`,
'sw':`baker-street`,
's':`magic-shop`,
},
'descriptions':{
'default':`A cramped back alley connects Cobble St. (to the {{ne}}) to Baker St. ({{sw}}) {{scenery}}`
}
});
// Cleverly concealed beneath a brightly-colored sign reading 'MAGIC SHOP' is a doorway to a magic shop.;
ECS.e(`magic-shop`, [`place`], {
'name':`Magic Shoppe`,
'region':`town-interior`,
'exits':{
'n':`back-alley`,
},
'descriptions':{
'default':`The interior of the magic shop may as well be from another world. Pure white walls emanate a soft, uniform glow, reducing any shadows to a faint afterthought. There is no shopkeeper here. {{scenery}}`
}
});
ECS.e(`magic-shop-bag`, [`scenery`,`container`], {
'name':`small paper bag`,
'nouns':[`small paper bag`,`paper bag`,`bag`],
'spawn':`magic-shop`,
'descriptions':{
'default':`A small paper bag with 'FREE MAGIC' written on the outside. {{inventory}}`,
'scenery':`In the center of the room, a small paper bag rests on the floor. Someone has scrawled 'FREE MAGIC' on the side.`,
},
'listInRoomDescription':true
});
ECS.e(`magic`, [`edible`], {
'name':`some magic`,
'nouns':[`some magic`,`magic`],
'spawn':`magic-shop-bag`,
'descriptions':{
'default':`An indeterminate quantity of magic.`,
},
'onAction.EAT':`You consume the magic. It's a bit fizzy. Nothing seems to happen immediately, but who knows. It's magic.`
});
ECS.e(`cemetery-entrance`, [`place`], {
'name':`Cemetery`, // `Cemetery Entrance`
'region':`cemetery`,
'exits':{
's':`cobble-street`,
//'n':`cemetery-north`,
},
'descriptions':{
'default':`The cemetery is quite respectable by small-town standards. Neatly-spaced {{nametag 'headstones'}}, {{nametag 'local-scenery-cemetery-entrance-fences' print='wrought-iron fences'}}, a suitably imposing {{tag 'crypt' command='look at crypt'}}; it's very nice. {{scenery}}`
}
});
ECS.e(`headstones`, [`readable`,`scenery`], {
'name':`headstones`,
'nouns':[`headstones`],
'spawn':`cemetery-entrance`,
'descriptions':{
'default':`Neatly-spaced and demonstrative of a couple hundred years of stylistic evolution.`
},
'onAction.READ':function(){
queueGMOutput(`One reads: `+this.inscriptions[dice(this.inscriptions.length) - 1]);
},
'inscriptions':[
`EDGAR TIMBERJAW, 700-730, "Don't let the demon dogs get you"`,
`MRS ALICE GRAVES, 740-795, "Always said the zombies would get her someday"`
]
});
localScenery([`fences`,`iron fences`,`wrought-iron fences`], `What hath this craftsman wrought, that it should cause a fence for so many poor souls.`);
localScenery([`crypt`], `A bit spooky with its pure white marble and vaguely skull-like shape.`);
ECS.e(`cemetery-north`, [`place`], {
'name':`Cemetery`,
'region':`cemetery`,
'exits':{
's':`cemetery-entrance`,
'e':`crypt`,
'nw':`gravekeepers-hut`,
},
'descriptions':{
'default':` {{scenery}}`
}
});
ECS.e(`gravekeepers-hut`, [`place`], {
'name':`Gravekeeper's Hut`,
'region':`cemetery`,
'exits':{
'se':`cemetery-north`,
},
'descriptions':{
'default':` {{scenery}}`
}
});
ECS.e(`crypt`, [`place`], {
'name':`Crypt`,
'region':`cemetery`,
'exits':{
'w':`cemetery-north`,
'd':`crypt-lower`,
},
'descriptions':{
'default':` {{scenery}}`
}
});
ECS.e(`crypt-lower`, [`place`], {
'name':`Lower Crypt`,
'region':`cemetery`,
'exits':{
'u':`crypt`,
},
'descriptions':{
'default':` {{scenery}}`
}
});
ECS.e(`winding-path`, [`place`], {
'name':`Winding Path`,
'region':`mountain`,
'exits':{
'se':`smoky-mountain`,
'ne':`steep-climb`,
},
'descriptions':{
'default':`The trail goes all squiggly for a bit here. Up ahead to the {{ne}}, it gets less squiggly but more vertically. Back to the {{se}} is The Smoky Mountain. {{scenery}}`
}
});
ECS.e(`steep-climb`, [`place`], {
'name':`Steep Climb`,
'region':`mountain`,
'exits':{
'sw':`winding-path`,
'n':`castle-exterior`,
'u':`castle-exterior`,
},
'descriptions':{
'default':`Like many other mountains, this one is very tall. The trail here grows steep, necessitating a sort of mixed jog/clamber to progress. Depending on your perspective, the castle lies {{n}} or {{u}}. You could also head back {{sw}} if this whole mountaineering thing isn't doing it for you. {{scenery}}`
}
});
ECS.e(`castle-exterior`, [`place`], {
'name':`Castle Exterior`,
'region':`mountain`,
'exits':{
's':`steep-climb`,
'd':`steep-climb`,
'n':`courtyard`,
},
'descriptions':{
'default':`Far above the little town filled with little people, you stand at the entrance to the {{nametag "large castle"}} filled with one spacefaring Baron. The castle looks like it predates the Baron by several hundred years, assuming she/he isn't an immortal architect merely returning to check up on a past project. If that were the case, you expect that the Baron feels a chagrin similar to that of any artist exposed to their own work later in life. The {{nametag "local-scenery-castle-exterior-stonework"}} is crumbling in places, the {{nametag "local-scenery-castle-exterior-portcullis"}} is propped up with the remains of a {{nametag "local-scenery-castle-exterior-wooden cart"}}, and the craftsmanship on the {{nametag "local-scenery-castle-exterior-balustrades"}} is simply embarrassing. It's a far cry from the high-tech aesthetic of the Baron's spacecraft, indicative of multiple centuries of progress in engineering and materials science. Really, the Baron should feel a sense of pride in the
ir obvious self-improvement, rather than dwelling on the mistakes of past inexperience. Introspection and self-criticism are healthy up to a point, but self-respect is equally important to a healthy mental state. There's a courtyard to the {{n}}. {{scenery}}`
}
});
localScenery([`wooden cart`,`cart`], `The remains of a wooden cart. RIP.`);
localScenery([`little town`,`town`], `From up here the buildings look like ants.`);
localScenery([`large castle`,`castle`], `Big, stony, and grim. It has the look of a place where a Baron would live.`);
localScenery([`stonework`], `Crumbly.`);
localScenery([`portcullis`], `A portcullis is a waffle-shaped gate and the medieval precursor to the screen door. Initially used to keep out large predators like bears and timberwolves, the portcullis was considered successful but extravagant given its size, material cost, and ineffectiveness against any creature smaller than an adolescent tapir. After the invention of plastic and the ensuing extinction of bears and timberwolves, the basic concept was adapted into a finer mesh for protection against mosquitoes, raccoons, and slow-learning toddlers.`);
localScenery([`balustrades`,`balustrade`], `Absolute rubbish. If your child made these balustrades, you would immediately disown them, set fire to the building, and frame your former child for the arson.`);
ECS.e(`courtyard`, [`place`], {
'name':`Courtyard`,
'region':`castle`,
'exits':{
's':`castle-exterior`,
'e':`castle-great-hall`,
'in':`castle-great-hall`,
},
'descriptions':{
'default':`Every castle needs a courtyard. That's the only reason this location is in the game. You can go back outside to the {{s}} or enter the great hall to the {{e}}. {{scenery}}`
}
});
ECS.e(`castle-great-hall`, [`place`], {
'name':`Great Hall`,
'region':`castle`,
'exits':{
'w':`courtyard`,
'out':`courtyard`,
'in':`elevator`,
'e':`elevator`,
},
'descriptions':{
'default':`The infamous Great Hall, known throughout the land for its greatness. An equally great archway leads {{west}} into the courtyard. To the {{east}}, through a marginally less great archway, an elevator sits open, waiting, like a reader unsure of where a sentence is going, or when it will get there, or what twists and turns it might take along the way, until the doubt starts to set in, wondering if they've missed a period and are actually in a new sentence entirely, but no, after double-checking it's just commas, endless commas, and nothing makes sense any more, clearly the author has lost it, made some kind of mistake, entered into an existential crisis from which escape is uncertain, a rollercoaster ride of grammar and emotions, that is to say, writing an elevator has its ups and downs. It seems like an odd addition to an otherwise medieval-period structure. {{scenery}}`
}
});
/**
Twins seen in the baron's castle, critiquing a piece of artwork on the wall. one likes it and is exploring themes, the other is arguing that the complexity and intent are imagined. they ask the player to weigh in, but do not seem satisfied with the answers. they will give a hint about the location of the elevator key.
*/;
ECS.e(`elevator`, [`place`], {
'name':`Elevator`,
'region':`castle`,
'exits':{
'w':`castle-great-hall`,
'out':`castle-great-hall`,
},
'descriptions':{
'default':`Polished steel walls form a roomy box for moving things up and down. A recent addition by the Baron, surely. The elevator occupies the space left by a demolished spiral staircase, and could comfortably hold a dozen people. Wide doors hint at cargo-carrying use. A simple control panel has buttons marked {{tag '1' command='press button 1'}} and {{tag 'B' command='press bee button'}}. There's also a small {{tag 'slot' command='investigate slot'}} below the buttons. {{scenery}}`
},
'state':`down`,
});
ECS.e(`elevator-control-panel`, [`scenery`], {
'name':`control panel`,
'nouns':[`panel`,`control panel`],
'spawn':`elevator`,
'descriptions':{
'default':`A simple control panel.`
}
});
ECS.e(`elevator-button-1`, [`scenery`,`device`,`part`], {
'spawn':`elevator-control-panel`,
'name':`1 button`,
'nouns':[`1 button`,`button 1`,`one button`,`button one`,`1`,`one`],
'descriptions':{
'default':`A shiny silver button, labeled with a numeral 1.`
},
'onAction.PRESS':function(action){
var elevator = ECS.getEntity(`elevator`);
if(elevator.state == `down` && ECS.getEntity(`elevator-keycard-slot`).hasChild(`keycard`)){
// Start elevator cutscene (1:06)
elevator.exits[`w`] = elevator.exits[`out`] = `boss-room`;
elevator.state = `up`;
if(player.checkProgress(`won the game`)) {
queueGMOutput(`The elevator ascends smoothly to the Throne Room`);
return;
}
var sequence = new Sequence();
sequence.add(function(){
// music plays
Sound.playMusic(`elevator-music`);
queueGMOutput(`Let's get real for a minute here.`, 2500);
queueGMOutput(`You're about to confront the final boss of this game.`, 2500);
queueGMOutput(`I know there's been a bit of a silly tone so far, but that's just me trying to make the game more approachable in its currently unpolished state. I want you to set aside your preconceptions from the last few hours and treat it more like a Darth Vader / Voldemort type situation.`, 7000);
queueGMOutput(`In a minute this stupid music is going to stop, the elevator doors are going to open, and you're going to be face to face with the baddest bad this world has ever seen. That element probably hasn't been sold super hard. That's on me, really. Anyway, it might take you a few tries to get through this encounter.`, 7000);
queueGMOutput(`Your opponent is a creature from another world, seeking refuge here to regain strength and build up some kind of defense against an implacable foe. This is all spoiler territory, but I didn't really have time to get into it with all the random detours you took. Anyway, he's the bad guy but maybe he's not a bad guy, so the intent is that you either don't murder him, or at least feel kinda bad about it afterward. The choice is up to you, as usual. I'm just outlining the canonical context to this encounter, and the expected resolutions.`, 14000);
}, Sequence.MODE_CONTINUE);
sequence.add(function(){
queueGMOutput(`This track is longer than I remember.`, 10000);
queueGMOutput(`Ok, that's enough elevator music. I'm getting annoyed by my own joke. Ready, set, go!`, 1000);
Engine.queueEvent(function() {
ECS.moveEntity(player, `boss-room`);
Rulebook.rules.after[`rule for entering throne room for the first time`].respond(action);
processDeferredOutputQueue();
});
}, Sequence.MODE_CONTINUE);
sequence.start();
} else if(elevator.state == `down`) {
queueGMOutput(`Nothing seems to happen. Maybe you need to put something in the slot first.`);
} else {
queueGMOutput(`You're already on that floor.`);
}
}
});
ECS.e(`elevator-button-b`, [`scenery`,`device`,`part`], {
'spawn':`elevator-control-panel`,
'name':`B button`,
'nouns':[`b button`,`button b`,`bee button`,`button bee`,`b`,`bee`],
'descriptions':{
'default':`A shiny silver button, labeled with a letter B.`
},
'onAction.PRESS':function(action){
var elevator = ECS.getEntity(`elevator`);
if(elevator.state == `up` && ECS.getEntity(`elevator-keycard-slot`).hasChild(`keycard`)){
queueGMOutput(`The elevator descends smoothly.`);
elevator.exits[`w`] = elevator.exits[`out`] = `castle-great-hall`;
elevator.state = `up`;
} else {
if(elevator.state == `down`) {
queueGMOutput(`Nothing seems to happen, probably because you're already on the bottom floor.`);
} else {
queueGMOutput(`Nothing happens. Maybe you need to put something in the slot first.`);
}
}
}
});
ECS.e(`elevator-keycard-slot`, [`scenery`,`part`,`container`], {
'spawn':`elevator-control-panel`,
'name':`keycard slot`,
'nouns':[`slot`,`keycard slot`],
'descriptions':{
'default':`A slot for a keycard.`
},
'canPutInto':[function(data){
return data.nouns[0].key == `keycard`;
}]
});
ECS.e(`boss-room`, [`place`], {
'name':`Throne Room`,
'region':`castle`,
'music':`barons-fight-theme`,
'exits':{
'e':`elevator`,
'in':`elevator`,
},
'descriptions':{
'default':`Thick wooden beams criss-cross between stone pillars, which in turn run the length of the ornate throne room. Rows of seats have been pushed up against the walls and stacked to make room for an assortment of unidentifiable machinery. Even the throne has been set aside, because who cares right? Iron chandeliers hang from the ceiling, stripped of their candles and wrapped in lengths of glowing cord. The room is visual chaos. Crashing music echoes throughout the room from an unknown source. {{#first 'visited-boss-room'}}{{nametag "baron" print="THE BARON"}} is here, looming over a cluttered table. Gray armor plates gleam in the strange lighting. A golden visor, perfectly smooth, reflects the throne room and obscures the Baron's face...if it has one. Not everyone has a face, and that's ok. Even so, the idea that the Baron might not have a face bugs you.{{/first}} {{scenery}}`
}
});
ECS.e(`boss-room-beams`, [`scenery`], {
'spawn':`boss-room`,
'name':`wooden beams`,
'nouns':[`beams`,`wooden beams`,`thick wooden beams`],
'descriptions':{
'default':`Each pair of wooden beams forms a sturdy X between two pillars. Additional beams arch across the center of the throne room, presumably holding up the ceiling.`
}
});
ECS.e(`boss-room-pillars`, [`scenery`], {
'spawn':`boss-room`,
'name':`stone pillars`,
'nouns':[`pillars`,`stone pillars`],
'descriptions':{
'default':`Some tall pillar-like things, made of stone.`
}
});
ECS.e(`boss-room-seats`, [`scenery`], {
'spawn':`boss-room`,
'name':`seats`,
'nouns':[`seats`,`rows of seats`,`pews`],
'descriptions':{
'default':`Pew, pew, pew.`
}
});
ECS.e(`boss-room-walls`, [`scenery`], {
'spawn':`boss-room`,
'name':`walls`,
'nouns':[`walls`],
'descriptions':{
'default':`Walls. Most buildings have them.`
}
});
ECS.e(`boss-room-machinery`, [`scenery`], {
'spawn':`boss-room`,
'name':`unidentifiable machinery`,
'nouns':[`machinery`,`machines`,`unidentifiable machinery`,`unidentifiable machines`],
'descriptions':{
'default':`You are unable to identify the purpose of the machines, but they look very busy and important.`
}
});
ECS.e(`boss-room-throne`, [`scenery`], {
'spawn':`boss-room`,
'name':`throne`,
'nouns':[`throne`,`ornate throne`],
'descriptions':{
'default':`An ugly, gilded seat for a noble bottom.`
}
});
ECS.e(`boss-room-chandeliers`, [`scenery`], {
'spawn':`boss-room`,
'name':`chandeliers`,
'nouns':[`chandelier`,`chandeliers`,`iron chandelier`,`iron chandeliers`],
'descriptions':{
'default':`Wrought-iron chandeliers, which once housed an impressive array of candles (that's where it gets its name, actually). The candles have been replaced with mysterious glowing cord.`
}
});
ECS.e(`boss-room-ceiling`, [`scenery`], {
'spawn':`boss-room`,
'name':`ceiling`,
'nouns':[`ceiling`],
'descriptions':{
'default':`It's the thing that sits atop the walls and keeps the rain out.`
}
});
ECS.e(`boss-room-glowing-cords`, [`scenery`], {
'spawn':`boss-room`,
'name':`glowing cords`,
'nouns':[`cord`,`cords`,`glowing cord`,`glowing cords`,`mysterious glowing cord`,`mysterious glowing cords`],
'descriptions':{
'default':`The glowing cords have no flame, yet produce a great deal of soft light. Curious.`
}
});
ECS.e(`boss-room-music`, [`scenery`], {
'spawn':`boss-room`,
'name':`crashing music`,
'nouns':[`music`,`crashing music`],
'descriptions':{
'default':`You can't look at sounds. You can look at the jukebox it's coming from though.`,
'sound':`It makes you want to fight something.`
}
});
ECS.e(`boss-room-table`, [`scenery`], {
'spawn':`boss-room`,
'name':`table`,
'nouns':[`table`,`cluttered table`],
'descriptions':{
'default':`A solid oak table, worn from many decades of use. It is presently quite cluttered.`
}
});
ECS.e(`jukebox`, [`scenery`,`device`], {
'spawn':`boss-room`,
'name':`jukebox`,
'nouns':[`jukebox`],
'descriptions':{
'default':`A high-tech jukebox. It seems to be stuck on {{tag 'shuffle' command='hit jukebox'}}.`
},
'onAction.USE':function(){
NLP.parse(`hit jukebox`);
},
'trackCounter':1,
'onAction.ATTACK':function(){
this.trackCounter = (this.trackCounter + 1) % this.tracks.length;
var track = new Track(`jukebox`, this.tracks[this.trackCounter],{});
if(track.file) {
queueGMOutput(`You give the jukebox a solid whack, skipping to another track.`);
Sound.playMusic(track);
} else {
queueGMOutput(`You smack the jukebox a little too hard, silencing it.`);
Sound.stopMusic();
}
},
'tracks':[
`JB Baron's Castle.ogg`,
`Baron's Fight Theme.ogg`,
`JB Crevasse.ogg`,
`JB Music Box Mockup.ogg`,
`JB Town Exterior 8 bit.ogg`,
`JB Tunnels.ogg`,
`JB Waterfall Song.ogg`,
`JB Wonder Theme.ogg`,
null // Turn off
]
});
var prefix = function(n) { return `<span class='npc npc2'>`+n+`:</span>`; };
understand(`rule for entering throne room for the first time`)
.book(`after`)
.verb(`move`)
.in(`boss-room`)
.doOnce(function(self, action) {
var sequence = new Sequence;
sequence.add(function() {
queueOutput(NLP.parse(`x`));
queueGMOutput(p(`At the ding of the elevator, the Baron pauses in its study. The helmet tilts up.`), `auto`);
queueOutput(prefix(`The Baron`) + p(`A challenger?`), `auto`);
Sound.playMusic(ECS.getEntity(`boss-room`).music);
var menuOptions = [
{'text':`YES`,'command':`yes`,'subtext':`Yes!`},
{'text':`NO`,'command':`no`,'subtext':`No, I concede defeat`},
];
// Class options
switch(player.class.toLowerCase()) {
case `liar`:
menuOptions.push({
'text':`[LIAR] Lie`,
'subtext':`I am the Baron now`,
'command':`liar`
});
break;
case `homemaker`:
menuOptions.push({
'text':`[HOMEMAKER] Help`,
'subtext':`Just look at this mess`,
'command':`homemaker`
});
break;
case `fighter`:
menuOptions.push({
'text':`[FIGHTER] Fight`,
'subtext':`Face me, villain!`,
'command':`fighter`
});
break;
case `wizard`:
menuOptions.push({
'text':`[WIZARD] Magic`,
'subtext':`You're no match`,
'command':`wizard`
});
break;
case `unicorn hunter`:
menuOptions.push({
'text':`[UNICORN HUNTER] Musk`,
'subtext':`The real threat is unicorns`,
'command':`unicorn hunter`
});
break;
}
var options = {'options':shuffle(menuOptions)};
var menu = parse(`{{menu options}}`, options);
NLP.interrupt(
function(){
queueOutput(menu);
},
function(string){
ECS.tick = false;
disableLastMenu(string);
var baron = ECS.getEntity(`baron`);
if(player.class == `liar` && string.is(`lie`,`liar`,`i am the baron now`)) {
queueGMOutput(`The Baron is visibly confused by your claim that you, not it, are in fact The Baron.`, `auto`);
queueGMOutput(`Rolling against your bluff...`, `auto`);
queueGMOutput(`Critical failure. Yikes.`, `auto`);
queueCharacterOutput(`baron`, `I see now. The true threat to this world will not be faced by me-you, but by you-me.`, `auto`);
queueCharacterOutput(`baron`, `I will serve faithfully at your side, Baron.`, `auto`);
win(`You tricked The Baron into giving you its job.`);
} else if(player.class == `homemaker` && string.is(`help`,`homemaker`,`just look at this mess`)) {
queueGMOutput(`The Baron considers your point carefully.`, `auto`);
queueCharacterOutput(`baron`, `I had not considered the mess. I am on a quest to save this world.`, `auto`);
queueCharacterOutput(`baron`, `This chaos is unbecoming, however, if I seek to be an agent of order.`, `auto`);
queueCharacterOutput(`baron`, `I would welcome your wise counsel on this and other matters. You have seen through to the heart of my quest in a mere moment.`, `auto`);
win(`You saw through to the heart of the Baron's quest in a mere moment.`);
} else if(player.class == `fighter` && string.is(`fight`,`fighter`,`face me villain!`)) {
queueGMOutput(`The Baron readies itself for combat.`, `auto`);
queueCharacterOutput(`baron`, `If you can defeat me, perhaps you can defeat the threat to this world as well.`, `auto`);
queueGMOutput(`Alright, roll your attack. I'm going to assume you're using the sword, per usual...`, `auto`);
queueGMOutput(`...aand of course, another critical hit. I think there's something wrong with that sword.`, `auto`);
Engine.queueRainbowOutput(getSpeechTag(`rainbow-sword`, `rainbow`) + p(`THERE IS NOTHING WRONG WITH ME. I AM A PARAGON OF EFFICIENCY.`), `auto`);
queueGMOutput(`In any case, you make swift work of the Baron, who collapses to the floor in a lifeless heap. The armor collapses in on itself, revealing it to be a hollow shell with no creature inside.`, `auto`);
baron.hp = 0;
baron[`onAction.TALK`] = function(){
queueGMOutput(`The Baron can't talk to you any more, on account of you killed it.`);
};
win(`You killed the Baron using the most overpowered sword ever made.`);
} else if(player.class == `wizard` && string.is(`magic`,`wizard`,`you're no match`)) {
queueGMOutput(`Your spellbook is still empty. That's a bit awkward.`, `auto`);
queueGMOutput(`Every wizard knows a few cantrips though, right? Roll for...illusion or something.`, `auto`);
queueCharacterOutput(`baron`, `What are you doing over there?`, `auto`);
queueGMOutput(`Ooh, critical failure. Rolling for the effect.`, `auto`);
queueGMOutput(`Oh. Lucky even when you're unlucky. Through sheer ineptitude, you accidentally transform the Baron into a toad.`, `auto`);
baron.name = `The Toad Baron`;
ECS.nouns[`toad`] = [baron];
ECS.nouns[`toad baron`] = [baron];
ECS.nouns[`the toad baron`] = [baron];
baron.descriptions = {
'default':`A toad who used to be the feared Baron.`
};
baron[`onAction.TALK`] = function() {
queueGMOutput(`The toad croaks at you in a sulky way.`);
};
queueCharacterOutput(`baron`, `*annoyed ribbit*`, `auto`);
win(`You utilized every bit of your university education to defeat the Baron.`);
} else if(player.class == `unicorn hunter` && string.is(`musk`,`unicorn hunter`,`the real threat is unicorns`)) {
queueGMOutput(`The Baron is visibly shocked despite your complete inability to see its face.`);
queueCharacterOutput(`baron`, `Unicorns? Finally, another who recognizes the threat! Their spies are already among us, and I had thought myself to be the only warrior prepared to stand against them.`, `auto`);
queueGMOutput(`I'm not really sure what's going on right now.`, `auto`);
queueCharacterOutput(`baron`, `I fled my homeworld's destruction at their hands.`, `auto`);
queueGMOutput(`I seriously don't remember writing anything about space unicorns.`, `auto`);
queueCharacterOutput(`baron`, `We'll stop them together. You have my word.`, `auto`);
win(`You joined forces with the Baron to fight space unicorns.`);
} else if(string.is(`yes`,`yeah`)) {
enableLastMenu();
queueCharacterOutput(`baron`, `Alright then. Now what?`, `auto`);
return false;
} else if(string.is(`no`,`nah`)) {
lose();
} else {
enableLastMenu();
queueOutput(prefix(`The Baron`) + p(`I do not understand.`));
return false;
}
sequence.next();
return true;
}
);
});
sequence.start();
processDeferredOutputQueue();
self.stop();
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
function win(reason) {
NLP.command_interrupt = [];
player.setProgress(`won the game`);
queueOutput(`{{box 'THE END' '`+reason+`'}}`, 2000, {});
queueGMOutput(`Alright. That's the end of my prepared adventure. If you want, you can play again, or you can keep playing this session and just poke around a bit.`, `auto`);
queueGMOutput(`Thanks for playing!`,`auto`);
queueOutput(`<p>Would you like to: {{tag 'RESTART' command='RESTART'}} or {{tag 'CONTINUE' command='CONTINUE'}}?</p>`);
NLP.interrupt(function(){}, function(s){
if(s.toLowerCase() == `restart`) {
window.location.reload();
} else if(s.toLowerCase() == `continue`) {
queueGMOutput(`On we go, then.`);
NLP.command_interrupt = [];
queueOutput(NLP.parse(`x`));
return true;
}
});
}
function lose() {
NLP.command_interrupt = [];
player.setProgress(`lost the game`);
queueOutput(`{{box 'THE END' 'YOU GAVE UP AT THE VERY END'}}`, 2000, {});
queueGMOutput(`Alright. That's the end of my prepared adventure. Bit anticlimactic of you to just give up like that, but I think player choice is very important.`, `auto`);
queueGMOutput(`Thanks for playing!`,`auto`);
queueOutput(`<p>Would you like to: {{tag 'RESTART' command='RESTART'}}?</p>`);
NLP.interrupt(function(){}, function(s){
if(s.toLowerCase() == `restart`) {
window.location.reload();
}
return false;
});
};
ECS.e(`baron`, [`living`], {
'name':`The Baron`,
'nouns':[`baron`,`the baron`],
'spawn':`boss-room`,
'descriptions':{
'default':`An intimidating, space-armored figure from offworld.`
}
});
ECS.e(`gravekeeper`, [`living`], {
'name':`The Gravekeeper`,
'nouns':[`gravekeeper`,`candice`],
'spawn':`cemetery-entrance`,
'descriptions':{
'default':`A sprightly woman named Candice. She took over the Gravekeeping business after her less sprightly mother died in a zombie uprising some 10 years back. It's not a glamorous job, but the pay is decent, you get plenty of time to yourself to reflect on things, and you'll be the first to know when the next zombie uprising comes around and you need to avenge a loved one.`
},
'conversation':new Conversation([
{
'id':`root`,
'key':``,
'callback':function(topic, conversation){
if(!conversation.prevNode) {
queueCharacterOutput(`gravekeeper`,`Hey.`);
} else {
queueCharacterOutput(`gravekeeper`,`Is there anything else I can do for you?`);
}
return true;
},
'nodes':[`who`,`baron`,`keycard`,`bone`]
},
{
'id':`who`,
'prompt':`Who are you?`,
'response':`I'm Gravekeeper Candice. I keep the dead where they belong, mostly.`,
'nodes':[`mostly`]
},
{
'id':`mostly`,
'prompt':`Mostly sounds...good`,
'response':`Nobody's perfect. I let some skeletons roam in the crypt for a while, sweeping and so forth. Then they started chattering about unionizing and things got a bit out of hand.`,
'forward':`root`
},
{
'id':`baron`,
'prompt':`You work for the baron?`,
'response':`Oh...not really. I've helped him procure some...supplies. Most people in town have had some kind of contact. I'm mostly up there at night though, so I've got my own keycard and everything.`,
'after':function(topic,conversation){
if(!ECS.getEntity(`gravekeeper`).hasChild(`finger-bone`)) {
conversation.enableNode(`keycard`);
}
},
'nodes':[`keycard`]
},
{
'id':`keycard`,
'prompt':`Can I borrow that keycard?`,
'enabled':false,
'callback':function(topic,conversation){
queueCharacterOutput(player, p(this.prompt));
if(ECS.getEntity(`gravekeeper`).hasChild(`finger-bone`)) {
queueGMOutput(`The gravekeeper hesitates.`);
queueCharacterOutput(`gravekeeper`, `Well...now that I have my mother back—her remains, rather—I suppose I don't need to be taking odd jobs for the Baron any more.`, `auto`);
queueCharacterOutput(`gravekeeper`, `You can have it, but you'd better not tell him you got it from me.`, `auto`);
ECS.moveEntity(`keycard`, player);
conversation.disableNode(`keycard`);
} else {
queueCharacterOutput(`gravekeeper`, `No, I need it. If I didn't...say, if I didn't need this job any more for some reason. Then we could talk.`);
}
return true;
},
'continue':true,
'forward':`root`
},
{
'id':`bone`,
'prompt':`Someone gave me this finger bone`,
'enabled':false,
'callback':function(topic,conversation){
queueCharacterOutput(player, p(this.prompt));
queueGMOutput(`Candice's eyes light up.`, `auto`);
queueCharacterOutput(`gravekeeper`, `That's my mother's finger bone! I can't believe someone found it. It's the last piece I need to put...her to rest. Forever.`, `auto`);
queueGMOutput(`Candice snatches the bone from you and holds it up to the light.`, `auto`);
queueCharacterOutput(`gravekeeper`, `Thank you! This means more than you could know.`, `auto`);
ECS.moveEntity(`finger-bone`, `gravekeeper`);
conversation.enableNode(`keycard`);
conversation.disableNode(topic);
conversation.findNode(`baron`).forward = `root`;
return true;
},
'continue':true,
'forward':`root`
},
])
});
ECS.e(`keycard`, [], {
'name':`keycard`,
'nouns':[`keycard`,`key card`],
'spawn':`gravekeeper`,
'descriptions':{
'default':`A black keycard with a black magnetic stripe.`
}
});
understand(`rule for giving the finger bone to the gravekeeper`)
.verb(`give`)
.in(`cemetery-entrance`)
.if(function(){ return player.hasChild(`finger-bone`); })
.attribute(`target`,`=`,`finger-bone`)
.attribute(`nouns`,`containsEntity`,[`gravekeeper`])
.do(function(self,action){
queueOutput(NLP.parse(`ask gravekeeper about finger bone`));
action.mode = Rulebook.ACTION_CANCEL;
})
.start();
},
// Intro script
'onGameStart':function(){
var sequence = new Sequence;
sequence.add(function() {
// Don't parse rules during the intro
Rulebook.pause();
// Set game stage
ECS.setData(`stage`, `act3`);
queueGMOutput(`Ok, you got me. The old version wasn't really done, and the parts that were...well, distilling rocket fuel was the least convoluted part. I can't salvage this, sorry.`, `auto`);
queueGMOutput(`I'll get the proper Act 2 fixed for the next playtest. In the meantime let's move on. You successfully refueled the Baron's escape pod and made a semi-controlled landing in the woods near a ramshackle cabin.`, `auto`);
queueGMOutput(`If the rumors are true, the Baron has taken over a nearby town...`, `auto`);
Engine.queueEvent(function(){
Display.loadTheme(`default`);
});
// Title box
queueOutput(`{{box 'ACT THREE' 'THE BIG BAD'}}`, 2000, {'effect':`fade`});
sequence.next();
});
sequence.add(function(){
// Resume rule processing
Rulebook.start();
ECS.moveEntity(player, `rustic-clearing`);
queueOutput(NLP.parse(`x`));
Engine.queueEvent(function(){ ECS.getModule(`Music`).checkForMusicAtLocation(); });
});
sequence.start();
},
});
// Register module
ECS.m(act3);