From d5680ebf8a01b1b906edb78c2c6bfd0e477fd557 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 18 Jun 2015 13:33:54 +0200 Subject: [PATCH 01/10] Add memoize functionality --- rpg-docs/lib/memoize/memoize.js | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 rpg-docs/lib/memoize/memoize.js diff --git a/rpg-docs/lib/memoize/memoize.js b/rpg-docs/lib/memoize/memoize.js new file mode 100644 index 00000000..6d510374 --- /dev/null +++ b/rpg-docs/lib/memoize/memoize.js @@ -0,0 +1,45 @@ +Tracker.memoize = function(func, hasher){ + var memoize = function(key) { + var cache = memoize.cache; + var address = "" + (hasher ? hasher.apply(this, arguments) : key); + if (!_.has(cache, address)) { + cache[address] = new CacheObject(func, address, arguments, cache, this); + } + return cache[address].get(); + }; + memoize.cache = {}; + return memoize; +}; + +function CacheObject(func, address, args, cache, context){ + var self = this; + self.currentValue = null; + self.dep = new Tracker.Dependency(); + + //spawn a new autorun that keeps the value up-to-date + Tracker.nonreactive(function() { + Tracker.autorun(function(computation) { + //if this isn't the first run and nobody is listening, + //delete itself from cache and stop the computation + if (!computation.firstRun && !self.dep.hasDependents()){ + computation.stop(); + delete cache[address]; + console.log("Nothing depends on '" + address + "', deleting"); + } + //call the expensive function + var newValue = func.apply(context, args); + //if the value changed, store the new value + if (self.currentValue !== newValue){ + self.currentValue = newValue; + //tell the dependents that we've changed + self.dep.changed(); + } + }); + }); +} + +CacheObject.prototype.get = function() { + //if there is an active computation, track dependents + if (Tracker.active) this.dep.depend(); + return this.currentValue; +}; From a034cbf30ee40015e2d451ab472501af03958948 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 18 Jun 2015 13:34:59 +0200 Subject: [PATCH 02/10] Duplicate character helpers with memoized functions not attached to characters --- rpg-docs/Model/Character/Characters.js | 302 ++++++++++++++++++++----- rpg-docs/lib/functions/evaluate.js | 7 +- 2 files changed, 247 insertions(+), 62 deletions(-) diff --git a/rpg-docs/Model/Character/Characters.js b/rpg-docs/Model/Character/Characters.js index c0dbab0c..7e83729b 100644 --- a/rpg-docs/Model/Character/Characters.js +++ b/rpg-docs/Model/Character/Characters.js @@ -190,79 +190,252 @@ var attributeBase = function(charId, statName){ check(statName, String); //if it's a damage multiplier, we treat it specially if (_.contains(DAMAGE_MULTIPLIERS, statName)){ - var effects = Effects.find( - {charId: charId, stat: statName, enabled: true, operation: "mul"} - ).fetch(); - var resistCount = 0; - var vulnCount = 0; - var multiplierEvaluationFail = false; - _.each(effects, function(effect){ - var val = evaluateEffect(charId, effect); - if (val === 0.5){ //resistance - resistCount += 1; - } else if (val === 2){ //vulnerability - vulnCount += 1; - } else if (val === 0){ //imunity - return 0; //imunity is absolute - } else { - multiplierEvaluationFail = true; - } - }); - if (multiplierEvaluationFail){ - //we can't work it out correctly, set the value to 1 - //and try work it out using regular maths below - value = 1; - } else if (resistCount && !vulnCount){ + var invulnerabilityCount = Effects.find({ + charId: charId, + stat: statName, + enabled: true, + operation: "mul", + value: 0, + }).count(); + if (invulnerabilityCount) return 0; + var resistCount = Effects.find({ + charId: charId, + stat: statName, + enabled: true, + operation: "mul", + value: 0.5, + }).count(); + var vulnCount = Effects.find({ + charId: charId, + stat: statName, + enabled: true, + operation: "mul", + value: 2, + }).count(); + if (!resistCount && !vulnCount){ + return 1; + } else if (resistCount && !vulnCount){ return 0.5; - } else if (!resistCount && vulnCount){ + } else if (!resistCount && vulnCount){ return 2; } else { return 1; } } + var value; + var base = 0; + var add = 0; + var mul = 1; + var min = Math.NEGATIVE_INFINITY; + var max = Math.POSITIVE_INFINITY; - var value = 0; - - //start with the highest base value - Effects.find( - {charId: charId, stat: statName, enabled: true, operation: "base"} - ).forEach(function(effect){ - var efv = evaluateEffect(charId, effect); - if (efv > value){ - value = efv; + Effects.find({ + charId: charId, + stat: statName, + enabled: true, + operation: {$in: ["base", "add", "mul", "min", "max"]}, + }).forEach(function(effect) { + value = evaluateEffect(charId, effect); + if (effect.operation === "base"){ + if (value > base) base = value; + } else if (effect.operation === "add"){ + add += value; + } else if (effect.operation === "mul"){ + mul *= value; + } else if (effect.operation === "min"){ + if (value > min) min = value; + } else if (effect.operation === "max"){ + if (value < max) max = value; } }); - //add all the add values - Effects.find( - {charId: charId, stat: statName, enabled: true, operation: "add"} - ).forEach(function(effect){ - value += evaluateEffect(charId, effect); - }); + var result = (base + add) * mul; + if (result < min) result = min; + if (result > max) result = max; - //multiply all the mul values - Effects.find( - {charId: charId, stat: statName, enabled: true, operation: "mul"} - ).forEach(function(effect){ - value *= evaluateEffect(charId, effect); - }); + return result; +}; - //ensure value is >= all mins - Effects.find( - {charId: charId, stat: statName, enabled: true, operation: "min"} - ).forEach(function(effect){ - var min = evaluateEffect(charId, effect); - value = value > min ? value : min; +if (Meteor.isClient) { + Template.registerHelper("charCalculate", function(func, charId, input) { + return Characters.calculate[func](charId, input); }); +} - //ensure value is <= all maxes - Effects.find( - {charId: charId, stat: statName, enabled: true, operation: "max"} - ).forEach(function(effect){ - var max = evaluateEffect(charId, effect); - value = value < max ? value : max; +//create a local memoize with a argument concatenating hash function +var memoize = function(f) { + return Tracker.memoize(f, function() { + return _.reduce(arguments, function(memo, arg) { + return memo + arg; + }, ""); }); - return value; +}; + +//memoize funcitons that have finds and slow loops +Characters.calculate = { + getField: function(charId, fieldName) { + var fieldSelector = {}; + fieldSelector[fieldName] = 1; + var char = Characters.findOne(charId, {fields: fieldSelector}); + var field = char[fieldName]; + if (field === undefined){ + throw new Meteor.Error( + "getField failed", + "getField could not find field " + + fieldName + + " in character " + + char._id + ); + } + return field; + }, + fieldValue: function(charId, fieldName) { + if (!Schemas.Character.schema(fieldName)){ + throw new Meteor.Error( + "Field not found", + "Character's schema does not contain a field called: " + fieldName + ); + } + //duck typing to get the right value function + //.ability implies skill + if (Schemas.Character.schema(fieldName + ".ability")){ + return Characters.calculate.skillMod(charId, fieldName); + } + //adjustment implies attribute + if (Schemas.Character.schema(fieldName + ".adjustment")){ + return Characters.calculate.attributeValue(charId, fieldName); + } + //fall back to just returning the field itself + return Characters.calculate.getField(charId, fieldName); + }, + attributeValue: memoize(function(charId, attributeName){ + var attribute = Characters.calculate.getField(charId, attributeName); + //base value + var value = Characters.calculate.attributeBase(attributeName); + //plus adjustment + value += attribute.adjustment; + return value; + }), + attributeBase: preventLoop(memoize(function(charId, attributeName){ + return attributeBase(charId, attributeName); + })), + skillMod: preventLoop(memoize(function(charId, skillName){ + var skill = Characters.calculate.getField(charId, skillName); + //get the final value of the ability score + var ability = Characters.calculate.attributeValue(charId, skill.ability); + + //base modifier + var mod = +getMod(ability); + + //multiply proficiency bonus by largest value in proficiency array + var prof = Characters.calculate.proficiency(charId, skillName); + + //add multiplied proficiency bonus to modifier + mod += prof * Characters.calculate.attributeValue(charId, "proficiencyBonus"); + + //apply all effects + var value; + var add = 0; + var mul = 1; + var min = Math.NEGATIVE_INFINITY; + var max = Math.POSITIVE_INFINITY; + + Effects.find({ + charId: charId, + stat: skillName, + enabled: true, + operation: {$in: ["base", "add", "mul", "min", "max"]}, + }).forEach(function(effect) { + value = evaluateEffect(charId, effect); + if (effect.operation === "add"){ + add += value; + } else if (effect.operation === "mul"){ + mul *= value; + } else if (effect.operation === "min"){ + if (value > min) min = value; + } else if (effect.operation === "max"){ + if (value < max) max = value; + } + }); + var result = (mod + add) * mul; + if (result < min) result = min; + if (result > max) result = max; + + return result; + })), + proficiency: memoize(function(charId, skillName){ + //return largest value in proficiency array + var prof = Proficiencies.findOne( + {charId: charId, name: skillName, enabled: true}, + {sort: {value: -1}} + ); + return prof && prof.value; + }), + passiveSkill: memoize(function(charId, skillName){ + var skill = Characters.calculate.getField(charId, skillName); + var mod = +Characters.calculate.skillMod(charId, skillName); + var value = 10 + mod; + Effects.find( + {charId: charId, stat: skillName, enabled: true, operation: "passiveAdd"} + ).forEach(function(effect){ + value += evaluateEffect(charId, effect); + }); + var advantage = Characters.calculate.advantage(charId, skillName); + value += 5 * advantage; + return value; + }), + advantage: memoize(function(charId, skillName){ + var advantage = Effects.find( + {charId: charId, stat: skillName, enabled: true, operation: "advantage"} + ).count(); + var disadvantage = Effects.find( + {charId: charId, stat: skillName, enabled: true, operation: "disadvantage"} + ).count(); + if (advantage && !disadvantage) return 1; + if (disadvantage && !advantage) return -1; + return 0; + }), + abilityMod: function(charId, attribute){ + return getMod( + Characters.calculate.attributeValue(charId, attribute) + ); + }, + passiveAbility: function(charId, attribute){ + var mod = +getMod(Characters.calculate.attributeValue(charId, attribute)); + return 10 + mod; + }, + xpLevel: function(charId){ + var xp = Characters.calculate.experience(charId); + for (var i = 0; i < 19; i++){ + if (xp < XP_TABLE[i]){ + return i; + } + } + if (xp > 355000) return 20; + return 0; + }, + level: memoize(function(charId){ + var level = 0; + Classes.find({charId: charId}).forEach(function(cls){ + level += cls.level; + }); + return level; + }), + experience: memoize(function(charId){ + var xp = 0; + Experiences.find( + {charId: charId}, + {fields: {value: 1}} + ).forEach(function(e){ + xp += e.value; + }); + return xp; + }), +}; + +var depreciated = function() { + var err = new Error(); + console.log("this function has been superceeded", {stacktrace: err.stack}); }; //functions and calculated values. @@ -272,6 +445,7 @@ Characters.helpers({ //returns the value stored in the field requested //will set up dependencies on just that field getField : function(fieldName){ + depreciated(); var fieldSelector = {}; fieldSelector[fieldName] = 1; var char = Characters.findOne(this._id, {fields: fieldSelector}); @@ -289,6 +463,7 @@ Characters.helpers({ }, //returns the value of a field fieldValue : function(fieldName){ + depreciated(); if (!Schemas.Character.schema(fieldName)){ throw new Meteor.Error( "Field not found", @@ -309,6 +484,7 @@ Characters.helpers({ }, attributeValue: function(attributeName){ + depreciated(); var charId = this._id; var attribute = this.getField(attributeName); //base value @@ -319,12 +495,14 @@ Characters.helpers({ }, attributeBase: preventLoop(function(attributeName){ + depreciated(); var charId = this._id; //base value return attributeBase(charId, attributeName); }), skillMod: preventLoop(function(skillName){ + depreciated(); var charId = this._id; var skill = this.getField(skillName); //get the final value of the ability score @@ -362,6 +540,7 @@ Characters.helpers({ }), proficiency: function(skillName){ + depreciated(); var charId = this._id; //return largest value in proficiency array var prof = 0; @@ -377,6 +556,7 @@ Characters.helpers({ }, passiveSkill: function(skillName){ + depreciated(); if (_.isString(skillName)){ var skill = this.getField(skillName); } @@ -393,6 +573,7 @@ Characters.helpers({ }, advantage: function(skillName){ + depreciated(); var charId = this._id; var advantage = Effects.find( {charId: charId, stat: skillName, enabled: true, operation: "advantage"} @@ -406,15 +587,18 @@ Characters.helpers({ }, abilityMod: function(attribute){ + depreciated(); return signedString(getMod(this.attributeValue(attribute))); }, passiveAbility: function(attribute){ + depreciated(); var mod = +getMod(this.attributeValue(attribute)); return 10 + mod; }, xpLevel: function(){ + depreciated(); var xp = this.experience(); for (var i = 0; i < 19; i++){ if (xp < XP_TABLE[i]){ @@ -426,6 +610,7 @@ Characters.helpers({ }, level: function(){ + depreciated(); var level = 0; Classes.find({charId: this._id}).forEach(function(cls){ level += cls.level; @@ -434,6 +619,7 @@ Characters.helpers({ }, experience: function(){ + depreciated(); var xp = 0; Experiences.find( {charId: this._id}, diff --git a/rpg-docs/lib/functions/evaluate.js b/rpg-docs/lib/functions/evaluate.js index 7e1d8adf..86d42e93 100644 --- a/rpg-docs/lib/functions/evaluate.js +++ b/rpg-docs/lib/functions/evaluate.js @@ -1,11 +1,10 @@ //evaluates a calculation string evaluate = function(charId, string){ if (!string) return string; - var char = Characters.findOne(charId, {fields: {_id: 1}}); string = string.replace(/\b[a-z]+\b/gi, function(sub){ //fields if (Schemas.Character.schema(sub)){ - return char.fieldValue(sub); + return Characters.calculate.fieldValue(charId, sub); } //ability modifiers var abilityMods = [ @@ -19,7 +18,7 @@ evaluate = function(charId, string){ if (_.contains(abilityMods, sub)){ var slice = sub.slice(0, -3); try { - return char.abilityMod(slice); + return Characters.calculate.abilityMod(charId, slice); } catch (e){ return sub; } @@ -33,7 +32,7 @@ evaluate = function(charId, string){ } //character level if (sub.toUpperCase() === "LEVEL"){ - return char.level(); + return Characters.calculate.level(charId); } return sub; }); From 86c934e8ac857c2de3ba6712baf2459a65a90d8c Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 18 Jun 2015 15:14:37 +0200 Subject: [PATCH 03/10] Began replacing calls to helpers with calls to memoized functions --- rpg-docs/Model/Character/Characters.js | 26 ++++++++++++++----- .../character/stats/hitDice/hitDice.html | 4 +-- .../stats/skillDialog/skillDialog.html | 10 +++---- .../stats/skillDialog/skillDialog.js | 25 ++++++------------ .../character/stats/skillRow/skillRow.html | 6 +++-- .../character/stats/skillRow/skillRow.js | 7 ++--- .../client/views/character/stats/stats.html | 4 +-- rpg-docs/lib/functions/preventLoop.js | 22 +++++++++------- rpg-docs/lib/memoize/memoize.js | 2 +- 9 files changed, 58 insertions(+), 48 deletions(-) diff --git a/rpg-docs/Model/Character/Characters.js b/rpg-docs/Model/Character/Characters.js index 7e83729b..eed8b734 100644 --- a/rpg-docs/Model/Character/Characters.js +++ b/rpg-docs/Model/Character/Characters.js @@ -257,8 +257,16 @@ var attributeBase = function(charId, statName){ }; if (Meteor.isClient) { - Template.registerHelper("charCalculate", function(func, charId, input) { - return Characters.calculate[func](charId, input); + Template.registerHelper("characterCalculate", function(func, charId, input) { + try { + return Characters.calculate[func](charId, input); + } catch (e){ + if (!Characters.calculate[func]){ + throw new Error(func + "is not a function name"); + } else { + throw e; + } + } }); } @@ -311,15 +319,15 @@ Characters.calculate = { attributeValue: memoize(function(charId, attributeName){ var attribute = Characters.calculate.getField(charId, attributeName); //base value - var value = Characters.calculate.attributeBase(attributeName); + var value = Characters.calculate.attributeBase(charId, attributeName); //plus adjustment value += attribute.adjustment; return value; }), - attributeBase: preventLoop(memoize(function(charId, attributeName){ + attributeBase: memoize(preventLoop(function(charId, attributeName){ return attributeBase(charId, attributeName); })), - skillMod: preventLoop(memoize(function(charId, skillName){ + skillMod: memoize(preventLoop(function(charId, skillName){ var skill = Characters.calculate.getField(charId, skillName); //get the final value of the ability score var ability = Characters.calculate.attributeValue(charId, skill.ability); @@ -369,7 +377,7 @@ Characters.calculate = { {charId: charId, name: skillName, enabled: true}, {sort: {value: -1}} ); - return prof && prof.value; + return prof && prof.value || 0; }), passiveSkill: memoize(function(charId, skillName){ var skill = Characters.calculate.getField(charId, skillName); @@ -435,7 +443,11 @@ Characters.calculate = { var depreciated = function() { var err = new Error(); - console.log("this function has been superceeded", {stacktrace: err.stack}); + var name = ""; + if(Template.instance()){ + name = Template.instance().view.name; + } + console.log("this function has been depreciated", {viewName: name, stacktrace: err.stack}); }; //functions and calculated values. diff --git a/rpg-docs/client/views/character/stats/hitDice/hitDice.html b/rpg-docs/client/views/character/stats/hitDice/hitDice.html index 1a1d0b83..3298bb32 100644 --- a/rpg-docs/client/views/character/stats/hitDice/hitDice.html +++ b/rpg-docs/client/views/character/stats/hitDice/hitDice.html @@ -18,10 +18,10 @@
- {{../attributeValue name}} + {{characterCalculate "attributeValue" ../_id name}}
- d{{diceNum}} {{../abilityMod "constitution"}} + d{{diceNum}} {{characterCalculate "abilityMod" ../_id "constitution"}}
diff --git a/rpg-docs/client/views/character/stats/skillDialog/skillDialog.html b/rpg-docs/client/views/character/stats/skillDialog/skillDialog.html index 954dcd61..10953f17 100644 --- a/rpg-docs/client/views/character/stats/skillDialog/skillDialog.html +++ b/rpg-docs/client/views/character/stats/skillDialog/skillDialog.html @@ -1,4 +1,4 @@ - +