diff --git a/rpg-docs/lib/constants/abilities.js b/rpg-docs/lib/constants/abilities.js index 646d318a..085a0f13 100644 --- a/rpg-docs/lib/constants/abilities.js +++ b/rpg-docs/lib/constants/abilities.js @@ -6,3 +6,14 @@ abilities = [ "wisdom", "charisma", ]; + +ABILITIES = abilities; + +ABILITY_MODS = [ + "strengthMod", + "dexterityMod", + "constitutionMod", + "intelligenceMod", + "wisdomMod", + "charismaMod", +]; diff --git a/rpg-docs/lib/constants/attributes.js b/rpg-docs/lib/constants/attributes.js new file mode 100644 index 00000000..115e7f7d --- /dev/null +++ b/rpg-docs/lib/constants/attributes.js @@ -0,0 +1,34 @@ +ATTRIBUTES = [ + "strength", + "dexterity", + "constitution", + "intelligence", + "wisdom", + "charisma", + "hitPoints", + "tempHP", + "experience", + "proficiencyBonus", + "speed", + "armor", + "carryMultiplier", + "level1SpellSlots", + "level2SpellSlots", + "level3SpellSlots", + "level4SpellSlots", + "level5SpellSlots", + "level6SpellSlots", + "level7SpellSlots", + "level8SpellSlots", + "level9SpellSlots", + "ki", + "sorceryPoints", + "rages", + "superiorityDice", + "expertiseDice", + "rageDamage", + "d6HitDice", + "d8HitDice", + "d10HitDice", + "d12HitDice", +]; diff --git a/rpg-docs/lib/constants/skills.js b/rpg-docs/lib/constants/skills.js index d11b1af0..0a47069c 100644 --- a/rpg-docs/lib/constants/skills.js +++ b/rpg-docs/lib/constants/skills.js @@ -19,3 +19,32 @@ SKILLS = [ "survival", "initiative", ]; + +ALL_SKILLS = [ + "strengthSave", + "dexteritySave", + "constitutionSave", + "intelligenceSave", + "wisdomSave", + "charismaSave", + "acrobatics", + "animalHandling", + "arcana", + "athletics", + "deception", + "history", + "insight", + "intimidation", + "investigation", + "medicine", + "nature", + "perception", + "performance", + "persuasion", + "religion", + "sleightOfHand", + "stealth", + "survival", + "initiative", + "dexterityArmor", +]; diff --git a/rpg-docs/lib/functions/characterExport.js b/rpg-docs/lib/functions/characterExport.js index dd8e95c5..fe56c6ae 100644 --- a/rpg-docs/lib/functions/characterExport.js +++ b/rpg-docs/lib/functions/characterExport.js @@ -1,5 +1,11 @@ characterExport = function(charId){ - var char = Characters.findOne(charId); + var { + character, classes, effects, proficiencies, + } = getCharacterForComputation(charId); + var char = character; + computedCharacter = computeCharacter({ + character, classes, effects, proficiencies, + }); if (!char) { return { error: charId + " character not found" @@ -11,25 +17,87 @@ characterExport = function(charId){ }; } var baseValue = function(attributeName){ - return Characters.calculate.attributeBase(charId, attributeName); + var attribute = computedCharacter[attributeName]; + return attribute && attribute.value; }; var attributeValue = function(attributeName){ - return Characters.calculate.attributeValue(charId, attributeName); + var base = baseValue(attributeName); + var adjustment = char[attributeName] && char[attributeName].adjustment; + return base + adjustment; }; var abilityMod = function(attributeName){ - return signedString( - Characters.calculate.abilityMod(charId, attributeName) - ); + return signedString(getMod(attributeValue(attributeName))); }; var skillMod = function(skillName){ - return signedString( - Characters.calculate.skillMod(charId, skillName) - ); + return signedString(baseValue(skillName)); }; var proficiency = function(skillName){ - return Characters.calculate.proficiency(charId, skillName); + var skill = computedCharacter[skillName]; + return skill && skill.proficiency; + }; + var passiveSkill = function(skillName){ + var attribute = computedCharacter[skillName]; + if (!attribute) return; + return 10 + baseValue(skillName) + attribute.passiveAdd; + }; + var experience = function(){ + var xp = 0; + Experiences.find( + {charId: charId}, + {fields: {value: 1}} + ).forEach(function(e){ + xp += e.value; + }); + return xp; + }; + var getClasses = function(){ + return _.map(classes, c => `${c.name} ${c.level}`).join(", "); + }; + var getHitDiceString = function(){ + var d6 = baseValue("d6HitDice"); + var d8 = baseValue("d8HitDice"); + var d10 = baseValue("d10HitDice"); + var d12 = baseValue("d12HitDice"); + var con = abilityMod("constitution"); + var string = "" + + (d6 ? `${d6}d6 + ` : "") + + (d8 ? `${d8}d8 + ` : "") + + (d10 ? `${d10}d10 + ` : "") + + (d12 ? `${d12}d12 + ` : "") + + con; + return string; } - var damageMods = getDamageMods(charId); + var getSkills = function(charId){ + var allSkills = [ + {name: "acrobatics", attribute: "dexterity"}, + {name: "animalHandling", attribute: "wisdom"}, + {name: "arcana", attribute: "intelligence"}, + {name: "athletics", attribute: "strength"}, + {name: "deception", attribute: "charisma"}, + {name: "history", attribute: "intelligence"}, + {name: "insight", attribute: "wisdom"}, + {name: "intimidation", attribute: "charisma"}, + {name: "investigation", attribute: "intelligence"}, + {name: "medicine", attribute: "wisdom"}, + {name: "nature", attribute: "intelligence"}, + {name: "perception", attribute: "wisdom"}, + {name: "performance", attribute: "charisma"}, + {name: "persuasion", attribute: "charisma"}, + {name: "religion", attribute: "intelligence"}, + {name: "sleightOfHand", attribute: "dexterity"}, + {name: "stealth", attribute: "dexterity"}, + {name: "survival", attribute: "wisdom"}, + ]; + var skills = {}; + _.each(allSkills, skill => { + var value = skillMod(skill.name); + var prof = proficiency(skill.name); + var name = skill.name.charAt(0).toUpperCase() + skill.name.slice(1); + skills[name] = value; + skills[name + "Proficiency"] = prof; + }); + return skills; + }; var character = { "Id": char._id, "Name": char.name, @@ -37,8 +105,8 @@ characterExport = function(charId){ "Alignment": char.alignment || "", "Gender": char.gender || "", "Race": char.race || "", - "Level": Characters.calculate.level(charId), - "Experience": Characters.calculate.experience(charId), + "Level": _.reduce(classes, (memo, cls) => memo + cls.level, 0), + "Experience": experience(), "Class": getClasses(charId), "HPBase": baseValue("hitPoints"), "HPValue": attributeValue("hitPoints"), @@ -47,7 +115,7 @@ characterExport = function(charId){ "Initiative": skillMod("initiative"), "Speed": attributeValue("speed"), "ProficiencyBonus": attributeValue("proficiencyBonus"), - "passivePerception": Characters.calculate.passiveSkill(charId, "perception"), + "passivePerception": passiveSkill("perception"), "Languages": getLanguages(charId), "Description": char.description || "", @@ -72,9 +140,9 @@ characterExport = function(charId){ "WisdomMod": abilityMod("wisdom"), "CharismaMod": abilityMod("charisma"), - "DamageVulnerabilities": damageMods.vulnerabilities, - "DamageResistances": damageMods.resistances, - "DamageImmunities": damageMods.immunities, + //"DamageVulnerabilities": damageMods.vulnerabilities, + //"DamageResistances": damageMods.resistances, + //"DamageImmunities": damageMods.immunities, "StrengthSave": skillMod("strengthSave"), "StrengthSaveProficiency": proficiency("strengthSave"), @@ -112,21 +180,6 @@ characterExport = function(charId){ return character; } -var getHitDiceString = function(charId){ - var d6 = Characters.calculate.attributeBase(charId, "d6HitDice"); - var d8 = Characters.calculate.attributeBase(charId, "d8HitDice"); - var d10 = Characters.calculate.attributeBase(charId, "d10HitDice"); - var d12 = Characters.calculate.attributeBase(charId, "d12HitDice"); - var con = Characters.calculate.abilityMod(charId,"constitution"); - var string = "" + - (d6 ? `${d6}d6 + ` : "") + - (d8 ? `${d8}d8 + ` : "") + - (d10 ? `${d10}d10 + ` : "") + - (d12 ? `${d12}d12 + ` : "") + - con; - return string; -} - var getArmorString = function(charId){ var bases = Effects.find({ charId: charId, @@ -148,7 +201,7 @@ var getArmorString = function(charId){ strings = strings.concat(effects); return strings.join(", "); } - +/* var getDamageMods = function(charId){ // jscs:disable maximumLineLength var multipliers = [ @@ -175,40 +228,7 @@ var getDamageMods = function(charId){ "vulnerabilities": _.map(multipliers["2"], names).join(", "), }; } - -var getSkills = function(charId){ - var allSkills = [ - {name: "acrobatics", attribute: "dexterity"}, - {name: "animalHandling", attribute: "wisdom"}, - {name: "arcana", attribute: "intelligence"}, - {name: "athletics", attribute: "strength"}, - {name: "deception", attribute: "charisma"}, - {name: "history", attribute: "intelligence"}, - {name: "insight", attribute: "wisdom"}, - {name: "intimidation", attribute: "charisma"}, - {name: "investigation", attribute: "intelligence"}, - {name: "medicine", attribute: "wisdom"}, - {name: "nature", attribute: "intelligence"}, - {name: "perception", attribute: "wisdom"}, - {name: "performance", attribute: "charisma"}, - {name: "persuasion", attribute: "charisma"}, - {name: "religion", attribute: "intelligence"}, - {name: "sleightOfHand", attribute: "dexterity"}, - {name: "stealth", attribute: "dexterity"}, - {name: "survival", attribute: "wisdom"}, - ]; - var skills = {}; - _.each(allSkills, skill => { - var value = signedString( - Characters.calculate.skillMod(charId, skill.name) - ); - var prof = Characters.calculate.proficiency(charId, skill.name); - var name = skill.name.charAt(0).toUpperCase() + skill.name.slice(1); - skills[name] = value; - skills[name + "Proficiency"] = prof; - }); - return skills; -}; +*/ var getLanguages = function(charId){ return Proficiencies.find({ @@ -218,10 +238,6 @@ var getLanguages = function(charId){ }).map(l => l.name).join(", "); }; -var getClasses = function(charId){ - return Classes.find({charId}).map(c => `${c.name} ${c.level}`).join(", "); -}; - var getAttacks = function(charId){ var attacks = {}; var i = 1; diff --git a/rpg-docs/lib/functions/computeCharacter.js b/rpg-docs/lib/functions/computeCharacter.js new file mode 100644 index 00000000..920e6fa8 --- /dev/null +++ b/rpg-docs/lib/functions/computeCharacter.js @@ -0,0 +1,223 @@ +getCharacterForComputation = function(charId){ + const character = Characters.findOne(charId); + const classes = Classes.find({charId}).fetch(); + const effects = Effects.find({charId, enabled: true}).fetch(); + const proficiencies = Proficiencies.find({ + charId, + enabled: true, + type: {$in: ["skill", "save"]}, + }).fetch(); + return {character, classes, effects, proficiencies}; +} + +computeCharacter = function({character, classes, effects, proficiencies}){ + let computedClasses = computeCharacterClasses(charId, classes); + let changed = false; + computedCharacter = {}; + let i; + for (i = 0; i < 15; i++){ + [computedCharacter, changed] = compute({ + classes: computedClasses, + oldChar: computedCharacter, + charId, + character, + effects, + proficiencies, + }); + if (!changed) break; + } + return computedCharacter; +}; + +var ensureCharacterExists = (character) => { + if (!character) { + throw new Meteor.Error("Character doesn't exist", + "You can't recompute a character that doesn't exist"); + } +}; + +var ensureWritePermissions = (character, userId) => { + if ( + userId && + userId !== character.owner && + !_.contains(character.writers, userId) + ){ + throw new Meteor.Error("Character write denied", + "You don't have permission to recompute this character"); + } +}; + +var computeCharacterClasses = function(charId, classes){ + let computedClasses = {}; + _.each(classes, (cls) => { + if (computedClasses[cls.name]){ + computedClasses[cls.name].level += cls.level; + } else { + computedClasses[cls.name] = cls; + } + }); + return computedClasses; +} + +var compute = function({ + charId, oldChar, character, classes, effects, proficiencies, +}){ + let newChar = {}; + _.each(effects, (effect, index) => { + if (!effect.stat || effect.operation === "conditional") return; + if (!newChar[effect.stat]) newChar[effect.stat] = defaultStat(); + let value = effect.calculation ? + computeEffect(effect.calculation, oldChar, classes) : + effect.value || 0; + let stat = newChar[effect.stat]; + if (!_.isNumber(value)) return; + switch (effect.operation) { + case "base": + if (value > stat.base) stat.base = value; + break; + case "proficiency": + if (value > stat.proficiency) stat.proficiency = value; + break; + case "add": + stat.add += value; + break; + case "mul": + stat.mul *= value; + break; + case "min": + if (value > stat.min) stat.min = value; + break; + case "max": + if (value < stat.max) stat.max = value; + break; + case "advantage": + stat.advantage++; + break; + case "disadvantage": + stat.disadvantage++; + break; + case "passiveAdd": + stat.passiveAdd += value; + break; + case "fail": + stat.fail = true; + break; + } + }); + _.each(proficiencies, proficiency => { + if (!proficiency.name) return; + if (!newChar[proficiency.name]) newChar[proficiency.name] = defaultStat(); + let stat = newChar[proficiency.name]; + let value = proficiency.value; + if (value > stat.proficiency) stat.proficiency = value; + }); + let changed = false; + _.each(ATTRIBUTES, function(statName) { + if (!newChar[statName]) newChar[statName] = defaultStat(); + let stat = newChar[statName]; + stat.value = (stat.base + stat.add) * stat.mul; + if (stat.value < stat.min) stat.value = stat.min; + if (stat.value > stat.max) stat.value = stat.max; + if (!_.isEqual(stat.value, oldChar[statName] && oldChar[statName].value)){ + changed = true; + } + }); + _.each(ALL_SKILLS, function(statName) { + if (!newChar[statName]) newChar[statName] = defaultStat(); + let stat = newChar[statName]; + stat.value = characterAbilityMod( + oldChar, character[statName] && character[statName].ability + ); + stat.value += stat.base + stat.add; + stat.value += stat.proficiency * + characterFieldValue(oldChar, "proficiencyBonus"); + stat.value *= stat.mul; + if (stat.value < stat.min) stat.value = stat.min; + if (stat.value > stat.max) stat.value = stat.max; + if (!_.isEqual(stat.value, oldChar[statName] && oldChar[statName].value)){ + changed = true; + } + }); + return [newChar, changed]; +}; + +var defaultStat = function(){ + return { + base: 0, + proficiency: 0, + add: 0, + mul: 1, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + advantage: 0, + disadvantage: 0, + passiveAdd: 0, + fail: false, + } +} + +var computeEffect = function(string, character, classes){ + if (!string) return string; + string = string.replace(/\b[a-z][\w]+/gi, function(sub){ + //fields + if (character[sub]){ + return characterFieldValue(character, sub); + } + //ability modifiers + if (_.contains(ABILITY_MODS, sub)){ + var slice = sub.slice(0, -3); + return getMod( + character[slice] ? characterFieldValue(character, slice) : 0 + ); + } + //class levels + if (/\w+levels?\b/gi.test(sub)){ + //strip out "level" + var className = sub.replace(/levels?\b/gi, ""); + return characterClassLevel(classes, className) + } + //character level + if (sub.toUpperCase() === "LEVEL"){ + return characterTotalLevel(classes); + } + // exclude math functions + if (math[sub]){ + return sub; + } + return 0; + }); + try { + var result = math.eval(string); + return result; + } catch (e){ + return string; + } +}; + +var characterFieldValue = function(character, field){ + if (_.isNumber(character[field] && character[field].value)){ + return character[field].value; + } else { + return field; + } +}; + +var characterClassLevel = function(classes, className){ + if (_.isNumber(classes[className] && classes[className].level)){ + return classes[className].level; + } else { + return className; + } +}; + +var characterTotalLevel = function(classes){ + return _.reduce(classes, (memo, cls) => memo + cls.level, 0); +}; + +var characterAbilityMod = function(character, abilityName){ + if (_.isNumber(character[abilityName] && character[abilityName].value)){ + return getMod(character[abilityName].value); + } else { + return 0; + } +};