From 4b900d566497e736ca37d6eb57bc76baa47d71e7 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 12 Oct 2018 09:21:03 +0200 Subject: [PATCH] lowercased all model directories --- app/imports/model/campaign/Instance.js | 9 + app/imports/model/campaign/Party.js | 44 +++ app/imports/model/creature/Actions.js | 42 +++ app/imports/model/creature/Attacks.js | 89 ++++++ app/imports/model/creature/Attributes.js | 87 ++++++ app/imports/model/creature/Buffs.js | 67 +++++ app/imports/model/creature/Classes.js | 32 +++ app/imports/model/creature/Conditions.js | 42 +++ app/imports/model/creature/Creatures.js | 168 +++++++++++ app/imports/model/creature/CustomBuffs.js | 53 ++++ .../model/creature/DamageMultipliers.js | 40 +++ app/imports/model/creature/Effects.js | 131 +++++++++ app/imports/model/creature/Experience.js | 27 ++ app/imports/model/creature/Features.js | 115 ++++++++ app/imports/model/creature/Notes.js | 19 ++ app/imports/model/creature/Proficiencies.js | 37 +++ app/imports/model/creature/Skills.js | 83 ++++++ app/imports/model/creature/SpellLists.js | 37 +++ app/imports/model/creature/Spells.js | 252 ++++++++++++++++ .../model/creature/TemporaryHitPoints.js | 41 +++ .../creature/creatureComputation.js} | 0 .../creature/creatureComputation.test.js} | 0 .../model/creature/subschemas/Adjustment.js | 21 ++ .../model/creature/subschemas/Attribute.js | 13 + .../model/creature/subschemas/DeathSaves.js | 22 ++ .../model/creature/subschemas/Skill.js | 4 + app/imports/model/inventory/Containers.js | 56 ++++ app/imports/model/inventory/Items.js | 268 ++++++++++++++++++ app/imports/model/library/Library.js | 47 +++ app/imports/model/library/LibraryItems.js | 44 +++ app/imports/model/library/LibrarySpells.js | 66 +++++ .../library/subSchemas/LibraryAttacks.js | 41 +++ .../library/subSchemas/LibraryEffects.js | 40 +++ app/imports/model/meta/Blacklist.js | 9 + app/imports/model/meta/ChangeLogs.js | 27 ++ app/imports/model/meta/Reports.js | 79 ++++++ app/imports/model/parenting.js | 242 ++++++++++++++++ app/imports/model/users/Users.js | 110 +++++++ 38 files changed, 2504 insertions(+) create mode 100644 app/imports/model/campaign/Instance.js create mode 100644 app/imports/model/campaign/Party.js create mode 100644 app/imports/model/creature/Actions.js create mode 100644 app/imports/model/creature/Attacks.js create mode 100644 app/imports/model/creature/Attributes.js create mode 100644 app/imports/model/creature/Buffs.js create mode 100644 app/imports/model/creature/Classes.js create mode 100644 app/imports/model/creature/Conditions.js create mode 100644 app/imports/model/creature/Creatures.js create mode 100644 app/imports/model/creature/CustomBuffs.js create mode 100644 app/imports/model/creature/DamageMultipliers.js create mode 100644 app/imports/model/creature/Effects.js create mode 100644 app/imports/model/creature/Experience.js create mode 100644 app/imports/model/creature/Features.js create mode 100644 app/imports/model/creature/Notes.js create mode 100644 app/imports/model/creature/Proficiencies.js create mode 100644 app/imports/model/creature/Skills.js create mode 100644 app/imports/model/creature/SpellLists.js create mode 100644 app/imports/model/creature/Spells.js create mode 100644 app/imports/model/creature/TemporaryHitPoints.js rename app/imports/{Model/Creature/CharacterComputation.js => model/creature/creatureComputation.js} (100%) rename app/imports/{Model/Creature/CharacterComputation.test.js => model/creature/creatureComputation.test.js} (100%) create mode 100644 app/imports/model/creature/subschemas/Adjustment.js create mode 100644 app/imports/model/creature/subschemas/Attribute.js create mode 100644 app/imports/model/creature/subschemas/DeathSaves.js create mode 100644 app/imports/model/creature/subschemas/Skill.js create mode 100644 app/imports/model/inventory/Containers.js create mode 100644 app/imports/model/inventory/Items.js create mode 100644 app/imports/model/library/Library.js create mode 100644 app/imports/model/library/LibraryItems.js create mode 100644 app/imports/model/library/LibrarySpells.js create mode 100644 app/imports/model/library/subSchemas/LibraryAttacks.js create mode 100644 app/imports/model/library/subSchemas/LibraryEffects.js create mode 100644 app/imports/model/meta/Blacklist.js create mode 100644 app/imports/model/meta/ChangeLogs.js create mode 100644 app/imports/model/meta/Reports.js create mode 100644 app/imports/model/parenting.js create mode 100644 app/imports/model/users/Users.js diff --git a/app/imports/model/campaign/Instance.js b/app/imports/model/campaign/Instance.js new file mode 100644 index 00000000..c47e2dd3 --- /dev/null +++ b/app/imports/model/campaign/Instance.js @@ -0,0 +1,9 @@ +let Instances = new Mongo.Collection("instances"); + +let instanceSchema = new SimpleSchema({ + //an instance is a single flow of time all parties in an instance are in-sync time wise +}); + +Instances.attachSchema(instanceSchema); + +export default Instances; diff --git a/app/imports/model/campaign/Party.js b/app/imports/model/campaign/Party.js new file mode 100644 index 00000000..6c8c4ae8 --- /dev/null +++ b/app/imports/model/campaign/Party.js @@ -0,0 +1,44 @@ +let Parties = new Mongo.Collection("parties"); + +let partySchema = new SimpleSchema({ + name: { + type: String, + defaultValue: "New Party", + trim: false, + optional: true, + }, + characters: { + type: [String], + regEx: SimpleSchema.RegEx.Id, + index: 1, + defaultValue: [], + }, + owner: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, +}); + +Parties.attachSchema(Schemas.Party); + +Parties.allow({ + insert: function(userId, doc) { + return userId && doc.owner === userId; + }, + update: function(userId, doc, fields, modifier) { + return userId && doc.owner === userId; + }, + remove: function(userId, doc) { + return userId && doc.owner === userId; + }, + fetch: ["owner"], +}); + +Parties.deny({ + update: function(userId, docs, fields, modifier) { + // can't change owners + return _.contains(fields, "owner"); + } +}); + +export default Parties; diff --git a/app/imports/model/creature/Actions.js b/app/imports/model/creature/Actions.js new file mode 100644 index 00000000..1318bb76 --- /dev/null +++ b/app/imports/model/creature/Actions.js @@ -0,0 +1,42 @@ +let Actions = new Mongo.Collection("actions"); + +/* + * Actions are given to a character by items and features + */ +let actionSchema = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + name: { + type: String, + optional: true, + trim: false, + }, + description: { + type: String, + optional: true, + trim: false, + }, + type: { + type: String, + allowedValues: ["action, bonus, reaction, free"], + defaultValue: "action", + }, + //the immediate impact of doing this action (eg. -1 rages) + adjustments: { + type: [Schemas.Adjustment], + defaultValue: [], + }, +}); + +Actions.attachSchema(actionSchema); + +Actions.attachBehaviour("softRemovable"); +makeChild(Actions); + +Actions.allow(CHARACTER_SUBSCHEMA_ALLOW); +Actions.deny(CHARACTER_SUBSCHEMA_DENY); + +export default Actions diff --git a/app/imports/model/creature/Attacks.js b/app/imports/model/creature/Attacks.js new file mode 100644 index 00000000..eeb8b2f6 --- /dev/null +++ b/app/imports/model/creature/Attacks.js @@ -0,0 +1,89 @@ +let Attacks = new Mongo.Collection("attacks"); + +/* + * Attacks are given to a character by items and features + */ +attackSchema = new SimpleSchema({ + 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, + }, + damage: { + type: String, + defaultValue: "1d8 + {strengthMod}", + optional: true, + trim: false, + }, + damageType: { + type: String, + allowedValues: [ + "bludgeoning", + "piercing", + "slashing", + "acid", + "cold", + "fire", + "force", + "lightning", + "necrotic", + "poison", + "psychic", + "radiant", + "thunder", + ], + defaultValue: "slashing", + }, + //the id of the feature, buff or item that created this effect + parent: { + type: Schemas.Parent + }, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, + 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}}); + } + } +}); + +Attacks.allow(CHARACTER_SUBSCHEMA_ALLOW); +Attacks.deny(CHARACTER_SUBSCHEMA_DENY); + +export default Attacks; diff --git a/app/imports/model/creature/Attributes.js b/app/imports/model/creature/Attributes.js new file mode 100644 index 00000000..e7abdad0 --- /dev/null +++ b/app/imports/model/creature/Attributes.js @@ -0,0 +1,87 @@ +Attributes = new Mongo.Collection("attributes"); + +/* + * Attributes are whole numbered stats of a character + */ +Schemas.Attribute = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + // The nice-to-read name + name: { + type: String, + index: 1, + }, + // The technical, lowercase, single-word name used in formulae + variableName: { + type: String, + }, + // Attributes need to store their order to keep the sheet consistent + order: { + type: Number, + index: 1, + }, + type: { + type: String, + allowedValues: [ + "ability", //Strength, Dex, Con, etc. + "stat", // Speed, Armor Class + "hitDice", + "healthBar", // Hitpoints, Temporary Hitpoints + "resource", // Rages, sorcery points + "spellSlot", // Level 1, 2, 3... spell slots + "utility", // Aren't displayed, Jump height, Carry capacity + ], + index: 1, + }, + baseValue: { + type: Number, + decimal: true, + optional: true, + }, + value: { + type: Number, + decimal: true, + defaultValue: 0, + }, + mod: { + type: Number, + optional: true, + }, + adjustment: { + type: Number, + optional: true, + }, + // Can the value be decimal? + decimal: { + type: Boolean, + optional: true, + }, + parent: { + type: Schemas.Parent + }, + enabled: { + type: Boolean, + defaultValue: true, + }, + reset: { + type: String, + optional: true, + allowedValues: ["shortRest", "longRest"], + }, + // Some things are only reset by half on rest + resetMultiplier: { + type: Number, + optional: true, + }, +}); + +Attributes.attachSchema(Schemas.Attribute); + +Attributes.attachBehaviour("softRemovable"); +makeChild(Attributes, ["enabled"]); //children of lots of things + +Attributes.allow(CHARACTER_SUBSCHEMA_ALLOW); +Attributes.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Buffs.js b/app/imports/model/creature/Buffs.js new file mode 100644 index 00000000..e6e00710 --- /dev/null +++ b/app/imports/model/creature/Buffs.js @@ -0,0 +1,67 @@ +Buffs = new Mongo.Collection("buffs"); + +Schemas.Buff = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + name: { + type: String, + optional: true, + trim: false, + }, + description: { + type: String, + 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.total": { + type: Number, + defaultValue: 0, //0 is infinite + min: 0, + }, + "lifeTime.spent": { + type: Number, + defaultValue: 0, + min: 0, + }, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, + 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, + optional: true, + }, + "appliedByDetails.name": { + type: String, + }, + "appliedByDetails.collection": { + type: String, + }, +}); + +Buffs.attachSchema(Schemas.Buff); + +Buffs.attachBehaviour("softRemovable"); +makeParent(Buffs, ["name", "enabled"]); //parents of effects, attacks, proficiencies + +Buffs.allow(CHARACTER_SUBSCHEMA_ALLOW); +Buffs.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Classes.js b/app/imports/model/creature/Classes.js new file mode 100644 index 00000000..52e4df9e --- /dev/null +++ b/app/imports/model/creature/Classes.js @@ -0,0 +1,32 @@ +Classes = new Mongo.Collection("classes"); + +Schemas.Class = new SimpleSchema({ + charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + name: {type: String, optional: true, trim: false}, + level: {type: Number}, + createdAt: { + type: Date, + autoValue: function() { + if (this.isInsert) { + return new Date(); + } else if (this.isUpsert) { + return {$setOnInsert: new Date()}; + } else { + this.unset(); + } + }, + }, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Classes.attachSchema(Schemas.Class); + +Classes.attachBehaviour("softRemovable"); +makeParent(Classes, "name"); //parents of effects and attacks + +Classes.allow(CHARACTER_SUBSCHEMA_ALLOW); +Classes.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Conditions.js b/app/imports/model/creature/Conditions.js new file mode 100644 index 00000000..ce894fe1 --- /dev/null +++ b/app/imports/model/creature/Conditions.js @@ -0,0 +1,42 @@ +Conditions = new Mongo.Collection("conditions"); + +Schemas.Conditions = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + name: { + type: String, + optional: true, + trim: false, + }, + description: { + type: String, + optional: true, + trim: false, + }, + "lifeTime.total": { + type: Number, + defaultValue: 0, //0 is infinite + min: 0, + }, + "lifeTime.spent": { + type: Number, + defaultValue: 0, + min: 0, + }, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Conditions.attachSchema(Schemas.Conditions); + +Conditions.attachBehaviour("softRemovable"); +makeParent(Conditions, ["name"]); //parents of effects, attacks, proficiencies + +Conditions.allow(CHARACTER_SUBSCHEMA_ALLOW); +Conditions.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Creatures.js b/app/imports/model/creature/Creatures.js new file mode 100644 index 00000000..bd9e531e --- /dev/null +++ b/app/imports/model/creature/Creatures.js @@ -0,0 +1,168 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; + +//set up the collection for creatures +Creatures = new Mongo.Collection("creatures"); + +Schemas.Creature = new SimpleSchema({ + //strings + name: {type: String, defaultValue: "", trim: false, optional: true}, + urlName: {type: String, defaultValue: "-", trim: false, optional: 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: Schemas.DeathSave}, + xp: {type: Number, defaultValue: 0}, + weightCarried: {type: Number, defaultValue: 0}, + level: {type: Number, defaultValue: 0}, + type: {type: String, defaultValue: "pc", allowedValues: ["pc", "npc", "monster"]}, + + //permissions + party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true}, + owner: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1}, + writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: [], index: 1}, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, + //TODO add per-creature settings + //how many experiences to load at a time in XP table + "settings.experiencesInc": {type: Number, 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": { + type: String, + defaultValue: "whitelist", + allowedValues: ["whitelist", "public"], + index: 1, + }, + "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(Schemas.Creature); + +Creatures.calculate = { + xpLevel: function(charId){ + var xp = Creatures.calculate.experience(charId); + for (var i = 0; i < 19; i++){ + if (xp < XP_TABLE[i]){ + return i; + } + } + if (xp > 355000) return 20; + return 0; + }, +}; + +const insertCharacter = new ValidatedMethod({ + + name: "Creatures.methods.insertCharacter", // DDP method name + + validate: new SimpleSchema({ + name: { + type: String, + optional: true, + }, + }).validator(), + + run({name}) { + if (!this.userId) { + throw new Meteor.Error("Creatures.methods.insert.denied", + "You need to be logged in to insert a creature"); + } + + // Create the creature document + Creatures.insert({name, owner: this.userId}); + this.unblock(); + //Add all the required attributes to it + if (Meteor.isServer){ + addDefaultStats(charId); + } + }, + +}); + +const addDefaultStats = function(charId){ + const defaultDocs = getDefaultCreatureDocs(charId); + Attributes.rawCollection().insert(defaultDocs.attributes, {ordered: false}); + Skills.rawCollection().insert(defaultDocs.skills, {ordered: false}); + DamageMultipliers.rawCollection().insert(defaultDocs.damageMultipliers, {ordered: false}); +}; + +//clean up all data related to that creature before removing it +if (Meteor.isServer){ + Creatures.after.remove(function(userId, creature) { + let charId = creature._id; + Actions .remove({charId}); + Attacks .remove({charId}); + Attributes .remove({charId}); + Buffs .remove({charId}); + Classes .remove({charId}); + CustomBuffs .remove({charId}); + DamageMultipliers.remove({charId}); + Effects .remove({charId}); + Experiences .remove({charId}); + Features .remove({charId}); + Notes .remove({charId}); + Proficiencies .remove({charId}); + Skills .remove({charId}); + SpellLists .remove({charId}); + Items .remove({charId}); + Containers .remove({charId}); + }); + Creatures.after.update(function(userId, doc, fieldNames, modifier, options) { + if (_.contains(fieldNames, "name")){ + var urlName = getSlug(doc.name, {maintainCase: true}) || "-"; + Creatures.update(doc._id, {$set: {urlName}}); + } + }); + Creatures.before.insert(function(userId, doc) { + doc.urlName = getSlug(doc.name, {maintainCase: true}) || "-"; + // The first creature a user creates should have the new user experience + if (!Creatures.find({owner: userId}).count()){ + doc.settings.newUserExperience = true; + } + }); +} + +Creatures.allow({ + insert: function(userId, doc) { + // the user must be logged in, and the document must be owned by the user + return (userId && doc.owner === userId); + }, + update: function(userId, doc, fields, modifier) { + // can only change documents you have write access to + return doc.owner === userId || + _.contains(doc.writers, userId); + }, + remove: function(userId, doc) { + // can only remove your own documents + return doc.owner === userId; + }, + fetch: ["owner", "writers"], +}); + +Creatures.deny({ + update: function(userId, docs, fields, modifier) { + // can't change owners + return _.contains(fields, "owner"); + } +}); diff --git a/app/imports/model/creature/CustomBuffs.js b/app/imports/model/creature/CustomBuffs.js new file mode 100644 index 00000000..7a766b01 --- /dev/null +++ b/app/imports/model/creature/CustomBuffs.js @@ -0,0 +1,53 @@ +CustomBuffs = new Mongo.Collection("customBuffs"); + +Schemas.CustomBuff = new SimpleSchema({ + 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.total": { + type: Number, + defaultValue: 0, //0 is infinite + min: 0, + }, + //the id of the feature, buff or item that creates this buff + parent: { + type: Schemas.Parent, + }, +}); + +CustomBuffs.attachSchema(Schemas.CustomBuff); + +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 + +CustomBuffs.allow(CHARACTER_SUBSCHEMA_ALLOW); +CustomBuffs.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/DamageMultipliers.js b/app/imports/model/creature/DamageMultipliers.js new file mode 100644 index 00000000..553dc09d --- /dev/null +++ b/app/imports/model/creature/DamageMultipliers.js @@ -0,0 +1,40 @@ +DamageMultipliers = new Mongo.Collection("damageMultipliers"); + +/* + * DamageMultipliers are whole numbered stats of a character + */ +Schemas.DamageMultiplier = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + // The nice-to-read name + name: { + type: String, + }, + // The technical, lowercase, single-word name used in formulae + variableName: { + type: String, + }, + value: { + type: Number, + decimal: true, + defaultValue: 1, + }, + parent: { + type: Schemas.Parent + }, + enabled: { + type: Boolean, + defaultValue: true, + }, +}); + +DamageMultipliers.attachSchema(Schemas.DamageMultiplier); + +DamageMultipliers.attachBehaviour("softRemovable"); +makeChild(DamageMultipliers, ["enabled"]); //children of lots of things + +DamageMultipliers.allow(CHARACTER_SUBSCHEMA_ALLOW); +DamageMultipliers.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Effects.js b/app/imports/model/creature/Effects.js new file mode 100644 index 00000000..4cf9d913 --- /dev/null +++ b/app/imports/model/creature/Effects.js @@ -0,0 +1,131 @@ +Effects = new Mongo.Collection("effects"); + +/* + * Effects are reason-value attached to skills and abilities + * that modify their final value or presentation in some way + */ +Schemas.Effect = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + name: { + type: String, + optional: true, //TODO make necessary if there is no owner + trim: false, + }, + operation: { + type: String, + defaultValue: "add", + allowedValues: [ + "base", + "add", + "mul", + "min", + "max", + "advantage", + "disadvantage", + "passiveAdd", + "fail", + "conditional", + ], + }, + value: { + type: Number, + decimal: true, + optional: true, + }, + calculation: { + type: String, + optional: true, + trim: false, + }, + //the thing that created this effect + parent: { + type: Schemas.Parent + }, + //which stat the effect is applied to + stat: { + type: String, + optional: true, + }, + enabled: { + type: Boolean, + defaultValue: true, + }, +}); + +Effects.attachSchema(Schemas.Effect); + +Effects.attachBehaviour("softRemovable"); +makeChild(Effects, ["enabled"]); //children of lots of things + +Effects.allow(CHARACTER_SUBSCHEMA_ALLOW); +Effects.deny(CHARACTER_SUBSCHEMA_DENY); + +//give characters default character effects +Characters.after.insert(function(userId, char) { + if (Meteor.isServer) { + Effects.insert({ + charId: char._id, + name: "Constitution modifier for each level", + stat: "hitPoints", + operation: "add", + calculation: "level * constitutionMod", + parent: { + id: char._id, + collection: "Characters", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Proficiency bonus by level", + stat: "proficiencyBonus", + operation: "add", + calculation: "floor(level / 4 + 1.75)", + parent: { + id: char._id, + collection: "Characters", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Dexterity Armor Bonus", + stat: "armor", + operation: "add", + calculation: "dexterityArmor", + parent: { + id: char._id, + collection: "Characters", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Natural Armor", + stat: "armor", + operation: "base", + value: 10, + parent: { + id: char._id, + collection: "Characters", + group: "Inate", + }, + }); + Effects.insert({ + charId: char._id, + name: "Natural Carrying Capacity", + stat: "carryMultiplier", + operation: "base", + value: "1", + parent: { + id: char._id, + collection: "Characters", + group: "Inate", + }, + }); + } +}); diff --git a/app/imports/model/creature/Experience.js b/app/imports/model/creature/Experience.js new file mode 100644 index 00000000..4dc22b2a --- /dev/null +++ b/app/imports/model/creature/Experience.js @@ -0,0 +1,27 @@ +Experiences = new Mongo.Collection("experience"); + +Schemas.Experience = new SimpleSchema({ + 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: Number, defaultValue: 0}, + dateAdded: { + type: Date, + autoValue: function() { + if (this.isInsert) { + return new Date(); + } else if (this.isUpsert) { + return {$setOnInsert: new Date()}; + } else { + this.unset(); + } + }, + }, +}); + +Experiences.attachSchema(Schemas.Experience); + +Experiences.attachBehaviour("softRemovable"); + +Experiences.allow(CHARACTER_SUBSCHEMA_ALLOW); +Experiences.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Features.js b/app/imports/model/creature/Features.js new file mode 100644 index 00000000..468d6fe3 --- /dev/null +++ b/app/imports/model/creature/Features.js @@ -0,0 +1,115 @@ +Features = new Mongo.Collection("features"); + +Schemas.Feature = new SimpleSchema({ + 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: Number, defaultValue: 0}, + reset: { + type: String, + allowedValues: ["manual", "longRest", "shortRest"], + defaultValue: "manual", + }, + enabled: {type: Boolean, defaultValue: true}, + alwaysEnabled:{type: Boolean, defaultValue: true}, + color: {type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Features.attachSchema(Schemas.Feature); + +Features.helpers({ + usesLeft: function(){ + return evaluate(this.charId, this.uses) - this.used; + }, + usesValue: function(){ + return evaluate(this.charId, this.uses); + }, +}); + +Features.attachBehaviour("softRemovable"); +makeParent(Features, ["name", "enabled"]); //parents of effects and attacks + +Features.allow(CHARACTER_SUBSCHEMA_ALLOW); +Features.deny(CHARACTER_SUBSCHEMA_DENY); + +//give characters default feature of base ability scores of 10 +Characters.after.insert(function(userId, char) { + if (Meteor.isServer){ + var featureId = Features.insert({ + name: "Base Ability Scores", + charId: char._id, + enabled: true, + alwaysEnabled: true, + }); + Effects.insert({ + stat: "strength", + charId: char._id, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.insert({ + stat: "dexterity", + charId: char._id, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.insert({ + stat: "constitution", + charId: char._id, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.insert({ + stat: "intelligence", + charId: char._id, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.insert({ + stat: "wisdom", + charId: char._id, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + Effects.insert({ + stat: "charisma", + charId: char._id, + parent: { + id: featureId, + collection: "Features", + }, + operation: "base", + value: 10, + enabled: true, + }); + } +}); diff --git a/app/imports/model/creature/Notes.js b/app/imports/model/creature/Notes.js new file mode 100644 index 00000000..f1ce7aa4 --- /dev/null +++ b/app/imports/model/creature/Notes.js @@ -0,0 +1,19 @@ +Notes = new Mongo.Collection("notes"); + +Schemas.Note = new SimpleSchema({ + charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + name: {type: String, optional: true, trim: false}, + description: {type: String, optional: true, trim: false}, + color: { + type: String, + allowedValues:_.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Notes.attachSchema(Schemas.Note); + +Notes.attachBehaviour("softRemovable"); + +Notes.allow(CHARACTER_SUBSCHEMA_ALLOW); +Notes.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Proficiencies.js b/app/imports/model/creature/Proficiencies.js new file mode 100644 index 00000000..22c6851f --- /dev/null +++ b/app/imports/model/creature/Proficiencies.js @@ -0,0 +1,37 @@ +Proficiencies = new Mongo.Collection("proficiencies"); + +Schemas.Proficiency = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + name: { + type: String, + trim: false, + optional: true, + }, + value: { + type: Number, + allowedValues: [0, 0.5, 1, 2], + defaultValue: 1, + decimal: true, + }, + type: { + type: String, + allowedValues: ["skill", "save", "weapon", "armor", "tool", "language"], + defaultValue: "skill", + }, + enabled: { + type: Boolean, + defaultValue: true, + }, +}); + +Proficiencies.attachSchema(Schemas.Proficiency); + +Proficiencies.attachBehaviour("softRemovable"); +makeChild(Proficiencies, ["enabled"]); + +Proficiencies.allow(CHARACTER_SUBSCHEMA_ALLOW); +Proficiencies.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Skills.js b/app/imports/model/creature/Skills.js new file mode 100644 index 00000000..b4abb2d8 --- /dev/null +++ b/app/imports/model/creature/Skills.js @@ -0,0 +1,83 @@ +Skills = new Mongo.Collection("skills"); + +/* + * Skills are anything that results in a modifier to be added to a D20 + * Skills usually have an ability score modifier that they use as their basis + */ +Schemas.Skill = new SimpleSchema({ + charId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + // The nice-to-read name + name: { + type: String, + }, + // The technical, lowercase, single-word name used in formulae + variableName: { + type: String, + }, + ability: { + type: String, + optional: true, + }, + type: { + type: String, + allowedValues: [ + "skill", + "save", + "stat", + "tool", + "weapon", + "language", + "utility", //not displayed anywhere + ], + }, + // Skills need to store their order to keep the sheet consistent + order: { + type: Number, + }, + value: { + type: Number, + decimal: true, + defaultValue: 0, + }, + advantage: { + type: Number, + optional: true, + allowedValues: [-1, 0, 1], + }, + passiveBonus: { + type: Number, + optional: true, + }, + proficiency: { + type: Number, + allowedValues: [0, 0.5, 1, 2], + defaultValue: 0, + }, + conditionalBenefits: { + type: Number, + optional: true, + }, + fail: { + type: Number, + optional: true, + }, + parent: { + type: Schemas.Parent + }, + enabled: { + type: Boolean, + defaultValue: true, + }, +}); + +Skills.attachSchema(Schemas.Skill); + +Skills.attachBehaviour("softRemovable"); +makeChild(Skills, ["enabled"]); //children of lots of things + +Skills.allow(CHARACTER_SUBSCHEMA_ALLOW); +Skills.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/SpellLists.js b/app/imports/model/creature/SpellLists.js new file mode 100644 index 00000000..982be871 --- /dev/null +++ b/app/imports/model/creature/SpellLists.js @@ -0,0 +1,37 @@ +SpellLists = new Mongo.Collection("spellLists"); + +Schemas.SpellLists = new SimpleSchema({ + 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}, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, + "settings.showUnprepared": {type: Boolean, defaultValue: true}, +}); + +SpellLists.attachSchema(Schemas.SpellLists); + +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.allow(CHARACTER_SUBSCHEMA_ALLOW); +SpellLists.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/model/creature/Spells.js b/app/imports/model/creature/Spells.js new file mode 100644 index 00000000..2b5876b2 --- /dev/null +++ b/app/imports/model/creature/Spells.js @@ -0,0 +1,252 @@ +Spells = new Mongo.Collection("spells"); + +Schemas.Spell = new SimpleSchema({ + charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + prepared: { + type: String, + defaultValue: "prepared", + allowedValues: ["prepared", "unprepared", "always"], + }, + name: { + type: String, + optional: true, + 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: Number, + defaultValue: 1, + }, + school: { + type: String, + defaultValue: "Abjuration", + allowedValues: magicSchools, + }, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Spells.attachSchema(Schemas.Spell); + +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}}); + }); + } + } +}); + +Spells.allow(CHARACTER_SUBSCHEMA_ALLOW); +Spells.deny(CHARACTER_SUBSCHEMA_DENY); + + + + +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); + }, +}); \ No newline at end of file diff --git a/app/imports/model/creature/TemporaryHitPoints.js b/app/imports/model/creature/TemporaryHitPoints.js new file mode 100644 index 00000000..65b2a44b --- /dev/null +++ b/app/imports/model/creature/TemporaryHitPoints.js @@ -0,0 +1,41 @@ +TemporaryHitPoints = new Mongo.Collection("temporaryHitPoints"); + +Schemas.TemporaryHitPoints = new SimpleSchema({ + charId: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + name: {type: String, optional: true}, + maximum: {type: Number, defaultValue: 0, min: 0, max: 500}, + used: {type: Number, defaultValue: 0, min: 0, max: 500}, + deleteOnZero:{type: Boolean, defaultValue: false}, + dateAdded: { + type: Date, + autoValue: function() { + if (this.isInsert) { + return new Date(); + } else if (this.isUpsert) { + return {$setOnInsert: new Date()}; + } else { + this.unset(); + } + }, + }, +}); + +TemporaryHitPoints.attachSchema(Schemas.TemporaryHitPoints); + +TemporaryHitPoints.helpers({ + left: function(){ + return this.maximum - this.used; + } +}); + +//remove the temporary hit points when they hit zero +TemporaryHitPoints.after.update( + function(userId, thp, fieldNames, modifier, options){ + if (thp.used >= thp.maximum && thp.deleteOnZero){ + TemporaryHitPoints.remove(thp._id); + } + }, {fetchPrevious: false} +); + +TemporaryHitPoints.allow(CHARACTER_SUBSCHEMA_ALLOW); +TemporaryHitPoints.deny(CHARACTER_SUBSCHEMA_DENY); diff --git a/app/imports/Model/Creature/CharacterComputation.js b/app/imports/model/creature/creatureComputation.js similarity index 100% rename from app/imports/Model/Creature/CharacterComputation.js rename to app/imports/model/creature/creatureComputation.js diff --git a/app/imports/Model/Creature/CharacterComputation.test.js b/app/imports/model/creature/creatureComputation.test.js similarity index 100% rename from app/imports/Model/Creature/CharacterComputation.test.js rename to app/imports/model/creature/creatureComputation.test.js diff --git a/app/imports/model/creature/subschemas/Adjustment.js b/app/imports/model/creature/subschemas/Adjustment.js new file mode 100644 index 00000000..8025f089 --- /dev/null +++ b/app/imports/model/creature/subschemas/Adjustment.js @@ -0,0 +1,21 @@ +/* + * Adjustments make instantaneous changes to the value of some attribute + * Damage, healing and resource cost/recovery are all adjustments + */ +Schemas.Adjustment = new SimpleSchema({ + //which stat the adjustment is applied to + stat: { + type: String, + optional: true, + }, + //the value added to the stat + value: { + type: Number, + decimal: true, + optional: true, + }, + calculation: { + type: String, + optional: true, + }, +}); diff --git a/app/imports/model/creature/subschemas/Attribute.js b/app/imports/model/creature/subschemas/Attribute.js new file mode 100644 index 00000000..d93f9a67 --- /dev/null +++ b/app/imports/model/creature/subschemas/Attribute.js @@ -0,0 +1,13 @@ +Schemas.Attribute = new SimpleSchema({ + //the temporary shift of the attribute + //should be zero after reset + adjustment: { + type: Number, + defaultValue: 0, + }, + reset: { + type: String, + defaultValue: "longRest", + allowedValues: ["longRest", "shortRest"], + }, +}); diff --git a/app/imports/model/creature/subschemas/DeathSaves.js b/app/imports/model/creature/subschemas/DeathSaves.js new file mode 100644 index 00000000..7d587aa7 --- /dev/null +++ b/app/imports/model/creature/subschemas/DeathSaves.js @@ -0,0 +1,22 @@ +Schemas.DeathSave = new SimpleSchema({ + pass: { + type: Number, + min: 0, + max: 3, + defaultValue: 0, + }, + fail: { + type: Number, + min: 0, + max: 3, + defaultValue: 0, + }, + canDeathSave: { + type: Boolean, + defaultValue: true, + }, + stable: { + type: Boolean, + defaultValue: false, + }, +}); diff --git a/app/imports/model/creature/subschemas/Skill.js b/app/imports/model/creature/subschemas/Skill.js new file mode 100644 index 00000000..430d83d7 --- /dev/null +++ b/app/imports/model/creature/subschemas/Skill.js @@ -0,0 +1,4 @@ +Schemas.Skill = new SimpleSchema({ + //attribute name that this skill used as base mod for roll + ability: {type: String, defaultValue: ""}, +}); diff --git a/app/imports/model/inventory/Containers.js b/app/imports/model/inventory/Containers.js new file mode 100644 index 00000000..2318c930 --- /dev/null +++ b/app/imports/model/inventory/Containers.js @@ -0,0 +1,56 @@ +//set up the collection for containers +Containers = new Mongo.Collection("containers"); + +Schemas.Container = new SimpleSchema({ + 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, decimal: true}, + value: {type: Number, min: 0, defaultValue: 0, decimal: true}, + description:{type: String, optional: true, trim: false}, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Containers.attachSchema(Schemas.Container); + +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; + }, + totalValue: function(){ + return this.contentsValue() + this.value; + }, + contentsWeight: function(){ + var weight = 0; + Items.find( + {"parent.id": this._id}, + {fields: {quantity: 1, weight: 1}} + ).forEach(function(item){ + weight += item.totalWeight(); + }); + return weight; + }, + totalWeight: function(){ + return this.contentsWeight() + this.weight; + }, + moveToCharacter: function(characterId){ + if (this.charId === characterId) return; + Items.update(this._id, {$set: {charId: characterId}}); + }, +}); + +Containers.attachBehaviour("softRemovable"); +makeParent(Containers); //parents of items + +Containers.allow(CHARACTER_SUBSCHEMA_ALLOW); diff --git a/app/imports/model/inventory/Items.js b/app/imports/model/inventory/Items.js new file mode 100644 index 00000000..6c5c00e1 --- /dev/null +++ b/app/imports/model/inventory/Items.js @@ -0,0 +1,268 @@ +Items = new Mongo.Collection("items"); + +Schemas.Item = new SimpleSchema({ + 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: Number, min: 0, defaultValue: 1}, + weight: {type: Number, min: 0, defaultValue: 0, decimal: true}, + value: {type: Number, min: 0, defaultValue: 0, decimal: true}, + enabled: {type: Boolean, defaultValue: false}, + requiresAttunement: {type: Boolean, defaultValue: false}, + "settings.showIncrement": {type: Boolean, defaultValue: false}, + color: { + type: String, + allowedValues: _.pluck(colorOptions, "key"), + defaultValue: "q", + }, +}); + +Items.attachSchema(Schemas.Item); + +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); + }, + moveItemToCharacter: function(itemId, charId) { + check(itemId, String); + check(charId, String); + moveItem(itemId, false, "Characters", charId); + }, + moveItemToContainer: function(itemId, containerId) { + check(itemId, String); + check(containerId, String); + moveItem(itemId, false, "Containers", containerId); + }, + equipItem: function(itemId, charId){ + check(itemId, String); + check(charId, String); + moveItem(itemId, true, "Characters", charId); + }, + unequipItem: function(itemId, charId){ + check(itemId, String); + check(charId, String); + moveItem(itemId, false, "Characters", charId); + }, + 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}}); + } + }, +}); + +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; + } + }, +}); + +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.attachBehaviour("softRemovable"); +makeChild(Items); //children of containers +makeParent(Items, ["name", "enabled"]); //parents of effects and attacks + +Items.allow(CHARACTER_SUBSCHEMA_ALLOW); + +//give characters default items +Characters.after.insert(function(userId, char) { + if (Meteor.isServer){ + var containerId = Containers.insert({ + name: "Coin Pouch", + charId: char._id, + isCarried: true, + description: "A sturdy pouch for coins", + color: "d", + }); + Items.insert({ + name: "Gold piece", + plural: "Gold pieces", + charId: char._id, + quantity: 0, + weight: 0.02, + value: 1, + color: "n", + parent: { + id: containerId, + collection: "Containers", + }, + settings: { + showIncrement: true, + }, + }); + Items.insert({ + name: "Silver piece", + plural: "Silver pieces", + charId: char._id, + quantity: 0, + weight: 0.02, + value: 0.1, + color: "q", + parent: { + id: containerId, + collection: "Containers", + }, + settings: { + showIncrement: true, + }, + }); + Items.insert({ + name: "Copper piece", + plural: "Copper pieces", + charId: char._id, + quantity: 0, + weight: 0.02, + value: 0.01, + color: "s", + parent: { + id: containerId, + collection: "Containers", + }, + settings: { + showIncrement: true, + }, + }); + } +}); diff --git a/app/imports/model/library/Library.js b/app/imports/model/library/Library.js new file mode 100644 index 00000000..6688d0e5 --- /dev/null +++ b/app/imports/model/library/Library.js @@ -0,0 +1,47 @@ +Libraries = new Mongo.Collection("library"); + +Schemas.Library = new SimpleSchema({ + name: {type: String}, + owner: {type: String, regEx: SimpleSchema.RegEx.Id}, + readers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []}, + writers: {type: [String], regEx: SimpleSchema.RegEx.Id, defaultValue: []}, + public: {type: Boolean, defaultValue: false}, +}); + +Libraries.attachSchema(Schemas.Library); + +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); +}; diff --git a/app/imports/model/library/LibraryItems.js b/app/imports/model/library/LibraryItems.js new file mode 100644 index 00000000..569e1df9 --- /dev/null +++ b/app/imports/model/library/LibraryItems.js @@ -0,0 +1,44 @@ +LibraryItems = new Mongo.Collection("libraryItems"); + +Schemas.LibraryItems = new SimpleSchema({ + 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: Number, min: 0, defaultValue: 1}, + weight: {type: Number, min: 0, defaultValue: 0, decimal: true}, + value: {type: Number, min: 0, defaultValue: 0, decimal: true}, + requiresAttunement: {type: Boolean, defaultValue: false}, + + library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + + "settings.category": { + type: String, + optional: true, + allowedValues: [ + "adventuringGear", "armor", "weapons", "tools", + ], + }, + "settings.showIncrement": { + type: Boolean, + defaultValue: false, + }, + + effects: {type: [Schemas.LibraryEffects], defaultValue: []}, + attacks: {type: [Schemas.LibraryAttacks], defaultValue: []}, +}); + +LibraryItems.attachSchema(Schemas.LibraryItems); + +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/model/library/LibrarySpells.js b/app/imports/model/library/LibrarySpells.js new file mode 100644 index 00000000..bdbb67de --- /dev/null +++ b/app/imports/model/library/LibrarySpells.js @@ -0,0 +1,66 @@ +LibrarySpells = new Mongo.Collection("librarySpells"); + +Schemas.LibrarySpells = new SimpleSchema({ + 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: Number, + defaultValue: 1, + }, + school: { + type: String, + defaultValue: "Abjuration", + allowedValues: magicSchools, + }, + library: {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + effects: {type: [Schemas.LibraryEffects], defaultValue: []}, + 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/model/library/subSchemas/LibraryAttacks.js b/app/imports/model/library/subSchemas/LibraryAttacks.js new file mode 100644 index 00000000..d606acd5 --- /dev/null +++ b/app/imports/model/library/subSchemas/LibraryAttacks.js @@ -0,0 +1,41 @@ +Schemas.LibraryAttacks = new SimpleSchema({ + 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", + }, +}); diff --git a/app/imports/model/library/subSchemas/LibraryEffects.js b/app/imports/model/library/subSchemas/LibraryEffects.js new file mode 100644 index 00000000..4abcb1ef --- /dev/null +++ b/app/imports/model/library/subSchemas/LibraryEffects.js @@ -0,0 +1,40 @@ +Schemas.LibraryEffects = new SimpleSchema({ + 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, + decimal: true, + optional: true, + }, + calculation: { + type: String, + optional: true, + trim: false, + }, + //which stat the effect is applied to + stat: { + type: String, + optional: true, + }, +}); diff --git a/app/imports/model/meta/Blacklist.js b/app/imports/model/meta/Blacklist.js new file mode 100644 index 00000000..221c64ec --- /dev/null +++ b/app/imports/model/meta/Blacklist.js @@ -0,0 +1,9 @@ +Blacklist = new Mongo.Collection("blacklist"); + +Schemas.Blacklist = new SimpleSchema({ + userId: { + type: String, + }, +}); + +Blacklist.attachSchema(Schemas.Blacklist); diff --git a/app/imports/model/meta/ChangeLogs.js b/app/imports/model/meta/ChangeLogs.js new file mode 100644 index 00000000..e56842a1 --- /dev/null +++ b/app/imports/model/meta/ChangeLogs.js @@ -0,0 +1,27 @@ +ChangeLogs = new Mongo.Collection("changeLogs"); + +Schemas.ChangeLog = new SimpleSchema({ + version: { + type: String, + }, + changes: { + type: [String], + }, +}); + +ChangeLogs.attachSchema(Schemas.ChangeLog); + +ChangeLogs.allow({ + insert: function(userId, doc) { + var user = Meteor.users.findOne(userId); + if (user) return _.contains(user.roles, "admin"); + }, + update: function(userId, doc, fields, modifier) { + var user = Meteor.users.findOne(userId); + if (user) return _.contains(user.roles, "admin"); + }, + remove: function(userId, doc) { + var user = Meteor.users.findOne(userId); + if (user) return _.contains(user.roles, "admin"); + }, +}); diff --git a/app/imports/model/meta/Reports.js b/app/imports/model/meta/Reports.js new file mode 100644 index 00000000..21ac4fe7 --- /dev/null +++ b/app/imports/model/meta/Reports.js @@ -0,0 +1,79 @@ +Reports = new Mongo.Collection("reports"); + +Schemas.Report = new SimpleSchema({ + owner: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + title: { + type: String, + trim: false, + optional: true, + }, + description: { + type: String, + trim: false, + optional: true, + }, + type: { + type: String, + allowedValues: ["General Feedback", "Bug", "Suggested Change", "Feature Request"], + defaultValue: "General Feedback", + }, + //the immediate impact of doing this action (eg. -1 rages) + severity: { + type: Number, + defaultValue: 5, + min: 1, + max: 10, + }, + metaData: { + type: Object, + blackbox: true, + }, +}); + +Reports.attachSchema(Schemas.Report); + +Meteor.methods({ + insertReport: function(report) { + check(report, { + title: String, + description: String, + type: String, + severity: Number, + metaData: Object, + }); + report.owner = this.userId; + var id = Reports.insert(report); + var user = Meteor.users.findOne(this.userId); + var sender = user && + user.emails && + user.emails[0] && + user.emails[0].address || + user.services && + user.services.google && + user.services.google.email || + "reports@dicecloud.com"; + var bodyText = "Report ID: " + id + + "\nSeverity: " + report.severity + + "\nType: " + report.type + + "\n\n" + report.description; + Email.send({ + from: sender, + to: "stefan.zermatten@gmail.com", + subject: "DiceCloud feedback - " + report.title, + text: bodyText, + }); + }, + deleteReport: function(id) { + var user = Meteor.users.findOne(this.userId); + if (!_.contains(user.roles, "admin")){ + throw new Meteor.Error( + "not admin", + "The user must be an administrator to delete feedback" + ); + } + Reports.remove(id); + }, +}); diff --git a/app/imports/model/parenting.js b/app/imports/model/parenting.js new file mode 100644 index 00000000..21edb341 --- /dev/null +++ b/app/imports/model/parenting.js @@ -0,0 +1,242 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; + +let childSchema = new SimpleSchema({ + parent: {type: Object}, + "parent.collection": {type: String}, + "parent.id": {type: String, regEx: SimpleSchema.RegEx.Id, index: 1}, + "parent.group": {type: String, optional: true}, + "removedWith": { + optional: true, + type: String, + regEx: SimpleSchema.RegEx.Id, + }, +}); + +let joinWithDefaultKeys = function(keys){ + let defaultKeys = [ + "charId", + ]; + return _.union(keys, defaultKeys); +}; + +let limitModifierToKeys = function(modifier, keys){ + if (!modifier) return; + modifier = _.pick(modifier, ["$set", "$unset"]); + if (modifier.$set) modifier.$set = _.pick(modifier.$set, keys); + if (modifier.$unset) modifier.$unset = _.pick(modifier.$unset, keys); + if (_.isEmpty(modifier.$set)) delete modifier.$set; + if (_.isEmpty(modifier.$unset)) delete modifier.$unset; + return modifier; +}; + +let getParent = function(doc){ + if (!doc || !doc.parent) return; + let parentCol = Meteor.isClient ? + window[doc.parent.collection] : global[doc.parent.collection]; + if (parentCol) + return parentCol.findOne(doc.parent.id, {removed: true}); +}; + +let inheritParentProperties = function(doc, collection){ + let parent = getParent(doc); + if (!parent) throw new Meteor.Error( + "Parenting Error", + "Document's parent does not exist" + ); + let handMeDowns = _.pick(parent, collection.inheritedKeys); + if ( + _.contains(collection.inheritedKeys, "charId") && + doc.parent.collection === "Characters" + ){ + handMeDowns.charId = doc.parent.id; + } + if (_.isEmpty(handMeDowns)) return; + collection.update(doc._id, {$set: handMeDowns}); +}; + +let childCollections = []; + +let makeChild = function(collection, inheritedKeys){ + inheritedKeys = inheritedKeys || []; + if (inheritedKeys) { + collection.inheritedKeys = joinWithDefaultKeys(inheritedKeys); + } + collection.helpers({ + //returns the parent even if it's removed + getParent: function(){ + return getParent(this); + }, + getParentCollection: function(){ + return Meteor.isClient ? + window[this.parent.collection] : global[this.parent.collection]; + }, + }); + + //when created, inherit parent properties + collection.after.insert(function(userId, doc){ + inheritParentProperties(doc, collection); + }); + + collection.before.update(function(userId, doc, fieldNames, modifier, options){ + //if we are restoring this asset, unmark that it was removed with its parent, we no longer care + if (modifier && modifier.$unset && modifier.$unset.removed) { + modifier.$unset.removedWith = ""; + } + }); + + collection.after.update(function(userId, doc, fieldNames, modifier, options) { + if (modifier && modifier.$set && modifier.$set["parent.id"]){ + //when we change parents, inherit its properties + inheritParentProperties(doc, collection); + } + }); + + collection.softRemoveNode = collection.softRemoveNode || function(id){ + collection.softRemove(id); + }; + + collection.restoreNode = collection.restoreNode || function(id){ + collection.restore(id); + }; + + collection.attachSchema(childSchema); + + childCollections.push(collection); +}; + +let makeParent = function(collection, donatedKeys){ + donatedKeys = joinWithDefaultKeys(donatedKeys); + let collectionName = collection._collection.name; + //after changing, push the changes to all children + collection.after.update(function(userId, doc, fieldNames, modifier, options) { + modifier = limitModifierToKeys(modifier, donatedKeys); + doc = _.pick(doc, ["_id", "charId"]); + if (!modifier) return; + Meteor.call("updateChildren", doc, modifier, true); + }); + collection.softRemoveNode = function(id){ + Meteor.call("softRemoveNode", collectionName, id); + }; + + collection.restoreNode = function(id){ + Meteor.call("restoreNode", collectionName, id); + }; + + if (Meteor.isServer) collection.after.remove(function(userId, doc) { + _.each(childCollections, function(collection){ + collection.remove( + {"parent.id": doc._id} + ); + }); + }); +}; + +let checkPermission = function(userId, charId){ + let char = Characters.findOne(charId, {fields: {owner: 1, writers: 1}}); + if (!char) + throw new Meteor.Error("Access Denied, no charId", + "Character " + charId + " does not exist"); + if (!userId) + throw new Meteor.Error("Access Denied, no userId", + "No UserId set when trying to update character asset."); + if (char.owner !== userId && !_.contains(char.writers, userId)) + throw new Meteor.Error("Access Denied, not permitted", + "Not permitted to update assets of this character."); + return true; +}; + +let cascadeSoftRemove = function(id, removedWithId){ + _.each(childCollections, function(treeCollection){ + treeCollection.update( + {"parent.id": id}, + {$set: { + removed: true, + removedWith: removedWithId, + }}, + {multi: true} + ); + treeCollection.find({"parent.id": id}).forEach(function(doc){ + cascadeSoftRemove(doc._id, removedWithId); + }); + }); +}; + +let checkRemovePermission = function(collectionName, id, self){ + check(collectionName, String); + check(id, String); + let collection = Mongo.Collection.get(collectionName); + let node = collection.findOne(id); + let charId = node && node.charId; + checkPermission(self.userId, charId); +}; + +const softRemoveNode = new ValidatedMethod({ + name: "parenting.methods.softRemoveNode", + + validate: new SimpleSchema({ + collectionName: {type: String,}, + id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), // argument validation + + run({collectionName, id}){ + checkRemovePermission(collectionName, id, this); + let collection = Mongo.Collection.get(collectionName); + collection.softRemove(id); + cascadeSoftRemove(id, id); + }, +}); + +const restoreNode = new ValidatedMethod({ + run(collectionName, id){ + checkRemovePermission(collectionName, id, this); + let collection = Mongo.Collection.get(collectionName); + collection.restore(id); + _.each(childCollections, function(treeCollection){ + treeCollection.update( + {removedWith: id, removed: true}, + {$unset: {removed: true, removedWith: ""}}, + {multi: true} + ); + }); + }, +}); + +const updateChildren = new ValidatedMethod({ + run({parent, modifier, limitToInheritance}){ + check(parent, {_id: String, charId: String}); + check(modifier, Object); + checkPermission(this.userId, parent.charId); + let selector = {"parent.id": parent._id}; + _.each(childCollections, function(collection){ + let thisModifier; + if (limitToInheritance){ + thisModifier = limitModifierToKeys(modifier, collection.inheritedKeys); + } else { + thisModifier = _.clone(modifier); + } + if (_.isEmpty(thisModifier)) return; + collection.update(selector, thisModifier, {multi: true, removed: true}); + }); + }, +}); + +const cloneChildren = new ValidatedMethod({ + run({objectId, newParent}){ + check(objectId, String); + check(newParent, {id: String, collection: String}); + + _.each(childCollections, function(collection){ + let keys = collection.simpleSchema().objectKeys(); + collection.find({"parent.id": objectId}).forEach(function(doc){ + let newDoc = _.pick(doc, keys); + newDoc.parent = newParent; + collection.insert(newDoc); + }); + }); + } +}) + +export {makeChild, makeParent, softRemoveNode}; diff --git a/app/imports/model/users/Users.js b/app/imports/model/users/Users.js new file mode 100644 index 00000000..a818962e --- /dev/null +++ b/app/imports/model/users/Users.js @@ -0,0 +1,110 @@ +Schemas.UserProfile = new SimpleSchema({ + username: { + type: String, + optional: true, + }, +}); + +Schemas.User = new SimpleSchema({ + username: { + type: String, + optional: true, + }, + profile: { + type: Schemas.UserProfile, + optional: true, + }, + emails: { + type: Array, + optional: true, + }, + "emails.$": { + type: Object, + }, + "emails.$.address": { + type: String, + regEx: SimpleSchema.RegEx.Email, + }, + "emails.$.verified": { + type: Boolean, + }, + registered_emails: { + type: Array, + optional: true, + }, + "registered_emails.$": { + type: Object, + blackbox: true, + }, + createdAt: { + type: Date + }, + services: { + type: Object, + optional: true, + blackbox: true, + }, + roles: { + type: Object, + optional: true, + blackbox: true, + }, + roles: { + type: Array, + optional: true, + }, + "roles.$": { + type: String + }, + // In order to avoid an 'Exception in setInterval callback' from Meteor + heartbeat: { + type: Date, + optional: true, + }, + apiKey: { + type: String, + index: 1, + optional: true, + }, +}); + +Meteor.users.attachSchema(Schemas.User); + +Meteor.users.gnerateApiKey = new ValidatedMethod({ + name: "Users.methods.generateApiKey", + validate: null, + run(){ + if(Meteor.isClient) return; + var user = Meteor.users.findOne(this.userId); + if (!user) return; + if (user && user.apiKey) return; + var apiKey = Random.id(30); + Meteor.users.update(this.userId, {$set: {apiKey}}); + }, +}); + +Meteor.users.sendVerificationEmail = new ValidatedMethod({ + name: "Users.methods.sendVerificationEmail", + validate: new SimpleSchema({ + userId:{ + type: String, + optional: true, + }, + address: { + type: String, + }, + }).validator(), + run(userId, address){ + userId = this.userId || userId; + let user = Meteor.users.findOne(); + if (!user) { + throw new Meteor.Error("User not found", + "Can't send a validation email to a user that does not exist"); + } + if (!_.some(user.emails, email => email.address === address)) { + throw new Meteor.Error("Email address not found", + "The specified email address wasn't found on this user account"); + } + Accounts.sendVerificationEmail(this.userId, address); + } +});