diff --git a/.codio b/.codio index 3c35c30e..b8a4894a 100644 --- a/.codio +++ b/.codio @@ -3,7 +3,7 @@ // Run button configuration "commands": { - "Run Meteor": "ROOT_URL=http://period-sheriff-3000.codio.io\ncd rpg-docs \n meteor run" + "Run Meteor": "cd rpg-docs \n ROOT_URL=http://period-sheriff.codio.io:3000 meteor" }, // Preview button configuration diff --git a/rpg-docs/Model/Character/Characters.js b/rpg-docs/Model/Character/Characters.js index ebba3cfb..5389c57c 100644 --- a/rpg-docs/Model/Character/Characters.js +++ b/rpg-docs/Model/Character/Characters.js @@ -257,87 +257,46 @@ Characters.helpers({ return value; }, - attributeBase: (function(){ - //store a private array of attributes we've visited without returning - //if we try to visit the same attribute twice before resolving its value - //we are in a dependency loop and need to GTFO - var visitedAttributes = []; - return function(attributeName){ - check(attributeName, String); - //we're still evaluating this attribute, must be in a loop - if(_.contains(visitedAttributes, attributeName)) { - console.log("dependency loop detected"); - return NaN; - } - //push this attribute to the list of visited attributes - //we can't visit it again unless it returns first - visitedAttributes.push(attributeName); - try{ - var charId = this._id; - //base value - var value = attributeBase(charId, attributeName); - }finally{ - //this attribute returns or fails, pull it from the array, we may visit it again safely - visitedAttributes = _.without(visitedAttributes, attributeName); - } - return value; - } - })(), + attributeBase: preventLoop(function(attributeName){ + var charId = this._id; + //base value + return attributeBase(charId, attributeName); + }), - skillMod: (function(){ - //store a private array of skills we've visited without returning - //if we try to visit the same skill twice before resolving its value - //we are in a dependency loop and need to GTFO - var visitedSkills = []; - return function(skillName){ - check(skillName, String); - //we're still evaluating this attribute, must be in a loop - if(_.contains(visitedSkills, skillName)) { - console.log("dependency loop detected"); - return NaN; - } - //push this skill to the list of visited skills - //we can't visit it again unless it returns first - visitedSkills.push(skillName); - try{ - var charId = this._id; - skill = this.getField(skillName); - //get the final value of the ability score - var ability = this.attributeValue(skill.ability); + skillMod: preventLoop(function(skillName){ + var charId = this._id; + var skill = this.getField(skillName); + //get the final value of the ability score + var ability = this.attributeValue(skill.ability); - //base modifier - var mod = +getMod(ability) + //base modifier + var mod = +getMod(ability) - //multiply proficiency bonus by largest value in proficiency array - var prof = this.proficiency(skillName); + //multiply proficiency bonus by largest value in proficiency array + var prof = this.proficiency(skillName); - //add multiplied proficiency bonus to modifier - mod += prof * this.attributeValue("proficiencyBonus"); - Effects.find({charId: charId, stat: skillName, enabled: true}).forEach(function(effect){ - switch(effect.operation) { - case "add": - mod += evaluateEffect(charId, effect); - break; - case "mul": - mod *= evaluateEffect(charId, effect); - break; - case "min": - var min = evaluateEffect(charId, effect); - mod = mod > min? mod : min; - break; - case "max": - var max = evaluateEffect(charId, effect); - mod = mod < max? mod : max; - break; - } - }); - } finally{ - //this skill returns or fails, pull it from the array - visitedSkills = _.without(visitedSkills, skillName); - } - return signedString(mod); - } - })(), + //add multiplied proficiency bonus to modifier + mod += prof * this.attributeValue("proficiencyBonus"); + + //apply all effects + var rawEffects = Effects.find({charId: charId, stat: skillName, enabled: true}).fetch(); + var effects = _.groupBy(rawEffects, "operation"); + _.forEach(effects.add, function(effect){ + mod += evaluateEffect(charId, effect); + }); + _.forEach(effects.mul, function(effect){ + mod *= evaluateEffect(charId, effect); + }); + _.forEach(effects.min, function(effect){ + var min = evaluateEffect(charId, effect); + mod = mod > min? mod : min; + }); + _.forEach(effects.max, function(effect){ + var max = evaluateEffect(charId, effect); + mod = mod < max? mod : max; + }); + return signedString(mod); + }), proficiency: function(skillName){ var charId = this._id; @@ -418,7 +377,7 @@ Characters.helpers({ }); //clean up all data related to that character before removing it -Characters.before.remove(function (userId, character) { +Characters.after.remove(function (userId, character) { if(Meteor.isServer){ Actions .remove({charId: character._id}); Attacks .remove({charId: character._id}); diff --git a/rpg-docs/lib/functions/preventLoop.js b/rpg-docs/lib/functions/preventLoop.js new file mode 100644 index 00000000..1a6e2355 --- /dev/null +++ b/rpg-docs/lib/functions/preventLoop.js @@ -0,0 +1,27 @@ +preventLoop = function(inputFunction){ + if(!_.isFunction(inputFunction)) throw new Meteor.Error("Not a function", + "preventLoop can only take a function as an argument"); + //store a private array of arguments we have been given without returning + //if we try to visit the same argument twice before resolving its value + //we are in a dependency loop and need to GTFO + var visitedArgs = []; + return function(argument){ + var value; + //we're still evaluating this attribute, must be in a loop + if(_.contains(visitedArgs, argument)) { + console.warn("dependency loop detected"); + return NaN; + } else{ + //push this skill to the list of visited skills + //we can't visit it again unless it returns first + visitedArgs.push(argument); + } + try{ + value = inputFunction.call(this, argument); + } finally{ + //this argument returns or fails, pull it from the array + visitedArgs = _.without(visitedArgs, argument); + } + return value; + } +}; diff --git a/rpg-docs/tests/mocha/server/model/character/effects.js b/rpg-docs/tests/mocha/server/model/character/effects.js index 8dce017a..c96ee306 100644 --- a/rpg-docs/tests/mocha/server/model/character/effects.js +++ b/rpg-docs/tests/mocha/server/model/character/effects.js @@ -1,8 +1,26 @@ var getEffect = function(charId, op, value){ + return getAttributeEffect(charId, "constitution", op, value); +}; + +var getAttributeEffect = function(charId, attribute, op, value){ return { charId: charId, type: "inate", - stat: "constitution", + stat: attribute, + operation: op, + value: value, + parent: { + id: charId, + collection: "Characters" + } + } +} + +var getSkillEffect = function(charId, op, value){ + return { + charId: charId, + type: "inate", + stat: "athletics", operation: op, value: value, parent: { @@ -15,81 +33,140 @@ var getEffect = function(charId, op, value){ if (!(typeof MochaWeb === 'undefined')){ MochaWeb.testOnly(function(){ describe("Character", function(){ - Effects.remove({}); - Characters.remove({}); - var charId = Characters.insert({owner: "FWeGYyDY5jc4HuTh8"}); - var char = Characters.findOne(charId); - var con = function(){return char.attributeValue("constitution")}; + var charId, char, con, ath, strMod; + + beforeEach(function(){ + Effects.remove({}); + Characters.remove({}); + charId = Characters.insert({owner: "FWeGYyDY5jc4HuTh8"}); + char = Characters.findOne(charId); + con = function(){return char.attributeValue("constitution")}; + ath = function(){return char.skillMod("athletics")}; + strMod = function(){return char.abilityMod("strength")}; + }); describe("effects", function(){ describe("attributeValue", function(){ - - it("should be set to highest base", function(done){ - Effects.insert(getEffect(charId, "base", 10), function(err, id){ - if(err) done(err); - }); - Effects.insert(getEffect(charId, "base", 6), function(err, id){ - if(err) done(err); - }); + beforeEach(function(){ + Effects.remove({}); + }); + + it("should be set to highest base", function(){ + Effects.insert(getEffect(charId, "base", 10)); + Effects.insert(getEffect(charId, "base", 6)); chai.assert.equal(10, con()); - done(); }); - it("should add", function(done){ - Effects.insert(getEffect(charId, "add", 2), function(err, id){ - if(err) done(err); - }); + it("should add", function(){ + Effects.insert(getEffect(charId, "add", 2)); + Effects.insert(getEffect(charId, "base", 10)); chai.assert.equal(12, con()); - done(); }); - it("should multiply", function(done){ - Effects.insert(getEffect(charId, "mul", 2), function(err, id){ - if(err) done(err); - }); + it("should multiply after adding", function(){ + Effects.insert(getEffect(charId, "mul", 2)); + Effects.insert(getEffect(charId, "base", 10)); + Effects.insert(getEffect(charId, "add", 2)); chai.assert.equal(24, con()); - done(); }); - it("should be at least highest min", function(done){ - Effects.insert(getEffect(charId, "min", 22), function(err, id){ - if(err) done(err); - }); + it("should be at least highest min", function(){ + Effects.insert(getEffect(charId, "min", 22)); + Effects.insert(getEffect(charId, "base", 10)); + Effects.insert(getEffect(charId, "add", 2)); + Effects.insert(getEffect(charId, "mul", 2)); chai.assert.equal(con(), 24); - Effects.insert(getEffect(charId, "min", 28), function(err, id){ - if(err) done(err); - }); + Effects.insert(getEffect(charId, "min", 28)); chai.assert.equal(28, con()); - done(); }); - it("should be at most lowest max", function(done){ - Effects.insert(getEffect(charId, "max", 30), function(err, id){ - if(err) done(err); - }); - chai.assert.equal(28, con()); - Effects.insert(getEffect(charId, "max", 5), function(err, id){ - if(err) done(err); - }); + it("should be at most lowest max after minning", function(){ + Effects.insert(getEffect(charId, "max", 5)); + Effects.insert(getEffect(charId, "min", 22)); + Effects.insert(getEffect(charId, "base", 10)); + Effects.insert(getEffect(charId, "add", 2)); + Effects.insert(getEffect(charId, "mul", 2)); + chai.assert.equal(5, con()); + Effects.insert(getEffect(charId, "max", 6)); chai.assert.equal(5, con()); - done(); }); - it("should respect adjustment", function(done){ - Characters.update(charId, {$set: {"constitution.adjustment": -2}}, function(err, num){ - if(err) done(err); - }) + it("should respect adjustment", function(){ + Effects.insert(getEffect(charId, "base", 10)); + Effects.insert(getEffect(charId, "add", 2)); + Effects.insert(getEffect(charId, "mul", 2)); + Effects.insert(getEffect(charId, "min", 28)); + Effects.insert(getEffect(charId, "max", 5)); + Characters.update(charId, {$set: {"constitution.adjustment": -2}}) chai.assert.equal(3, con()); var conBase = char.attributeBase("constitution"); chai.assert.equal(5, conBase) - done(); + }); + + it("should be removed when the character is deleted", function(){ + Effects.insert(getEffect(charId, "base", 10)); + var count = Effects.find({charId: charId}).count(); + chai.assert.equal(count, 1); + Characters.remove(charId); + var count = Effects.find({charId: charId}).count(); + chai.assert.equal(count, 0); + }); + + }); + + describe("skillMod", function(){ + beforeEach(function(){ + Effects.remove({}); }); + it("should get its base value from the ability mod", function(){ + Effects.insert(getAttributeEffect(charId, "strength", "base", 16)); + chai.assert.equal(3, strMod()); + chai.assert.equal(3, ath()); + }); + + it("should add a multiple of proficiency bonus", function(){ + Effects.insert(getAttributeEffect(charId, "strength", "base", 16)); + Effects.insert(getAttributeEffect(charId, "proficiencyBonus", "base", 7)); + chai.assert.equal(7, char.attributeValue("proficiencyBonus"), "the proficiency bonus is calculated correctly"); + Effects.insert(getSkillEffect(charId, "proficiency", 0.5)); + Effects.insert(getSkillEffect(charId, "proficiency", 2)); + chai.assert.equal(17, ath(), "3 strength + (7 x 2) proficiency bonus"); + }); + + it("should add", function(){ + Effects.insert(getAttributeEffect(charId, "strength", "base", 16)); + Effects.insert(getSkillEffect(charId, "add", 2)); + chai.assert.equal(5, ath()); + }); + + it("should multiply", function(){ + Effects.insert(getAttributeEffect(charId, "strength", "base", 16)); + Effects.insert(getSkillEffect(charId, "mul", 2)); + chai.assert.equal(6, ath()); + }); + + it("should be at least highest min", function(){ + Effects.insert(getAttributeEffect(charId, "strength", "base", 16)); + Effects.insert(getSkillEffect(charId, "min", 1)); + chai.assert.equal(3, ath()); + Effects.insert(getSkillEffect(charId, "min", 5)); + chai.assert.equal(5, ath()); + }); + + it("should be at most lowest max", function(){ + Effects.insert(getAttributeEffect(charId, "strength", "base", 16)); + Effects.insert(getSkillEffect(charId, "max", 5)); + chai.assert.equal(3, ath()); + Effects.insert(getSkillEffect(charId, "max", 2)); + chai.assert.equal(2, ath()); + }); + it("should be removed when the character is deleted", function(){ Characters.remove(charId); var count = Effects.find({charId: charId}).count(); chai.assert.equal(count, 0); - }) + }); }); });