diff --git a/app/.meteor/packages b/app/.meteor/packages index d28447d3..3c7a9d9d 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -54,3 +54,5 @@ dynamic-import@0.4.0 ddp-rate-limiter@1.0.7 rate-limit@1.0.9 iron:router +meteortesting:mocha +mdg:validated-method diff --git a/app/.meteor/versions b/app/.meteor/versions index e5c754a6..aca52256 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -65,13 +65,19 @@ lai:collection-extensions@0.2.1_1 launch-screen@1.1.1 less@2.7.12 livedata@1.0.18 +lmieulet:meteor-coverage@1.1.4 localstorage@1.2.0 logging@1.1.20 matb33:collection-hooks@0.8.4 +mdg:validated-method@1.1.0 mdg:validation-error@0.5.1 meteor@1.9.2 meteor-base@1.4.0 +meteorhacks:picker@1.0.3 meteorhacks:subs-manager@1.6.4 +meteortesting:browser-tests@1.0.0 +meteortesting:mocha@1.0.1 +meteortesting:mocha-core@1.0.1 minifier-css@1.3.1 minifier-js@2.3.5 minimongo@1.4.4 diff --git a/app/Model/Character/CharacterComputation.js b/app/Model/Character/CharacterComputation.js index 6455bee5..54e9ae5b 100644 --- a/app/Model/Character/CharacterComputation.js +++ b/app/Model/Character/CharacterComputation.js @@ -1,9 +1,11 @@ // TODO make sure all attributes can only have lowercase, stripped, no spaced names // TODO make sure proficiencies are indexed by type +// TODO skills rely on an ability's modifier +import { ValidatedMethod } from 'meteor/mdg:validated-method'; const recomputeCharacter = new ValidatedMethod({ - "Characters.methods.recomputeCharacter", // DDP method name + name: "Characters.methods.recomputeCharacter", // DDP method name validate: new SimpleSchema({ charId: { type: String } @@ -12,6 +14,7 @@ const recomputeCharacter = new ValidatedMethod({ applyOptions: { noRetry: true, }, + run({ charId }) { // `this` is the same method invocation object you normally get inside // Meteor.methods @@ -23,7 +26,7 @@ const recomputeCharacter = new ValidatedMethod({ computeCharacterById(charId); - }); + }, }); @@ -76,6 +79,8 @@ const buildCharacter = function (charId){ atts: {}, skills: {}, dms: {}, + classes: {}, + level: 0, }; // Fetch the attributes of the character and add them to an object for quick lookup Attributes.find({charId}).forEach(attribute => { @@ -83,12 +88,12 @@ const buildCharacter = function (charId){ char.atts[attribute.name] = { computed: false, busyComputing: false, - type: "attribute"; + type: "attribute", result: 0, mod: 0, // The resulting modifier if this is an ability base: 0, add: 0, - mul: 0, + mul: 1, min: Number.NEGATIVE_INFINITY, max: Number.POSITIVE_INFINITY, effects: [], @@ -102,11 +107,12 @@ const buildCharacter = function (charId){ char.skills[skill.name] = { computed: false, busyComputing: false, - type: "skill"; + type: "skill", + ability: skill.ability, result: 0, // For skills the result is the skillMod proficiency: 0, add: 0, - mul: 0, + mul: 1, min: Number.NEGATIVE_INFINITY, max: Number.POSITIVE_INFINITY, advantage: 0, @@ -126,7 +132,7 @@ const buildCharacter = function (charId){ char.dms[damageMultiplier.name] = { computed: false, busyComputing: false, - type: "damageMultiplier"; + type: "damageMultiplier", result: 0, immunityCount: 0, ressistanceCount: 0, @@ -137,12 +143,11 @@ const buildCharacter = function (charId){ }); // Fetch the class levels and store them - char.level = 0; - char.classes = {}; - Classes.find({charId}).forEach(class => { - if (!char.classes[class.name]){ - char.classes[class.name] = {level: class.level}; - char.level += class.level; + // don't use the word "class" it's reserved + Classes.find({charId}).forEach(cls => { + if (!char.classes[cls.name]){ + char.classes[cls.name] = {level: cls.level}; + char.level += cls.level; } }); @@ -151,14 +156,19 @@ const buildCharacter = function (charId){ charId: charId, enabled: true, }).forEach(effect => { - effect.computed = false; - effect.result = 0; + let storedEffect = { + computed: false, + result: 0, + operation: effect.operation, + value: effect.value, + calculation: effect.calculation, + } if (char.atts[effect.stat]) { - char.atts[effect.stat].effects.push(effect); + char.atts[effect.stat].effects.push(storedEffect); } else if (char.skills[effect.stat]) { - char.skills[effect.stat].effects.push(effect); + char.skills[effect.stat].effects.push(storedEffect); } else if (char.dms[effect.stat]) { - char.dms[effect.stat].effects.push(effect); + char.dms[effect.stat].effects.push(storedEffect); } else { // ignore effects that don't apply to an actual stat } @@ -174,20 +184,24 @@ const buildCharacter = function (charId){ char.skills[proficiency.name].proficiencies.push(effect); } }); + return char; } /* * Compute the character's stats in-place, returns the same char object */ -const computeCharacter = function (char){ +export const computeCharacter = function (char){ // Iterate over each stat in order and compute it - for (stat in char.atts){ + for (statName in char.atts){ + let stat = char.atts[statName] computeStat (stat, char); } - for (stat in char.skills){ + for (statName in char.skills){ + let stat = char.skills[statName] computeStat (stat, char); } - for (stat in char.dms){ + for (statName in char.dms){ + let stat = char.dms[statName] computeStat (stat, char); } return char; @@ -211,10 +225,10 @@ const computeStat = function(stat, char){ } // Iterate over each effect which applies to the stat - for (effect in stat.effects){ - computeEffect(effect, char); + for (i in stat.effects){ + computeEffect(stat.effects[i], char); // apply the effect to the stat - applyEffect(effect, stat); + applyEffect(stat.effects[i], stat); } // Conglomerate all the effects to compute the final stat values @@ -229,15 +243,18 @@ const computeStat = function(stat, char){ * Compute a single effect on a character */ const computeEffect = function(effect, char){ + if (effect.computed) return; if (_.isFinite(effect.value)){ effect.result = effect.value; } else if(effect.operation === "conditional"){ effect.result = effect.calculation; - } else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation){ + } else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){ effect.result = 1; } else if (_.isString(effect.calculation)){ - effect.result = evaluateCalculation(charId, effect.calculation); + effect.result = evaluateCalculation(effect.calculation, char); } + effect.computed = true; + console.log({effect}); }; /* @@ -320,20 +337,37 @@ const combineAttribute = function(stat, char){ if (stat.result < stat.min) stat.result = stat.min; if (stat.result > stat.max) stat.result = stat.max; // Round everything that isn't the carry multiplier - if (stat.name !== "carryMultiplier") stat.result = Math.floor(stat.result); - stat.mod = Math.floor((stat.result - 10) / 2); + if (!stat.decimal) stat.result = Math.floor(stat.result); + if (stat.attributeType === "ability") { + stat.mod = Math.floor((stat.result - 10) / 2); + } + console.log({statResult: stat.result}) } const combineSkill = function(stat, char){ - for (prof in stat.proficiencies){ + for (i in stat.proficiencies){ + let prof = stat.proficiencies[i]; if (prof.value > stat.proficiency) stat.proficiency = prof.value; } - if (!char.atts.proficiencyBonus.computed){ - computeStat(char.atts.proficiencyBonus, char); + let profBonus; + if (char.atts.proificiencyBonus){ + if (!char.atts.proficiencyBonus.computed){ + computeStat(char.atts.proficiencyBonus, char); + } + profBonus = char.atts.proficiencyBonus.result; + } else { + profBonus = Math.floor(char.level / 4 + 1.75); } - const profBonus = char.atts.proficiencyBonus.result; - const base = profBonus * stat.proficiency; - stat.result = (base + stat.add) * stat.mul; + profBonus *= stat.proficiency; + // Skills are based on some ability Modifier + let abilityMod = 0; + if (stat.ability && char.atts[stat.ability]){ + if (!char.atts[stat.ability].computed){ + computeStat(char.atts[stat.ability], char); + } + abilityMod = char.atts[stat.ability].mod; + } + stat.result = (abilityMod + profBonus + stat.add) * stat.mul; if (stat.result < stat.min) stat.result = stat.min; if (stat.result > stat.max) stat.result = stat.max; stat.result = Math.floor(stat.result); @@ -341,9 +375,9 @@ const combineSkill = function(stat, char){ const combineDamageMultiplier = function(stat, char){ if (stat.immunityCount) return 0; - if (ressistanceCount && !vulnerabilityCount){ + if (stat.ressistanceCount && !stat.vulnerabilityCount){ stat.result = 0.5; - } else if (!ressistanceCount && vulnerabilityCount){ + } else if (!stat.ressistanceCount && stat.vulnerabilityCount){ stat.result = 2; } else { stat.result = 1; diff --git a/app/Model/Character/CharacterComputation.test.js b/app/Model/Character/CharacterComputation.test.js new file mode 100644 index 00000000..688a8e87 --- /dev/null +++ b/app/Model/Character/CharacterComputation.test.js @@ -0,0 +1,97 @@ +import {computeCharacter} from "./CharacterComputation.js"; +import assert from "assert"; + +const makeEffect = function(operation, value){ + let effect = {computed: false, result: 0, operation} + if (_.isFinite(value)){ + effect.value = +value; + } else { + effect.calculation = value; + } + return effect; +} + +describe('computeCharacter', function () { + it('computes an aritrary character', function () { + let char = { + atts: { + attribute1: { + computed: false, + busyComputing: false, + type: "attribute", + attributeType: "ability", + result: 0, + mod: 0, // The resulting modifier if this is an ability + base: 0, + add: 0, + mul: 1, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + effects: [ + makeEffect("base", 10), + makeEffect("add", 5), + makeEffect("mul", 2), + ], + }, + attribute2: { + computed: false, + busyComputing: false, + type: "attribute", + result: 0, + mod: 0, // The resulting modifier if this is an ability + base: 0, + add: 0, + mul: 1, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + effects: [ + makeEffect("base", "attribute1"), + makeEffect("max", 2), + ], + }, + }, + skills: { + skill1: { + computed: false, + busyComputing: false, + type: "skill", + ability: "attribute1", + result: 0, + proficiency: 0, + add: 0, + mul: 1, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + advantage: 0, + disadvantage: 0, + passiveAdd: 0, + fail: 0, + conditional: 0, + effects: [], + proficiencies: [], + }, + }, + dms: { + dm1: { + computed: false, + busyComputing: false, + type: "damageMultiplier", + result: 0, + immunityCount: 0, + ressistanceCount: 0, + vulnerabilityCount: 0, + effects: [], + } + }, + classes: { + Barbarian: { + level: 5, + }, + }, + level: 5, + }; + char = computeCharacter(char); + console.log(char); + assert(false); + }); +});