| /* 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(`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 action '`+this.key+`'`); | |
| this.stop(); | |
| this.exit(Rulebook.ACTION_NONE); | |
| } | |
| // 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] == `!`); | |
| // 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 == `==`) { | |
| 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 == `containsType`) { | |
| // Set contains item of given type | |
| result = attribute.hasChildOfType(value); | |
| } 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(typeof target == `string`) { | |
| target = ECS.getEntity(target); | |
| } | |
| this.addFilter(function (self, action, params) { | |
| return params.target == action.target; | |
| }, {'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 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 args.location == args.obj.location(); | |
| }, | |
| function(args){ | |
| // Entity is scenery in region | |
| return 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)) { | |
| return; | |
| } | |
| } | |
| // 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 components) { | |
| ECS.lastOfType[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); | |
| } | |
| // Add entity to destination | |
| d.addChild(e); | |
| if(d.hasComponent(`place`)) { | |
| e.place = d; | |
| } | |
| }; | |
| // 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; | |
| }; | |
| /** | |
| * 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 through the telescope at bob | |
| // 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 | |
| ], | |
| // 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; | |
| } | |
| }, | |
| // 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) { | |
| 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 | |
| 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.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`); | |
| if (object != null) { | |
| return {'match': object, 'string': tokens.slice(i).join(` `)}; | |
| } | |
| } | |
| return {'match': null, 'string': string}; | |
| }, | |
| }; | |
| var changelog = [ | |
| { | |
| '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, | |
| '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(`,`); | |
| }, | |
| 'stopWaiting':function() { | |
| $(`.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); | |
| } | |
| }; | |
| /** | |
| * 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); | |
| } | |
| }; | |
| /** | |
| * 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 | |
| // disabled for faster testing | |
| delay = item.delay; | |
| if (delay > 0 || delay == `auto`) { | |
| $(`.input`).animate({'color': `transparent`}, 200); | |
| $(`.input`).addClass(`waiting`); | |
| Engine.inputEnabled = false; | |
| } | |
| // 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 = 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; | |
| // 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 != `undefined` && 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; | |
| }; | |
| /** | |
| * 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(); | |
| menu.find(`li.selected`).removeClass(`selected`); | |
| menu.removeClass(`disabled`); | |
| }; | |
| // 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'>`); | |
| 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; | |
| ECS.e(nouns[0], [`scenery`], { | |
| 'name':nouns[0], | |
| 'nouns':nouns, | |
| 'spawn':parent.key, | |
| 'descriptions':{ | |
| 'default':description | |
| } | |
| }); | |
| } | |
| /** | |
| * Get a canonical direction. | |
| */ | |
| 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`; | |
| } | |
| 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`, | |
| `Music`, | |
| `Clothing`, | |
| `Social`, | |
| `Quests`, | |
| `Temperature`, | |
| `Sleep`, | |
| // Only initiate prologue campaign module for now | |
| // Just kidding we're in debug mode for the foreseeable future | |
| `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`] | |
| }); | |
| // 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); | |
Xet Storage Details
- Size:
- 92.1 kB
- Xet hash:
- dd7fe066e17bc1231e653ed8f2c31b8c063f8e3c821feaeb6d4549de5d6f8b4b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.