diff --git a/app/imports/api/creature/Attributes.js b/app/imports/api/creature/Attributes.js index e7abdad0..bc30a521 100644 --- a/app/imports/api/creature/Attributes.js +++ b/app/imports/api/creature/Attributes.js @@ -1,9 +1,11 @@ -Attributes = new Mongo.Collection("attributes"); +import {makeChild} from "/imports/api/parenting.js"; + +let Attributes = new Mongo.Collection("attributes"); /* * Attributes are whole numbered stats of a character */ -Schemas.Attribute = new SimpleSchema({ +attributeSchema = new SimpleSchema({ charId: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -59,9 +61,6 @@ Schemas.Attribute = new SimpleSchema({ type: Boolean, optional: true, }, - parent: { - type: Schemas.Parent - }, enabled: { type: Boolean, defaultValue: true, @@ -78,10 +77,9 @@ Schemas.Attribute = new SimpleSchema({ }, }); -Attributes.attachSchema(Schemas.Attribute); +Attributes.attachSchema(attributeSchema); Attributes.attachBehaviour("softRemovable"); makeChild(Attributes, ["enabled"]); //children of lots of things -Attributes.allow(CHARACTER_SUBSCHEMA_ALLOW); -Attributes.deny(CHARACTER_SUBSCHEMA_DENY); +export default Attributes; diff --git a/app/imports/api/creature/Creatures.js b/app/imports/api/creature/Creatures.js index bd9e531e..06934fd6 100644 --- a/app/imports/api/creature/Creatures.js +++ b/app/imports/api/creature/Creatures.js @@ -1,9 +1,11 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import Effects from "/imports/api/creature/Effects.js" +import deathSaveSchema from "/imports/api/creature/subSchemas/DeathSaves.js" //set up the collection for creatures Creatures = new Mongo.Collection("creatures"); -Schemas.Creature = new SimpleSchema({ +let creatureSchema = new SimpleSchema({ //strings name: {type: String, defaultValue: "", trim: false, optional: true}, urlName: {type: String, defaultValue: "-", trim: false, optional: true}, @@ -19,7 +21,7 @@ Schemas.Creature = new SimpleSchema({ backstory: {type: String, defaultValue: "", trim: false, optional: true}, //mechanics - deathSave: {type: Schemas.DeathSave}, + deathSave: {type: deathSaveSchema}, xp: {type: Number, defaultValue: 0}, weightCarried: {type: Number, defaultValue: 0}, level: {type: Number, defaultValue: 0}, @@ -32,8 +34,9 @@ Schemas.Creature = new SimpleSchema({ writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1}, color: { type: String, - allowedValues: _.pluck(colorOptions, "key"), - defaultValue: "q", + defaultValue: "#9E9E9E", + // match hex colors of the form #A23 or #A23f56 + regEx: /^#([a-f0-9]{3}){1,2}\b$/i, }, //TODO add per-creature settings //how many experiences to load at a time in XP table @@ -57,7 +60,7 @@ Schemas.Creature = new SimpleSchema({ "settings.newUserExperience": {type: Boolean, optional: true}, }); -Creatures.attachSchema(Schemas.Creature); +Creatures.attachSchema(creatureSchema); Creatures.calculate = { xpLevel: function(charId){ @@ -89,7 +92,7 @@ const insertCharacter = new ValidatedMethod({ "You need to be logged in to insert a creature"); } - // Create the creature document + // Create the creature document Creatures.insert({name, owner: this.userId}); this.unblock(); //Add all the required attributes to it @@ -141,6 +144,59 @@ if (Meteor.isServer){ doc.settings.newUserExperience = true; } }); + //give ceatures default ceature effects + Creatures.after.insert(function(userId, char) { + if (Meteor.isServer) { + Effects.insert({ + charId: char._id, + name: "Proficiency bonus by level", + stat: "proficiencyBonus", + operation: "add", + calculation: "floor(level / 4 + 1.75)", + parent: { + id: char._id, + collection: "Ceatures", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Dexterity Armor Bonus", + stat: "armor", + operation: "add", + calculation: "dexterityArmor", + parent: { + id: char._id, + collection: "Ceatures", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Natural Armor", + stat: "armor", + operation: "base", + value: 10, + parent: { + id: char._id, + collection: "Ceatures", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Natural Carrying Capacity", + stat: "carryMultiplier", + operation: "base", + value: "1", + parent: { + id: char._id, + collection: "Ceatures", + group: "Inate", + }, + }); + } + }); } Creatures.allow({ @@ -166,3 +222,5 @@ Creatures.deny({ return _.contains(fields, "owner"); } }); + +export default Creatures; diff --git a/app/imports/api/creature/Effects.js b/app/imports/api/creature/Effects.js index 4cf9d913..6b12ae25 100644 --- a/app/imports/api/creature/Effects.js +++ b/app/imports/api/creature/Effects.js @@ -1,10 +1,12 @@ +import {makeChild} from "/imports/api/parenting.js"; + Effects = new Mongo.Collection("effects"); /* * Effects are reason-value attached to skills and abilities * that modify their final value or presentation in some way */ -Schemas.Effect = new SimpleSchema({ +effectSchema = new SimpleSchema({ charId: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -41,10 +43,6 @@ Schemas.Effect = new SimpleSchema({ optional: true, trim: false, }, - //the thing that created this effect - parent: { - type: Schemas.Parent - }, //which stat the effect is applied to stat: { type: String, @@ -56,76 +54,9 @@ Schemas.Effect = new SimpleSchema({ }, }); -Effects.attachSchema(Schemas.Effect); +Effects.attachSchema(effectSchema); Effects.attachBehaviour("softRemovable"); makeChild(Effects, ["enabled"]); //children of lots of things -Effects.allow(CHARACTER_SUBSCHEMA_ALLOW); -Effects.deny(CHARACTER_SUBSCHEMA_DENY); - -//give characters default character effects -Characters.after.insert(function(userId, char) { - if (Meteor.isServer) { - Effects.insert({ - charId: char._id, - name: "Constitution modifier for each level", - stat: "hitPoints", - operation: "add", - calculation: "level * constitutionMod", - parent: { - id: char._id, - collection: "Characters", - group: "Inate", - }, - }); - Effects.insert({ - charId: char._id, - name: "Proficiency bonus by level", - stat: "proficiencyBonus", - operation: "add", - calculation: "floor(level / 4 + 1.75)", - parent: { - id: char._id, - collection: "Characters", - group: "Inate", - }, - }); - Effects.insert({ - charId: char._id, - name: "Dexterity Armor Bonus", - stat: "armor", - operation: "add", - calculation: "dexterityArmor", - parent: { - id: char._id, - collection: "Characters", - group: "Inate", - }, - }); - Effects.insert({ - charId: char._id, - name: "Natural Armor", - stat: "armor", - operation: "base", - value: 10, - parent: { - id: char._id, - collection: "Characters", - group: "Inate", - }, - }); - Effects.insert({ - charId: char._id, - name: "Natural Carrying Capacity", - stat: "carryMultiplier", - operation: "base", - value: "1", - parent: { - id: char._id, - collection: "Characters", - group: "Inate", - }, - }); - } -}); +export default Effects; diff --git a/app/imports/api/creature/Skills.js b/app/imports/api/creature/Skills.js index b4abb2d8..99777fbd 100644 --- a/app/imports/api/creature/Skills.js +++ b/app/imports/api/creature/Skills.js @@ -1,10 +1,12 @@ +import {makeChild} from "/imports/api/parenting.js"; + Skills = new Mongo.Collection("skills"); /* * Skills are anything that results in a modifier to be added to a D20 * Skills usually have an ability score modifier that they use as their basis */ -Schemas.Skill = new SimpleSchema({ +skillSchema = new SimpleSchema({ charId: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -65,19 +67,15 @@ Schemas.Skill = new SimpleSchema({ type: Number, optional: true, }, - parent: { - type: Schemas.Parent - }, enabled: { type: Boolean, defaultValue: true, }, }); -Skills.attachSchema(Schemas.Skill); +Skills.attachSchema(skillSchema); Skills.attachBehaviour("softRemovable"); makeChild(Skills, ["enabled"]); //children of lots of things -Skills.allow(CHARACTER_SUBSCHEMA_ALLOW); -Skills.deny(CHARACTER_SUBSCHEMA_DENY); +export default Skills; diff --git a/app/imports/api/creature/creatureComputation.js b/app/imports/api/creature/creatureComputation.js index a01f3165..e81eed99 100644 --- a/app/imports/api/creature/creatureComputation.js +++ b/app/imports/api/creature/creatureComputation.js @@ -1,32 +1,36 @@ -// TODO allow abilities to get disadvantage, making all skills that are based +// TODO allow abilities to get advantage/disadvantage, making all skills that are based // on them disadvantaged as well import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import Creatures from "/imports/api/creature/Creatures.js"; +import Attributes from "/imports/api/creature/Attributes.js"; +import Skills from "/imports/api/creature/Skills.js"; +import Effects from "/imports/api/creature/Effects.js"; -const recomputeCharacter = new ValidatedMethod({ +const recomputeCreature = new ValidatedMethod({ - name: "Characters.methods.recomputeCharacter", + name: "Creatures.methods.recomputeCreature", validate: new SimpleSchema({ charId: { type: String } }).validator(), run({charId}) { - if (!canEditCharacter(charId, this.userId)) { - throw new Meteor.Error('Characters.methods.recomputeCharacter.denied', - 'You do not have permission to recompute this character'); + if (!canEditCreature(charId, this.userId)) { + throw new Meteor.Error('Creatures.methods.recomputeCreature.denied', + 'You do not have permission to recompute this creature'); } - computeCharacterById(charId); + computeCreatureById(charId); }, }); /* - * This function is the heart of DiceCloud. It recomputes a character's stats, + * This function is the heart of DiceCloud. It recomputes a creature's stats, * distilling down effects and proficiencies into the final stats that make up - * a character. + * a creature. * * Essentially this is a backtracking algorithm that computes stats' * dependencies before computing stats themselves, while detecting @@ -41,7 +45,7 @@ const recomputeCharacter = new ValidatedMethod({ * expanded to meet demand. * * A brief overview: - * - Fetch the stats of the character and add them to + * - Fetch the stats of the creature and add them to * an object for quick lookup * - Fetch the effects and proficiencies which apply to each stat and store them with the stat * - Fetch the class levels and store them as well @@ -59,21 +63,21 @@ const recomputeCharacter = new ValidatedMethod({ * - Mark the stat as computed * - Write the computed results back to the database */ -const computeCharacterById = function (charId){ - let char = buildCharacter(); - char = computeCharacter(char); - writeCharacter(char); +const computeCreatureById = function (charId){ + let char = buildCreature(); + char = computeCreature(char); + writeCreature(char); return char; }; /* - * Write the in-memory character to the database docs + * Write the in-memory creature to the database docs */ -const writeCharacter = function (char) { +const writeCreature = function (char) { writeAttributes(char); writeSkills(char); writeDamageMultipliers(char); - Characters.update(char.id, {$set: {level: char.level}}); + Creatures.update(char.id, {$set: {level: char.level}}); }; /* @@ -157,10 +161,10 @@ const writeDamageMultipliers = function (char) { } /* - * Get the character's data from the database and build an in-memory model that + * Get the creature's data from the database and build an in-memory model that * can be computed. Hits 6 database collections with indexed queries. */ -const buildCharacter = function (charId){ +const buildCreature = function (charId){ let char = { id: charId, atts: {}, @@ -169,7 +173,7 @@ const buildCharacter = function (charId){ classes: {}, level: 0, }; - // Fetch the attributes of the character and add them to an object for quick lookup + // Fetch the attributes of the creature and add them to an object for quick lookup Attributes.find({charId}).forEach(attribute => { if (!char.atts[attribute.variableName]){ char.atts[attribute.variableName] = { @@ -190,7 +194,7 @@ const buildCharacter = function (charId){ } }); - // Fetch the skills of the character and store them + // Fetch the skills of the creature and store them Skills.find({charId}).forEach(skill => { if (!char.skills[skill.variableName]){ char.skills[skill.variableName] = { @@ -215,7 +219,7 @@ const buildCharacter = function (charId){ } }); - // Fetch the damage multipliers of the character and store them + // Fetch the damage multipliers of the creature and store them DamageMultipliers.find({charId}).forEach(damageMultiplier =>{ if (!char.dms[damageMultiplier.variableName]){ char.dms[damageMultiplier.variableName] = { @@ -277,9 +281,9 @@ const buildCharacter = function (charId){ } /* - * Compute the character's stats in-place, returns the same char object + * Compute the creature's stats in-place, returns the same char object */ -export const computeCharacter = function (char){ +const computeCreature = function (char){ // Iterate over each stat in order and compute it for (statName in char.atts){ let stat = char.atts[statName] @@ -297,7 +301,7 @@ export const computeCharacter = function (char){ } /* - * Compute a single stat on a character + * Compute a single stat on a creature */ const computeStat = function(stat, char){ // If the stat is already computed, skip it @@ -329,7 +333,7 @@ const computeStat = function(stat, char){ } /* - * Compute a single effect on a character + * Compute a single effect on a creature */ const computeEffect = function(effect, char){ if (effect.computed) return; @@ -516,7 +520,7 @@ const evaluateCalculation = function(string, char){ var className = sub.replace(/levels?$/, ""); return char.classes[className] && char.classes[className].level || sub; } - // Character level + // Creature level if (sub === "level"){ return char.level; } @@ -533,18 +537,18 @@ const evaluateCalculation = function(string, char){ } }; -const recomputeCharacterXP = new ValidatedMethod({ - name: "Characters.methods.recomputeCharacterXP", +const recomputeCreatureXP = new ValidatedMethod({ + name: "Creatures.methods.recomputeCreatureXP", validate: new SimpleSchema({ charId: { type: String } }).validator(), run({charId}) { - if (!canEditCharacter(charId, this.userId)) { + if (!canEditCreature(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"); + throw new Meteor.Error("Creatures.methods.recomputeCreatureXP.denied", + "You do not have permission to recompute this creature's XP"); } var xp = 0; Experiences.find( @@ -554,23 +558,23 @@ const recomputeCharacterXP = new ValidatedMethod({ xp += e.value; }); - Characters.update(charId, {$set: {xp}}) + Creatures.update(charId, {$set: {xp}}) return xp; }, }); -const recomputeCharacterWeightCarried = new ValidatedMethod({ - name: "Character.methods.recomputeCharacterWeightCarried", +const recomputeCreatureWeightCarried = new ValidatedMethod({ + name: "Creature.methods.recomputeCreatureWeightCarried", validate: new SimpleSchema({ charId: { type: String } }).validator(), run({charId}){ - if (!canEditCharacter(charId, this.userId)) { + if (!canEditCreature(charId, this.userId)) { // Throw errors with a specific error code - throw new Meteor.Error("Characters.methods.recomputeCharacterWeightCarried.denied", - "You do not have permission to recompute this character's carried weight"); + throw new Meteor.Error("Creatures.methods.recomputeCreatureWeightCarried.denied", + "You do not have permission to recompute this creature's carried weight"); } var weightCarried = 0; // store a dictionary of carried containers @@ -603,7 +607,14 @@ const recomputeCharacterWeightCarried = new ValidatedMethod({ } }); - Characters.update(charId, {$set: {weightCarried}}) + Creatures.update(charId, {$set: {weightCarried}}) return weightCarried; } }); + +export { + recomputeCreature, + computeCreature, + recomputeCreatureXP, + recomputeCreatureWeightCarried +}; diff --git a/app/imports/api/creature/creatureComputation.test.js b/app/imports/api/creature/creatureComputation.test.js index 2688138a..19c64c88 100644 --- a/app/imports/api/creature/creatureComputation.test.js +++ b/app/imports/api/creature/creatureComputation.test.js @@ -1,4 +1,4 @@ -import {computeCharacter} from "./CharacterComputation.js"; +import {computeCreature} from "./creatureComputation.js"; import assert from "assert"; const makeEffect = function(operation, value){ @@ -11,8 +11,8 @@ const makeEffect = function(operation, value){ return effect; } -describe('computeCharacter', function () { - it('computes an aritrary character', function () { +describe('computeCreature', function () { + it('computes an aritrary creature', function () { let char = { atts: { attribute1: { @@ -90,7 +90,7 @@ describe('computeCharacter', function () { }, level: 5, }; - char = computeCharacter(char); + char = computeCreature(char); console.log(char); assert(true); }); diff --git a/app/lib/constants/defaultCharacterStats.js b/app/imports/api/creature/creatureDefaultStats.js similarity index 90% rename from app/lib/constants/defaultCharacterStats.js rename to app/imports/api/creature/creatureDefaultStats.js index f006beeb..bcd2ab7b 100644 --- a/app/lib/constants/defaultCharacterStats.js +++ b/app/imports/api/creature/creatureDefaultStats.js @@ -5,7 +5,7 @@ DEFAULT_CHARACTER_STATS = { ], "stat": [ "Speed", - {"name": "Armor Class", "variableName": "armor"}, + {"name": "Armor Class", "variableName": "armor", "baseValue": 10}, ], "hitDice": [ {"name": "d6 Hit Dice", "variableName": "d6HitDice"}, @@ -35,7 +35,7 @@ DEFAULT_CHARACTER_STATS = { {"name": "Level 9 Spell Slots", "variableName": "level9SpellSlots"}, ], "utility": [ - {"name": "Carry Capacity Multiplier", "variableName": "carryMultiplier"}, + {"name": "Carry Capacity Multiplier", "variableName": "carryMultiplier", "baseValue": 1}, {"name": "Rage Damage", "variableName": "rageDamage"}, ], }, @@ -73,9 +73,6 @@ DEFAULT_CHARACTER_STATS = { {"name": "Proficiency Bonus", "variableName": "proficiencyBonus"}, {"name": "initiative", "variableName": "initiative"}, ], - "utility": [ - {"name": "Dexterity Armor", "variableName": "dexterityArmor", "ability": "dexterity"}, - ], }, "damageMultipliers": [ @@ -92,19 +89,33 @@ DEFAULT_CHARACTER_STATS = { {"name": "Radiant Multiplier", "variableName":"radiantMultiplier"}, {"name": "Slashing Multiplier", "variableName":"slashingMultiplier"}, {"name": "Thunder Multiplier", "variableName":"thunderMultiplier"}, + ], + + "effects": [ + { + "name": "Proficiency bonus by level", + "stat": "proficiencyBonus", + "operation": "add", + "calculation": "floor(level / 4 + 1.75)", + }, ] } -getDefaultCharacterDocs = function(charId){ +getDefaultCreatureDocs = function(charId, creatureType = "pc"){ let docs = {attributes: [], skills: [], damageMultipliers: []}; - const stats = DEFAULT_CHARACTER_STATS; + if (creatureType === "pc"){ + const stats = DEFAULT_CHARACTER_STATS; + } else { + throw new Meteor.Error("Not implemented", + "Default stats for non-player characters aren't implemented yet"); + } let order = 0; const baseParent = { collection: "Characters", id: charId, group: "default", }; - let name, variableName, parent, attribute, skill, ability, dm, type; + let name, variableName, parent, attribute, skill, ability, dm, type, baseValue; for (type in stats.attributes){ for (let i in stats.attributes[type]){ attribute = stats.attributes[type][i]; @@ -115,10 +126,11 @@ getDefaultCharacterDocs = function(charId){ name = attribute.name; variableName = attribute.variableName; } + baseValue = attribute.baseValue; parent = _.clone(baseParent); docs.attributes.push({ _id: Random.id, - charId, name, variableName, order, type, parent + charId, name, variableName, order, type, parent, baseValue, }); order++; } diff --git a/app/imports/api/creature/subSchemas/Adjustment.js b/app/imports/api/creature/subSchemas/Adjustment.js deleted file mode 100644 index 8025f089..00000000 --- a/app/imports/api/creature/subSchemas/Adjustment.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Adjustments make instantaneous changes to the value of some attribute - * Damage, healing and resource cost/recovery are all adjustments - */ -Schemas.Adjustment = new SimpleSchema({ - //which stat the adjustment is applied to - stat: { - type: String, - optional: true, - }, - //the value added to the stat - value: { - type: Number, - decimal: true, - optional: true, - }, - calculation: { - type: String, - optional: true, - }, -}); diff --git a/app/imports/api/creature/subSchemas/Attribute.js b/app/imports/api/creature/subSchemas/Attribute.js deleted file mode 100644 index d93f9a67..00000000 --- a/app/imports/api/creature/subSchemas/Attribute.js +++ /dev/null @@ -1,13 +0,0 @@ -Schemas.Attribute = new SimpleSchema({ - //the temporary shift of the attribute - //should be zero after reset - adjustment: { - type: Number, - defaultValue: 0, - }, - reset: { - type: String, - defaultValue: "longRest", - allowedValues: ["longRest", "shortRest"], - }, -}); diff --git a/app/imports/api/creature/subSchemas/DeathSaves.js b/app/imports/api/creature/subSchemas/DeathSaves.js index 7d587aa7..4e5c1200 100644 --- a/app/imports/api/creature/subSchemas/DeathSaves.js +++ b/app/imports/api/creature/subSchemas/DeathSaves.js @@ -1,4 +1,4 @@ -Schemas.DeathSave = new SimpleSchema({ +const deathSaveSchema = new SimpleSchema({ pass: { type: Number, min: 0, @@ -20,3 +20,5 @@ Schemas.DeathSave = new SimpleSchema({ defaultValue: false, }, }); + +export default deathSaveSchema; diff --git a/app/imports/api/creature/subSchemas/Skill.js b/app/imports/api/creature/subSchemas/Skill.js deleted file mode 100644 index 430d83d7..00000000 --- a/app/imports/api/creature/subSchemas/Skill.js +++ /dev/null @@ -1,4 +0,0 @@ -Schemas.Skill = new SimpleSchema({ - //attribute name that this skill used as base mod for roll - ability: {type: String, defaultValue: ""}, -}); diff --git a/app/imports/api/parenting.js b/app/imports/api/parenting.js index 21edb341..6b5b3f0a 100644 --- a/app/imports/api/parenting.js +++ b/app/imports/api/parenting.js @@ -190,6 +190,8 @@ const softRemoveNode = new ValidatedMethod({ }); const restoreNode = new ValidatedMethod({ + name: "parenting.methods.restoreNode", + validate: null, run(collectionName, id){ checkRemovePermission(collectionName, id, this); let collection = Mongo.Collection.get(collectionName); @@ -205,6 +207,8 @@ const restoreNode = new ValidatedMethod({ }); const updateChildren = new ValidatedMethod({ + name: "parenting.methods.updateChildren", + validate: null, run({parent, modifier, limitToInheritance}){ check(parent, {_id: String, charId: String}); check(modifier, Object); @@ -224,6 +228,8 @@ const updateChildren = new ValidatedMethod({ }); const cloneChildren = new ValidatedMethod({ + name: "parenting.methods.cloneChildren", + validate: null, run({objectId, newParent}){ check(objectId, String); check(newParent, {id: String, collection: String}); diff --git a/app/package.json b/app/package.json index 3eeb3d50..a690dc4d 100644 --- a/app/package.json +++ b/app/package.json @@ -8,6 +8,10 @@ "url": "https://github.com/ThaumRystra/RPG-Docs" }, "author": "Stefan Zermatten", + "scripts": { + "run": "meteor --once", + "test": "meteor test --driver-package meteortesting:mocha" + }, "dependencies": { "@babel/runtime": "7.0.0-beta.55", "animejs": "^2.2.0",