/* exported StartGame */
/* exported StartGame */
// Include core files
/**
* Rulebook system
* Allows for dynamic creation of complex rulesets/filters to be applied to existing verbs, raw text and regex'd input.
* Uses method chaining for expressive style.
*/
var Rules = function () {
this.rules = {
'before': {},
'after': {},
'internal': {}
};
// Add default rule filter groups
// TODO: I don't remember how these work
this.filterGroups.default = new RuleFilterGroup(`default`, RuleFilterGroup.MODE_AND);
this.filterGroups.region = new RuleFilterGroup(`region`, RuleFilterGroup.MODE_OR);
this.filterGroups.location = new RuleFilterGroup(`location`, RuleFilterGroup.MODE_OR);
this.filterGroups.target = new RuleFilterGroup(`target`, RuleFilterGroup.MODE_OR);
this.filterGroups.modifier = new RuleFilterGroup(`modifier`, RuleFilterGroup.MODE_OR);
this.filterGroups.internal = new RuleFilterGroup(`internal`, RuleFilterGroup.MODE_OR);
// Add default objects
this.registerObject(`player`, null);
this.registerObject(`actor`, function (action) {
return action.actor;
});
this.registerObject(`location`, function (action) {
return action.actor.location();
});
this.registerObject(`target`, function (action) {
return action.target;
});
this.registerObject(`nouns`, function (action) {
return action.nouns;
});
this.registerObject(`region`, function (action) {
return action.actor.location.region;
});
};
Rules.prototype = {
// Constants
ACTION_CANCEL: 0, // Cancel action
ACTION_PREPEND: 1, // Prepend output and continue as normal
ACTION_APPEND: 1, // Finish action then append output
ACTION_NONE: 3, // Not handled, run action as usual
RULE_BEFORE: `before`, // Standard rule, happens before action processing
RULE_AFTER: `after`, // After rule, happens after action processing
RULE_INTERNAL: `internal`, // Internal rule, handled specially
// Rule list
active: true,
rules: {},
filterGroups: {},
// Object list
objects: {},
// Helpers
'add': function (rule) {
this.rules[rule.type][rule.key] = rule;
},
'remove': function (book, key) {
delete this.rules[book][key];
},
'registerObject': function (key, object) {
this.objects[key] = object;
},
// Check command against rulebook
'check': function (type, action) {
console.log(`Rulebook: Checking book '`+type+`'`);
if (!this.active) {
return action;
}
for (var r in this.rules[type]) {
this.rules[type][r].run(action);
if (action.mode === this.ACTION_CANCEL) {
break;
}
}
return action;
},
'pause': function () {
this.active = false;
},
'start': function () {
this.active = true;
}
};
/**
* Rule Filter Group constructor
* Filter groups are used to organize related filters and specify whether ALL or ANY are required.
*
* @param key
* @param mode
* @constructor
*/
var RuleFilterGroup = function (key, mode) {
this.key = key;
this.mode = mode;
};
RuleFilterGroup.prototype = {
MODE_AND: `and`,
MODE_OR: `or`,
key: null,
mode: null
};
var Rulebook = new Rules();
/**
* Filter constructor. A filter is a generic processor used by a rule. It always includes a callback and can
* optionally include additional parameters specified for the rule. When a callback is executed, it will be given:
* - actor (the entity initiating the action)
* - action (the text of the actor's action)
* - params (the saved parameters for the rule)
* @param callback function
* @param params object
*/
var RuleFilter = function (callback, params) {
this.callback = callback;
this.params = params;
this.type = this.TYPE_DEFAULT;
};
RuleFilter.prototype = {
TYPE_DEFAULT: 0,
TYPE_REVERSE: 1,
'callback': function () {
},
'params': {},
'type': null
};
RuleFilter.prototype.run = function (self, action) {
var result = this.callback(self, action, this.params);
return (this.type == this.TYPE_REVERSE) ? !result : result;
};
var understand = function (key) {
return new Rule(key);
};
var Rule = function (key) {
this.key = key;
this.commandType = null;
this.command = null;
this.filters = {};
this.response = null;
this.mode = Rulebook.ACTION_NONE;
this.responseText = ``;
this.prepend = ``;
this.type = Rulebook.RULE_BEFORE;
this.currentData = {};
for (var g in Rulebook.filterGroups) {
this.filters[g] = [];
}
};
Rule.prototype = {
// Constants
COMMAND_TEXT: 0,
COMMAND_REGEX: 1,
COMMAND_VERB: 2,
COMMAND_CALLBACK: 3,
// Storage for current instance
'key': null,
'commandType': null,
'command': null,
'filters': [],
'response': null,
'mode': null,
'responseText': ``,
'prepend': ``,
'type': Rulebook.RULE_BEFORE,
'currentData': null,
'doUntil': function () {
return false;
},
// Register and unregister self
'start': function () {
console.log(`Started Rule: ` + this.key);
Rulebook.add(this);
return this;
},
'stop': function () {
Rulebook.remove(this.type, this.key);
},
// Execute the rule
'run': function (action) {
console.log(`Checking Rule '` + this.key + `'...`);
this.currentData = {'action': action};
// Validate command and fail fast
// Action is expected to be {'verb':verb,'text':text}
switch (this.commandType) {
case this.COMMAND_TEXT:
if ($.inArray(action.text, this.command) < 0) {
return this.exit(Rulebook.ACTION_NONE);
}
break;
case this.COMMAND_REGEX:
if (action.text.match(this.command) === null) {
return this.exit(Rulebook.ACTION_NONE);
}
break;
case this.COMMAND_VERB:
if (action.verb != this.command) {
return this.exit(Rulebook.ACTION_NONE);
}
break;
case this.COMMAND_CALLBACK:
if (!this.command(this, action)) {
return this.exit(Rulebook.ACTION_NONE);
}
break;
}
// Run filters
for (var g in this.filters) {
for (var f in this.filters[g]) {
if (!this.filters[g][f].run(this, action)) {
// Filter failed, bail
return this.exit();
}
}
}
// Check 'do until' condition
if (this.doUntil(action)) {
console.log(`Rulebook: Stopping rule '`+this.key+`'`);
this.stop();
this.exit(Rulebook.ACTION_NONE); // This probably shouldn't be here?
}
// Return response
return this.respond();
},
'exit': function (mode) {
if (typeof mode != `undefined`) {
this.currentData.action.mode = mode;
}
return this.currentData.action;
},
'respond': function () {
if (this.response !== null) {
if (typeof this.response == `string`) {
queueGMOutput(this.response);
} else {
return this.response(this, this.currentData.action);
}
}
return this.exit();
},
// Helper methods
'addFilter': function (callback, params, group) {
group = group || `default`;
this.filters[group].push(new RuleFilter(callback, params));
},
'addReverseFilter': function (callback, params, group) {
group = group || `default`;
var rf = new RuleFilter(callback, params);
rf.type = RuleFilter.TYPE_REVERSE;
this.filters[group].push(rf);
},
'checkAttribute': function (params, action) {
var attribute = this.getAttribute(params.chain, action);
var result = false;
var value = params.value;
// Validate attribute
if (typeof attribute == `undefined`) {
console.log(`FAILED TO FIND ATTRIBUTE REFERENCE '` + params.chain + `'`);
return result;
}
// Get value result first. It could be a raw value, an attribute reference, or a function.
if (typeof params.value == `string`) {
value = this.getAttribute(params.value, action);
} else if (typeof params.value == `function`) {
value = params.value(params, action);
}
// Handle operator. Could be a function or one of the following:
// Math: > < >= <= = ==
// Set: contains
// Any operator can be preceded by ! to negate it, e.g. !contains
if (typeof params.operator == `function`) {
// Callback operator
result = action(attribute, value, action);
}
else {
// Standard operator
var operator = params.operator;
var negate = (operator[0] == `!`);
var k;
// Trim ! from operator
if (negate) {
operator = operator.substr(1);
if (operator.length == 0) {
operator = `==`;
}
}
// Special case operators
if (operator == `in`) {
// Swap 'in' operator as reverse alias for 'contains' operator
var tmp = attribute;
value = attribute;
attribute = tmp;
operator = `contains`;
}
if (operator == `=` || operator == `==`) {
if(attribute instanceof Entity && typeof value == `string`) {
result = attribute.key == value;
} else {
result = attribute == value;
}
} else if (operator == `>`) {
result = attribute > value;
} else if (operator == `<`) {
result = attribute < value;
} else if (operator == `>=`) {
result = attribute >= value;
} else if (operator == `<=`) {
result = attribute <= value;
} else if (operator == `contains`) {
// Set contains item
if (attribute instanceof Entity) {
// Attribute is an Entity, check its inventory / children
result = attribute.hasChild(value);
} else if (typeof attribute == `object`) {
// Attribute is a key/value object, check its properties
result = attribute.hasOwnProperty(value);
} else if ($.isArray(attribute)) {
// Attribute is an array, check its contents
result = attribute.indexOf(value) >= 0;
}
} else if (operator == `containsEntity`) {
// Set contains a specified entity
result = false;
value = !$.isArray(value) ? [value] : value;
if(typeof attribute == `object` || $.isArray(attribute)) {
// Attribute is a key/value object, check its properties
for(k in attribute) {
if(attribute[k] instanceof Entity && value.indexOf(attribute[k].key) >= 0) {
result = true;
}
}
}
} else if (operator == `containsType`) {
// Set contains item of given type
if(attribute instanceof Entity) {
// Attribute is an Entity, check its inventory / children
result = attribute.hasChildOfType(value);
} else if(typeof attribute == `object` || $.isArray(attribute)) {
// Attribute is a key/value object, check its properties
result = false;
for(k in attribute) {
if(attribute[k] instanceof Entity && attribute[k].hasComponent(value)) {
result = true;
}
}
}
} else if (operator == `is`) {
// Reference is of given type
if (attribute instanceof Entity) {
// Entity has component(s)
if (typeof value == `string`) {
result = attribute.hasComponent(value);
} else if ($.isArray(value)) {
result = attribute.components.indexOf(value) >= 0;
}
} else {
// Non-entity matches type
result = (typeof attribute == value);
}
}
if (negate) {
result = !result;
}
}
return result;
},
'getAttribute': function (chain, action) {
// If this isn't a rulebook object, return the string
if (!$.isArray(chain) || !Rulebook.objects.hasOwnProperty(chain[0])) {
return chain;
}
var value = Rulebook.objects[chain[0]];
if (typeof value == `function`) {
value = value(action);
}
for (var i = 1; i < chain.length; i++) {
// If the child property doesn't exist, return
if (!value.hasOwnProperty(chain[i])) {
return;
} else if (typeof value[chain[i]] == `function`) {
// Otherwise execute/get as new value
value = ECS.run(value, chain[i]);
} else {
value = value[chain[i]];
}
}
return value;
},
// Chain: Flag rule as internal
'internal': function (key) {
this.type = Rulebook.RULE_INTERNAL;
this.addFilter(function (self, action, params) {
return action.internal == params.key;
}, {'key': key}, `internal`);
return this;
},
// Chain: Flag rule type
'book': function(type) {
this.type = type;
return this;
},
// Chain: Add simple text match
'text': function (text) {
if(!Array.isArray(text)) {
text = [text];
}
this.command = text;
this.commandType = this.COMMAND_TEXT;
return this;
},
// Chain: Add verb match
'verb': function (verb) {
this.command = ECS.getAction(verb);
this.commandType = this.COMMAND_VERB;
return this;
},
// Chain: Add regex match
'regex': function (pattern) {
this.command = pattern;
this.commandType = this.COMMAND_REGEX;
return this;
},
// Chain: Add callback match
'action': function (callback) {
this.command = callback;
this.commandType = this.COMMAND_CALLBACK;
return this;
},
// Chain: Location filter
'in': function (loc) {
this.addFilter(function (self, action, params) {
return action.actor.locationIs(params.loc);
}, {'loc': loc}, `location`);
return this;
},
// Chain: Not In Location filter
'notIn': function (loc) {
this.addFilter(function (self, action, params) {
return !action.actor.locationIs(params.loc);
}, {'loc': loc}, `location`);
return this;
},
// Chain: Region filter
'inRegion': function (text) {
this.addFilter(function (self, action, params) {
return action.actor.regionIs(params.text);
}, {'text': text}, `region`);
return this;
},
// Chain: Not in Region filter
'notInRegion': function (text) {
this.addFilter(function (self, action, params) {
return !action.actor.regionIs(params.text);
}, {'text': text}, `region`);
return this;
},
// If/While (aliases), add a filter callback to restrict requirements
'if': function (callback, group) {
return this.while(callback, group);
},
'while': function (callback, group) {
this.addFilter(callback, {}, group);
return this;
},
// Chain: Reverse while condition
'unless': function (callback, group) {
this.addReverseFilter(callback, {}, group);
return this;
},
// Chain: Restrict to certain entity targets
'on': function (target) {
if(!$.isArray(target)) {
target = [target];
}
for(var t in target) {
if(typeof target[t] == `string`) {
target[t] = ECS.getEntity(target);
}
}
this.addFilter(function (self, action, params) {
return params.target.indexOf(action.target) >= 0;
}, {'target': target}, `target`);
return this;
},
// Chain: Restrict to certain modifiers
'modifier': function (modifier) {
this.addFilter(function (self, action, params) {
return action.modifiers.indexOf(params.modifier) >= 0;
}, {'modifier': modifier}, `modifier`);
return this;
},
// Chain: Check for matching attribute
'attribute': function (a, operator, value) {
value = (value) ? value : null;
// Explode attribute
var chain = a.split(`.`);
this.addFilter(function (self, action, params) {
return self.checkAttribute(params, action);
}, {'chain': chain, 'operator': operator, 'value': value});
return this;
},
// Chain: check for matching target entity
'entity': function (t) {
if(t instanceof Entity) { t = t.key; }
return this.attribute(`target.key`,`=`,t);
},
// Chain: Return a raw response if the requirements are met
'as': function (response, mode) {
this.response = response;
this.mode = mode;
return this;
},
// Chain: Execute a callback if the requirements are met
'do': function (callback) {
this.response = callback;
return this;
},
// Chain: Execute a callback and then cancel the rule
'doOnce': function (callback) {
var f = function(self, action) {
callback(self,action);
self.stop();
//action.mode = Rulebook.ACTION_CANCEL;
};
this.response = f;
return this;
},
// Chain: Specify a condition at which the Rule will be removed
'until': function (callback) {
this.doUntil = callback;
return this;
},
// Chain: Append a response for after the default response
'append': function (response) {
this.responseText = response;
this.mode = Rulebook.ACTION_APPEND;
return this;
}
};
//
// Entity-Component System
//
// Utility functions
function isArray(value) {
return toString.apply(value) === `[object Array]`;
}
//
// Core stuff
//
var ecs = ECS = {
availableModules: {},
modules: {},
components: {},
entities: {},
systems: [],
actions: {},
nouns: {},
internalActions: {},
tick: true,
// Last entity added, by type
lastOfType: {},
// General-purpose storage
data: {}
};
ECS.setData = function (key, value) {
this.data[key] = value;
};
ECS.getData = function (key) {
if (this.data.hasOwnProperty(key)) {
return this.data[key];
}
return null;
};
ECS.isValidMenuOption = function (options, command) {
for (var o in options) {
if (options[o].command == command.toLowerCase() || options[o].text.toLowerCase() == command) {
return true;
}
}
return false;
};
ECS.getMenuOption = function (options, command) {
if (typeof options == `string`) {
options = ECS.getData(options);
}
for (var o in options) {
if (options[o].command == command.toLowerCase() || options[o].text.toLowerCase() == command) {
return options[o];
}
}
return false;
};
ECS.getMenuOptionValue = function (options, command) {
command = command.toLowerCase();
for (var o in options) {
if (options[o].command == command || options[o].text.toLowerCase() == command) {
return options[o].command;
}
}
return false;
};
ECS.setOptions = function (obj, options) {
$.extend(true, obj, options);
};
ECS.isComponentLoaded = function (component) {
return (typeof this.components[component] != `undefined`);
};
ECS.getComponent = function (component) {
return this.components[component];
};
ECS.hasAction = function (action) {
return (typeof this.actions[action] != `undefined`);
};
ECS.getAction = function (action) {
if (!this.hasAction(action)) {
return null;
}
return this.actions[action];
};
ECS.run = function (object, callback, args) {
if (typeof args != `object`) {
args = {};
}
if (object.hasOwnProperty(callback)) {
if (typeof object[callback] === `string`) {
return object[callback];
}
return object[callback](args);
}
return ``;
};
ECS.runCallbacks = function (object, callback, args) {
// Add target object to args list
if (typeof args != `object`) {
args = {};
}
args.obj = object;
// Get callback list from object
if (object.hasOwnProperty(callback)) {
var callbacks = object[callback];
if (typeof callbacks == `object`) {
for (var i = 0; i < callbacks.length; i++) {
if (callbacks[i](args)) {
return true;
}
}
}
}
return false;
};
ECS.runFilters = function (object, callback, args) {
// Add target object to args list
if (typeof args != `object`) {
args = {};
}
args.obj = object;
// Get callback list from object
if (object.hasOwnProperty(callback)) {
var callbacks = object[callback];
if (typeof callbacks == `object`) {
for (var i = 0; i < callbacks.length; i++) {
if (callbacks[i](args) === false) {
return false;
}
}
}
}
return true;
};
ECS.addInternalAction = function (key, callback) {
this.internalActions[key] = callback;
};
ECS.runInternalAction = function (key, data) {
data = $.extend({}, NLP.lastAction, data);
data.internal = key;
// Check rules
var action = Rulebook.check(`internal`, data);
if (action.mode === Rulebook.ACTION_CANCEL) {
return action.output;
} else if (action.mode === Rulebook.ACTION_APPEND) {
return this.internalActions[key](data) + action.output;
}
return this.internalActions[key](data);
};
ECS.findEntityByName = function (noun, scope) {
if (typeof this.nouns[noun] != `undefined`) {
for (var n in this.nouns[noun]) {
var tmp = this.nouns[noun][n];
var isOk = (scope == null);
var isGlobal = (tmp.scope == `global`);
var isLocal = (scope == `local` && tmp.visibleFrom(player.location()));
var isInPlayerInventory = (tmp.parent == player);
if (isOk || isInPlayerInventory || isLocal || isGlobal) {
return tmp;
}
}
}
return null;
};
ECS.hasEntity = function (key) {
return this.entities.hasOwnProperty(key);
};
ECS.getEntity = function (key) {
if (this.hasEntity(key)) {
return this.entities[key];
}
return null;
};
ECS.findEntity = function (component, noun) {
if (this.entities.hasOwnProperty(noun)) {
if (this.entities[noun].components.indexOf(component) >= 0) {
return this.entities[noun];
}
}
return null;
};
ECS.findEntitiesByComponent = function (component) {
var matches = [];
for (var e in this.entities) {
if (this.entities[e].components.indexOf(component) >= 0) {
matches.push(this.entities[e]);
}
}
return matches;
};
ECS.getEntityPrefix = function (e) {
if(typeof key != `object`) { e = this.getEntity(e); }
return ECS.run(e, `prefix`);
};
ECS.init = function (modules) {
for (var m in modules) {
console.log(`INITIALIZING MODULE `+modules[m]);
this.modules[modules[m]] = this.availableModules[modules[m]];
this.modules[modules[m]].init();
}
};
//
// System stuff
//
var System = function (name, options) {
this.name = name;
this.components = [];
this.priority = 1;
this.onTick = function () {
};
};
// Add system to module
ecs.s = function (system) {
var index = 0;
// Get index based on priority
if (this.systems.length > 0) {
// Find first item of lower priority (higher #)
for (var s in this.systems) {
if (this.systems[s].priority > system.priority) {
index = s;
}
}
}
// Insert system into list based on priority index
this.systems.splice(index, 0, system);
};
//
// Module stuff
//
var Module = function (name, options) {
this.name = name;
this.dependencies = []; // Required modules
this.components = []; // Components supplied by this module
this.systems = []; // Systems supplied by this module
this.actions = []; // Actions supplied by this module
ECS.setOptions(this, options);
};
// Default init function for modules
Module.prototype.init = function () {
console.log(`Module '` + this.name + `' initialized [default].`);
};
// Add system to module
Module.prototype.s = function (system) {
this.systems.push(system);
};
// Add component to module
Module.prototype.c = function (name, options) {
this.components[name] = options;
};
// Add action to module
Module.prototype.a = function (name, options) {
if (!options.hasOwnProperty(`modifiers`)) {
options.modifiers = [];
}
if (!options.hasOwnProperty(`filters`)) {
options.filters = [];
}
this.actions[name] = options;
};
// Add module to ECS
ecs.m = ecs.module = function (module) {
if (typeof this.modules[module.name] !== `undefined`) {
console.log(`Module '` + module.name + `' already loaded.`);
return;
}
// Register module
this.availableModules[module.name] = module;
// Register systems
for (var s in module.systems) {
ecs.s(module.systems[s]);
}
// Register components
for (var c in module.components) {
ecs.c(c, module.components[c]);
}
// Register actions
for (var a in module.actions) {
for (var i = 0; i < module.actions[a].aliases.length; i++) {
this.actions[module.actions[a].aliases[i]] = module.actions[a];
}
console.log(`Added action '` + a + `' from module '` + module.name + `'`);
}
};
// Get system from ECS
ecs.getSystem = function (system) {
for(var s in this.systems) {
if(this.systems[s].name == system) {
return this.systems[s];
}
}
throw `Could not find system '` + system + `'`;
};
// Get module from ECS
ecs.getModule = function (module) {
return this.modules[module];
};
//
// Component stuff
//
var Component = function () {
this.name = ``;
this.parent = null; // Parent component to inherit from
this.dependencies = []; // Required components
this.onAdd = [];
};
// Default init for components. Does nothing.
Component.prototype.onInit = function () {
};
// Register component with ECS
ECS.c = ecs.c = ecs.component = function (name, options) {
if (this.isComponentLoaded(name)) {
console.log(`Component '` + name + `' already loaded.`);
return;
}
// Create component instance
var instance = new Component();
instance.name = name;
// Set options
for (var option in options) {
instance[option] = options[option];
}
// Add component to internal list
this.components[name] = instance;
// Run component's init callback
this.components[name].onInit();
};
//
// Entity stuff
//
var Entity = function () {
this.key = ``; // Identifier
this.parent = null; // Parent entity
this.children = []; // Child entities
this.empty = true; // Whether the entity is empty (has no children)
this.components = []; // Component list, for convenience / searching
this.onComponentAdd = []; // Add component callback
this.tags = []; // Tag list; generally the same as the component list
this.persist = [`parent`]; // Raw attributes to persist
this.persistActive = true; // Save objects by default
this.isVisible = [
function(args){
// Entity is in location
return typeof args.obj.location == `function` && args.location == args.obj.location();
},
function(args){
// Entity is scenery in region
return args.obj.scope == `region` && args.obj.regionIs && args.obj.regionIs(args.location.region) && args.obj.hasComponent(`scenery`);
}
]; // Visibility callbacks
};
// Entity prototype; includes global variables
Entity.prototype = {
contextActions: [],
};
// Default init for entities
Entity.prototype.init = function () {
console.log(`Entity '` + this.key + `' initialized [default].`);
};
// Check for component on entity
Entity.prototype.hasComponent = function (component) {
return (this.components.indexOf(component) >= 0);
};
// Alias for hasComponent
Entity.prototype.is = function (component) {
return this.hasComponent(component);
};
// Add component to entity
Entity.prototype.c = function (component) {
// Skip if already loaded for this entity
if (this.hasComponent(component)) {
return true;
}
// Make sure component is available
if (ecs.isComponentLoaded(component)) {
var success = true;
var tmp = $.extend(true, {'e': this}, ecs.getComponent(component));
// Load dependencies
for (var c in tmp.dependencies) {
success &= this.c(tmp.dependencies[c]);
}
// Add to entity's component list
this.components.push(component);
// Add to tag list
this.tags.push(component);
// Handle onComponentAdd callback
if (tmp.hasOwnProperty(`onAdd`)) {
var callbacks = tmp.onAdd;
if (!$.isArray(tmp.onAdd)) {
callbacks = [tmp.onAdd];
}
for (c in callbacks) {
this.onComponentAdd.push(callbacks[c]);
}
delete tmp.onAdd;
}
// Handle persist data to avoid overwrite
if (typeof tmp.persist != `undefined`) {
$.merge(tmp.persist, this.persist);
}
// Load component
$.extend(true, this, tmp);
console.log(`Added component '` + component + `' to entity '` + this.key + `'`);
return success;
}
console.log(`Failed to load component '` + component + `' or dependent component for Entity '` + this.name + `'`);
return false;
};
// Update stats for entity
Entity.prototype.updateStats = function () {
// Set entity as empty if it has no children
this.empty = (this.children.length == 0);
};
// Add child to entity
Entity.prototype.addChild = function (e) {
if (this.hasChild(e.key)) {
return;
}
this.children.push(e);
e.parent = this;
ECS.runCallbacks(this, `onAddChild`, {'child': e});
this.updateStats();
};
// Remove child from entity
Entity.prototype.removeChild = function (e) {
var i = this.children.indexOf(e);
if (i >= 0) {
this.children.splice(i, 1);
}
ECS.runCallbacks(this, `onRemoveChild`, {'child': e});
this.updateStats();
};
// Check if entity has a specific child, by key
Entity.prototype.hasChild = function (e) {
var key = e;
if(typeof e != `string`) {
key = e.key;
}
for (e in this.children) {
if (this.children[e].key == key) {
return true;
}
}
return false;
};
// Find children with matching component
Entity.prototype.findChildren = function (component) {
var matches = [];
for (var c in this.children) {
if (this.children[c].hasComponent(component)) {
matches.push(this.children[c]);
}
}
return matches;
};
// Automatically determine article (a, an, etc) from name
Entity.prototype.article = function () {
var vowels = [`a`, `e`, `i`, `o`, `u`];
var firstLetter = this.name.substring(0, 1);
var article = `a`;
// For proper nouns, use no article
if(firstLetter == firstLetter.toUpperCase()) {
return ``;
}
if (vowels.indexOf(firstLetter.toLowerCase()) >= 0) {
article = `an`;
}
return article;
};
// Save an entity
Entity.prototype.save = function () {
var e = this;
var data = {'key': this.key, 'values': {}};
$.each(this.persist, function (i, v) {
data.values[v] = ecs.getSaveValue(e[v]);
});
return data;
};
// Load an entity
Entity.prototype.load = function (data) {
var e = this;
$.each(this.persist, function (i, v) {
if (typeof data.values[v] != `undefined`) {
var value = data.values[v][1];
if (data.values[v][0] == `reference`) {
value = ECS.entities[value];
}
if (v == `parent` && e.parent != null) {
e.parent.removeChild(e);
}
if ((v == `place` || v == `parent`) && value instanceof Entity) {
value.addChild(e);
}
e[v] = value;
}
});
// If parent and place don't match, remove from place children
if (this.parent != null && this.parent != this.place && this.place instanceof Entity) {
this.place.removeChild(this);
}
};
// Add context action
Entity.prototype.addContext = function(callback) {
this.contextActions.push(callback);
};
// Get context actions
Entity.prototype.getContextActions = function () {
var actions = [];
for(var a in this.contextActions) {
var tmp = this.contextActions[a](this);
if(tmp !== false) {
actions.push(tmp);
}
}
return actions;
};
// Get a value from the entity, or a default value if specified
Entity.prototype.get = function (key,defaultValue) {
if(this.hasOwnProperty(key)) {
return this[key];
}
return defaultValue;
};
// Set a value on the entity
Entity.prototype.set = function (key,value) {
this[key] = value;
};
// Check visibility
Entity.prototype.visibleFrom = function(location) {
return ECS.runCallbacks(this, `isVisible`, {'location': location});
};
// Add entity to ECS
ECS.e = ECS.entity = function (key, components, options) {
// Create instance
var e = new Entity();
e.key = key;
// Check for existing key and abort if found
// This is considered an unrecoverable error
if(ECS.hasEntity(key)) {
throw `ECS: Entity with key '`+key+`' already exists.`;
}
// Load components
if (!isArray(components) || components.length == 0) {
components = [`thing`];
}
for (var c in components) {
var componentName = components[c];
if (!e.c(componentName)) {
throw `ECS: Could not find entity component '` + componentName + `'.`;
}
}
// Handle persist data to avoid overwrite
if (typeof options.persist != `undefined`) {
$.merge(options.persist, e.persist);
}
// Set options
ECS.setOptions(e, options);
// Add tags
if (typeof options.extraTags !== `undefined`) {
e.tags = e.tags.concat(options.extraTags);
}
// Add nouns for entity (used by NLP)
if (!e.hasOwnProperty(`nouns`)) {
e.nouns = [];
}
// Add full name to noun list
e.nouns.push(e.name);
for (var i = 0; i < e.nouns.length; i++) {
var noun = e.nouns[i].toLowerCase();
if (!isArray(
this.nouns[noun]
)) {
this.nouns[noun] = [];
}
this.nouns[noun].push(e);
}
// Execute Add Component callbacks
ECS.runCallbacks(e, `onComponentAdd`);
// Init entity
e.init();
// Add entity to ECS
this.entities[key] = e;
// Save entry to 'last of type' list
for(c in e.components) {
ECS.lastOfType[e.components[c]] = e;
}
return e;
};
// Remove entity from ECS
ecs.removeEntity = function (e) {
// Remove from noun list
if (e.hasOwnProperty(`nouns`)) {
for (var i = 0; i < e.nouns.length; i++) {
delete this.nouns[e.nouns[i]];
}
}
// Remove from entity list
delete this.entities[e.key];
};
ecs.moveEntity = function (e, d) {
// Get target entity from key
if(typeof e == `string`) {
e = ECS.getEntity(e);
}
// Get destination entity from key
if (typeof d == `string`) {
d = ECS.getEntity(d);
}
// Remove entity from current location, if any
if (e.place != null) {
e.place.removeChild(e);
}
// Remove entity from current parent, if any
if (e.parent != null) {
e.parent.removeChild(e);
}
// Add entity to destination
d.addChild(e);
if(d.hasComponent(`place`)) {
e.place = d;
}
};
ecs.annihilateEntity = function (e) {
// Get target entity from key
if(typeof e == `string`) {
e = ECS.getEntity(e);
}
// Remove entity from current location, if any
if (e.place != null) {
e.place.removeChild(e);
e.place = null;
}
if (e.parent != null) {
e.parent.removeChild(e);
e.parent = null;
}
};
// Get save-safe value from an entity
ecs.getSaveValue = function (v) {
if (typeof v == `undefined`) {
return [`null`, null];
}
if (v instanceof Entity) {
return [`reference`, v.key];
}
return [`value`, v];
};
// Save ECS
ecs.save = function () {
var data = {
'seed': seed,
'entities': {}
};
for (var i in this.entities) {
if (this.entities[i].persistActive) {
data.entities[this.entities[i].key] = this.entities[i].save();
}
}
return data;
};
// Load from json string
ecs.load = function (data) {
data = JSON.parse(data);
// Replace current seed with value from save data
seed = data.seed;
for (var i in data.entities) {
var e = data.entities[i];
this.entities[e.key].load(data.entities[i]);
}
};
/**
* Counter helpers to keep track of how many times an arbitrary event has happened.
*/
ECS.setData(`counters`, {});
/**
* Get the count value for a given key, or set a new value.
*
* @param string key
* @param int value
* @returns
*/
var count = function (key, value) {
var counters = ECS.getData(`counters`);
// Set a new value for a given key
if (typeof value != `undefined`) {
counters[key] = value;
return;
}
// Check if the given key exists, and return the value if it does
if (counters.hasOwnProperty(key)) {
return counters[key];
}
// The requested key doesn't exist
return null;
};
/**
* Count a given key only once.
* @param string key
*/
var countOnce = function(key) {
return count(key, 1);
};
/**
* Increment a counter.
*
* @param key
*/
var incrementCounter = function(key) {
var c = count(key);
c = (c) ? c + 1 : 1;
count(key, c);
};
/**
* Decrement a counter.
*
* @param key
*/
var decrementCounter = function(key) {
var c = count(key);
c = (c) ? c - 1 : -1;
count(key, c);
};
/**
* Check if the given key matches the given value
*
* @param key
* @param value
* @returns {boolean}
*/
var nth = function (key, value) {
return count(key) === value;
};
/**
* Check if the given key has a value of 1
* Alias for nth(key,1)
*
* @param key
* @returns {boolean}
*/
var first = function (key) {
return nth(key, 1);
};
/**
* Check if the given key has a value of 2
* Alias for nth(key,2)
*
* @param key
* @returns {boolean}
*/
var second = function (key) {
return nth(key, 2);
};
/**
* Check if the given key has a value of 3
* Alias for nth(key,3)
*
* @param key
* @returns {boolean}
*/
var third = function (key) {
return nth(key, 3);
};
/**
* Check if the given key is on a repeating nth count.
*
* @param key
* @param n
* @returns {boolean}
*/
var everyNth = function (key, n) {
console.log(`CHECKING `+key+` for `+n);
return (count(key) % n ) == 0;
};
/**
* Check if the given key is on a repeating 2nd count.
* Alias for everyNth(key,2)
*
* @param key
* @returns {boolean}
*/
var everyOther = function(key) {
return everyNth(key, 2);
};
/**
* Check if the given key is on a repeating 3rd count.
* Alias for everyNth(key,3)
*
* @param key
* @returns {boolean}
*/
var everyThird = function(key) {
return everyNth(key, 3);
};
Handlebars.registerHelper(`first`, function(counter, options) {
if(first(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`second`, function(counter, options) {
if(second(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`third`, function(counter, options) {
if(third(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`nth`, function(counter, options) {
if(nth(counter, options.hash.n)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`everyOther`, function(counter, options) {
if(everyOther(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`everyThird`, function(counter, options) {
if(everyThird(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`everyNth`, function(counter, options) {
console.log(options.hash);
if(everyNth(counter, options.hash.n)) {
return new Handlebars.SafeString(options.fn(this));
}
});
//
// Natural Language Processor
//
var Action = function (actor, text) {
this.actor = actor;
this.string = text;
this.verb = null;
this.target = null;
this.nouns = [];
this.modifiers = [];
this.output = ``;
};
Action.prototype = {
actor: null,
string: ``,
verb: null,
target: null,
nouns: [],
modifiers: [],
output: ``,
update: function (data) {
$.extend(this, data);
}
};
var Response = function (mode, output) {
this.mode = mode;
this.out = output;
};
Response.prototype = {
'output':function(action) {
if(typeof this.out == `string`) {
action.output += this.out;
} else if(typeof this.out == `function`) {
this.out(action);
}
}
};
var nlp = NLP = {
// Response flags
RESPONSE_BEFORE: 0,
RESPONSE_AFTER: 1,
RESPONSE_INSTEAD: 2,
// Trigger a tick for the current action
// Time only passes for ticks, so non-tick actions will not trigger most systems
'tick': true,
// Verb list
'verbs': [],
// Command Interrupt
// Used for modules to interrupt the normal game cycle and take direct input (e.g. inputting a name)
// Null by default. Modules should attach a callback as needed. The callback is responsible for self-deactivation.
'command_interrupt': [],
// Pattern list
// Cannot start with a modifier under any circumstances. Any command that would make sense that way should be handled as an interrupt or a rule.
// In order of priority (highest to lowest):
'patterns': [
`VERB`, // inventory
`VERB NOUN`, // eat baby
`VERB MODIFIER`, // saunter west
`VERB MODIFIER NOUN`, // get in closet
`VERB NOUN MODIFIER`, // turn wheel clockwise
`VERB NOUN MODIFIER NOUN`, // attack goblin with hammer
`VERB MODIFIER MODIFIER NOUN`, // look north through telescope
`VERB MODIFIER NOUN MODIFIER NOUN`, // look at bob through telescope
// Overflow patterns
`VERB MODIFIER NOUN MODIFIER TEXT`, // talk to goblin about greatest fears
`VERB MODIFIER MODIFIER TEXT`, // look north through telescope saucily
`VERB NOUN MODIFIER TEXT`, // ask bob about back pain
`VERB MODIFIER TEXT`, // talk about floops
`VERB TEXT`,
],
// Current action data
'actor': null,
'currentAction': [],
'lastAction': null,
'afterAction': null, // Prev action reference for After rulebook
// Command interrupt function
'interrupt': function (init, callback) {
if(typeof callback == `undefined`) {
console.log(`NLP: INTERRUPT MISSING CALLBACK, REJECTING`);
return;
}
this.command_interrupt.push({'init': init, 'callback': callback});
this.init_next_interrupt();
},
// Simple command interrupt variant
'interrupt_simple': function (command, response) {
this.interrupt(
null,
function (string) {
if (string == command) {
queueOutput(response);
} else {
NLP.parse(string);
}
return true;
}
);
},
// Initialize next interrupt
'init_next_interrupt': function () {
if (this.command_interrupt.length > 0 && this.command_interrupt[0].init !== null) {
this.command_interrupt[0].init();
this.command_interrupt[0].init = null;
}
},
// Check if an interrupt is queued or active
'interruptActive': function () {
return (this.command_interrupt.length > 0);
},
// Create and register a new action
'newAction': function(input_string) {
var action = new Action(this.actor, input_string);
this.currentAction.push(action);
this.lastAction = action;
return action;
},
// Get top action in stack
'topAction': function() {
var c = this.currentAction.length;
return (c > 0) ? this.currentAction[c] : undefined;
},
// Clean up and exit
'exit': function(output) {
this.currentAction.pop();
return output;
},
// Parsing function. Translates input like 'look at box' to something internally useful
// Actions are generally dispatched directly, so return values are only used when parsing has failed
'parse': function (input_string) {
// Clear previous action data
var currentAction = this.newAction(input_string);
// Convert multiple spaces/whitespaces characters to a single space
input_string = input_string.replace(/\s{2,}/g, ` `);
var string = input_string;
// Make sure command isn't empty
if(typeof string == `undefined` || !string.length) {
console.log(`NLP: Empty command string!`);
console.trace();
}
// Trigger a tick by default
this.tick = true;
// If a command interrupt is enabled, skip normal input
if (this.command_interrupt.length > 0) {
var interrupt = this.command_interrupt.shift();
var handled = interrupt.callback(string);
if (handled) {
console.log(`Interrupt handled, removing`);
this.init_next_interrupt();
return this.exit();
}
// Wasn't handled, return the interrupt to the beginning of the queue
console.log(`Interrupt not handled, re-queuing`);
this.command_interrupt.unshift(interrupt);
return this.exit();
}
// Convert string to lowercase for easier matching to verbs/nouns
// If case-sensitive matching is required, a command interrupt must be used
input_string = input_string.toLowerCase();
var target = null;
var modifier = null;
var halt = false;
var tmp = null;
// Loop through patterns and attempt to match against string
var verb = null; // Only one verb is allowed
var modifiers = null; // Any number of modifiers are allowed
var nouns = null; // Any number of nouns are allowed
for (var p = 0; p < this.patterns.length; p++) {
// If a hard halt has been triggered, cancel here
// Used in cases where the input is obviously malformed, such as GO NORTH NORTH
if (halt) {
console.log(`NLP: Halted`);
break;
}
// Reset the modifier and noun lists. Some items may have been parsed last time,
// even if the pattern was not successful overall
modifiers = [];
nouns = [];
// Make a copy of the input string, since we're going to modify it
var parse_string = input_string;
// Tokenize the command
// Using the term 'tokenize' generously
var command_tokens = parse_string.toLowerCase().split(` `);
// Get current pattern and tokenize
var pattern = this.patterns[p];
var pattern_tokens = pattern.toLowerCase().split(` `);
// If there are less tokens in the command than in the pattern, skip this pattern; it won't be a match
if (command_tokens.length < pattern_tokens.length) {
continue;
}
// Loop through pattern tokens and start matching by type
for (var t = 0; t < pattern_tokens.length; t++) {
var token = pattern_tokens[t];
// Run matching function
if (token == `verb`) {
tmp = this.matchVerb(parse_string);
if (tmp.match != null) {
verb = tmp.match;
parse_string = tmp.string;
} else {
// Didn't find matching verb, bail
break;
}
} else if (token == `modifier`) {
tmp = this.matchModifier(parse_string, verb);
if (tmp.match != null) {
if (modifiers.length > 0 && modifiers[0] == tmp.match) {
// Can't match the same modifier twice
halt = true;
break;
}
modifiers.push(tmp.match);
parse_string = tmp.string;
} else {
// Didn't find modifier, bail
break;
}
} else if (token == `noun`) {
tmp = this.matchNoun(parse_string);
if (tmp.match != null) {
nouns.push(tmp.match);
parse_string = tmp.string;
} else {
// Didn't find noun, bail
console.log(`NLP: No noun match for token '`+parse_string+`'`);
break;
}
} else if (token == `text`) {
// Some verbs allow overflow words to be parsed specially
// Example: writing text on a sign, or indicating a topic of discussion
if (verb.hasOwnProperty(`overflow`) && verb.overflow && parse_string.length > 0) {
nouns.push(parse_string);
parse_string = ``;
} else {
// Overflow not allowed, bail
console.log(`NLP: No overflow allowed, no match found`);
break;
}
}
}
// If we've matched all tokens in the pattern, break out
if (parse_string.length == 0) {
console.log(`MATCHED PATTERN: ` + pattern);
break;
}
}
// Get target (for rule purposes)
target = (nouns.length > 0) ? nouns[0] : null;
// Build data for action
currentAction.update({
'verb': verb, // The matched verb object
'text': input_string, // The original input string (some verbs and rules will use it)
'target': target, // The first target object
'modifiers': modifiers, // A list of modifiers fed to the pattern
'nouns': nouns, // A list of nouns fed to the pattern
});
// Pre-process verb
if(verb && verb.hasOwnProperty(`pre`)) {
verb.pre(currentAction);
}
// Run 'Before' ruleset
currentAction = Rulebook.check(`before`, currentAction);
if (currentAction.mode === Rulebook.ACTION_CANCEL) {
return this.exit();
}
console.log(currentAction);
if(currentAction.output) {
queueOutput(currentAction.output);
currentAction.output = ``;
}
// No commands without verbs are allowed
if (verb == null) {
return this.exit(`<p>I don't understand.</p>`);
}
// If remainder parse string length is not 0, we failed to fully parse the string
// Fail to avoid unintended consequences
if (parse_string.length > 0) {
// Get the understood portion of the command
var clean_string = string.replace(` ` + parse_string, ``);
// Allow actions to handle broken inputs on their own, if the verb was matched but the rest of the string didn't quite make sense
if (typeof verb.onBadInput == `function`) {
return this.exit(verb.onBadInput(parse_string));
}
return this.exit(`<p>I understood everything up until '` + parse_string + `'. You want to ` + clean_string.toUpperCase() + `, plus something.</p>`);
}
// Run verb filters
// Some verbs only act on certain types of targets, for example
if (verb.filters.length > 0) {
var args = {'action': currentAction, 'verb': verb, 'nouns': nouns};
if (!ECS.runFilters(verb, `filters`, args)) {
return this.exit();
}
}
// Let target handle action if appropriate callback is provided
// Objects can define their own behaviors for verbs, which will bypass the standard verb behavior
var performDefault = true;
var objectCallback = `onAction.` + verb.aliases[0].toUpperCase().replace(/[\s-]/g, `.`);
var response = new Response(NLP.RESPONSE_AFTER, null);
if (nouns.length > 0 && typeof nouns[0][objectCallback] == `function`) {
response = nouns[0][objectCallback](currentAction);
if (typeof response == `string`) {
response = new Response(NLP.RESPONSE_INSTEAD, response);
} else if (typeof response == `boolean`) {
performDefault = response;
response = new Response(performDefault ? NLP.RESPONSE_BEFORE : NLP.RESPONSE_INSTEAD, null);
}
}
if(response) {
if(response.mode == NLP.RESPONSE_INSTEAD) {
currentAction.output = response.out;
}
if(response.mode == NLP.RESPONSE_BEFORE) {
response.output(currentAction);
}
if (response.mode != NLP.RESPONSE_INSTEAD) {
var result = verb.callback(currentAction);
if(typeof result == `string`) {
currentAction.output += result;
}
}
if(response.mode == NLP.RESPONSE_AFTER) {
response.output(currentAction);
}
}
// Save action for After Rulebook
this.afterAction = currentAction;
return this.exit(currentAction.output);
},
// Match verb
'matchVerb': function (string) {
var tokens = string.split(` `);
// Loop through token list, trying to parse longest string first (most tokens)
for (var i = tokens.length; i > 0; i--) {
var verb = tokens.slice(0, i).join(` `);
var action = ECS.getAction(verb);
if (action != null) {
return {'match': action, 'string': tokens.slice(i).join(` `)};
}
}
return {'match': null, 'string': string};
},
// Match modifier for action
'matchModifier': function (string, action) {
var tokens = string.split(` `);
// Loop through token list, trying to parse longest string first (most tokens)
for (var i = tokens.length; i > 0; i--) {
var modifier = tokens.slice(0, i).join(` `);
if (action.modifiers.indexOf(modifier) >= 0) {
// Return found match and leftover string
return {'match': modifier, 'string': tokens.slice(i).join(` `)};
}
}
// No match found
return {'match': null, 'string': string};
},
// Match noun
'matchNoun': function (string) {
var tokens = string.split(` `);
// Loop through token list, trying to parse longest string first (most tokens)
for (var i = tokens.length; i > 0; i--) {
var noun = tokens.slice(0, i).join(` `);
var object = ECS.findEntityByName(noun, `local`);
console.log(`Checking noun: `+noun);
if (object != null) {
return {'match': object, 'string': tokens.slice(i).join(` `)};
}
}
return {'match': null, 'string': string};
},
};
var changelog = [
{
'version':`1.0.5`,
'notes':[
`Fixed broken interaction for taking telescope.`,
`Fixed missing items in room descriptions`,
]
},
{
'version':`1.0.4`,
'notes':[
`1.0.4-RC`,
`Whitespace fix for input`,
`Various timing fixes`,
`Act 2 and 3 transition fixes`,
`Changed tavern music swap`,
`Fixed credits command`,
]
},
{
'version':`1.0.2`,
'notes':[
`1.0.2-RC`,
`Bug fixes for boss encounter`,
`Fail state for boss encounter`,
`Fixed ability to lose sword`,
`Fixed elevator interactions`,
`Fixed Jane and Jack interactions in Act 3`,
`Fixed some music captions`,
`Fixed snowglobe exit`,
]
},
{
'version':`1.0.0`,
'notes':[
`1.0.0-RC`,
`Added boss encounters for each class`
]
},
{
'version':`0.10.8`,
'notes':[
`Fix for ogre interrupt handling and load operation`,
`Undo rejection`,
`Made Sea Witch 50% less edgy`,
`Fixed Troglodyte combat encounter post-death`,
`Potted plant is now a potted plastic plant`,
`Delay tweaks for Act3 twins encounter`,
]
},
{
'version':`0.10.7`,
'notes':[
`Act3 RC`,
]
},
{
'version':`0.10.5`,
'notes':[
`Music fixes`,
`Spider conversation fixes`,
`Better handling of arbitrary speech`,
]
},
{
'version':`0.10.4`,
'notes':[
`Playtested Act 2`,
`Fixed Act 2 to Act 3 transition`,
`Fixed general scenery bug`,
]
},
{
'version':`0.10.1`,
'notes':[
`Fixed observer dialogue tree`,
`Fixed rainbow sword first interaction`
]
},
{
'version':`0.10.0`,
'notes':[
`Loaded all music files`,
`Loaded music captions`,
`Act 3 boss work & jukebox`,
`Added output queue event handler for easier sequences`
]
},
{
'version':`0.9.1`,
'notes':[
`Minor Act1 cleanup`,
`Improved version check`,
]
},
{
'version':`0.9.0`,
'notes':[
`Revamped ice key quest`,
`Revamped benefactor quest`,
`Act1 RC`
]
},
{
'version':`0.8.2`,
'notes':[
`Snowglobe land implementation`,
`Act 1 puzzle restructure`
]
},
{
'version':`0.8.1`,
'notes':[
`Prologue bug fixes.`,
`Act 1 cleanup.`
]
},
{
'version':`0.8.0`,
'notes':[
`Mass linkage of all locations.`,
`Filled out descriptions for most/all locations.`,
`Prologue RC`
]
},
{
'version':`0.7.1`,
'notes':[
`Added snowglobe and music`,
`Connected vault, treasury, bedchamber locations`,
`Fixed Part component parent handling`,
]
},
{
'version':`0.7.0`,
'notes':[
`Added Space Creature file (inactive)`,
`Improved telescope handling`,
`Added some tutorializing for ice cube`,
`Miscellaneous bug fixes`
]
},
{
'version':`0.6.3`,
'notes':[
`Added contextual help command`,
`Added version checking with notification`,
`Improvements to telescope interactions`,
]
},
{
'version':`0.6.2`,
'notes':[
`Added bridge music files`,
`Improved music transition handling`,
`Added support for linked music files and playback resume`,
`Added some missing scenery`
]
},
{
'version':`0.6.1`,
'notes':[
`First playtest`,
`Added missing scenery in most underground locations`,
`Most locations linked for Prologue, Acts 1-3`
]
},
{
'version':`0.2.1`,
'notes':[
`Added Response handling to NLP for more flexible onAction callbacks.`,
`Added bridge direction text to hot springs.`,
`Fixed Bridge state handling.`,
]
},
{
'version':`0.2.0`,
'notes':[
`Stubbed in Act 1, Act 2, Act 3 locations (all added and linked).`,
`Added sea witch encounter.`,
`Added Act 1 transition and starting sequence.`,
`Added Bridge zones and Underground.`,
`Reworked process for obtaining gate key.`,
`Conversation and output handling improvements.`,
]
},
{
'version':`0.1.0`,
'notes':[
`Fleshed out starting forest region with new locations and scenery.`,
`Added bonus descriptions for races and classes.`,
`Added music and closed captioning support.`,
`Stubbed in social module.`,
`Improved matching of menu elements.`,
`Interrupts can now be queued instead of nested.`,
`Revamped callback handling.`,
`Revamped handling of multiple objects with overlapping nouns.`
]
},
{
'version':`0.0.5`,
'notes':[
`Added basic SAVE and LOAD support`,
`Added LOAD option to endgame screen`,
`Added onRemoveChild callback to handle removal of objects from containers`,
`Stubbed out Acts 1-3 modules`
]
},
{
'version':`0.0.4`,
'notes':[
`Added Systems with game tick support`,
`Added Living system for NPC activities`,
`Moved player to campaign module`,
`Added queueGMOutput convenience function`,
`THE BLACK BOX:`,
`Troglodyte will now get annoyed and eventually angry when attacked`,
`Troglodyte will attack and kill player.`
]
},
{
'version':`0.0.3`,
'notes':[
`Added basic combat action, hp handling, and death state`,
`Added basic hug action`,
`Added callback fallback (callback can indicate action not handled, fallback to generic)`,
`Added basic dice roller`,
`Fixed room descriptions when in darkness`,
`THE BLACK BOX:`,
`Changed torch to orb to eliminate need for on/off right now`,
`Fixed double echo issue with Musty Cave entry interrupt`,
`Added death/roll/vomit sequence for troglodyte death`,
`Added a couple end-game states`,
`First BB version where end state can be reached`
]
},
{
'version':`0.0.2`,
'notes':[
`Remove old entries from output list to avoid infinitely-expanding DOM.`,
`Added RAINBOW SWORD and MUSTY CAVE to Black Box module.`,
`Refactored ECS to avoid nesting properties per-component.`,
`Added support for callback lists and filter lists to ECS.`,
`Added Darkness module with Emitter component and light attributes for places.`,
`Added action filters to prevent actions in certain circumstances, e.g. darkness.`,
`Added simple 'last input' feature (hit up arrow to select previous input string).`,
`Added object list to room descriptions. Components and objects can specify whether they are listed automatically.`,
`Improved item tagging.`,
`Improved container/supporter descriptions.`,
`Improved local object listings.`,
`Added missing descriptions for items/places in Black Box module.`
]
},
{
'version':`0.0.1`,
'notes':[
`Started keeping a changelog.`,
`Basic ECS structure.`,
`Working starting location, intro sequence, and common actions (movement, look, etc).`,
`Support for scenery items.`,
`Ability to TAKE items.`,
`Queued output with optional effects (fade, etc).`,
`Command interrupts for special input handling (e.g. 'what is your name?')`,
`Handlebars templating with common handlers (tagged items, paragraphs, etc).`
]
}
];
//
// Create World
//
var game = null;
var player = null;
var Display = {};
var Engine = {
// Global variables
'outputTimer': null,
'outputProgress': null,
'waiting': false,
'blockId': 0,
'flags': [],
'seed': Math.random(),
'inputEnabled': false,
'inputPaused': false,
'lastInput': ``,
'inputLimit': 100,
'inputQueue': [],
'inputReplayQueue': {'default':[]},
'outputQueue': [],
'outputQueueDeferred': [],
// Default engine functions
'init': function() {
// Get hash (url fragment) flags
var tmp = window.location.hash.substr(1);
this.flags = tmp.split(`,`);
// Check for newer version
this.checkVersion();
},
'stopWaiting':function() {
if(this.inputPaused) { return; }
$(`.input`).removeClass(`waiting waiting-1 waiting-2 waiting-3`).attr(`data-waiting`, 0);
this.inputEnabled = true;
this.waiting = false;
Display.giveInputFocus();
// Execute queued input
if (this.inputQueue.length > 0) {
this.execute(this.inputQueue.shift());
}
},
'hasFlag': function(flag) {
return (this.flags.indexOf(flag) >= 0);
},
'checkVersion': function() {
$.ajax({
url: `
http://stupidrpg.com/version`,
success: function(data) {
data = data.split(`.`);
var version = changelog[0].version.split(`.`);
if(data[0] > version[0] || (data[0] == version[0] && data[1] > version[1]) || (data[0] == version[0] && data[1] == version[1] && data[2] > version[2])) {
$(`.title .alert`).html(`There is a newer version available: <a href='
http://stupidrpg.com/` + data.join(`.`) + `'>` + data.join(`.`) + `</a>`).show();
}
},
dataType: `text`
});
}
};
/**
* Queues text output.
*
* @param tmp the unprocessed text to output
* @param delay the delay to add, in milliseconds
* @param data extra data to use in the text template
* @param deferred defer the output as part of action processing
*/
var queueOutput = Engine.queueOutput = function(tmp, delay, data, deferred) {
if(typeof tmp == `undefined`) {
console.log(`UNDEFINED OUTPUT`);
console.trace();
}
if (typeof(deferred) === `undefined`) {
deferred = true;
}
if (typeof delay == `undefined`) {
delay = 0;
}
var output = {'tmp': tmp, 'data': data, 'delay': delay};
if(deferred) {
Engine.outputQueueDeferred.push(output);
} else {
Engine.outputQueue.push(output);
}
};
/**
* Queues output for a specific character
*
* @param character the character speaking
* @param tmp the unprocessed text to output
* @param delay the delay to add, in milliseconds
* @param data extra data to use in the text template
*/
var queueCharacterOutput = Engine.queueCharacterOutput = function(character, tmp, delay, data) {
queueOutput(getSpeechTag(character)+tmp, delay, data);
};
/**
* Queues output prefixed with a GM tag. Convenience function.
*
* @param tmp the unprocessed text to output
* @param delay the delay to add, in milliseconds
* @param data extra data to use in the text template
*/
var queueGMOutput = Engine.queueGMOutput = function(tmp, delay, data) {
queueOutput(`{{gm}}<p>` + tmp + `</p>`, delay, data);
};
/**
* Queues output only if the originator (the source of the event) and
* the player are in the same location.
*
* TODO: This should be deprecated and replaced with something less ridiculous. Entwining display logic and game logic like this is super dumb.
*
* @param source the originating entity
* @param tmp the unprocessed text to output
* @param delay the delay to add, in milliseconds
* @param data extra data to use in the text template
*/
var queueLocalOutput = Engine.queueLocalOutput = function(source, tmp, delay, data) {
if (source.location() == player.location()) {
queueGMOutput(tmp, delay, data);
}
};
/**
* Queues an event
* @param callback
*/
var queueEvent = Engine.queueEvent = function(callback) {
queueOutput(callback);
};
/**
* Pushes all deferred output to the regular output queue.
*/
var processDeferredOutputQueue = Engine.processDeferredOutputQueue = function() {
while(Engine.outputQueueDeferred.length > 0) {
Engine.outputQueue.push(Engine.outputQueueDeferred.shift());
}
};
/**
* Process the output queue.
*
* Handles output delays, special effects, and input toggling.
*/
var processOutputQueue = Engine.processOutputQueue = function() {
var effect = null;
var delay = 100;
window.clearInterval(Engine.outputProgress);
if (Engine.outputQueue.length > 0) {
// Shift item from beginning of queue
var item = Engine.outputQueue.shift();
// Flag Engine as waiting
Engine.waiting = true;
// Set timer for next item
delay = item.delay || 100;
if (delay > 0 || delay == `auto`) {
$(`.input`).animate({'color': `transparent`}, 200);
$(`.input`).addClass(`waiting`);
Engine.inputEnabled = false;
}
// Handle event
if(typeof item.tmp == `function`) {
item.tmp();
} else {
// Parse queued item
var output = parse(item.tmp, item.data);
// Handle auto delay
if(delay == `auto`) {
// Count words in the output
var words = $(output).text().split(` `).length;
// Assuming read speed of 300WPM, padded by 20% + 500ms
// 300WPM is 5 words per second
delay = Math.max(2000, 500 + Math.round((words / 5) * 1000 * 1.2));
}
// Disable delay in debug mode
if(Engine.hasFlag(`debug`)) {
delay = 0;
}
// Handle effect if set
if (typeof item.data != `undefined` && typeof item.data.effect != `undefined`) {
effect = item.data.effect;
}
// Wrap output
var classes = (item.data && item.data.classes) ? item.data.classes : [];
var id = `block-` + (Engine.blockId++);
output = `<div class='output-line `+classes+`' id='`+id+`'>` + output + `</div>`;
// Actually output
if (effect == null) {
$(`.input`).before(output);
} else if (effect == `fade`) {
$(output).hide().insertBefore(`.input`).fadeIn(1000);
}
// Handle prefix if set. Prefix setter is responsible for unsetting it later.
if (typeof item.data != `undefined` && typeof item.data.prefix != `undefined`) {
Display.setInputPrefix(item.data.prefix);
}
// Handle suffix if set. Suffix setter is responsible for unsetting it later.
if (typeof item.data != `undefined` && typeof item.data.suffix != `undefined`) {
Display.setInputSuffix(item.data.suffix);
}
// Save to transcript
$(`#transcript`).append(output);
}
// If queue is now empty, re-enable input
if (Engine.outputQueue.length == 0) {
$(`.input`).delay(delay).animate({'color': `auto`}, 200).promise().done(function () {
Engine.stopWaiting();
});
}
}
Engine.outputTimer = window.setTimeout(processOutputQueue, delay);
if(delay > 3000) {
Engine.outputProgress = window.setInterval(function(){
var wait = Math.min(3, $(`.input`).attr(`data-waiting`) || 0);
if(wait == 3) {
$(`.input`).attr(`data-waiting`, 0);
$(`.input`).removeClass(`waiting-3 waiting-2 waiting-1`);
return;
}
$(`.input`).attr(`data-waiting`, ++wait);
$(`.input`).addClass(`waiting-`+wait);
}, 1000);
}
};
// Execute input
var execute = Engine.execute = function(input) {
ECS.tick = true;
input = input.trim();
// Save input to replay queue
Engine.inputReplayQueue.default.push(input);
// Execute player input
if (Engine.inputEnabled) {
Engine.lastInput = input;
// Fade previous text
$(`.output .output-line`).addClass(`old`);
// Execute player action
var inputOutput = parse(`echo`, {'text': input});
queueOutput(inputOutput);
// Save input line to transcript
//$(`#transcript`).append(inputOutput);
// Parse input and queue generated response
NLP.actor = player;
var response = NLP.parse(input);
// NLP may output/queue response itself if command is handled by an interrupt
if(typeof response == `string` && response.length > 0) {
var output = parse(response, {});
queueOutput(output, 0, {}, true);
}
// Execute tick
if (ECS.tick) {
console.log(`TICK`);
for (var s in ECS.systems) {
var system = ECS.systems[s];
var matches = [];
// Get relevant entities for system
for (var c in system.components) {
matches = matches.concat(ECS.findEntitiesByComponent(system.components[c]));
}
console.log(system);
system.onTick(matches);
}
console.log(`TOCK`);
}
// Check 'after' rules (can't modify/cancel command, but can add on to it)
Rulebook.check(`after`, NLP.afterAction);
// Queue deferred output
processDeferredOutputQueue();
// Do cleanup on items that are offscreen
$(`.output`).find(`:offscreen`).remove();
} else {
Engine.inputQueue.push(input);
}
};
// Sequence object
var Sequence = function() {
this.blocks = [];
this.index = -1;
};
Object.defineProperty(Sequence, `MODE_WAIT`, { value: 0 });
Object.defineProperty(Sequence, `MODE_CONTINUE`, { value: 1 });
Sequence.prototype = {
'MODE_WAIT': 0,
'MODE_CONTINUE': 1,
'add': function(f, mode) {
console.log(`Sequence: Adding block with mode `+mode+`(`+((typeof mode == `undefined`) ? this.MODE_WAIT : mode)+`)`);
this.blocks.push({
'mode': (typeof mode == `undefined`) ? this.MODE_WAIT : mode,
'function': f
});
},
'start': function() {
this.next();
},
'next': function() {
this.index++;
console.log(`Sequence: Checking block `+this.index);
if(this.blocks.length > this.index) {
this.blocks[this.index][`function`]();
if(this.blocks[this.index][`mode`] == this.MODE_CONTINUE) {
console.log(`Sequence: Auto-Continuing`);
this.next();
} else {
console.log(`Sequence: Waiting`);
console.log(this.blocks[this.index]);
}
}
}
};
// Common template list. Submodules can add their own templates to this list
var templates = {
// Player output echo format
'default': `<p>{{text}}</p>`,
'echo': `{{p 'player' text}}`,
};
// Conditional helper
// Invoke with {{when value '<=' otherValue}}
// Credit:
http://stackoverflow.com/a/16315366/96089
Handlebars.registerHelper(`when`, function (v1, operator, v2, options) {
switch (operator) {
case `==`:
return (v1 == v2) ? options.fn(this) : options.inverse(this);
case `===`:
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case `<`:
return (v1 < v2) ? options.fn(this) : options.inverse(this);
case `<=`:
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
case `>`:
return (v1 > v2) ? options.fn(this) : options.inverse(this);
case `>=`:
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
case `&&`:
return (v1 && v2) ? options.fn(this) : options.inverse(this);
case `||`:
return (v1 || v2) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
});
// Expression-based conditional helper
// Credit:
http://stackoverflow.com/a/21915381/96089
Handlebars.registerHelper(`xif`, function (expression, options) {
return Handlebars.helpers[`x`].apply(this, [expression, options]) ? options.fn(this) : options.inverse(this);
});
Handlebars.registerHelper(`x`, function (expression) {
var fn = function(){}, result;
// in a try block in case the expression have invalid javascript
try {
// create a new function using Function.apply, notice the capital F in Function
fn = Function.apply(
this,
[
`window`, // or add more '_this, window, a, b' you can add more params if you have references for them when you call fn(window, a, b, c);
`return ` + expression + `;` // edit that if you know what you're doing
]
);
} catch (e) {
console.warn(`[warning] {{x ` + expression + `}} is invalid javascript`, e);
}
// then let's execute this new function, and pass it window, like we promised
// so you can actually use window in your expression
// i.e expression ==> 'window.config.userLimit + 10 - 5 + 2 - user.count' //
// or whatever
try {
// if you have created the function with more params
// that would like fn(window, a, b, c)
result = fn.call(this, window);
} catch (e) {
console.warn(`[warning] {{x ` + expression + `}} runtime error`, e);
}
// return the output of that result, or undefined if some error occured
return result;
});
// Header helper (wraps text in a header tag with optional classes)
// Invoke with {{header 'classes here' text}}
Handlebars.registerHelper(`header`, function (classes, text) {
return new Handlebars.SafeString(
`<header class='` + classes + `'>` + text + `</header>`
);
});
// Paragraph helper (wraps text in a p tag with optional classes)
// Invoke with {{p 'classes here' text}}
Handlebars.registerHelper(`p`, function (classes, text) {
return new Handlebars.SafeString(
`<p class='` + classes + `'>` + text + `</p>`
);
});
// Box helper (create an announcement box with title and text)
// Invoke with {{box title text}}
Handlebars.registerHelper(`box`, function (title, text, classes) {
return new Handlebars.SafeString(
`<div class='box ` + classes + `'><header>` + title + `</header><p>` + text + `</p></div>`
);
});
// Tag helper (create higlight / command tags)
// Invoke with {{tag text options}}
Handlebars.registerHelper(`tag`, function (text, options) {
var attrs = [];
var tag = ``;
var tmp = ``;
var c = ``;
// Force well-formed option hash
var hash = $.extend({}, options.hash);
console.log(this);
// Set default class(es)
var classes = [`tag`];
// Check for command
if (typeof hash.command != `undefined`) {
classes.push(`command`);
attrs.push(`data-command='` + hash.command + `'`);
}
// Process additional classes
if (typeof hash.classes != `undefined`) {
tmp = hash.classes.split(` `);
for (c in tmp) {
classes.push(tmp[c]);
}
}
// Check for context menu
var contextMenu = ``;
if (typeof this.context != `undefined`) {
classes.push(`context`);
tmp = ``;
for (c in this.context) {
tmp += `<li><div class='tag command' data-command='` + this.context[c].command + `'>` + this.context[c].text + `</li>`;
}
contextMenu = `<ul>` + tmp + `</ul>`;
}
// Build tag
tag = `<div class='` + classes.join(` `) + `' ` + attrs.join(` `) + `>` + text + contextMenu + `</div>`;
return new Handlebars.SafeString(tag);
});
// Name tag helper
// Invoke with {{nametag name options}}
Handlebars.registerHelper(`nametag`, function (name, options) {
// Force well-formed option hash
var hash = $.extend({}, options.hash);
// Get object
var t = ECS.findEntity(`thing`, name);
if (t == null) {
return;
}
var classes = [].concat(t.tags);
// Check for extra classes
if (typeof hash.classes != `undefined`) {
classes = classes.concat(hash.classes.split(` `));
}
// Check for command
var command = `x ` + t.name;
if (typeof hash.command != `undefined`) {
command = hash.command;
}
// Check for printed name
var printedName = t.name;
if (typeof hash.print != `undefined`) {
printedName = hash.print;
}
// Build context options
var data = {'context': []};
data.context = t.getContextActions();
var tag = `{{tag '` + printedName + `' classes='` + classes.join(` `) + `' command='` + command + `'}}`;
return new Handlebars.SafeString(parse(tag, data));
});
// Menu helper (create list of command items that can be triggered directly)
// Invoke with: {{menu options}}
// Options is a key-value set where the value is a command string
Handlebars.registerHelper(`menu`, function (options) {
var output = `<ul class='menu'>`;
for (var item in options) {
if (typeof options[item].subtext == `undefined`) {
options[item].subtext = ``;
}
output += `<li class='command' data-command='` + options[item].command + `'>` + options[item].text + `<span class='right subtext'>` + options[item].subtext + `</span></li>`;
}
return new Handlebars.SafeString(output);
});
// GM helper (output the GM prefix)
// Invoke with {{gm}}
Handlebars.registerHelper(`gm`, function () {
return new Handlebars.SafeString(`<span class='gm'>GM:</span> `);
});
// Template parser
function parse(tmp, data) {
if (typeof tmp == `string` && tmp.length > 0) {
// By default, use the passed value as a template
var source = tmp;
// If the passed value is a string and a common template matches the name,
// load and use the common template
if (typeof tmp == `string` && typeof templates[tmp] != `undefined`) {
source = templates[tmp];
}
// Combine data with standard values
data = $.extend({'player': player, 'place': player.place}, data);
// Compile template
var template = Handlebars.compile(source);
return template(data);
}
};
/**
* Counter helpers to keep track of how many times an arbitrary event has happened.
*/
ECS.setData(`counters`, {});
/**
* Get the count value for a given key, or set a new value.
*
* @param string key
* @param int value
* @returns
*/
var count = function (key, value) {
var counters = ECS.getData(`counters`);
// Set a new value for a given key
if (typeof value != `undefined`) {
counters[key] = value;
return;
}
// Check if the given key exists, and return the value if it does
if (counters.hasOwnProperty(key)) {
return counters[key];
}
// The requested key doesn't exist
return null;
};
/**
* Count a given key only once.
* @param string key
*/
var countOnce = function(key) {
return count(key, 1);
};
/**
* Increment a counter.
*
* @param key
*/
var incrementCounter = function(key) {
var c = count(key);
c = (c) ? c + 1 : 1;
count(key, c);
};
/**
* Decrement a counter.
*
* @param key
*/
var decrementCounter = function(key) {
var c = count(key);
c = (c) ? c - 1 : -1;
count(key, c);
};
/**
* Check if the given key matches the given value
*
* @param key
* @param value
* @returns {boolean}
*/
var nth = function (key, value) {
return count(key) === value;
};
/**
* Check if the given key has a value of 1
* Alias for nth(key,1)
*
* @param key
* @returns {boolean}
*/
var first = function (key) {
return nth(key, 1);
};
/**
* Check if the given key has a value of 2
* Alias for nth(key,2)
*
* @param key
* @returns {boolean}
*/
var second = function (key) {
return nth(key, 2);
};
/**
* Check if the given key has a value of 3
* Alias for nth(key,3)
*
* @param key
* @returns {boolean}
*/
var third = function (key) {
return nth(key, 3);
};
/**
* Check if the given key is on a repeating nth count.
*
* @param key
* @param n
* @returns {boolean}
*/
var everyNth = function (key, n) {
console.log(`CHECKING `+key+` for `+n);
return (count(key) % n ) == 0;
};
/**
* Check if the given key is on a repeating 2nd count.
* Alias for everyNth(key,2)
*
* @param key
* @returns {boolean}
*/
var everyOther = function(key) {
return everyNth(key, 2);
};
/**
* Check if the given key is on a repeating 3rd count.
* Alias for everyNth(key,3)
*
* @param key
* @returns {boolean}
*/
var everyThird = function(key) {
return everyNth(key, 3);
};
Handlebars.registerHelper(`first`, function(counter, options) {
if(first(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`second`, function(counter, options) {
if(second(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`third`, function(counter, options) {
if(third(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`nth`, function(counter, options) {
if(nth(counter, options.hash.n)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`everyOther`, function(counter, options) {
if(everyOther(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`everyThird`, function(counter, options) {
if(everyThird(counter)) {
return new Handlebars.SafeString(options.fn(this));
}
});
Handlebars.registerHelper(`everyNth`, function(counter, options) {
console.log(options.hash);
if(everyNth(counter, options.hash.n)) {
return new Handlebars.SafeString(options.fn(this));
}
});
// Replay the session from the beginning, repeating all commands in order
// Optionally restart a named replay
Engine.replay = function(name) {
// Get named replay or default replay queue
var queue = (typeof name != `undefined`) ? name : `default`;
var replayQueue = Engine.inputReplayQueue[queue];
// Store commands in local storage
localStorage.setItem(`replay`, true);
localStorage.setItem(`replay-data`, JSON.stringify({
'seed':0,
'commands':replayQueue
}));
// Refresh page
window.location.reload();
};
// Set a named replay marker (save replay up until this point)
Engine.replay_marker = function(name) {
Engine.inputReplayQueue[name] = Engine.inputReplayQueue.default.slice();
console.log(`Replay Queue '` + name + `' saved.`);
};
Engine.loadReplayIfAvailable = function() {
if(localStorage.getItem(`replay-data`)) {
seed = localStorage.getItem(`replay-data`).seed;
Engine.inputQueue = JSON.parse(localStorage.getItem(`replay-data`)).commands;
localStorage.removeItem(`replay`);
localStorage.removeItem(`replay-data`);
}
};
Display.giveInputFocus = function() {
$(`.input`).trigger(`focus`);
};
Display.setInputPrefix = function(prefix) {
$(`.input .prefix`).html(`>` + prefix);
};
Display.resetInputPrefix = function() {
$(`.input .prefix`).html(`>`);
};
Display.setInputSuffix = function(suffix) {
$(`.input .suffix`).html(suffix);
$(`.input .entry`).addClass(`suffixed`);
};
Display.resetInputSuffix = function() {
$(`.input .suffix`).html(``);
$(`.input .entry`).removeClass(`suffixed`);
};
Display.resetInputFixes = function() {
Display.resetInputPrefix();
Display.resetInputSuffix();
};
// Keypress for most keys (characters, numerals, spaces, enter)
$(`.input`).keypress(function (event) {
var k = event.which;
var output = null;
var curStr = $(`.input .text`).html();
if(Engine.waiting && k == 32) { // Spacebar
console.log(`User skipping wait period.`);
clearTimeout(Engine.outputTimer);
Engine.outputTimer = null;
Engine.processOutputQueue();
return;
}
if (
(k >= 65 && k <= 90) || // A-Z
(k >= 97 && k <= 122) || // a-z
(k >= 48 && k <= 57) || // 0-9
k == 39 || // ' (apostrophe)
k == 33 || // !
k == 36 || // $
k == 32 || // (space)
k == 45 // - (dash)
) {
output = curStr + String.fromCharCode(k);
}
else if (k == 13) {
// ENTER
output = ``;
// Tick
Engine.execute(curStr);
}
else {
// No changes
return;
}
$(`.input .text`).html(output.substr(0, Engine.inputLimit));
});
// Keydown for special keys (backspace, escape)
$(`.input`).keydown(function (event) {
var k = event.which;
var output = null;
var curStr = $(`.input .text`).html();
if (k == 8) {
// BACKSPACE
output = curStr.substr(0, curStr.length - 1);
}
else if (k == 27) {
// ESCAPE (clear)
output = ``;
}
else if (k == 38) {
// UP ARROW (last item)
output = Engine.lastInput;
}
else {
// No changes
return;
}
event.preventDefault();
$(`.input .text`).html(output.substr(0, Engine.inputLimit));
});
// Command tag click handler
$(`body`).on(`click`, `.command`, function (e) {
console.log(e);
e.stopPropagation();
// If engine is waiting, ignore
if(Engine.waiting) { return; }
// Get command
var command = $(this).attr(`data-command`);
var parent = $(this).parent();
// Check if this is a menu command
if (parent.is(`ul.menu`)) {
// Check if already disabled
if (parent.hasClass(`disabled`)) {
// Cancel the command
return;
}
Display.disableMenu(parent, this);
}
// Flag in transcript
$(`#transcript`).append(p(`(from context menu)`));
Engine.execute(command);
});
/**
* Disable commands in the specified menu.
* Typically triggered when a menu item is clicked.
*
* @param menu the menu to update
* @param selected the selected item in the menu
*/
Display.disableMenu = function(menu, selected) {
// Mark menu as disabled
menu.addClass(`disabled`);
// Mark this item as selected
$(selected).addClass(`selected`);
};
// Disable the latest menu and set the selected item to one matching the given text
var disableLastMenu = Display.disableLastMenu = function(selected) {
var menu = $(`.output`).find(`.output-line .menu`).last();
selected = menu.find(`li[data-command="` + selected + `"]`);
Display.disableMenu(menu, selected);
};
// Enable the latest menu and clear the selected item, if any
var enableLastMenu = Display.enableLastMenu = function() {
var menu = $(`.output`).find(`.output-line .menu`).last().clone();
menu.find(`li.selected`).removeClass(`selected`);
menu.removeClass(`disabled`);
queueOutput(menu[0].outerHTML);
};
// Load a different visual theme
Display.loadTheme = function(theme) {
$(`#theme`).replaceWith(`<link id="theme" rel="stylesheet" href="css/` + theme + `.css">`);
};
//
// FEEDBACK
//
function saveFeedback(blockId, text) {
$(`#` + blockId).append(`<div class='feedback'>` + text + `</div>`);
}
$(document).bind(`keydown`, `ctrl+shift+f`, function(e) {
e.stopPropagation();
var selection = getSelection();
// Get selected block or last block if none selected
var block = null;
var selectedText = $(`<div/>`);
if(selection.type == `Range`) {
block = $(selection.anchorNode).closest(`.output-line`);
selectedText.addClass(`selection`);
selectedText.html(`"` + selection.toString() + `"`);
} else {
block = $(`.output .output-line`).last();
}
console.log(selectedText);
console.log(block);
var feedback = $(`<div/>`);
feedback.addClass(`feedback`);
feedback.attr(`data-block`, block.attr(`id`));
var response = window.prompt(`Your Feedback:`);
feedback.html(selectedText[0].outerHTML + response);
$(block).append(feedback.clone());
$(`#transcript #`+block.attr(`id`)).append(feedback.clone());
//$(`tmp`).css({"background":`yellow`});
});
/**
* Save the game transcript.
* Writes an HTML document with the transcript and pushes it as a download to the browser.
*/
function saveTranscript() {
var html = [];
var date = (new Date).toDateString();
var name = player.name;
html.push(`<html><head><title>SRPG Transcript - `+ date +` - `+ name +`</title>`);
html.push(`<link href='
https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700' rel='stylesheet' type='text/css'>`);
html.push(`<link id='theme' rel='stylesheet' href='Onyx/css/default.css'>`);
html.push(`<script src='Onyx/js/vendor/modernizr-2.6.2.min.js'></script>`);
html.push(`</head><body>`);
html.push(`<div class='frame'><div class='output' style='position: static'>`);
html.push($(`#transcript`).html());
html.push(`</div></div>`);
html.push(`</body></html>`);
var blob = new Blob([html.join(``)], {type: `text/html;charset=utf-8`});
saveAs(blob, `SRPG-Transcript.html`);
}
/**
* Add local scenery for the most recently added location.
*/
function localScenery(nouns, description) {
var parent = ECS.lastOfType.place;
// Auto-generate a location-based key to avoid conflicts with generically-named scenery
var key = [`local-scenery`, parent.key, nouns[0]].join(`-`);
ECS.e(key, [`scenery`], {
'name':nouns[0],
'nouns':nouns,
'spawn':parent.key,
'descriptions':{
'default':description
}
});
return ECS.getEntity(key);
}
/**
* Get a canonical direction, i.e. its most abbreviated form.
*/
function getCanonicalDirection(d) {
d = d.toLowerCase();
if ([`n`, `north`].indexOf(d) >= 0) {
return `n`;
}
if ([`e`, `east`].indexOf(d) >= 0) {
return `e`;
}
if ([`s`, `south`].indexOf(d) >= 0) {
return `s`;
}
if ([`w`, `west`].indexOf(d) >= 0) {
return `w`;
}
if ([`nw`, `northwest`].indexOf(d) >= 0) {
return `nw`;
}
if ([`sw`, `southwest`].indexOf(d) >= 0) {
return `sw`;
}
if ([`ne`, `northeast`].indexOf(d) >= 0) {
return `ne`;
}
if ([`se`, `southeast`].indexOf(d) >= 0) {
return `se`;
}
if ([`d`, `down`].indexOf(d) >= 0) {
return `d`;
}
if ([`u`, `up`].indexOf(d) >= 0) {
return `u`;
}
if ([`in`, `enter`].indexOf(d) >= 0) {
return `in`;
}
if ([`out`, `exit`].indexOf(d) >= 0) {
return `out`;
}
return d;
};
// jQuery filter to see if element is offscreen
$.expr.filters.offscreen = function (el) {
return ($(el).offset().top < -500);
};
/**
* Shuffle an array.
*
* Uses the Fisher-Yates shuffle algorithm.
*
* @param array
* @returns {*}
*/
function shuffle(array) {
var currentIndex = array.length
, temporaryValue
, randomIndex
;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// Seeded PRNG
function random() {
var x = Math.sin(Engine.seed++) * 10000;
return x - Math.floor(x);
}
/**
* Roll a single die with a specified number of sides.
*
* @param sides
* @returns {number}
*/
function dice(sides) {
return Math.floor(random() * sides) + 1;
}
/**
* Wrap a piece of text in a paragraph container.
* <div> is used instead of <p> to allow for nested tags.
*
* @param text
* @returns {string}
*/
function p(text) { return `<div class='p'>`+text+`</div>`; }
/**
* Check whether a string matches any of the given values
*/
String.prototype.is = function(...args) {
return (args.indexOf(this.valueOf()) >= 0);
};
var StartGame = function () {
// Set game version
$(`.info .version`).html(`v` + changelog[0].version);
// Initialize engine
Engine.init();
// Initialize ecs
game = ECS;
game.init([
`Core`,
`Darkness`,
`Combat`,
`Containers`,
`Doors`,
`Locks`,
`Reading`,
`Music`,
`Clothing`,
`Social`,
`Quests`,
`Temperature`,
`Sleep`,
// Campaign
`BlackBox`,
`Act1`,
`Act2`,
`Act3`,
]);
// GM entity
game.e(`gm`, [`living`], {
'name': `GM`,
'descriptions': {
'default': `Just pretend I'm not here.`
},
'scope': `global`,
'hp': 1,
'nouns': [`gm`],
'gameStartTime':new Date(),
'onTick': function(system) {
/*
if(system == `living`) {
var diff = (new Date() - this.gameStartTime) / 1000;
if(ECS.getData(`stage`) != `act3` && !NLP.interruptActive && diff > (105 * 60)) {
queueGMOutput(`Going to just throw this out there. We've been going for a while and I know this test session wasn't supposed to take quite so long. That said, I'm really excited for the final encounter in this module, so if you'd like to {{tag 'skip to the end' command='skip to the end'}} we can always get the mid-game portion during the next playtest. Entirely up to you.`);
}
}
*/
}
});
// Fire onGameStart
game.getModule(`BlackBox`).onGameStart();
// Get replay commands
Engine.loadReplayIfAvailable();
// Kick off the output queue, which will already have a bunch of stuff in it from the onGameStart call above
processDeferredOutputQueue();
};
// Start output processor
Engine.outputTimer = window.setTimeout(Engine.processOutputQueue, 100);