From b4506fd939892cd1452c954bd7930cffb855bdfd Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 24 Aug 2018 13:30:31 +0200 Subject: [PATCH] Began abstracting default abilities, damage multipliers and skills into rulesets. --- app/Model/Creature/Attributes.js | 6 +- app/Model/Creature/CharacterComputation.js | 40 +- app/Model/Creature/Characters.js | 498 +-------------------- app/Model/Creature/Skills.js | 2 + dataSources/rulesets/character5e.json | 96 ++++ 5 files changed, 157 insertions(+), 485 deletions(-) create mode 100644 dataSources/rulesets/character5e.json diff --git a/app/Model/Creature/Attributes.js b/app/Model/Creature/Attributes.js index db2ff5cb..9885564d 100644 --- a/app/Model/Creature/Attributes.js +++ b/app/Model/Creature/Attributes.js @@ -17,6 +17,10 @@ Schemas.Attribute = new SimpleSchema({ variableName: { type: String, }, + // Attributes need to store their order to keep the sheet consistent + order: { + type: Number, + }, type: { type: String, allowedValues: [ @@ -26,7 +30,7 @@ Schemas.Attribute = new SimpleSchema({ "healthBar", // Hitpoints, Temporary Hitpoints "resource", // Rages, sorcery points "spellSlot", // Level 1, 2, 3... spell slots - "attribute", // Aren't displayed, Jump height, Carry capacity + "utility", // Aren't displayed, Jump height, Carry capacity ], }, value: { diff --git a/app/Model/Creature/CharacterComputation.js b/app/Model/Creature/CharacterComputation.js index d71b429d..ad9f00a9 100644 --- a/app/Model/Creature/CharacterComputation.js +++ b/app/Model/Creature/CharacterComputation.js @@ -1,3 +1,8 @@ +// TODO recalculate carried weight method +// TODO Add attribute, skill, damageMultiplier and bundle library models +// Bundle libraries should support races, classes, and rulesets +// TODO Add monster library + import { ValidatedMethod } from 'meteor/mdg:validated-method'; const recomputeCharacter = new ValidatedMethod({ @@ -8,7 +13,7 @@ const recomputeCharacter = new ValidatedMethod({ charId: { type: String } }).validator(), - run({ charId }) { + run({charId}) { if (!canEditCharacter(charId, this.userId)) { // Throw errors with a specific error code throw new Meteor.Error('Characters.methods.recomputeCharacter.denied', @@ -21,6 +26,31 @@ const recomputeCharacter = new ValidatedMethod({ }); +const recomputeCharacterXP = new ValidatedMethod({ + name: "Characters.methods.recomputeCharacterXP", + + validate: new SimpleSchema({ + charId: { type: String } + }).validator(), + + run({charId}) { + if (!canEditCharacter(charId, this.userId)) { + // Throw errors with a specific error code + throw new Meteor.Error("Characters.methods.recomputeCharacterXP.denied", + "You do not have permission to recompute this character's XP"); + } + var xp = 0; + Experiences.find( + {charId: charId}, + {fields: {value: 1}} + ).forEach(function(e){ + xp += e.value; + }); + //TODO write the XP to the database, don't just return it + return xp; + }, +}) + /* * This function is the heart of DiceCloud. It recomputes a character's stats, * distilling down effects and proficiencies into the final stats that make up @@ -342,11 +372,11 @@ const combineSkill = function(stat, char){ if (prof.value > stat.proficiency) stat.proficiency = prof.value; } let profBonus; - if (char.atts.proificiencyBonus){ - if (!char.atts.proficiencyBonus.computed){ - computeStat(char.atts.proficiencyBonus, char); + if (char.skills.proificiencyBonus){ + if (!char.skills.proficiencyBonus.computed){ + computeStat(char.skills.proficiencyBonus, char); } - profBonus = char.atts.proficiencyBonus.result; + profBonus = char.skills.proficiencyBonus.result; } else { profBonus = Math.floor(char.level / 4 + 1.75); } diff --git a/app/Model/Creature/Characters.js b/app/Model/Creature/Characters.js index 7ba2ac97..263ff862 100644 --- a/app/Model/Creature/Characters.js +++ b/app/Model/Creature/Characters.js @@ -16,151 +16,9 @@ Schemas.Character = new SimpleSchema({ flaws: {type: String, defaultValue: "", trim: false, optional: true}, backstory: {type: String, defaultValue: "", trim: false, optional: true}, - //attributes - //ability scores - strength: {type: Schemas.Attribute}, - dexterity: {type: Schemas.Attribute}, - constitution: {type: Schemas.Attribute}, - intelligence: {type: Schemas.Attribute}, - wisdom: {type: Schemas.Attribute}, - charisma: {type: Schemas.Attribute}, - - //stats - hitPoints: {type: Schemas.Attribute}, - tempHP: {type: Schemas.Attribute}, - experience: {type: Schemas.Attribute}, - proficiencyBonus: {type: Schemas.Attribute}, - speed: {type: Schemas.Attribute}, - weight: {type: Schemas.Attribute}, - age: {type: Schemas.Attribute}, - ageRate: {type: Schemas.Attribute}, - armor: {type: Schemas.Attribute}, - carryMultiplier: {type: Schemas.Attribute}, - - //resources - level1SpellSlots: {type: Schemas.Attribute}, - level2SpellSlots: {type: Schemas.Attribute}, - level3SpellSlots: {type: Schemas.Attribute}, - level4SpellSlots: {type: Schemas.Attribute}, - level5SpellSlots: {type: Schemas.Attribute}, - level6SpellSlots: {type: Schemas.Attribute}, - level7SpellSlots: {type: Schemas.Attribute}, - level8SpellSlots: {type: Schemas.Attribute}, - level9SpellSlots: {type: Schemas.Attribute}, - ki: {type: Schemas.Attribute}, - sorceryPoints: {type: Schemas.Attribute}, - rages: {type: Schemas.Attribute}, - superiorityDice: {type: Schemas.Attribute}, - expertiseDice: {type: Schemas.Attribute}, - - //specific features - rageDamage: {type: Schemas.Attribute}, - - //hit dice - d6HitDice: {type: Schemas.Attribute}, - d8HitDice: {type: Schemas.Attribute}, - d10HitDice: {type: Schemas.Attribute}, - d12HitDice: {type: Schemas.Attribute}, - - //vulnerabilities - acidMultiplier: {type: Schemas.Attribute}, - bludgeoningMultiplier: {type: Schemas.Attribute}, - coldMultiplier: {type: Schemas.Attribute}, - fireMultiplier: {type: Schemas.Attribute}, - forceMultiplier: {type: Schemas.Attribute}, - lightningMultiplier: {type: Schemas.Attribute}, - necroticMultiplier: {type: Schemas.Attribute}, - piercingMultiplier: {type: Schemas.Attribute}, - poisonMultiplier: {type: Schemas.Attribute}, - psychicMultiplier: {type: Schemas.Attribute}, - radiantMultiplier: {type: Schemas.Attribute}, - slashingMultiplier: {type: Schemas.Attribute}, - thunderMultiplier: {type: Schemas.Attribute}, - - //skills - //saves - strengthSave: {type: Schemas.Skill}, - "strengthSave.ability": {type: String, defaultValue: "strength"}, - - dexteritySave: {type: Schemas.Skill}, - "dexteritySave.ability": {type: String, defaultValue: "dexterity"}, - - constitutionSave:{type: Schemas.Skill}, - "constitutionSave.ability": {type: String, defaultValue: "constitution"}, - - intelligenceSave:{type: Schemas.Skill}, - "intelligenceSave.ability": {type: String, defaultValue: "intelligence"}, - - wisdomSave: {type: Schemas.Skill}, - "wisdomSave.ability": {type: String, defaultValue: "wisdom"}, - - charismaSave: {type: Schemas.Skill}, - "charismaSave.ability": {type: String, defaultValue: "charisma"}, - - //skill skills - acrobatics: {type: Schemas.Skill}, - "acrobatics.ability": {type: String, defaultValue: "dexterity"}, - - animalHandling: {type: Schemas.Skill}, - "animalHandling.ability": {type: String, defaultValue: "wisdom"}, - - arcana: {type: Schemas.Skill}, - "arcana.ability": {type: String, defaultValue: "intelligence"}, - - athletics: {type: Schemas.Skill}, - "athletics.ability": {type: String, defaultValue: "strength"}, - - deception: {type: Schemas.Skill}, - "deception.ability": {type: String, defaultValue: "charisma"}, - - history: {type: Schemas.Skill}, - "history.ability": {type: String, defaultValue: "intelligence"}, - - insight: {type: Schemas.Skill}, - "insight.ability": {type: String, defaultValue: "wisdom"}, - - intimidation: {type: Schemas.Skill}, - "intimidation.ability": {type: String, defaultValue: "charisma"}, - - investigation: {type: Schemas.Skill}, - "investigation.ability": {type: String, defaultValue: "intelligence"}, - - medicine: {type: Schemas.Skill}, - "medicine.ability": {type: String, defaultValue: "wisdom"}, - - nature: {type: Schemas.Skill}, - "nature.ability": {type: String, defaultValue: "intelligence"}, - - perception: {type: Schemas.Skill}, - "perception.ability": {type: String, defaultValue: "wisdom"}, - - performance: {type: Schemas.Skill}, - "performance.ability": {type: String, defaultValue: "charisma"}, - - persuasion: {type: Schemas.Skill}, - "persuasion.ability": {type: String, defaultValue: "charisma"}, - - religion: {type: Schemas.Skill}, - "religion.ability": {type: String, defaultValue: "intelligence"}, - - sleightOfHand: {type: Schemas.Skill}, - "sleightOfHand.ability": {type: String, defaultValue: "dexterity"}, - - stealth: {type: Schemas.Skill}, - "stealth.ability": {type: String, defaultValue: "dexterity"}, - - survival: {type: Schemas.Skill}, - "survival.ability": {type: String, defaultValue: "wisdom"}, - - //Mechanical Skills - initiative: {type: Schemas.Skill}, - "initiative.ability": {type: String, defaultValue: "dexterity"}, - - dexterityArmor: {type: Schemas.Skill}, - "dexterityArmor.ability": {type: String, defaultValue: "dexterity"}, - //mechanics - deathSave: {type: Schemas.DeathSave}, + deathSave: {type: Schemas.DeathSave}, + xp: {type: Number, defaultValue: 0}, //permissions party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true}, @@ -196,238 +54,8 @@ Schemas.Character = new SimpleSchema({ Characters.attachSchema(Schemas.Character); -var attributeBase = preventLoop(function(charId, statName){ - check(statName, String); - //if it's a damage multiplier, we treat it specially - if (_.contains(DAMAGE_MULTIPLIERS, statName)){ - 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){ - return 2; - } else { - return 1; - } - } - var value; - var base = 0; - var add = 0; - var mul = 1; - var min = Number.NEGATIVE_INFINITY; - var max = Number.POSITIVE_INFINITY; - - 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; - } - }); - - var result = (base + add) * mul; - if (result < min) result = min; - if (result > max) result = max; - // Don't round carry multiplier - if (statName === "carryMultiplier"){ - return result; - } - return Math.floor(result); -}); - -if (Meteor.isClient) { - 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; - } - } - }); -} - -//create a local memoize with a argument concatenating hash function -var memoize = function(f) { - if (Meteor.isServer) return f; - return Tracker.memoize(f, function() { - return _.reduce(arguments, function(memo, arg) { - return memo + arg; - }, ""); - }); -}; - //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}); - if (!char) return; - 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); - if (!attribute) return; - //base value - var value = Characters.calculate.attributeBase(charId, attributeName); - //plus adjustment - value += attribute.adjustment; - return value; - }), - attributeBase: memoize(function(charId, attributeName){ - return attributeBase(charId, attributeName); - }), - skillMod: memoize(preventLoop(function(charId, skillName){ - var skill = Characters.calculate.getField(charId, skillName); - if (!skill) return; - //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 = Number.NEGATIVE_INFINITY; - var max = Number.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 Math.floor(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 || 0; - }), - passiveSkill: memoize(function(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 Math.floor(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++){ @@ -438,116 +66,28 @@ Characters.calculate = { 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 deprecated = function() { - //var err = new Error("this function has been deprecated"); - var name = ""; - if (Template.instance()){ - name = Template.instance().view.name; - } - var logString = "this function has been deprecated \n"; - if (name){ - logString += "View: " + name + "\n\n"; - } - //logString += err.stack + "\n\n---------------------\n\n"; - console.log(logString); -}; - -//functions and calculated values. -//These functions can only rely on this._id since no other -//field is likely to be attached to all returned characters -Characters.helpers({ - //returns the value stored in the field requested - //will set up dependencies on just that field - getField : function(fieldName){ - deprecated(); - return Characters.calculate.getField(this._id, fieldName); - }, - //returns the value of a field - fieldValue : function(fieldName){ - deprecated(); - return Characters.calculate.fieldValue(this._id, fieldName); - }, - attributeValue: function(attributeName){ - deprecated(); - return Characters.calculate.attributeValue(this._id, attributeName); - }, - attributeBase: function(attributeName){ - deprecated(); - return Characters.calculate.attributeBase(this._id, attributeName); - }, - skillMod: function(skillName){ - deprecated(); - return Characters.calculate.skillMod(this._id, skillName); - }, - proficiency: function(skillName){ - deprecated(); - return Characters.calculate.proficiency(this._id, skillName); - }, - passiveSkill: function(skillName){ - deprecated(); - return Characters.calculate.passiveSkill(this._id, skillName); - }, - advantage: function(skillName){ - deprecated(); - return Characters.calculate.advantage(this._id, skillName); - }, - abilityMod: function(attribute){ - deprecated(); - return Characters.calculate.abilityMod(this._id, attribute); - }, - passiveAbility: function(attribute){ - deprecated(); - return Characters.calculate.passiveAbility(this._id, attribute); - }, - xpLevel: function(){ - deprecated(); - return Characters.calculate.xpLevel(this._id); - }, - level: function(){ - deprecated(); - return Characters.calculate.level(this._id); - }, - experience: function(){ - deprecated(); - return Characters.calculate.experience(this._id); - }, -}); - //clean up all data related to that character before removing it if (Meteor.isServer){ Characters.after.remove(function(userId, character) { - Actions .remove({charId: character._id}); - Attacks .remove({charId: character._id}); - Buffs .remove({charId: character._id}); - Classes .remove({charId: character._id}); - CustomBuffs .remove({charId: character._id}); - Effects .remove({charId: character._id}); - Experiences .remove({charId: character._id}); - Features .remove({charId: character._id}); - Notes .remove({charId: character._id}); - Proficiencies .remove({charId: character._id}); - SpellLists .remove({charId: character._id}); - Items .remove({charId: character._id}); - Containers .remove({charId: character._id}); + let charId = character._id; + Actions .remove({charId}); + Attacks .remove({charId}); + Attributes .remove({charId}); + Buffs .remove({charId}); + Classes .remove({charId}); + CustomBuffs .remove({charId}); + DamageMultipliers.remove({charId}); + Effects .remove({charId}); + Experiences .remove({charId}); + Features .remove({charId}); + Notes .remove({charId}); + Proficiencies .remove({charId}); + Skills .remove({charId}); + SpellLists .remove({charId}); + Items .remove({charId}); + Containers .remove({charId}); }); Characters.after.update(function(userId, doc, fieldNames, modifier, options) { if (_.contains(fieldNames, "name")){ diff --git a/app/Model/Creature/Skills.js b/app/Model/Creature/Skills.js index 52dbdf12..42bb8b8f 100644 --- a/app/Model/Creature/Skills.js +++ b/app/Model/Creature/Skills.js @@ -27,9 +27,11 @@ Schemas.Skill = new SimpleSchema({ allowedValues: [ "skill", "save", + "stat", "tool", "weapon", "language", + "utility", //not displayed anywhere ], }, value: { diff --git a/dataSources/rulesets/character5e.json b/dataSources/rulesets/character5e.json new file mode 100644 index 00000000..0df4e426 --- /dev/null +++ b/dataSources/rulesets/character5e.json @@ -0,0 +1,96 @@ +{ + "attributes": { + "ability": [ + "Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma" + ], + "stat": [ + "Speed", + {"name": "Armor Class", "variableName": "armor"}, + ], + "hitDice": [ + {"name": "d6 Hit Dice", "variableName": "d6HitDice"}, + {"name": "d8 Hit Dice", "variableName": "d8HitDice"}, + {"name": "d10 Hit Dice", "variableName": "d10HitDice"}, + {"name": "d12 Hit Dice", "variableName": "d12HitDice"}, + ], + "healthBar": [ + {"name": "Hit Points", "variableName": "hitPoints"}, + {"name": "Temporary Hit Points", "variableName": "tempHitPoints"}, + ], + "resource": [ + "Ki", "Rages", + {"name": "Sourcery Points", "variableName": "sorceryPoints"}, + {"name": "Superiority Dice", "variableName": "superiorityDice"}, + {"name": "Expertise Dice", "variableName": "expertiseDice"}, + ], + "spellSlot": [ + {"name": "Level 1 Spell Slots", "variableName": "level1SpellSlots"}, + {"name": "Level 2 Spell Slots", "variableName": "level2SpellSlots"}, + {"name": "Level 3 Spell Slots", "variableName": "level3SpellSlots"}, + {"name": "Level 4 Spell Slots", "variableName": "level4SpellSlots"}, + {"name": "Level 5 Spell Slots", "variableName": "level5SpellSlots"}, + {"name": "Level 6 Spell Slots", "variableName": "level6SpellSlots"}, + {"name": "Level 7 Spell Slots", "variableName": "level7SpellSlots"}, + {"name": "Level 8 Spell Slots", "variableName": "level8SpellSlots"}, + {"name": "Level 9 Spell Slots", "variableName": "level9SpellSlots"}, + ], + "utility": [ + {"name": "Carry Capacity Multiplier", "variableName": "carryMultiplier"}, + {"name": "Rage Damage", "variableName": "rageDamage"}, + ], + }, + + "skills": { + "skill": [ + {"name": "Acrobatics", "variableName": "acrobatics", "ability": "dexterity"}, + {"name": "Animal Handling", "variableName": "animalHandling", "ability": "wisdom"}, + {"name": "Arcana", "variableName": "arcana", "ability": "intelligence"}, + {"name": "Athletics", "variableName": "athletics", "ability": "strength"}, + {"name": "Deception", "variableName": "deception", "ability": "charisma"}, + {"name": "History", "variableName": "history", "ability": "intelligence"}, + {"name": "Insight", "variableName": "insight", "ability": "wisdom"}, + {"name": "Intimidation", "variableName": "intimidation", "ability": "charisma"}, + {"name": "Investigation", "variableName": "investigation", "ability": "intelligence"}, + {"name": "Medicine", "variableName": "medicine", "ability": "wisdom"}, + {"name": "Nature", "variableName": "nature", "ability": "intelligence"}, + {"name": "Perception", "variableName": "perception", "ability": "wisdom"}, + {"name": "Performance", "variableName": "performance", "ability": "charisma"}, + {"name": "Persuasion", "variableName": "persuasion", "ability": "charisma"}, + {"name": "Religion", "variableName": "religion", "ability": "intelligence"}, + {"name": "Sleight of Hand", "variableName": "sleightOfHand", "ability": "dexterity"}, + {"name": "Stealth", "variableName": "stealth", "ability": "dexterity"}, + {"name": "Survival", "variableName": "survival", "ability": "wisdom"}, + ], + "save": [ + {"name": "Strength Save", "variableName": "strengthSave", "ability": "strength"}, + {"name": "Dexterity Save", "variableName": "dexteritySave", "ability": "dexterity"}, + {"name": "Constitution Save", "variableName": "constitutionSave", "ability": "constitution"}, + {"name": "Intelligence Save", "variableName": "intelligenceSave", "ability": "intelligence"}, + {"name": "Wisdom Save", "variableName": "wisdomSave", "ability": "wisdom"}, + {"name": "Charisma Save", "variableName": "charismaSave", "ability": "charisma"}, + ], + "stat": [ + {"name": "Proficiency Bonus", "variableName": "proficiencyBonus"}, + {"name": "initiative", "variableName": "initiative"}, + ], + "utility": [ + {"name": "Dexterity Armor", "variableName": "dexterityArmor", "ability": "dexterity"}, + ], + }, + + "damageMultipliers": [ + {"name": "Acid Multiplier", "variableName":"acidMultiplier"}, + {"name": "Bludgeoning Multiplier", "variableName":"bludgeoningMultiplier"}, + {"name": "Cold Multiplier", "variableName":"coldMultiplier"}, + {"name": "Fire Multiplier", "variableName":"fireMultiplier"}, + {"name": "Force Multiplier", "variableName":"forceMultiplier"}, + {"name": "Lightning Multiplier", "variableName":"lightningMultiplier"}, + {"name": "Necrotic Multiplier", "variableName":"necroticMultiplier"}, + {"name": "Piercing Multiplier", "variableName":"piercingMultiplier"}, + {"name": "Poison Multiplier", "variableName":"poisonMultiplier"}, + {"name": "Psychic Multiplier", "variableName":"psychicMultiplier"}, + {"name": "Radiant Multiplier", "variableName":"radiantMultiplier"}, + {"name": "Slashing Multiplier", "variableName":"slashingMultiplier"}, + {"name": "Thunder Multiplier", "variableName":"thunderMultiplier"}, + ] +}