From 94f6631a7d2748fce4a83e0a897db59b9092ff86 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 12 Mar 2019 16:47:20 +0200 Subject: [PATCH] Overhauled data models to make actions and libraries more universal --- README.md | 1 + app/imports/api/creature/Creatures.js | 159 +++++++---- .../api/creature/creatureCollections.js | 18 +- .../api/creature/creatureComputation.js | 20 +- .../api/creature/creaturePermission.js | 53 ---- .../api/creature/creaturePermissions.js | 28 ++ .../api/creature/properties/Actions.js | 59 +++- .../api/creature/properties/Attacks.js | 64 ----- .../api/creature/properties/Attributes.js | 198 ++++++------- app/imports/api/creature/properties/Buffs.js | 107 +++---- .../api/creature/properties/Bundles.js | 77 ------ .../api/creature/properties/ClassLevels.js | 19 ++ .../api/creature/properties/Classes.js | 31 +-- .../api/creature/properties/Conditions.js | 45 --- .../api/creature/properties/CustomBuffs.js | 55 ---- .../creature/properties/DamageMultipliers.js | 38 +-- .../api/creature/properties/Effects.js | 61 ++-- .../api/creature/properties/Experiences.js | 44 ++- .../api/creature/properties/Features.js | 81 +++--- .../api/creature/properties/Folders.js | 18 ++ app/imports/api/creature/properties/Notes.js | 21 +- .../api/creature/properties/Proficiencies.js | 32 +-- app/imports/api/creature/properties/Rolls.js | 97 +++++++ app/imports/api/creature/properties/Skills.js | 35 ++- .../api/creature/properties/SpellLists.js | 57 ++-- app/imports/api/creature/properties/Spells.js | 260 +++--------------- app/imports/api/creature/removeCreature.js | 80 ++---- .../creature/subSchemas/AdjustmentSchema.js | 34 ++- .../api/creature/subSchemas/ColorSchema.js | 25 +- .../api/creature/subSchemas/DamageSchema.js | 31 --- .../creature/subSchemas/DeathSavesSchema.js | 3 +- .../api/creature/subSchemas/OrderSchema.js | 6 - .../api/creature/subSchemas/PropertySchema.js | 11 +- app/imports/api/icons/Icons.js | 3 + app/imports/api/inventory/Containers.js | 69 ++--- app/imports/api/inventory/Items.js | 240 +++------------- app/imports/api/library/Libraries.js | 17 ++ app/imports/api/library/Library.js | 54 ---- app/imports/api/library/LibraryItems.js | 49 ---- app/imports/api/library/LibraryNodes.js | 29 ++ app/imports/api/library/LibrarySpells.js | 68 ----- app/imports/api/library/librarySchemas.js | 40 +++ .../api/library/subSchemas/LibraryAttacks.js | 46 ---- .../api/library/subSchemas/LibraryEffects.js | 44 --- app/imports/api/parenting/ChildSchema.js | 1 + .../api/parenting/SoftRemovableSchema.js | 4 +- .../parenting/getInheritPropertiesSchema.js | 16 -- app/imports/api/parenting/parenting.js | 2 +- app/imports/api/sharing/SharingSchema.js | 34 +++ app/imports/api/sharing/sharingPermissions.js | 53 ++++ app/imports/constants/DAMAGE_TYPES.js | 20 ++ app/imports/server/publications/library.js | 2 +- .../server/publications/singleCharacter.js | 41 +-- app/imports/ui/StoryBook.vue | 4 +- .../components/attributes/AttributeDialog.vue | 2 +- .../children/effects/EffectChildList.vue | 2 +- .../children/effects/EffectEdit.Story.vue | 2 +- .../effects/EffectEditExpansionList.Story.vue | 2 +- .../effects/EffectEditExpansionList.vue | 4 +- .../ui/components/skills/SkillDialog.vue | 2 +- app/package-lock.json | 62 ----- app/server/lib/cron/deleteRemovedDocuments.js | 30 +- 62 files changed, 1076 insertions(+), 1734 deletions(-) delete mode 100644 app/imports/api/creature/creaturePermission.js create mode 100644 app/imports/api/creature/creaturePermissions.js delete mode 100644 app/imports/api/creature/properties/Attacks.js delete mode 100644 app/imports/api/creature/properties/Bundles.js create mode 100644 app/imports/api/creature/properties/ClassLevels.js delete mode 100644 app/imports/api/creature/properties/Conditions.js delete mode 100644 app/imports/api/creature/properties/CustomBuffs.js create mode 100644 app/imports/api/creature/properties/Folders.js create mode 100644 app/imports/api/creature/properties/Rolls.js delete mode 100644 app/imports/api/creature/subSchemas/DamageSchema.js delete mode 100644 app/imports/api/creature/subSchemas/OrderSchema.js create mode 100644 app/imports/api/library/Libraries.js delete mode 100644 app/imports/api/library/Library.js delete mode 100644 app/imports/api/library/LibraryItems.js create mode 100644 app/imports/api/library/LibraryNodes.js delete mode 100644 app/imports/api/library/LibrarySpells.js create mode 100644 app/imports/api/library/librarySchemas.js delete mode 100644 app/imports/api/library/subSchemas/LibraryAttacks.js delete mode 100644 app/imports/api/library/subSchemas/LibraryEffects.js delete mode 100644 app/imports/api/parenting/getInheritPropertiesSchema.js create mode 100644 app/imports/api/sharing/SharingSchema.js create mode 100644 app/imports/api/sharing/sharingPermissions.js create mode 100644 app/imports/constants/DAMAGE_TYPES.js diff --git a/README.md b/README.md index 187318a5..c1dd7f4c 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Then, it's just a matter of cloning this repository into a folder, and running `git clone https://github.com/ThaumRystra/DiceCloud dicecloud` `cd dicecloud` `cd app` +`meteor npm install` `meteor` You should see this: diff --git a/app/imports/api/creature/Creatures.js b/app/imports/api/creature/Creatures.js index 7323ee86..aad6df87 100644 --- a/app/imports/api/creature/Creatures.js +++ b/app/imports/api/creature/Creatures.js @@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; import deathSaveSchema from "/imports/api/creature/subSchemas/DeathSavesSchema.js" import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; +import SharingSchema from '/imports/api/sharing/SharingSchema.js'; //Methods import '/imports/api/creature/insertCreature.js'; @@ -10,64 +11,120 @@ import '/imports/api/creature/removeCreature.js'; //set up the collection for creatures Creatures = new Mongo.Collection("creatures"); -let creatureSchema = schema({ - //strings - name: {type: String, defaultValue: "", trim: false, optional: true}, - urlName: {type: String, trim: false, optional: true, +let CreatureSettingsSchema = new SimpleSchema({ + //slowed down by carrying too much? + useVariantEncumbrance: { + type: Boolean, + optional: true, + }, + //hide spellcasting tab + hideSpellcasting: { + type: Boolean, + optional: true, + }, + // Swap around the modifier and stat + swapStatAndModifier: { + type: Boolean, + optional: true, + }, +}); + +let CreatureSchema = schema({ + // Strings + name: { + type: String, + defaultValue: "", + optional: true, + }, + urlName: { + type: String, + optional: true, autoValue: function() { return getSlug(this.field("name").value, {maintainCase: true}) || "-"; }, }, - alignment: {type: String, defaultValue: "", trim: false, optional: true}, - gender: {type: String, defaultValue: "", trim: false, optional: true}, - race: {type: String, defaultValue: "", trim: false, optional: true}, - picture: {type: String, defaultValue: "", trim: true, optional: true}, - description: {type: String, defaultValue: "", trim: false, optional: true}, - personality: {type: String, defaultValue: "", trim: false, optional: true}, - ideals: {type: String, defaultValue: "", trim: false, optional: true}, - bonds: {type: String, defaultValue: "", trim: false, optional: true}, - flaws: {type: String, defaultValue: "", trim: false, optional: true}, - backstory: {type: String, defaultValue: "", trim: false, optional: true}, - - //mechanics - deathSave: {type: deathSaveSchema, defaultValue: {}}, - xp: {type: SimpleSchema.Integer, defaultValue: 0}, - weightCarried: {type: Number, defaultValue: 0}, - level: {type: SimpleSchema.Integer, defaultValue: 0}, - type: {type: String, defaultValue: "pc", allowedValues: ["pc", "npc", "monster"]}, - - //computed - variables: {type: Object, blackbox: true, defaultValue: {}}, - - //permissions - owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - readers: {type: Array, defaultValue: [], index: 1}, - "readers.$": {type: String, regEx: SimpleSchema.RegEx.Id}, - writers: {type: Array, defaultValue: [], index: 1}, - "writers.$": {type: String, regEx: SimpleSchema.RegEx.Id}, - settings: {type: Object, defaultValue: {}}, - //how many experiences to load at a time in XP table - "settings.experiencesInc": {type: SimpleSchema.Integer, defaultValue: 20}, - //slowed down by carrying too much? - "settings.useVariantEncumbrance": {type: Boolean, defaultValue: false}, - "settings.useStandardEncumbrance": {type: Boolean, defaultValue: true}, - //hide spellcasting - "settings.hideSpellcasting": {type: Boolean, defaultValue: false}, - //show to anyone with link - "settings.viewPermission": { + alignment: { type: String, - defaultValue: "whitelist", - allowedValues: ["whitelist", "public"], - index: 1, + optional: true + }, + gender: { + type: String, + optional: true + }, + race: { + type: String, + optional: true + }, + picture: { + type: String, + optional: true + }, + description: { + type: String, + optional: true + }, + personality: { + type: String, + optional: true + }, + ideals: { + type: String, + optional: true + }, + bonds: { + type: String, + optional: true + }, + flaws: { + type: String, + optional: true + }, + backstory: { + type: String, + optional: true + }, + + // Mechanics + deathSave: { + type: deathSaveSchema, + defaultValue: {}, + }, + xp: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, + weightCarried: { + type: Number, + defaultValue: 0, + }, + level: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, + type: { + type: String, + defaultValue: "pc", + allowedValues: ["pc", "npc", "monster"], + }, + + // Computed + variables: { + type: Object, + blackbox: true, + defaultValue: {} + }, + + // Settings + settings: { + type: CreatureSettingsSchema, + defaultValue: {}, }, - "settings.swapStatAndModifier": {type: Boolean, defaultValue: false}, - "settings.exportFeatures": {type: Boolean, defaultValue: true}, - "settings.exportAttacks": {type: Boolean, defaultValue: true}, - "settings.exportDescription": {type: Boolean, defaultValue: true}, - "settings.newUserExperience": {type: Boolean, optional: true}, }); -Creatures.attachSchema(creatureSchema); -Creatures.attachSchema(ColorSchema); +CreatureSchema.extend(ColorSchema); +CreatureSchema.extend(SharingSchema); + +Creatures.attachSchema(CreatureSchema); export default Creatures; +export { CreatureSchema }; diff --git a/app/imports/api/creature/creatureCollections.js b/app/imports/api/creature/creatureCollections.js index a0c8c903..783433a6 100644 --- a/app/imports/api/creature/creatureCollections.js +++ b/app/imports/api/creature/creatureCollections.js @@ -1,17 +1,16 @@ import Actions from '/imports/api/creature/properties/Actions.js'; -import Attacks from '/imports/api/creature/properties/Attacks.js'; import Attributes from '/imports/api/creature/properties/Attributes.js'; import Buffs from '/imports/api/creature/properties/Buffs.js'; -import Bundles from '/imports/api/creature/properties/Bundles.js'; import Classes from '/imports/api/creature/properties/Classes.js'; -import Conditions from '/imports/api/creature/properties/Conditions.js'; -import CustomBuffs from '/imports/api/creature/properties/CustomBuffs.js'; +import ClassLevels from '/imports/api/creature/properties/ClassLevels.js'; import DamageMultipliers from '/imports/api/creature/properties/DamageMultipliers.js'; import Effects from '/imports/api/creature/properties/Effects.js'; import Experiences from '/imports/api/creature/properties/Experiences.js'; import Features from '/imports/api/creature/properties/Features.js'; +import Folders from '/imports/api/creature/properties/Folders.js'; import Notes from '/imports/api/creature/properties/Notes.js'; import Proficiencies from '/imports/api/creature/properties/Proficiencies.js'; +import Rolls from '/imports/api/creature/properties/Rolls.js'; import Skills from '/imports/api/creature/properties/Skills.js'; import SpellLists from '/imports/api/creature/properties/SpellLists.js'; import Spells from '/imports/api/creature/properties/Spells.js'; @@ -22,24 +21,25 @@ import Items from '/imports/api/inventory/Items.js'; // Collate them here in case we need to do something on all the collections of // a creature -export default [ +let creatureCollections = [ Actions, - Attacks, Attributes, Buffs, - Bundles, Classes, - Conditions, - CustomBuffs, + ClassLevels, DamageMultipliers, Effects, Experiences, Features, + Folders, Notes, Proficiencies, + Rolls, Skills, SpellLists, Spells, Containers, Items, ]; + +export default creatureCollections; diff --git a/app/imports/api/creature/creatureComputation.js b/app/imports/api/creature/creatureComputation.js index 4372923b..e7e7a796 100644 --- a/app/imports/api/creature/creatureComputation.js +++ b/app/imports/api/creature/creatureComputation.js @@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import SimpleSchema from 'simpl-schema'; -import { canEditCreature } from '/imports/api/creature/creaturePermission.js'; +import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import Creatures from "/imports/api/creature/Creatures.js"; import Attributes from "/imports/api/creature/properties/Attributes.js"; import Skills from "/imports/api/creature/properties/Skills.js"; @@ -22,15 +22,11 @@ export const recomputeCreature = new ValidatedMethod({ run({charId}) { // Permission - if (!canEditCreature(charId, this.userId)) { - throw new Meteor.Error('Creatures.methods.recomputeCreature.denied', - 'You do not have permission to recompute this creature'); - } + assertEditPermission(charId, this.userId) // Work, call this direcly if you are already in a method that has checked // for permission to edit a given character recomputeCreatureById(charId); - }, }); @@ -675,11 +671,7 @@ export const recomputeCreatureXP = new ValidatedMethod({ }).validator(), run({charId}) { - if (!canEditCreature(charId, this.userId)) { - // Throw errors with a specific error code - throw new Meteor.Error("Creatures.methods.recomputeCreatureXP.denied", - "You do not have permission to recompute this creature's XP"); - } + assertEditPermission(charId, this.userId) var xp = 0; Experiences.find( {charId: charId}, @@ -705,11 +697,7 @@ export const recomputeCreatureWeightCarried = new ValidatedMethod({ }).validator(), run({charId}){ - if (!canEditCreature(charId, this.userId)) { - // Throw errors with a specific error code - throw new Meteor.Error("Creatures.methods.recomputeCreatureWeightCarried.denied", - "You do not have permission to recompute this creature's carried weight"); - } + assertEditPermission(charId, this.userId) var weightCarried = 0; // store a dictionary of carried containers var carriedContainers = {}; diff --git a/app/imports/api/creature/creaturePermission.js b/app/imports/api/creature/creaturePermission.js deleted file mode 100644 index c090d973..00000000 --- a/app/imports/api/creature/creaturePermission.js +++ /dev/null @@ -1,53 +0,0 @@ -import Creatures from '/imports/api/creature/Creatures.js'; -import { _ } from 'meteor/underscore'; - -export function canEditCreature(charId, userId) { - if (!charId || typeof charId !== 'string'){ - throw new Meteor.Error("Edit permission denied", - "No creature ID given for edit permission check"); - } - if (!userId || typeof userId !== 'string'){ - throw new Meteor.Error("Edit permission denied", - "No user ID given for edit permission check"); - } - let creature = Creatures.findOne(charId, {fields: {owner: 1, writers: 1}}); - if (!creature){ - throw new Meteor.Error("Edit permission denied", - `No creature exists with the given id: ${charId}`); - } - if (creature.owner === userId || _.contains(creature.writers, userId)){ - return true; - } else { - throw new Meteor.Error("Edit permission denied", - `You do not have permission to edit this character`); - } -}; - -export function canViewCreature(charId, userId) { - if (!charId || typeof charId !== 'string'){ - throw new Meteor.Error("View permission denied", - "No creature ID given for view permission check"); - } - if (!userId || typeof userId !== 'string'){ - throw new Meteor.Error("View permission denied", - "No user ID given for view permission check"); - } - let creature = Creatures.findOne(charId, { - fields: {owner: 1, writers: 1, readers: 1, settings: 1} - }); - if (!creature){ - throw new Meteor.Error("View permission denied", - `No creature exists with the given id: ${charId}`); - } - if ( - creature.owner === userId || - settings.viewPermission === 'public' || - _.contains(creature.readers, userId) || - _.contains(creature.writers, userId) - ){ - return true; - } else { - throw new Meteor.Error("View permission denied", - `You do not have permission to view this character`); - } -}; diff --git a/app/imports/api/creature/creaturePermissions.js b/app/imports/api/creature/creaturePermissions.js new file mode 100644 index 00000000..b67c03d6 --- /dev/null +++ b/app/imports/api/creature/creaturePermissions.js @@ -0,0 +1,28 @@ +import { + assertEditPermission as editPermission, + assertViewPermission as viewPermission, + assertOwnership as ownership +} from '/imports/api/sharing/sharingPermissions.js'; + +function getCreature(creature, fields){ + if (typeof creature === 'string'){ + return Creatures.findOne(id, {fields}); + } else { + return creature; + } +} + +export function assertOwnership(creature, userId){ + creature = getCreature(creature, {owner: 1}); + ownership(creature, userId); +} + +export function assertEditPermission(creature, userId) { + creature = getCreature(creature, {owner: 1, writers: 1}); + editPermission(creature, userId); +}; + +export function assertViewPermission(creature, userId) { + creature = getCreature(creature, {owner: 1, writers: 1, public: 1}); + viewPermission(creature, userId); +}; diff --git a/app/imports/api/creature/properties/Actions.js b/app/imports/api/creature/properties/Actions.js index 3813ec5e..8e0dd4e6 100644 --- a/app/imports/api/creature/properties/Actions.js +++ b/app/imports/api/creature/properties/Actions.js @@ -1,31 +1,43 @@ +import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; import AdjustmentSchema from '/imports/api/creature/subSchemas/AdjustmentSchema.js'; -import DamageSchema from '/imports/api/creature/subSchemas/DamageSchema.js'; +import StoredBuffSchema from '/imports/api/creature/properties/Buffs.js'; import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import ColorSchema from '/imports/api/creature/subSchemas/ColorSchema.js'; let Actions = new Mongo.Collection('actions'); /* - * Actions are given to a character by items and features + * Actions are things a character can do */ -let actionSchema = schema({ +let ActionSchema = schema({ + // Overrides the inherited name name: { type: String, optional: true, - trim: false, }, description: { type: String, optional: true, - trim: false, }, + // What time-resource is used to take the action in combat type: { type: String, - allowedValues: ['action', 'bonus', 'reaction', 'free'], + allowedValues: ['attack', 'action', 'bonus', 'reaction', 'free'], defaultValue: 'action', }, - //the immediate impact of doing this action (eg. -1 rages) + // Who is the action directed at + target: { + type: String, + allowedValues: [ + 'self', + 'singleTarget', + 'multipleTargets', + ], + }, + // Adjustments applied when taking this action, regardless of roll outcomes + // If these adjustments can't be made, the action should be unusable adjustments: { type: Array, defaultValue: [], @@ -33,19 +45,38 @@ let actionSchema = schema({ 'adjustments.$': { type: AdjustmentSchema, }, - damages: { + // Buffs applied when taking this action, regardless of roll outcomes + buffs: { type: Array, defaultValue: [], }, - 'damages.$': { - type: DamageSchema, + 'buffs.$': { + type: StoredBuffSchema, + }, + // Calculation of how many times this action can be used + // Only set if this action tracks its own uses + uses: { + type: String, + optional: true, + }, + // Integer of how many times it has already been used + usesUsed: { + type: SimpleSchema.Integer, + optional: true, + }, + // How this action's uses are reset automatically + reset: { + type: String, + allowedValues: ["longRest", "shortRest"], + optional: true, }, }); -Actions.attachSchema(actionSchema); +ActionSchema.extend(ColorSchema); + +Actions.attachSchema(ActionSchema); Actions.attachSchema(PropertySchema); Actions.attachSchema(ChildSchema); -// makeChild(Actions, ["name", "enabled"]); - -export default Actions +export default Actions; +export { ActionSchema }; diff --git a/app/imports/api/creature/properties/Attacks.js b/app/imports/api/creature/properties/Attacks.js deleted file mode 100644 index dac22795..00000000 --- a/app/imports/api/creature/properties/Attacks.js +++ /dev/null @@ -1,64 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; -import {makeChild} from "/imports/api/parenting.js"; -import DamageSchema from '/imports/api/creature/subSchemas/DamageSchema.js'; - -let Attacks = new Mongo.Collection("attacks"); - -/* - * Attacks are given to a character by items and features - */ -attackSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, - name: { - type: String, - defaultValue: "New Attack", - optional: true, - trim: false, - }, - details: { - type: String, - optional: true, - trim: false, - }, - attackBonus: { - type: String, - defaultValue: "strengthMod + proficiencyBonus", - optional: true, - trim: false, - }, - damages: { - type: Array, - defaultValue: [], - }, - 'damages.$': { - type: DamageSchema, - }, - enabled: { - type: Boolean, - defaultValue: true, - }, -}); - -Attacks.attachSchema(attackSchema); - -//Attacks.attachBehaviour("softRemovable"); -makeChild(Attacks, ["name", "enabled"]); //children of lots of things - -Attacks.after.insert(function (userId, attack) { - //Check to see if this attack's parent is a spell, if so, mirror prepared state to enabled - if (attack.parent.collection === "Spells") { - var parentSpell = Spells.findOne(attack.parent.id); - if (parentSpell.prepared === "unprepared") { - Attacks.update(attack._id, {$set: {enabled: false}}); - } else if (parentSpell.prepared === "prepared" || "always") { - Attacks.update(attack._id, {$set: {enabled: true}}); - } - } -}); - -export default Attacks; diff --git a/app/imports/api/creature/properties/Attributes.js b/app/imports/api/creature/properties/Attributes.js index c181d3f5..e456ba65 100644 --- a/app/imports/api/creature/properties/Attributes.js +++ b/app/imports/api/creature/properties/Attributes.js @@ -1,24 +1,19 @@ -import {makeChild} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import ColorSchema from '/imports/api/creature/subSchemas/ColorSchema.js'; import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; -import OrderSchema from "/imports/api/creature/subSchemas/OrderSchema.js"; -import { canEditCreature } from '/imports/api/creature/creaturePermission.js'; +import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js' import { getHighestOrder } from '/imports/api/order.js'; import pickKeysAsOptional from '/imports/api/pickKeysAsOptional.js'; -let Attributes = new Mongo.Collection("attributes"); +let Attributes = new Mongo.Collection('attributes'); /* * Attributes are numbered stats of a character */ -let attributeSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, +let AttributeSchema = schema({ // The nice-to-read name name: { type: String, @@ -28,146 +23,130 @@ let attributeSchema = schema({ type: String, // Must contain a letter, and be made of word characters only regEx: /^\w*[a-z]\w*$/i, - index: 1, }, + // How it is displayed and computed is determined by type type: { type: String, allowedValues: [ - "ability", //Strength, Dex, Con, etc. - "stat", // Speed, Armor Class - "modifier", // Proficiency Bonus, Initiative - "hitDice", // d12 hit dice - "healthBar", // Hitpoints, Temporary Hitpoints - "resource", // Rages, sorcery points - "spellSlot", // Level 1, 2, 3... spell slots - "utility", // Aren't displayed, Jump height, Carry capacity + 'ability', //Strength, Dex, Con, etc. + 'stat', // Speed, Armor Class + 'modifier', // Proficiency Bonus, Initiative + 'hitDice', // d12 hit dice + 'healthBar', // Hitpoints, Temporary Hitpoints, can take damage + 'bar', // Displayed as a health bar, can't take damage + 'resource', // Rages, sorcery points + 'spellSlot', // Level 1, 2, 3... spell slots + 'utility', // Aren't displayed, Jump height, Carry capacity ], index: 1, }, + // The starting value, before effects baseValue: { type: Number, optional: true, }, - // The computed value of the attribute - value: { - type: Number, - defaultValue: 0, - }, - enabled: { - type: Boolean, - defaultValue: true, - }, - // The computed modifier, provided the attribute is an ability - mod: { - type: SimpleSchema.Integer, - optional: true, - }, + // The damage done to the attribute, always negative adjustment: { type: SimpleSchema.Integer, optional: true, + max: 0, }, // Can the value be decimal? decimal: { type: Boolean, optional: true, }, + // Automatically zero the adjustment on these conditions reset: { type: String, optional: true, - allowedValues: ["shortRest", "longRest"], + allowedValues: ['shortRest', 'longRest'], }, - // Some things are only reset by half on rest + // Some things are only reset their adjustment by up to half of the value resetMultiplier: { type: Number, optional: true, }, - // Attributes need to store their order to keep the sheet consistent - order: OrderSchema(), - color: ColorSchema(), }); -Attributes.attachSchema(attributeSchema); +AttributeSchema.extend(ColorSchema); -//Attributes.attachBehaviour("softRemovable"); -makeChild(Attributes, ["enabled"]); //children of lots of things +const ComputedAttributeSchema = schema({ + // The computed value of the attribute + value: { + type: Number, + defaultValue: 0, + }, + // The computed modifier, provided the attribute type is `ability` + mod: { + type: SimpleSchema.Integer, + optional: true, + }, +}).extend(AttributeSchema); -let updateAttributeSchema = pickKeysAsOptional(attributeSchema, [ - 'name', - 'variableName', - 'type', - 'baseValue', - 'decimal', - 'adjustment', - 'reset', - 'resetMultiplier', - 'color', -]); +Attributes.attachSchema(ComputedAttributeSchema); +Attributes.attachSchema(PropertySchema); +Attributes.attachSchema(ChildSchema); const insertAttribute = new ValidatedMethod({ - name: "Attributes.methods.insert", + name: 'Attributes.methods.insert', - validate: schema({ - attribute: { - type: attributeSchema.omit('order', 'parent'), - }, - }).validator({ clean: true }), + validate: AttributeSchema.validator({ clean: true }), run({attribute}) { const charId = attribute.charId; - if (canEditCreature(charId, this.userId)){ - attribute.order = getHighestOrder({ - collection: Attributes, - charId, - }) + 1; - attribute.parent = { - id: charId, - collection: 'Creatures', - }; - let attId = Attributes.insert(attribute); - recomputeCreatureById(charId); - return attId; - } + assertEditPermission(charId, this.userId); + attribute.order = getHighestOrder({ + collection: Attributes, + charId, + }) + 1; + attribute.parent = { + id: charId, + collection: 'Creatures', + }; + let attId = Attributes.insert(attribute); + recomputeCreatureById(charId); + return attId; }, }); const updateAttribute = new ValidatedMethod({ - name: "Attributes.methods.update", + name: 'Attributes.methods.update', validate: schema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, }, - update: updateAttributeSchema, + update: AttributeSchema, }).validator(), run({_id, update}) { let currentAttribute = Attributes.findOne(_id, {fields: {value: 1, charId: 1}}); if (!currentAttribute){ - throw new Meteor.Error("Attributes.methods.update.denied", + throw new Meteor.Error('Attributes.methods.update.denied', `No attributes exist with the id: ${_id}`); } let charId = currentAttribute.charId; - if (canEditCreature(charId, this.userId)){ - if (typeof update.adjustment === 'number'){ - let val = currentAttribute.value; - if (update.adjustment < -val) update.adjustment = -val; - if (update.adjustment > 0) update.adjustment = 0; - } - Attributes.update(_id, {$set: update}); - recomputeCreatureById(charId); + assertEditPermission(charId, this.userId) + if (typeof update.adjustment === 'number'){ + let val = currentAttribute.value; + if (update.adjustment < -val) update.adjustment = -val; + if (update.adjustment > 0) update.adjustment = 0; } + Attributes.update(_id, {$set: update}); + recomputeCreatureById(charId); }, }); const adjustAttribute = new ValidatedMethod({ - name: "Attributes.methods.adjust", + name: 'Attributes.methods.adjust', - validate: schema({ + validate: new SimpleSchema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -193,33 +172,32 @@ const adjustAttribute = new ValidatedMethod({ run({_id, increment, set}) { let currentAttribute = Attributes.findOne(_id); if (!currentAttribute){ - throw new Meteor.Error("Attributes.methods.update.denied", + throw new Meteor.Error('Attributes.methods.update.denied', `No attributes exist with the id: ${_id}`); } let charId = currentAttribute.charId; - if (canEditCreature(charId, this.userId)){ - if (typeof set === 'number'){ - let val = currentAttribute.value; - // Set represents what we want the value to be after adjustment - // So we need the actual adjustment to get to that value - let adjustment = set - val; - // Ajustment can't exceed total value - if (-adjustment > val) adjustment = -val; - // Adjustment must be negative - if (adjustment > 0) adjustment = 0; - Attributes.update(_id, {$set: {adjustment}}); - } else if (typeof increment === 'number'){ - let remaining = currentAttribute.value + (currentAttribute.adjustment || 0); - let adj = currentAttribute.adjustment; - // Can't decrease adjustment below remaining value - if (-increment > remaining) increment = -remaining; - // Can't increase adjustment above zero - if (increment > -adj) increment = -adj; - if (typeof currentAttribute.adjustment === 'number'){ - Attributes.update(_id, {$inc: {adjustment: increment}}); - } else { - Attributes.update(_id, {$set: {adjustment: increment}}); - } + assertEditPermission(charId, this.userId) + if (typeof set === 'number'){ + let val = currentAttribute.value; + // Set represents what we want the value to be after adjustment + // So we need the actual adjustment to get to that value + let adjustment = set - val; + // Ajustment can't exceed total value + if (-adjustment > val) adjustment = -val; + // Adjustment must be negative + if (adjustment > 0) adjustment = 0; + Attributes.update(_id, {$set: {adjustment}}); + } else if (typeof increment === 'number'){ + let remaining = currentAttribute.value + (currentAttribute.adjustment || 0); + let adj = currentAttribute.adjustment; + // Can't decrease adjustment below remaining value + if (-increment > remaining) increment = -remaining; + // Can't increase adjustment above zero + if (increment > -adj) increment = -adj; + if (typeof currentAttribute.adjustment === 'number'){ + Attributes.update(_id, {$inc: {adjustment: increment}}); + } else { + Attributes.update(_id, {$set: {adjustment: increment}}); } } }, @@ -227,4 +205,4 @@ const adjustAttribute = new ValidatedMethod({ }); export default Attributes; -export { insertAttribute, updateAttribute, adjustAttribute }; +export { AttributeSchema, insertAttribute, updateAttribute, adjustAttribute }; diff --git a/app/imports/api/creature/properties/Buffs.js b/app/imports/api/creature/properties/Buffs.js index bd6fdc6c..f4942305 100644 --- a/app/imports/api/creature/properties/Buffs.js +++ b/app/imports/api/creature/properties/Buffs.js @@ -1,16 +1,12 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeParent} from "/imports/api/parenting.js"; -import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import { EffectSchema } from '/imports/api/creature/properties/Effects.js'; -let Buffs = new Mongo.Collection("buffs"); +let Buffs = new Mongo.Collection('buffs'); -let buffSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, +let BuffSchema = new SimpleSchema({ name: { type: String, optional: true, @@ -21,50 +17,63 @@ let buffSchema = schema({ optional: true, trim: false, }, - enabled: { - type: Boolean, - defaultValue: true, - }, - type: { - type: String, - allowedValues: [ - "inate", //this should be "innate", but changing it could be problematic - "custom", - ], - }, - lifeTime: { - type: Object, - }, - "lifeTime.total": { - type: Number, - defaultValue: 0, //0 is infinite - min: 0, - }, - "lifeTime.spent": { - type: Number, - defaultValue: 0, - min: 0, - }, - appliedBy: { //the charId of whoever applied the buff - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - appliedByDetails: {//the name and collection of the thing that applied the buff - type: Object, + duration: { + type: SimpleSchema.Integer, optional: true, - }, - "appliedByDetails.name": { - type: String, - }, - "appliedByDetails.collection": { - type: String, + min: 0, }, }); -Buffs.attachSchema(buffSchema); -Buffs.attachSchema(ColorSchema); +// The effects in the stored buff need to be resolved to a number before being +// placed on other characters, if they are applied to self, they can remain as +// calculations, provided they don't contain any rolls +let StoredBuffSchema = new SimpleSchema({ + effects: { + type: Array, + defaultValue: [], + }, + 'effects.$': { + type: EffectSchema, + }, + target: { + type: String, + allowedValues: [ + // the character who took the action + 'self', + // the singular `target` of the action + 'target', + // rolled once for `each` target + 'each', + // rolled once and applied to `every` target + 'every' + ], + }, +}).extend(BuffSchema); -//Buffs.attachBehaviour("softRemovable"); -makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies +let AppliedBuffSchema = schema({ + durationSpent: { + type: Number, + optional: true, + min: 0, + }, + appliedBy: { + type: Object, + }, + 'appliedBy.name': { + type: String, + }, + 'appliedBy.id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + 'appliedBy.collection': { + type: String, + }, +}).extend(BuffSchema); + +Buffs.attachSchema(AppliedBuffSchema); +Buffs.attachSchema(PropertySchema); +Buffs.attachSchema(ChildSchema); export default Buffs; +export { AppliedBuffSchema, StoredBuffSchema }; diff --git a/app/imports/api/creature/properties/Bundles.js b/app/imports/api/creature/properties/Bundles.js deleted file mode 100644 index 3bfd8874..00000000 --- a/app/imports/api/creature/properties/Bundles.js +++ /dev/null @@ -1,77 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; - -let Bundles = new Mongo.Collection("bundle"); - -let attributeSchema = schema({ - name: String, - variableName: String, - baseValue: String, - type: String, -}); - -let skillSchema = schema({ - name: String, - variableName: String, - ability: String, - type: String, -}); - -let damageMultiplierSchema = schema({ - name: String, - variableName: String, -}); - -let effectSchema = schema({ - name: String, - stat: String, - operation: {type: String}, - calculation: {type: String, optional: true}, - value: {type: Number, optional: true} -}); - -let itemSchema = schema({ - name: String, - plural: {type: String, optional: true,}, - description: {type: String, optional: true,}, - quantity: {type: SimpleSchema.Integer, min: 0,}, - weight: {type: Number, min: 0,}, - value: {type: Number, min: 0,}, - requiresAttunement: {type: Boolean, optional: true}, - settings: {type: Object, optional: true}, - "settings.showIncrement": {type: Boolean, optional: true}, -}); - -let containerSchema = schema({ - name: String, - isCarried: Boolean, - weight: {type: Number, min: 0}, - value: {type: Number, min: 0}, - description:{type: String, optional: true}, - items: Array, - "items.$": itemSchema, -}); - -let featureSchema = schema({ - name: String, - description: {type: String, optional: true}, - uses: {type: String, optional: true}, - alwaysEnabled: {type: Boolean, defaultValue: true}, - effects: Array, - "effects.$": effectSchema, -}); - -let bundleSchema = schema({ - attributes: Array, - "attributes.$": attributeSchema, - skills: Array, - "skills.$": skillSchema, - damageMultipliers: Array, - "damageMultipliers.$": damageMultiplierSchema, - effects: Array, - "effects.$": effectSchema, - containers: Array, - "containers.$": containerSchema, -}); - -export default Bundles; diff --git a/app/imports/api/creature/properties/ClassLevels.js b/app/imports/api/creature/properties/ClassLevels.js new file mode 100644 index 00000000..885d08df --- /dev/null +++ b/app/imports/api/creature/properties/ClassLevels.js @@ -0,0 +1,19 @@ +import SimpleSchema from 'simpl-schema'; +import schema from '/imports/api/schema.js'; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; + +let ClassLevels = new Mongo.Collection("classLevels"); + +let ClassLevelSchema = schema({ + level: { + type: SimpleSchema.Integer, + }, +}); + +ClassLevels.attachSchema(ClassLevelSchema); +ClassLevels.attachSchema(PropertySchema); +ClassLevels.attachSchema(ChildSchema); + +export default ClassLevels; +export { ClassLevelSchema }; diff --git a/app/imports/api/creature/properties/Classes.js b/app/imports/api/creature/properties/Classes.js index acc2f828..4af97c2b 100644 --- a/app/imports/api/creature/properties/Classes.js +++ b/app/imports/api/creature/properties/Classes.js @@ -1,32 +1,21 @@ -import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeParent} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; let Classes = new Mongo.Collection("classes"); -classSchema= schema({ - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - name: {type: String, optional: true, trim: false}, - level: {type: SimpleSchema.Integer}, - createdAt: { - type: Date, - autoValue: function() { - if (this.isInsert) { - return new Date(); - } else if (this.isUpsert) { - return {$setOnInsert: new Date()}; - } else { - this.unset(); - } - }, +let ClassSchema = schema({ + name: { + type: String, }, }); -Classes.attachSchema(classSchema); -Classes.attachSchema(ColorSchema); +ClassSchema.extend(ColorSchema); -//Classes.attachBehaviour("softRemovable"); -makeParent(Classes, "name"); //parents of effects and attacks +Classes.attachSchema(ClassSchema); +Classes.attachSchema(PropertySchema); +Classes.attachSchema(ChildSchema); export default Classes; +export { ClassSchema }; diff --git a/app/imports/api/creature/properties/Conditions.js b/app/imports/api/creature/properties/Conditions.js deleted file mode 100644 index 5b7692bb..00000000 --- a/app/imports/api/creature/properties/Conditions.js +++ /dev/null @@ -1,45 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; -import {makeParent} from "/imports/api/parenting.js"; -import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; - -let Conditions = new Mongo.Collection("conditions"); - -conditionSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, - name: { - type: String, - optional: true, - trim: false, - }, - description: { - type: String, - optional: true, - trim: false, - }, - lifeTime: { - type: Object, - }, - "lifeTime.total": { - type: Number, - defaultValue: 0, //0 is infinite - min: 0, - }, - "lifeTime.spent": { - type: Number, - defaultValue: 0, - min: 0, - }, -}); - -Conditions.attachSchema(conditionSchema); -Conditions.attachSchema(ColorSchema); - -//Conditions.attachBehaviour("softRemovable"); -makeParent(Conditions, ["name"]); //parents of effects, attacks, proficiencies - -export default Conditions; diff --git a/app/imports/api/creature/properties/CustomBuffs.js b/app/imports/api/creature/properties/CustomBuffs.js deleted file mode 100644 index 34c0b5a2..00000000 --- a/app/imports/api/creature/properties/CustomBuffs.js +++ /dev/null @@ -1,55 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; -import {makeParent, makeChild} from "/imports/api/parenting.js"; - -let CustomBuffs = new Mongo.Collection("customBuffs"); - -customBuffSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, - name: { - type: String, - optional: true, - trim: false, - }, - description: { - type: String, - optional: true, - trim: false, - }, - target: { - type: String, - allowedValues: [ - "self", - "others", - "both" - ], - defaultValue: "self", - }, - enabled: { - type: Boolean, - autoValue: function(){ - return false; - //enabled is ALWAYS false on these, so that its children are also not enabled, so that the buff templates have no effects. - }, - }, - lifeTime: { - type: Object, - }, - "lifeTime.total": { - type: Number, - defaultValue: 0, //0 is infinite - min: 0, - }, -}); - -CustomBuffs.attachSchema(customBuffSchema); - -//CustomBuffs.attachBehaviour("softRemovable"); -makeParent(CustomBuffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies. Since this represents a template, "enabled" is always false. -makeChild(CustomBuffs); //children of lots of things - -export default CustomBuffs; diff --git a/app/imports/api/creature/properties/DamageMultipliers.js b/app/imports/api/creature/properties/DamageMultipliers.js index 6e99b0d0..9789726f 100644 --- a/app/imports/api/creature/properties/DamageMultipliers.js +++ b/app/imports/api/creature/properties/DamageMultipliers.js @@ -1,39 +1,31 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeChild} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; -const DamageMultipliers = new Mongo.Collection("damageMultipliers"); +let DamageMultipliers = new Mongo.Collection("damageMultipliers"); /* - * DamageMultipliers are whole numbered stats of a character + * DamageMultipliers are multipliers that affect */ -const damageMultiplierSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, - // The nice-to-read name - name: { - type: String, - }, +let DamageMultiplierSchema = schema({ // The technical, lowercase, single-word name used in formulae - variableName: { + damageType: { type: String, + allowedValues: DAMAGE_TYPES, }, - value: { + // The value of the damage multiplier + value: { type: Number, defaultValue: 1, + allowedValues: [0, 0.5, 1, 2], }, - enabled: { - type: Boolean, - defaultValue: true, - }, }); -DamageMultipliers.attachSchema(damageMultiplierSchema); - -// DamageMultipliers.attachBehaviour("softRemovable"); -makeChild(DamageMultipliers, ["enabled"]); //children of lots of things +DamageMultipliers.attachSchema(DamageMultiplierSchema); +DamageMultipliers.attachSchema(PropertySchema); +DamageMultipliers.attachSchema(ChildSchema); export default DamageMultipliers; +export { DamageMultiplierSchema }; diff --git a/app/imports/api/creature/properties/Effects.js b/app/imports/api/creature/properties/Effects.js index d264b80b..2a2a4d8d 100644 --- a/app/imports/api/creature/properties/Effects.js +++ b/app/imports/api/creature/properties/Effects.js @@ -1,64 +1,57 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeChild} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; -let Effects = new Mongo.Collection("effects"); +let Effects = new Mongo.Collection('effects'); /* * Effects are reason-value attached to skills and abilities * that modify their final value or presentation in some way */ -let effectSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, +let EffectSchema = schema({ name: { type: String, - optional: true, //TODO make necessary if there is no owner - trim: false, + optional: true, }, operation: { type: String, - defaultValue: "add", + defaultValue: 'add', allowedValues: [ - "base", - "add", - "mul", - "min", - "max", - "advantage", - "disadvantage", - "passiveAdd", - "fail", - "conditional", + 'base', + 'add', + 'mul', + 'min', + 'max', + 'advantage', + 'disadvantage', + 'passiveAdd', + 'fail', + 'conditional', ], }, calculation: { type: String, optional: true, - trim: false, - }, - // The computed result of the effect - result: { - type: SimpleSchema.oneOf(Number, String), - optional: true, }, //which stat the effect is applied to stat: { type: String, optional: true, }, - enabled: { - type: Boolean, - defaultValue: true, - }, }); -Effects.attachSchema(effectSchema); +const EffectComputedSchema = new SimpleSchema({ + // The computed result of the effect + result: { + type: SimpleSchema.oneOf(Number, String), + optional: true, + }, +}).extend(EffectSchema); -//Effects.attachBehaviour("softRemovable"); -makeChild(Effects, ["enabled"]); //children of lots of things +Effects.attachSchema(PropertySchema); +Effects.attachSchema(ChildSchema); +Effects.attachSchema(EffectComputedSchema); export default Effects; +export { EffectSchema }; diff --git a/app/imports/api/creature/properties/Experiences.js b/app/imports/api/creature/properties/Experiences.js index cc58ecfa..2e5d83be 100644 --- a/app/imports/api/creature/properties/Experiences.js +++ b/app/imports/api/creature/properties/Experiences.js @@ -1,29 +1,45 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; -Experiences = new Mongo.Collection("experience"); +let Experiences = new Mongo.Collection("experience"); -let experienceSchema = schema({ - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - name: {type: String, optional: true, trim: false, defaultValue: "New Experience"}, - description: {type: String, optional: true, trim: false}, - value: {type: SimpleSchema.Integer, defaultValue: 0}, - dateAdded: { +let ExperienceSchema = schema({ + name: { + type: String, + optional: true, + }, + // Potentially long description of the event + description: { + type: String, + optional: true, + }, + // The amount of XP this experience gives + value: { + type: SimpleSchema.Integer, + defaultValue: 0 + }, + // The real-world date that it occured + date: { type: Date, autoValue: function() { - if (this.isInsert) { + // If the date isn't set, set it to now on insert + if (this.isInsert && !this.isSet) { return new Date(); - } else if (this.isUpsert) { + } else if (this.isUpsert && !this.isSet) { return {$setOnInsert: new Date()}; - } else { - this.unset(); } }, }, + // The date in-world of this event + worldDate: { + type: String, + optional: true, + }, }); -Experiences.attachSchema(experienceSchema); - -//Experiences.attachBehaviour("softRemovable"); +Experiences.attachSchema(PropertySchema); +Experiences.attachSchema(ExperienceSchema); export default Experiences; +export { ExperienceSchema }; diff --git a/app/imports/api/creature/properties/Features.js b/app/imports/api/creature/properties/Features.js index 859bf97c..f168b700 100644 --- a/app/imports/api/creature/properties/Features.js +++ b/app/imports/api/creature/properties/Features.js @@ -1,68 +1,63 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; -import OrderSchema from "/imports/api/creature/subSchemas/OrderSchema.js"; -import { canEditCreature } from '/imports/api/creature/creaturePermission.js'; +import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { recomputeCreatureById } from '/imports/api/creature/creatureComputation.js' import { getHighestOrder } from '/imports/api/order.js'; -import {makeParent} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import ColorSchema from '/imports/api/creature/subSchemas/ColorSchema.js'; -let Features = new Mongo.Collection("features"); +let Features = new Mongo.Collection('features'); -let featureSchema = schema({ - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - name: {type: String, optional: true, trim: false}, - description: {type: String, optional: true, trim: false}, - uses: {type: String, optional: true, trim: false}, - used: {type: SimpleSchema.Integer, defaultValue: 0}, - reset: { +let FeatureSchema = schema({ + name: { type: String, - allowedValues: ["longRest", "shortRest"], optional: true, }, - enabled: {type: Boolean, defaultValue: true}, - alwaysEnabled:{type: Boolean, defaultValue: true}, - order: OrderSchema(), - color: ColorSchema(), + description: { + type: String, + optional: true, + }, + alwaysEnabled: { + type: Boolean, + defaultValue: true + }, }); -Features.attachSchema(featureSchema); +FeatureSchema.extend(ColorSchema); -//Features.attachBehaviour("softRemovable"); -makeParent(Features, ["name", "enabled"]); //parents of effects and attacks +Features.attachSchema(FeatureSchema); +Features.attachSchema(PropertySchema); +Features.attachSchema(ChildSchema); const insertFeature = new ValidatedMethod({ - name: "Features.methods.insert", + name: 'Features.methods.insert', - validate: schema({ - feature: { - type: featureSchema.omit('order', 'parent'), - }, - }).validator({clean: true}), + validate: FeatureSchema.validator({clean: true}), run({feature}) { const charId = feature.charId; - if (canEditCreature(charId, this.userId)){ - // Set order - feature.order = getHighestOrder({ - collection: Features, - charId, - }) + 1; + assertEditPermission(charId, this.userId); - // Set parent - feature.parent = { - id: charId, - collection: 'Creatures', - }; + // Set order + feature.order = getHighestOrder({ + collection: Features, + charId, + }) + 1; - // Insert - let featureId = Features.insert(feature); - recomputeCreatureById(charId); - return featureId; - } + // Set parent + feature.parent = { + id: charId, + collection: 'Creatures', + }; + + // Insert + let featureId = Features.insert(feature); + recomputeCreatureById(charId); + return featureId; }, }); export default Features; -export { insertFeature } +export { FeatureSchema, insertFeature } diff --git a/app/imports/api/creature/properties/Folders.js b/app/imports/api/creature/properties/Folders.js new file mode 100644 index 00000000..d7510460 --- /dev/null +++ b/app/imports/api/creature/properties/Folders.js @@ -0,0 +1,18 @@ +import schema from '/imports/api/schema.js'; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; + +let Folders = new Mongo.Collection('folders'); + +let FolderSchema = schema({ + name: { + type: String, + }, +}); + +Folders.attachSchema(FolderSchema); +Folders.attachSchema(PropertySchema); +Folders.attachSchema(ChildSchema); + +export default Folders; +export { FolderSchema }; diff --git a/app/imports/api/creature/properties/Notes.js b/app/imports/api/creature/properties/Notes.js index 278a78a1..1d8a0872 100644 --- a/app/imports/api/creature/properties/Notes.js +++ b/app/imports/api/creature/properties/Notes.js @@ -1,18 +1,25 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; let Notes = new Mongo.Collection("notes"); -noteSchema = schema({ - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - name: {type: String, optional: true, trim: false}, - description: {type: String, optional: true, trim: false}, +let NoteSchema = schema({ + name: { + type: String, + optional: true, + }, + description: { + type: String, + optional: true, + }, }); -Notes.attachSchema(noteSchema); -Notes.attachSchema(ColorSchema); +NoteSchema.extend(ColorSchema); -//Notes.attachBehaviour("softRemovable"); +Notes.attachSchema(NoteSchema); +Notes.attachSchema(PropertySchema); export default Notes; +export { NoteSchema }; diff --git a/app/imports/api/creature/properties/Proficiencies.js b/app/imports/api/creature/properties/Proficiencies.js index 4c3e8dff..66755029 100644 --- a/app/imports/api/creature/properties/Proficiencies.js +++ b/app/imports/api/creature/properties/Proficiencies.js @@ -1,43 +1,31 @@ -import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeChild} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; -Proficiencies = new Mongo.Collection("proficiencies"); +let Proficiencies = new Mongo.Collection("proficiencies"); -proficiencySchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, +let ProficiencySchema = schema({ name: { type: String, trim: false, optional: true, }, + // A number representing how proficient the character is value: { type: Number, allowedValues: [0, 0.5, 1, 2], defaultValue: 1, }, + // The variableName of the skill to apply this to skill: { type: String, optional: true, }, - type: { - type: String, - allowedValues: ["skill", "save", "weapon", "armor", "tool", "language"], - defaultValue: "skill", - }, - enabled: { - type: Boolean, - defaultValue: true, - }, }); -Proficiencies.attachSchema(proficiencySchema); - -// Proficiencies.attachBehaviour("softRemovable"); -makeChild(Proficiencies, ["enabled", "name"]); +Proficiencies.attachSchema(ProficiencySchema); +Proficiencies.attachSchema(PropertySchema); +Proficiencies.attachSchema(ChildSchema); export default Proficiencies; +export { ProficiencySchema }; diff --git a/app/imports/api/creature/properties/Rolls.js b/app/imports/api/creature/properties/Rolls.js new file mode 100644 index 00000000..a04af17e --- /dev/null +++ b/app/imports/api/creature/properties/Rolls.js @@ -0,0 +1,97 @@ +import SimpleSchema from 'simpl-schema'; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import AdjustmentSchema from '/imports/api/creature/subSchemas/AdjustmentSchema.js'; +import StoredBuffSchema from '/imports/api/creature/properties/Buffs.js'; + +let Rolls = new Mongo.Collection('rolls'); + +let RollChildrenSchema = new SimpleSchema({ + // The adjustments to be applied + adjustments: { + type: Array, + defaultValue: [], + }, + 'adjustments.$': { + type: AdjustmentSchema, + }, + // The buffs to be applied + buffs: { + type: Array, + defaultValue: [], + }, + 'buffs.$': { + type: StoredBuffSchema, + }, +}); + +/** + * Rolls are children to actions or other rolls, they are triggered with 0 or + * more targets and then apply their results to the character taking the action + * or the target of the action. + * + * # Rolls are resolved in one of two ways: + * Regular rolls: + * The target number is computed in the target's context + * The roll is computed in the action taker's context + * If the roll meets or exceeds the target number, the adjustments and buffs + * are applied + * + * Saving throws: + * The target number is computed in the action taker's context + * The roll is computed in the target's context + * If the roll fails to meet or exceed the target number, the adjustments and + * child rolls are applied + */ +let RollSchema = new SimpleSchema({ + // Apply this only if the parent roll missed + // i.e. roll failed or target suceeded on their save + onMiss: { + type: Boolean, + optional: true, + }, + // The target number to meet or exceed + targetNumber: { + type: String, + optional: true, + }, + // Swap who wins ties + invertTies: { + type: Boolean, + optional: true, + }, + // Is this roll a saving throw + isSavingThrow: { + type: Boolean, + optional: true, + }, + // Effects can apply to this tag specifically + // Ranged spell attack, Ranged weapon attack, etc. + tags: { + type: Array, + defaultValue: [], + }, + 'tags.$': { + type: String, + }, + // The roll made against the target value. A calculation that resolves to a + // number or a roll. If it is a number, it will be added to a d20 roll + roll: { + type: String, + optional: true, + }, + // The buffs and adjustments to apply based on the outcome of the roll + hit: { + type: RollChildrenSchema, + }, + miss: { + type: RollChildrenSchema, + }, +}); + +Rolls.attachSchema(RollSchema); +Rolls.attachSchema(PropertySchema); +Rolls.attachSchema(ChildSchema); + +export default Rolls; +export { RollSchema }; diff --git a/app/imports/api/creature/properties/Skills.js b/app/imports/api/creature/properties/Skills.js index b95dd80a..79ec7621 100644 --- a/app/imports/api/creature/properties/Skills.js +++ b/app/imports/api/creature/properties/Skills.js @@ -1,19 +1,16 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeChild} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import ColorSchema from '/imports/api/creature/subSchemas/ColorSchema.js'; let 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 + * Skills have an ability score modifier that they use as their basis */ -let skillSchema = schema({ - charId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - index: 1, - }, +let SkillSchema = schema({ // The nice-to-read name name: { type: String, @@ -21,6 +18,7 @@ let skillSchema = schema({ // The technical, lowercase, single-word name used in formulae variableName: { type: String, + regEx: /^\w*[a-z]\w*$/i, }, // The variable name of the ability this skill relies on ability: { @@ -40,10 +38,6 @@ let skillSchema = schema({ "utility", //not displayed anywhere ], }, - // Skills need to store their order to keep the sheet consistent - order: { - type: SimpleSchema.Integer, - }, // If the baseValue is higher than the computed value, it will be used as `value` baseValue: { type: Number, @@ -54,6 +48,11 @@ let skillSchema = schema({ type: Number, optional: true, }, +}); + +SkillSchema.extend(ColorSchema); + +let ComputedSkillSchema = schema({ // Computed value of skill to be added to skill rolls value: { type: Number, @@ -86,16 +85,16 @@ let skillSchema = schema({ type: SimpleSchema.Integer, optional: true, }, - // Computed boolean of whether this skill is forced to fail + // Computed number of things forcing this skill to fail fail: { type: SimpleSchema.Integer, optional: true, }, -}); +}).extend(SkillSchema); -Skills.attachSchema(skillSchema); - -//Skills.attachBehaviour("softRemovable"); -makeChild(Skills); //children of lots of things +Skills.attachSchema(ComputedSkillSchema); +Skills.attachSchema(PropertySchema); +Skills.attachSchema(ChildSchema); export default Skills; +export { SkillSchema }; diff --git a/app/imports/api/creature/properties/SpellLists.js b/app/imports/api/creature/properties/SpellLists.js index be3c7465..e632e537 100644 --- a/app/imports/api/creature/properties/SpellLists.js +++ b/app/imports/api/creature/properties/SpellLists.js @@ -1,37 +1,40 @@ -import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import Attributes from "/imports/api/creature/properties/Attributes.js"; import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; -import {makeParent} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; let SpellLists = new Mongo.Collection("spellLists"); -let spellListSchema = schema({ - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - name: {type: String, optional: true, trim: false}, - description: {type: String, optional: true, trim: false}, - saveDC: {type: String, optional: true, trim: false}, - attackBonus: {type: String, optional: true, trim: false}, - maxPrepared: {type: String, optional: true, trim: false}, +let SpellListSchema = schema({ + name: { + type: String, + optional: true, + }, + description: { + type: String, + optional: true, + }, + // Calculation of save DC used for all spells in this list + saveDC: { + type: String, + optional: true, + }, + // Calculation of attack bonus used for all spells in this list + attackBonus: { + type: String, + optional: true, + }, + // Calculation of how many spells in this list can be prepared + maxPrepared: { + type: String, + optional: true, + }, }); -SpellLists.attachSchema(spellListSchema); -Attributes.attachSchema(ColorSchema); +SpellListSchema.extend(ColorSchema); -SpellLists.helpers({ - numPrepared: function(){ - var num = 0; - Spells.find( - {charId: this.charId, listId: this._id, prepared: 1}, - {fields: {prepareCost: 1}} - ).forEach(function(spell){ - num += spell.prepareCost; - }); - return num; - } -}); - -//SpellLists.attachBehaviour("softRemovable"); -makeParent(SpellLists); //parents of spells +SpellLists.attachSchema(SpellListSchema); +SpellLists.attachSchema(PropertySchema); +SpellLists.attachSchema(ChildSchema); export default SpellLists; diff --git a/app/imports/api/creature/properties/Spells.js b/app/imports/api/creature/properties/Spells.js index 36f1ddb6..3f14c695 100644 --- a/app/imports/api/creature/properties/Spells.js +++ b/app/imports/api/creature/properties/Spells.js @@ -1,261 +1,87 @@ -import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; +import ColorSchema from '/imports/api/creature/subSchemas/ColorSchema.js'; import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeParent, makeChild} from "/imports/api/parenting.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; const magicSchools = [ - "Abjuration", - "Conjuration", - "Divination", - "Enchantment", - "Evocation", - "Illusion", - "Necromancy", - "Transmutation", + 'Abjuration', + 'Conjuration', + 'Divination', + 'Enchantment', + 'Evocation', + 'Illusion', + 'Necromancy', + 'Transmutation', ]; -let Spells = new Mongo.Collection("spells"); +let Spells = new Mongo.Collection('spells'); -let spellSchema = schema({ - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, +let SpellSchema = schema({ prepared: { type: String, - defaultValue: "prepared", - allowedValues: ["prepared", "unprepared", "always"], + defaultValue: 'prepared', + allowedValues: ['prepared', 'unprepared', 'always'], }, name: { type: String, optional: true, - trim: false, - defaultValue: "New Spell", + defaultValue: 'New Spell', }, description: { type: String, optional: true, - trim: false, }, castingTime: { type: String, optional: true, - defaultValue: "action", - trim: false, + defaultValue: 'action', }, range: { type: String, optional: true, - trim: false, }, duration: { type: String, optional: true, - trim: false, - defaultValue: "Instantaneous", + defaultValue: 'Instantaneous', }, - components: Object, - "components.verbal": {type: Boolean, defaultValue: false}, - "components.somatic": {type: Boolean, defaultValue: false}, - "components.concentration": {type: Boolean, defaultValue: false}, - "components.material": {type: String, optional: true}, - ritual: { + verbal: { + type: Boolean, + defaultValue: false + }, + somatic: { + type: Boolean, + defaultValue: false + }, + concentration: { + type: Boolean, + defaultValue: false + }, + material: { + type: String, + optional: true + }, + ritual: { type: Boolean, defaultValue: false, }, - level: { + level: { type: SimpleSchema.Integer, defaultValue: 1, }, - school: { + school: { type: String, - defaultValue: "Abjuration", + defaultValue: 'Abjuration', allowedValues: magicSchools, }, }); -Spells.attachSchema(spellSchema); -Spells.attachSchema(ColorSchema); +SpellSchema.extend(ColorSchema); -//Spells.attachBehaviour("softRemovable"); -makeChild(Spells); //children of spell lists -makeParent(Spells, ["name", "enabled"]); //parents of attacks - -Spells.after.update(function (userId, spell, fieldNames) { - //Update prepared state of spell and child attacks to be enabled or not - if (_.contains(fieldNames, "prepared")) { - var childAttacks = Attacks.find({"parent.id": spell._id}).fetch(); - if (spell.prepared === "unprepared") { - _.each(childAttacks, function(attack){ - Attacks.update(attack._id, {$set: {enabled: false}}); - }); - } else if (spell.prepared === "prepared" || "always") { - _.each(childAttacks, function(attack){ - Attacks.update(attack._id, {$set: {enabled: true}}); - }); - } - } -}); - -var checkMovePermission = function(spellId, parent, destinationOnly) { - var spell = Spells.findOne(spellId); - if (!spell) - throw new Meteor.Error("No such spell", - "An spell could not be found to move"); - //handle permissions - var permission; - - if (!destinationOnly) { //if we're not modifying the origin, only the destination - permission = Meteor.call("canWriteCharacter", spell.charId); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move spells from this character"); - } - } - if (parent.collection === "Characters"){ - permission = Meteor.call("canWriteCharacter", parent.id); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move spells to this character"); - } - } else { - var parentCollectionObject = global[parent.collection]; - var parentObject = null; - if (parentCollectionObject) - parentObject = parentCollectionObject.findOne( - parent.id, {fields: {_id: 1, charId: 1}} - ); - if (!parentObject) throw new Meteor.Error( - "Invalid parent", - "The destination parent " + parent.id + - " does not exist in the collection " + parent.collection - ); - if (parentObject.charId){ - permission = Meteor.call("canWriteCharacter", parentObject.charId); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move spells to this character"); - } - } - } -}; - -var moveSpell = function(spellId, targetCollection, targetId) { - var spell = Spells.findOne(spellId); - if (!spell) return; - targetCollection = targetCollection || spell.parent.collection; - targetId = targetId || spell.parent.id; - - if (Meteor.isServer) { - checkMovePermission(spellId, {collection: targetCollection, id: targetId}, false); - } - - if (targetCollection == "Characters") { //then we are copying the spell to a different character. - targetList = SpellLists.findOne({"charId": targetId}); - targetListId = targetList && targetList._id; - if (!targetListId) { - targetListId = SpellLists.insert({ //create a spell list if we don't already have one - name: "New SpellList", - charId: targetId, - saveDC: "8 + intelligenceMod + proficiencyBonus", - attackBonus: "intelligenceMod + proficiencyBonus", - }); - } - - Spells.update( - spellId, - {$set: { - charId: targetId, - "parent.collection": "SpellLists", - "parent.id": targetListId, - }} - ); - } - else { //we are moving the spell within the same character - //update the spell provided the update will actually change something - if ( - spell.parent.collection !== targetCollection || - spell.parent.id !== targetId - ){ - Spells.update( - spellId, - {$set: { - "parent.collection": targetCollection, - "parent.id": targetId, - }} - ); - } - } -}; - -var copySpell = function(spellId, targetCollection, targetId) { - var spell = Spells.findOne(spellId); - if (!spell) return; - targetCollection = targetCollection || spell.parent.collection; - targetId = targetId || spell.parent.id; - - if (Meteor.isServer) { - checkMovePermission(spellId, {collection: targetCollection, id: targetId}, true); //we're only reading from the source character - } - - - if (targetCollection == "Characters") { //then we are copying the spell to a different character. - targetList = SpellLists.findOne({"charId": targetId}); - targetListId = targetList && targetList._id; - if (!targetListId) { - targetListId = SpellLists.insert({ //create a spell list if we don't already have one - name: "New SpellList", - charId: targetId, - saveDC: "8 + intelligenceMod + proficiencyBonus", - attackBonus: "intelligenceMod + proficiencyBonus", - }); - } - - newSpell = _.clone(spell); - delete newSpell._id; - newSpellId = Spells.insert(newSpell); //add a new copy of the spell - Spells.update( - newSpellId, - {$set: { - charId: targetId, - "parent.collection": "SpellLists", - "parent.id": targetListId, - }} - ); - } - else { //else we are copying the spell within the same character - newSpell = _.clone(spell); - delete newSpell._id; - newSpellId = Spells.insert(newSpell); //add a new copy of the spell - Spells.update( - newSpellId, - {$set: { - "parent.collection": targetCollection, - "parent.id": targetId, - }} - ); - } -}; - - -Meteor.methods({ - moveSpellToList: function(spellId, spellListId) { - check(spellId, String); - check(spellListId, String); - moveSpell(spellId, "SpellLists", spellListId); - }, - copySpellToList: function(spellId, spellListId) { - check(spellId, String); - check(spellListId, String); - copySpell(spellId, "SpellLists", spellListId); - }, - moveSpellToCharacter: function(spellId, charId) { - check(spellId, String); - check(charId, String); - moveSpell(spellId, "Characters", charId); - }, - copySpellToCharacter: function(spellId, charId) { - check(spellId, String); - check(charId, String); - copySpell(spellId, "Characters", charId); - }, -}); +Spells.attachSchema(SpellSchema); +Spells.attachSchema(PropertySchema); +Spells.attachSchema(ChildSchema); export default Spells; +export { SpellSchema }; diff --git a/app/imports/api/creature/removeCreature.js b/app/imports/api/creature/removeCreature.js index 29856cde..2964bfbf 100644 --- a/app/imports/api/creature/removeCreature.js +++ b/app/imports/api/creature/removeCreature.js @@ -1,72 +1,26 @@ +import SimpleSchema from 'simpl-schema'; import Creatures from '/imports/api/creature/Creatures.js'; -import Actions from '/imports/api/creature/properties/Actions.js'; -import Attacks from '/imports/api/creature/properties/Attacks.js'; -import Attributes from '/imports/api/creature/properties/Attributes.js'; -import Buffs from '/imports/api/creature/properties/Buffs.js'; -import Bundles from '/imports/api/creature/properties/Bundles.js'; -import Classes from '/imports/api/creature/properties/Classes.js'; -import Conditions from '/imports/api/creature/properties/Conditions.js'; -import CustomBuffs from '/imports/api/creature/properties/CustomBuffs.js'; -import DamageMultipliers from '/imports/api/creature/properties/DamageMultipliers.js'; -import Effects from '/imports/api/creature/properties/Effects.js'; -import Experiences from '/imports/api/creature/properties/Experiences.js'; -import Features from '/imports/api/creature/properties/Features.js'; -import Notes from '/imports/api/creature/properties/Notes.js'; -import Proficiencies from '/imports/api/creature/properties/Proficiencies.js'; -import Skills from '/imports/api/creature/properties/Skills.js'; -import SpellLists from '/imports/api/creature/properties/SpellLists.js'; -import Spells from '/imports/api/creature/properties/Spells.js'; -import Items from '/imports/api/inventory/Items.js'; -import Containers from '/imports/api/inventory/Containers.js'; +import creatureCollections from '/imports/api/creature/creatureCollections.js'; +import { assertOwnership } from '/imports/api/creature/creaturePermissions.js'; -const checkRemovePermissions = function(userId, charId, creature){ - // Must be logged in - if (!userId) { - throw new Meteor.Error("Creatures.methods.removeCreature.denied", - "You need to be logged in to remove a creature"); - } - - // Creature must exist - if (!creature) { - throw new Meteor.Error("Creatures.methods.removeCreature.denied", - `No creature exists with the given id: ${charId}`); - } - - // Must be creatures owner - if (creature.owner !== userId){ - throw new Meteor.Error("Creatures.methods.removeCreature.denied", - "Only the owner is allowed to remove a creature, you are not the owner"); - } -} - -const removeRelatedDocuments = function(charId){ - Actions .remove({charId}); - Attacks .remove({charId}); - Attributes .remove({charId}); - Buffs .remove({charId}); - Bundles .remove({charId}); - Classes .remove({charId}); - Conditions .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}); - Spells .remove({charId}); - Items .remove({charId}); - Containers .remove({charId}); +function removeRelatedDocuments(charId){ + creatureCollections.forEach(collection => { + collection.remove({charId}, error => { + if (error) console.error(error); + }); + }); }; const removeCreature = new ValidatedMethod({ name: "Creatures.methods.removeCreature", // DDP method name - validate: null, - run(charId) { - let creature = Creatures.findOne(charId); - checkRemovePermissions(this.userId, charId, creature); + validate: new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + run({charId}) { + assertOwnership(charId, this.userId) Creatures.remove(charId); this.unblock(); removeRelatedDocuments(charId); diff --git a/app/imports/api/creature/subSchemas/AdjustmentSchema.js b/app/imports/api/creature/subSchemas/AdjustmentSchema.js index cff0e434..459cd6a5 100644 --- a/app/imports/api/creature/subSchemas/AdjustmentSchema.js +++ b/app/imports/api/creature/subSchemas/AdjustmentSchema.js @@ -1,15 +1,37 @@ import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; +import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; -const AdjustmentSchema = schema({ - stat: { - type: String, - optional: true, - }, +const AdjustmentSchema = new SimpleSchema({ + // The calculation that determines the adjustment roll: { type: String, optional: true, }, + // Who this adjustment applies to + target: { + type: String, + allowedValues: [ + // the character who took the action + 'self', + // the singular `target` of the action + 'target', + // rolled once for `each` target + 'each', + // rolled once and applied to `every` target + 'every' + ], + }, + // The stat this rolls applies to, if damage type is set, this is ignored + stat: { + type: String, + optional: true, + }, + // If set, the type of damage this adjustment causes + damageType: { + type: String, + allowedValues: DAMAGE_TYPES, + optional: true, + }, }); export default AdjustmentSchema; diff --git a/app/imports/api/creature/subSchemas/ColorSchema.js b/app/imports/api/creature/subSchemas/ColorSchema.js index 4c768017..e0b74c61 100644 --- a/app/imports/api/creature/subSchemas/ColorSchema.js +++ b/app/imports/api/creature/subSchemas/ColorSchema.js @@ -1,9 +1,22 @@ -const ColorSchema = ({optional = false} = {}) => ({ - type: String, - defaultValue: "#9E9E9E", - // match hex colors of the form #A23 or #A23f56 - regEx: /^#([a-f0-9]{3}){1,2}\b$/i, - optional +import SimpleSchema from 'simpl-schema'; + +const getColorSchema = function({optional = true} = {}){ + let schema = { + type: String, + // match hex colors of the form #A23 or #A23f56 + regEx: /^#([a-f0-9]{3}){1,2}\b$/i, + }; + if (optional) { + schema.optional = true; + } else { + schema.defaultValue = "#9E9E9E"; + } + return schema +}; + +const ColorSchema = new SimpleSchema({ + color: getColorSchema(), }); export default ColorSchema; +export { getColorSchema }; diff --git a/app/imports/api/creature/subSchemas/DamageSchema.js b/app/imports/api/creature/subSchemas/DamageSchema.js deleted file mode 100644 index 71d81b63..00000000 --- a/app/imports/api/creature/subSchemas/DamageSchema.js +++ /dev/null @@ -1,31 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; - -const DamageSchema = schema({ - type: { - type: String, - allowedValues: [ - "bludgeoning", - "piercing", - "slashing", - "acid", - "cold", - "fire", - "force", - "lightning", - "necrotic", - "poison", - "psychic", - "radiant", - "thunder", - ], - defaultValue: "slashing", - }, - roll: { - type: String, - optional: true, - defaultValue: '1d8 + strengthMod', - }, -}); - -export default DamageSchema; diff --git a/app/imports/api/creature/subSchemas/DeathSavesSchema.js b/app/imports/api/creature/subSchemas/DeathSavesSchema.js index 92820848..ea7f2933 100644 --- a/app/imports/api/creature/subSchemas/DeathSavesSchema.js +++ b/app/imports/api/creature/subSchemas/DeathSavesSchema.js @@ -1,7 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; -const DeathSavesSchema = schema({ +const DeathSavesSchema = new SimpleSchema({ pass: { type: SimpleSchema.Integer, min: 0, diff --git a/app/imports/api/creature/subSchemas/OrderSchema.js b/app/imports/api/creature/subSchemas/OrderSchema.js deleted file mode 100644 index 3b0ae145..00000000 --- a/app/imports/api/creature/subSchemas/OrderSchema.js +++ /dev/null @@ -1,6 +0,0 @@ -const OrderSchema = () => ({ - type: Number, - index: true, -}); - -export default OrderSchema; diff --git a/app/imports/api/creature/subSchemas/PropertySchema.js b/app/imports/api/creature/subSchemas/PropertySchema.js index c4727e57..b1830511 100644 --- a/app/imports/api/creature/subSchemas/PropertySchema.js +++ b/app/imports/api/creature/subSchemas/PropertySchema.js @@ -1,16 +1,23 @@ import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; +import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js'; -const PropertySchema = schema({ +const PropertySchema = new SimpleSchema({ charId: { type: String, regEx: SimpleSchema.RegEx.Id, index: 1, + optional: true, }, enabled: { type: Boolean, defaultValue: true, }, + order: { + type: SimpleSchema.Integer, + index: true, + }, }); +PropertySchema.extend(SoftRemovableSchema); + export default PropertySchema; diff --git a/app/imports/api/icons/Icons.js b/app/imports/api/icons/Icons.js index 3719629b..9229c27f 100644 --- a/app/imports/api/icons/Icons.js +++ b/app/imports/api/icons/Icons.js @@ -36,6 +36,8 @@ if (Meteor.isServer) { Icons.attachSchema(iconsSchema); +/* +console.warn("Write Icons is not secure, disable before deployment") const writeIcons = new ValidatedMethod({ name: 'writeIcons', validate: null, @@ -46,6 +48,7 @@ const writeIcons = new ValidatedMethod({ } } }); +*/ export { writeIcons }; export default Icons; diff --git a/app/imports/api/inventory/Containers.js b/app/imports/api/inventory/Containers.js index 86eb30f3..98ac4bf5 100644 --- a/app/imports/api/inventory/Containers.js +++ b/app/imports/api/inventory/Containers.js @@ -1,57 +1,44 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeParent} from "/imports/api/parenting.js"; import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; //set up the collection for containers let Containers = new Mongo.Collection("containers"); -let containerSchema = schema({ - name: {type: String, optional: true, trim: false}, - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - isCarried: {type: Boolean}, - weight: {type: Number, min: 0, defaultValue: 0}, - value: {type: Number, min: 0, defaultValue: 0}, - description:{type: String, optional: true, trim: false}, -}); - -Containers.attachSchema(containerSchema); -Containers.attachSchema(ColorSchema); - -Containers.helpers({ - contentsValue: function(){ - var value = 0; - Items.find( - {"parent.id": this._id}, - {fields: {quantity: 1, value: 1}} - ).forEach(function(item){ - value += item.totalValue(); - }); - return value; +let ContainerSchema = schema({ + name: { + type: String, + optional: true, + trim: false }, - totalValue: function(){ - return this.contentsValue() + this.value; + isCarried: { + type: Boolean, + defaultValue: true, }, - contentsWeight: function(){ - var weight = 0; - Items.find( - {"parent.id": this._id}, - {fields: {quantity: 1, weight: 1}} - ).forEach(function(item){ - weight += item.totalWeight(); - }); - return weight; + weight: { + type: Number, + min: 0, + defaultValue: 0 }, - totalWeight: function(){ - return this.contentsWeight() + this.weight; + value: { + type: Number, + min: 0, + defaultValue: 0 }, - moveToCharacter: function(characterId){ - if (this.charId === characterId) return; - Items.update(this._id, {$set: {charId: characterId}}); + description: { + type: String, + optional: true, + trim: false }, }); -// Containers.attachBehaviour("softRemovable"); -makeParent(Containers); //parents of items +ContainerSchema.extend(ColorSchema); + +Containers.attachSchema(ContainerSchema); +Containers.attachSchema(PropertySchema); +Containers.attachSchema(ChildSchema); export default Containers; +export { ContainerSchema }; diff --git a/app/imports/api/inventory/Items.js b/app/imports/api/inventory/Items.js index f041f440..c704b942 100644 --- a/app/imports/api/inventory/Items.js +++ b/app/imports/api/inventory/Items.js @@ -1,212 +1,62 @@ import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; -import {makeParent, makeChild} from "/imports/api/parenting.js"; import ColorSchema from "/imports/api/creature/subSchemas/ColorSchema.js"; +import PropertySchema from '/imports/api/creature/subSchemas/PropertySchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; Items = new Mongo.Collection("items"); -itemSchema = schema({ - name: {type: String, optional: true, trim: false, defaultValue: "New Item"}, - plural: {type: String, optional: true, trim: false}, - description: {type: String, optional: true, trim: false}, - charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, //id of owner - quantity: {type: SimpleSchema.Integer, min: 0, defaultValue: 1}, - weight: {type: Number, min: 0, defaultValue: 0}, - value: {type: Number, min: 0, defaultValue: 0}, - uses: {type: SimpleSchema.Integer, min: 0, optional: true}, - usesUsed: {type: SimpleSchema.Integer, min: 0, optional: true}, - enabled: {type: Boolean, defaultValue: false}, - requiresAttunement: {type: Boolean, defaultValue: false}, - settings: {type: Object}, - "settings.showIncrement": {type: Boolean, defaultValue: false}, -}); - -Items.attachSchema(itemSchema); -Items.attachSchema(ColorSchema); - -var checkMovePermission = function(itemId, parent) { - var item = Items.findOne(itemId); - if (!item) - throw new Meteor.Error("No such item", - "An item could not be found to move"); - //handle permissions - var permission = Meteor.call("canWriteCharacter", item.charId); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move items from this character"); - } - if (parent.collection === "Characters"){ - permission = Meteor.call("canWriteCharacter", parent.id); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move items to this character"); - } - } else { - var parentCollectionObject = global[parent.collection]; - var parentObject = null; - if (parentCollectionObject) - parentObject = parentCollectionObject.findOne( - parent.id, {fields: {_id: 1, charId: 1}} - ); - if (!parentObject) throw new Meteor.Error( - "Invalid parent", - "The destination parent " + parent.id + - " does not exist in the collection " + parent.collection - ); - if (parentObject.charId){ - permission = Meteor.call("canWriteCharacter", parentObject.charId); - if (!permission){ - throw new Meteor.Error("Access denied", - "Not permitted to move items to this character"); - } - } - } -}; - -var moveItem = function(itemId, enable, parentCollection, parentId) { - var item = Items.findOne(itemId); - if (!item) return; - parentCollection = parentCollection || item.parent.collection; - parentId = parentId || item.parent.id; - - if (Meteor.isServer) { - checkMovePermission(itemId, {collection: parentCollection, id: parentId}); - } - - //update the item provided the update will actually change something - if ( - item.parent.collection !== parentCollection || - item.parent.id !== parentId || - item.enabled !== enable - ){ - Items.update( - itemId, - {$set: { - "parent.collection": parentCollection, - "parent.id": parentId, - enabled: enable, - }} - ); - } -}; - -Meteor.methods({ - moveItemToParent: function(itemId, parent) { - check(itemId, String); - check(parent, {collection: String, id: String}); - moveItem(itemId, false, parent.collection, parent.id); +ItemSchema = schema({ + name: { + type: String, + optional: true, + defaultValue: "New Item", }, - moveItemToCharacter: function(itemId, charId) { - check(itemId, String); - check(charId, String); - moveItem(itemId, false, "Characters", charId); + // Plural name of the item, if there is more than one + plural: { + type: String, + optional: true, }, - moveItemToContainer: function(itemId, containerId) { - check(itemId, String); - check(containerId, String); - moveItem(itemId, false, "Containers", containerId); + description: { + type: String, + optional: true, }, - equipItem: function(itemId, charId){ - check(itemId, String); - check(charId, String); - moveItem(itemId, true, "Characters", charId); + // Number currently held + quantity: { + type: SimpleSchema.Integer, + min: 0, + defaultValue: 1 }, - unequipItem: function(itemId, charId){ - check(itemId, String); - check(charId, String); - moveItem(itemId, false, "Characters", charId); + // Weight per item in the stack + weight: { + type: Number, + min: 0, + defaultValue: 0, }, - splitItemToParent: function(itemId, moveQuantity, parent){ - check(itemId, String); - check(moveQuantity, Number); - check(parent, {id: String, collection: String}); - - //get the item - var item = Items.findOne(itemId); - if (!item) return; - - //don't bother moving nothing - if (moveQuantity <= 0 || item.quantity <= 0){ - return; - } - //ensure we are only moving up to the current stack size - if (item.quantity < moveQuantity){ - moveQuantity = this.quantity; - } - - if (Meteor.isServer) { - checkMovePermission(itemId, parent); - } - - //create a new item stack - var newStack = _.omit(EJSON.clone(item), "_id"); - newStack.parent = parent; - newStack.quantity = moveQuantity; - - //find out if we have an exact replica in the destination - var query = _.omit(newStack, ["parent", "quantity"]); - query["parent.collection"] = newStack.parent.collection; - query["parent.id"] = newStack.parent.id; - query._id = {$ne: itemId}; //make sure we don't join it to itself - var existingStack = Items.findOne(query); - if (existingStack){ - //increase the existing stack's size - Items.update( - existingStack._id, - {$inc: {quantity: moveQuantity}} - ); - } else { - //insert the new stack - Items.insert(newStack, function(err, id){ - if (err) throw err; - //copy the children also - Meteor.call("cloneChildren", item._id, {collection: "Items", id: id}); - }); - } - - //reduce the old stack's size - var oldQuantity = item.quantity - moveQuantity; - if (oldQuantity === 0){ - Items.remove(itemId); - } else { - Items.update(itemId, {$set: {quantity: oldQuantity}}); - } + // Value per item in the stack, in gold pieces + value: { + type: Number, + min: 0, + defaultValue: 0, + }, + // If this item is equipped, it requires attunement + // Being equipped is `enabled === true` + requiresAttunement: { + type: Boolean, + optional: true, + }, + // Show increment/decrement buttons in item lists + showIncrement: { + type: Boolean, + optional: true, }, }); -Items.helpers({ - totalValue: function(){ - return this.value * this.quantity; - }, - totalWeight: function(){ - return this.weight * this.quantity; - }, - pluralName: function(){ - if (this.plural && this.quantity !== 1){ - return this.plural; - } else { - return this.name; - } - }, -}); +ItemSchema.extend(ColorSchema); -Items.before.update(function(userId, doc, fieldNames, modifier, options){ - if ( - modifier && modifier.$set && modifier.$set.enabled && //we are equipping this item - !( - modifier.$set["parent.collection"] === "Characters" && - modifier.$set["parent.id"] - ) //and we haven"t specified a character to equip to - ){ - //equip it to the current character - modifier.$set["parent.collection"] = "Characters"; - modifier.$set["parent.id"] = doc.charId; - } -}); +Items.attachSchema(ItemSchema); +Items.attachSchema(PropertySchema); +Items.attachSchema(ChildSchema); -// Items.attachBehaviour("softRemovable"); -makeChild(Items); //children of containers -makeParent(Items, ["name", "enabled"]); //parents of effects and attacks - -//Items.allow(CHARACTER_SUBSCHEMA_ALLOW); export default Items; +export { ItemSchema }; diff --git a/app/imports/api/library/Libraries.js b/app/imports/api/library/Libraries.js new file mode 100644 index 00000000..48b37390 --- /dev/null +++ b/app/imports/api/library/Libraries.js @@ -0,0 +1,17 @@ +import schema from '/imports/api/schema.js'; +import SharingSchema from '/imports/api/sharing/SharingSchema.js'; + +let Libraries = new Mongo.Collection('libraries'); + +let LibrarySchema = schema({ + name: { + type: String, + }, +}); + +LibrarySchema.extend(SharingSchema); + +Libraries.attachSchema(LibrarySchema); + +export default Libraries; +export { LibrarySchema }; diff --git a/app/imports/api/library/Library.js b/app/imports/api/library/Library.js deleted file mode 100644 index 9cf2ad84..00000000 --- a/app/imports/api/library/Library.js +++ /dev/null @@ -1,54 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; - -Libraries = new Mongo.Collection("library"); - -librarySchema = schema({ - name: {type: String}, - owner: {type: String, regEx: SimpleSchema.RegEx.Id}, - readers: {type: Array, defaultValue: []}, - "readers.$": {type: String, regEx: SimpleSchema.RegEx.Id}, - writers: {type: Array, defaultValue: []}, - "writers.$": {type: String, regEx: SimpleSchema.RegEx.Id}, - public: {type: Boolean, defaultValue: false}, -}); - -Libraries.attachSchema(librarySchema); - -Libraries.allow({ - insert(userId, doc) { - return userId && doc.owner === userId; - }, - update(userId, doc, fields, modifier) { - return canEdit(userId, doc); - }, - remove(userId, doc) { - return canEdit(userId, doc); - }, - fetch: ["owner", "writers"], -}); - -Libraries.deny({ - // For now, only admins can manage libraries - insert(userId, doc){ - var user = Meteor.users.findOne(userId); - return !user || !_.contains(user.roles, "admin"); - }, - update(userId, doc, fields, modifier) { - // Can't change owners - return _.contains(fields, "owner") - }, - fetch: [], -}); - -const canEdit = function(userId, library){ - if (!userId || !library) return; - return library.owner === userId || _.contains(library.writers, userId); -}; - -Libraries.canEdit = function(userId, libraryId){ - const library = Libraries.findOne(libraryId); - return canEdit(userId, library); -}; - -export default Libraries; diff --git a/app/imports/api/library/LibraryItems.js b/app/imports/api/library/LibraryItems.js deleted file mode 100644 index 43694f4e..00000000 --- a/app/imports/api/library/LibraryItems.js +++ /dev/null @@ -1,49 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; -import libraryAttacksSchema from "/imports/api/library/"; - -LibraryItems = new Mongo.Collection("libraryItems"); - -libraryItemsSchema = schema({ - libraryName:{type: String, optional: true, trim: false}, - name: {type: String, defaultValue: "New Item", trim: false}, - plural: {type: String, optional: true, trim: false}, - description:{type: String, optional: true, trim: false}, - quantity: {type: SimpleSchema.Integer, min: 0, defaultValue: 1}, - weight: {type: Number, min: 0, defaultValue: 0}, - value: {type: Number, min: 0, defaultValue: 0}, - requiresAttunement: {type: Boolean, defaultValue: false}, - - library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - - settings: {type: Object}, - "settings.category": { - type: String, - optional: true, - allowedValues: [ - "adventuringGear", "armor", "weapons", "tools", - ], - }, - "settings.showIncrement": { - type: Boolean, - defaultValue: false, - }, - - effects: [Schemas.LibraryEffects], - attacks: [Schemas.LibraryAttacks], -}); - -LibraryItems.attachSchema(libraryItemsSchema); - -LibraryItems.allow({ - insert(userId, doc) { - return Libraries.canEdit(userId, doc.library); - }, - update(userId, doc, fields, modifier) { - return Libraries.canEdit(userId, doc.library); - }, - remove(userId, doc) { - return Libraries.canEdit(userId, doc.library); - }, - fetch: ["library"], -}); diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js new file mode 100644 index 00000000..e052059a --- /dev/null +++ b/app/imports/api/library/LibraryNodes.js @@ -0,0 +1,29 @@ +import schema from '/imports/api/schema.js'; +import SharingSchema from '/imports/api/sharing/SharingSchema.js'; +import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import librarySchemas from '/imports/api/library/librarySchemas.js'; + +let LibraryNodes = new Mongo.Collection('libraryNodes'); + +let LibraryNodeSchema = schema({ + type: { + type: String, + allowedValues: Object.keys(librarySchemas), + }, + data: { + type: Object, + custom(){ + let type = this.field('type'); + let schema = librarySchemas[type]; + schema.validate(this.value) + }, + }, +}); + +LibraryNodeSchema.extend(SharingSchema); + +LibraryNodes.attachSchema(LibraryNodeSchema); +LibraryNodes.attachSchema(ChildSchema); + +export default LibraryNodes; +export { LibraryNodeSchema }; diff --git a/app/imports/api/library/LibrarySpells.js b/app/imports/api/library/LibrarySpells.js deleted file mode 100644 index a456786a..00000000 --- a/app/imports/api/library/LibrarySpells.js +++ /dev/null @@ -1,68 +0,0 @@ -LibrarySpells = new Mongo.Collection("librarySpells"); - -Schemas.LibrarySpells = schema({ - name: { - type: String, - trim: false, - defaultValue: "New Spell", - }, - description: { - type: String, - optional: true, - trim: false, - }, - castingTime: { - type: String, - optional: true, - defaultValue: "action", - trim: false, - }, - range: { - type: String, - optional: true, - trim: false, - }, - duration: { - type: String, - optional: true, - trim: false, - defaultValue: "Instantaneous", - }, - "components.verbal": {type: Boolean, defaultValue: false}, - "components.somatic": {type: Boolean, defaultValue: false}, - "components.concentration": {type: Boolean, defaultValue: false}, - "components.material": {type: String, optional: true}, - ritual: { - type: Boolean, - defaultValue: false, - }, - level: { - type: SimpleSchema.Integer, - defaultValue: 1, - }, - school: { - type: String, - defaultValue: "Abjuration", - allowedValues: magicSchools, - }, - library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, - effects: {type: Array}, - "effects.$": {type: Schemas.LibraryEffects, defaultValue: []}, - attacks: {type: Array}, - "attacks.$": {type: Schemas.LibraryAttacks, defaultValue: []}, -}); - -LibrarySpells.attachSchema(Schemas.LibrarySpells); - -LibrarySpells.allow({ - insert(userId, doc) { - return Libraries.canEdit(userId, doc.library); - }, - update(userId, doc, fields, modifier) { - return Libraries.canEdit(userId, doc.library); - }, - remove(userId, doc) { - return Libraries.canEdit(userId, doc.library); - }, - fetch: ["library"], -}); diff --git a/app/imports/api/library/librarySchemas.js b/app/imports/api/library/librarySchemas.js new file mode 100644 index 00000000..f9f99128 --- /dev/null +++ b/app/imports/api/library/librarySchemas.js @@ -0,0 +1,40 @@ +import { CreatureSchema } from '/imports/api/creature/properties/Creatures.js'; +import { ActionSchema } from '/imports/api/creature/properties/Actions.js'; +import { AttributeSchema } from '/imports/api/creature/properties/Attributes.js'; +import { ClassSchema } from '/imports/api/creature/properties/Classes.js'; +import { ClassLevelSchema } from '/imports/api/creature/properties/ClassLevels.js'; +import { DamageMultiplierSchema } from '/imports/api/creature/properties/DamageMultipliers.js'; +import { EffectSchema } from '/imports/api/creature/properties/Effects.js'; +import { ExperienceSchema } from '/imports/api/creature/properties/Experiences.js'; +import { FeatureSchema } from '/imports/api/creature/properties/Features.js'; +import { FolderSchema } from '/imports/api/creature/properties/Folders.js'; +import { NoteSchema } from '/imports/api/creature/properties/Notes.js'; +import { ProficiencySchema } from '/imports/api/creature/properties/Proficiencies.js'; +import { SkillSchema } from '/imports/api/creature/properties/Skills.js'; +import { SpellListSchema } from '/imports/api/creature/properties/SpellLists.js'; +import { SpellSchema } from '/imports/api/creature/properties/Spells.js'; +import { ContainerSchema } from '/imports/api/creature/properties/Containers.js'; +import { ItemSchema } from '/imports/api/creature/properties/Items.js'; + + +const librarySchemas = { + creature: CreatureSchema, + action: ActionSchema, + attribute: AttributeSchema, + class: ClassSchema, + classLevel: ClassLevelSchema, + damageMultiplier: DamageMultiplierSchema, + effect: EffectSchema, + experience: ExperienceSchema, + feature: FeatureSchema, + folder: FolderSchema, + note: NoteSchema, + proficiency: ProficiencySchema, + skill: SkillSchema, + spellList: SpellListSchema, + spell: SpellSchema, + container: ContainerSchema, + item: ItemSchema, +}; + +export default librarySchemas; diff --git a/app/imports/api/library/subSchemas/LibraryAttacks.js b/app/imports/api/library/subSchemas/LibraryAttacks.js deleted file mode 100644 index 3498ae61..00000000 --- a/app/imports/api/library/subSchemas/LibraryAttacks.js +++ /dev/null @@ -1,46 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; - -libraryAttacksSchema = schema({ - name: { - type: String, - defaultValue: "New Attack", - trim: false, - }, - details: { - type: String, - optional: true, - trim: false, - }, - attackBonus: { - type: String, - optional: true, - trim: false, - }, - damage: { - type: String, - optional: true, - trim: false, - }, - damageType: { - type: String, - allowedValues: [ - "bludgeoning", - "piercing", - "slashing", - "acid", - "cold", - "fire", - "force", - "lightning", - "necrotic", - "poison", - "psychic", - "radiant", - "thunder", - ], - defaultValue: "slashing", - }, -}); - -export default libraryAttacksSchema; diff --git a/app/imports/api/library/subSchemas/LibraryEffects.js b/app/imports/api/library/subSchemas/LibraryEffects.js deleted file mode 100644 index ec72b8e6..00000000 --- a/app/imports/api/library/subSchemas/LibraryEffects.js +++ /dev/null @@ -1,44 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import schema from '/imports/api/schema.js'; - -libraryEffectsSchema = schema({ - name: { - type: String, - optional: true, //TODO make necessary if there is no owner - trim: false, - }, - operation: { - type: String, - defaultValue: "add", - allowedValues: [ - "base", - "proficiency", - "add", - "mul", - "min", - "max", - "advantage", - "disadvantage", - "passiveAdd", - "fail", - "conditional", - ], - }, - // Effects either have a value OR a calculation - value: { - type: Number, - optional: true, - }, - calculation: { - type: String, - optional: true, - trim: false, - }, - //which stat the effect is applied to - stat: { - type: String, - optional: true, - }, -}); - -export default libraryEffectsSchema; diff --git a/app/imports/api/parenting/ChildSchema.js b/app/imports/api/parenting/ChildSchema.js index 9bf0d971..33a84928 100644 --- a/app/imports/api/parenting/ChildSchema.js +++ b/app/imports/api/parenting/ChildSchema.js @@ -1,3 +1,4 @@ +import SimpleSchema from 'simpl-schema'; import schema from '/imports/api/schema.js'; const refSchema = new SimpleSchema({ diff --git a/app/imports/api/parenting/SoftRemovableSchema.js b/app/imports/api/parenting/SoftRemovableSchema.js index 15634ebb..8c120553 100644 --- a/app/imports/api/parenting/SoftRemovableSchema.js +++ b/app/imports/api/parenting/SoftRemovableSchema.js @@ -1,6 +1,6 @@ -import schema from '/imports/api/schema.js'; +import SimpleSchema from 'simpl-schema'; -let SoftRemovableSchema = schema({ +let SoftRemovableSchema = new SimpleSchema({ "removed": { type: Boolean, optional: true, diff --git a/app/imports/api/parenting/getInheritPropertiesSchema.js b/app/imports/api/parenting/getInheritPropertiesSchema.js deleted file mode 100644 index 41f53b4d..00000000 --- a/app/imports/api/parenting/getInheritPropertiesSchema.js +++ /dev/null @@ -1,16 +0,0 @@ -import schema from '/imports/api/schema.js'; - -let getInheritPropertiesSchema = function(keys){ - let options = { - 'parent.properties': { - type: Object, - optional: true, - }, - }; - for (let key in keys){ - options[`parent.properties.${key}`] = keys[key]; - } - return schema(options); -}; - -export default getInheritPropertiesSchema; diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js index 682d4661..df99244b 100644 --- a/app/imports/api/parenting/parenting.js +++ b/app/imports/api/parenting/parenting.js @@ -1,5 +1,5 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import getCollectionByName from 'app/imports/api/parenting/getCollectionByName.js'; +import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; // n = collections.length let collections = []; diff --git a/app/imports/api/sharing/SharingSchema.js b/app/imports/api/sharing/SharingSchema.js new file mode 100644 index 00000000..3098ead6 --- /dev/null +++ b/app/imports/api/sharing/SharingSchema.js @@ -0,0 +1,34 @@ +import SimpleSchema from 'simpl-schema'; + +let SharingSchema = new SimpleSchema({ + owner: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1 + }, + readers: { + type: Array, + defaultValue: [], + index: 1 + }, + "readers.$": { + type: String, + regEx: SimpleSchema.RegEx.Id + }, + writers: { + type: Array, + defaultValue: [], + index: 1 + }, + "writers.$": { + type: String, + regEx: SimpleSchema.RegEx.Id + }, + public: { + type: Boolean, + optional: true, + index: 1, + }, +}); + +export default SharingSchema; diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js new file mode 100644 index 00000000..5f4587fd --- /dev/null +++ b/app/imports/api/sharing/sharingPermissions.js @@ -0,0 +1,53 @@ +import { _ } from 'meteor/underscore'; + +function assertIdValid(userId){ + if (!userId || typeof userId !== 'string'){ + throw new Meteor.Error("Permission denied", + "No user ID given for edit permission check"); + } +}; + +function assertdocExists(doc){ + if (!doc){ + throw new Meteor.Error("Edit permission denied", + `No doc exists with the given id: ${charId}`); + } +}; + +export function assertOwnership(doc, userId){ + assertIdValid(userId); + assertdocExists(doc); + if (doc.owner === userId ){ + return true; + } else { + throw new Meteor.Error("Permission denied", + `You are not the owner of this doc`); + } +} + +export function assertEditPermission(doc, userId) { + assertIdValid(userId); + assertdocExists(doc); + if (doc.owner === userId || _.contains(doc.writers, userId)){ + return true; + } else { + throw new Meteor.Error("Edit permission denied", + `You do not have permission to edit this character`); + } +}; + +export function assertViewPermission(doc, userId) { + assertIdValid(userId); + assertdocExists(doc); + if ( + doc.owner === userId || + doc.public || + _.contains(doc.readers, userId) || + _.contains(doc.writers, userId) + ){ + return true; + } else { + throw new Meteor.Error("View permission denied", + `You do not have permission to view this character`); + } +}; diff --git a/app/imports/constants/DAMAGE_TYPES.js b/app/imports/constants/DAMAGE_TYPES.js new file mode 100644 index 00000000..9cd25043 --- /dev/null +++ b/app/imports/constants/DAMAGE_TYPES.js @@ -0,0 +1,20 @@ +const DAMAGE_TYPES = Object.freeze([ + "bludgeoning", + "piercing", + "slashing", + "magicalBludgeoning", + "magicalPiercing", + "magicalSlashing", + "acid", + "cold", + "fire", + "force", + "lightning", + "necrotic", + "poison", + "psychic", + "radiant", + "thunder", +]); + +export default DAMAGE_TYPES; diff --git a/app/imports/server/publications/library.js b/app/imports/server/publications/library.js index 97ea2b3f..e8089567 100644 --- a/app/imports/server/publications/library.js +++ b/app/imports/server/publications/library.js @@ -1,4 +1,4 @@ -import Libraries from "/imports/api/library/Library.js" +import Libraries from "/imports/api/library/Libraries.js" const standardLibraryIds = [ "SRDLibraryGA3XWsd", diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index 04c9b5e6..33fa3622 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -1,22 +1,5 @@ import Creatures from "/imports/api/creature/Creatures.js"; -import Actions from "/imports/api/creature/properties/Actions.js"; -import Attacks from "/imports/api/creature/properties/Attacks.js"; -import Attributes from "/imports/api/creature/properties/Attributes.js"; -import Buffs from "/imports/api/creature/properties/Buffs.js"; -import Classes from "/imports/api/creature/properties/Classes.js"; -import Conditions from "/imports/api/creature/properties/Conditions.js"; -import CustomBuffs from "/imports/api/creature/properties/CustomBuffs.js"; -import DamageMultipliers from "/imports/api/creature/properties/DamageMultipliers.js"; -import Effects from "/imports/api/creature/properties/Effects.js"; -import Experiences from "/imports/api/creature/properties/Experiences.js"; -import Features from "/imports/api/creature/properties/Features.js"; -import Notes from "/imports/api/creature/properties/Notes.js"; -import Skills from "/imports/api/creature/properties/Skills.js"; -import Spells from "/imports/api/creature/properties/Spells.js"; -import SpellLists from "/imports/api/creature/properties/SpellLists.js"; -import Proficiencies from "/imports/api/creature/properties/Proficiencies.js"; -import Containers from "/imports/api/inventory/Containers.js"; -import Items from "/imports/api/inventory/Items.js"; +import creatureCollections from '/imports/api/creature/creatureCollections.js'; Meteor.publish("singleCharacter", function(charId){ userId = this.userId; @@ -32,25 +15,9 @@ Meteor.publish("singleCharacter", function(charId){ if (char){ return [ Creatures.find({_id: charId}), - //get all the assets for this character including soft deleted ones - Actions.find ({charId}, {removed: true}), - Attacks.find ({charId}, {removed: true}), - Attributes.find ({charId}, {removed: true}), - Buffs.find ({charId}, {removed: true}), - Classes.find ({charId}, {removed: true}), - Conditions.find ({charId}, {removed: true}), - Containers.find ({charId}, {removed: true}), - CustomBuffs.find ({charId}, {removed: true}), - DamageMultipliers.find ({charId}, {removed: true}), - Effects.find ({charId}, {removed: true}), - Experiences.find ({charId}, {removed: true}), - Features.find ({charId}, {removed: true}), - Items.find ({charId}, {removed: true}), - Notes.find ({charId}, {removed: true}), - Skills.find ({charId}, {removed: true}), - Spells.find ({charId}, {removed: true}), - SpellLists.find ({charId}, {removed: true}), - Proficiencies.find ({charId}, {removed: true}), + ...creatureCollections.map( + collection => collection.find({charId}) + ) ]; } else { return []; diff --git a/app/imports/ui/StoryBook.vue b/app/imports/ui/StoryBook.vue index 038ff285..a2323e21 100644 --- a/app/imports/ui/StoryBook.vue +++ b/app/imports/ui/StoryBook.vue @@ -45,8 +45,8 @@ import ColorPicker from '/imports/ui/components/ColorPicker.Story.vue'; import ColumnLayout from "/imports/ui/components/ColumnLayout.Story.vue"; import DialogStack from '/imports/ui/dialogStack/DialogStack.Story.vue'; - import EffectEdit from '/imports/ui/components/effects/EffectEdit.Story.vue'; - import EffectEditExpansionList from '/imports/ui/components/effects/EffectEditExpansionList.Story.vue'; + import EffectEdit from '/imports/ui/components/children/effects/EffectEdit.Story.vue'; + import EffectEditExpansionList from '/imports/ui/components/children/effects/EffectEditExpansionList.Story.vue'; import FeatureCard from '/imports/ui/components/features/FeatureCard.Story.vue'; import HealthBar from '/imports/ui/components/attributes/HealthBar.Story.vue'; import HitDiceListTile from '/imports/ui/components/attributes/HitDiceListTile.Story.vue'; diff --git a/app/imports/ui/components/attributes/AttributeDialog.vue b/app/imports/ui/components/attributes/AttributeDialog.vue index 9529d8d2..81a1ae60 100644 --- a/app/imports/ui/components/attributes/AttributeDialog.vue +++ b/app/imports/ui/components/attributes/AttributeDialog.vue @@ -35,7 +35,7 @@