diff --git a/README.md b/README.md index c1dd7f4c..e7d3b9f3 100644 --- a/README.md +++ b/README.md @@ -66,5 +66,20 @@ You should see this: => App running at: http://localhost:3000/ ``` +Environmental Variables +----------------------- + +``` +MAIL_URL=smtp:// +METEOR_SETTINGS={ "public": { "environment": "production", "patreon": { "clientId": "", "campaignId": "" } }, "patreon": { "clientSecret": "", "creatorAccessToken": "" } } +MONGO_OPLOG_URL=mongodb+srv:// +MONGO_URL=mongodb+srv:// +NPM_CONFIG_PRODUCTION=true +PROJECT_DIR=app +ROOT_URL=https:// +DEFAULT_LIBRARIES= +DISABLE_PATREON=<"true" if you want to prevent features being locked behind Patreon tiers> +``` + Now, visiting [](http://localhost:3000/) should show you an empty instance of DiceCloud running. diff --git a/app/.meteor/packages b/app/.meteor/packages index 2c8bea0d..d0f672c2 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -3,42 +3,42 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -accounts-password@1.7.0 -random@1.2.0 +accounts-password +random dburles:collection-helpers -reactive-var@1.0.11 -underscore@1.0.10 +reactive-var +underscore momentjs:moment dburles:mongo-collection-instances -accounts-google@1.3.3 -email@2.0.0 +accounts-google +email meteorhacks:subs-manager chuangbo:marked -meteor-base@1.4.0 -mobile-experience@1.1.0 -mongo@1.11.0 -session@1.2.0 -tracker@1.2.0 -logging@1.2.0 -reload@1.3.1 -ejson@1.1.1 -check@1.3.1 -standard-minifier-js@2.6.0 -shell-server@0.5.0 +meteor-base +mobile-experience +mongo +session +tracker +logging +reload +ejson +check +standard-minifier-js +shell-server templates:array -ecmascript@0.15.1 -es5-shim@4.8.0 -reactive-dict@1.3.0 +ecmascript +es5-shim +reactive-dict percolate:synced-cron ongoworks:speakingurl -service-configuration@1.0.11 -dynamic-import@0.6.0 -ddp-rate-limiter@1.0.9 -rate-limit@1.0.9 +service-configuration +dynamic-import +ddp-rate-limiter +rate-limit mdg:validated-method akryum:vue-router2 static-html -aldeed:collection2@3.0.0 +aldeed:collection2 aldeed:schema-index zer0th:meteor-vuetify-loader accounts-patreon diff --git a/app/.meteor/release b/app/.meteor/release index d8fd7cf5..59245ca5 100644 --- a/app/.meteor/release +++ b/app/.meteor/release @@ -1 +1 @@ -METEOR@2.2 +METEOR@2.2.1 diff --git a/app/.meteor/versions b/app/.meteor/versions index 77db19b5..586384a6 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -1,7 +1,7 @@ accounts-base@1.9.0 accounts-google@1.3.3 accounts-oauth@1.2.0 -accounts-password@1.7.0 +accounts-password@1.7.1 accounts-patreon@0.1.0 akryum:npm-check@0.1.2 akryum:vue-component@0.15.2 @@ -9,19 +9,19 @@ akryum:vue-component-dev-client@0.4.7 akryum:vue-component-dev-server@0.1.4 akryum:vue-router2@0.2.3 akryum:vue-sass@0.1.2 -aldeed:collection2@3.3.0 +aldeed:collection2@3.4.1 aldeed:schema-index@3.0.0 allow-deny@1.1.0 autoupdate@1.7.0 -babel-compiler@7.6.1 +babel-compiler@7.6.2 babel-runtime@1.5.0 base64@1.0.12 binary-heap@1.0.11 -blaze-tools@1.1.1 +blaze-tools@1.1.2 boilerplate-generator@1.7.1 -bozhao:link-accounts@2.3.2 +bozhao:link-accounts@2.4.0 caching-compiler@1.2.2 -caching-html-compiler@1.2.0 +caching-html-compiler@1.2.1 callback-hook@1.3.0 check@1.3.1 chuangbo:marked@0.3.5_1 @@ -30,17 +30,17 @@ coffeescript-compiler@2.4.1 dburles:collection-helpers@1.1.0 dburles:mongo-collection-instances@0.3.5 ddp@1.4.0 -ddp-client@2.4.0 +ddp-client@2.4.1 ddp-common@1.4.0 ddp-rate-limiter@1.0.9 -ddp-server@2.3.2 +ddp-server@2.3.3 deps@1.0.12 diff-sequence@1.1.1 dynamic-import@0.6.0 ecmascript@0.15.1 ecmascript-runtime@0.7.0 -ecmascript-runtime-client@0.11.0 -ecmascript-runtime-server@0.10.0 +ecmascript-runtime-client@0.11.1 +ecmascript-runtime-server@0.10.1 ejson@1.1.1 email@2.0.0 es5-shim@4.8.0 @@ -48,10 +48,10 @@ fetch@0.1.1 geojson-utils@1.0.10 google-oauth@1.3.0 hot-code-push@1.0.4 -html-tools@1.1.1 -htmljs@1.1.0 -http@1.4.3 -id-map@1.1.0 +html-tools@1.1.2 +htmljs@1.1.1 +http@1.4.4 +id-map@1.1.1 inter-process-messaging@0.1.1 lai:collection-extensions@0.2.1_1 launch-screen@1.2.1 @@ -63,8 +63,8 @@ meteor@1.9.3 meteor-base@1.4.0 meteorhacks:subs-manager@1.6.4 mikowals:batch-insert@1.2.0 -minifier-css@1.5.3 -minifier-js@2.6.0 +minifier-css@1.5.4 +minifier-js@2.6.1 minimongo@1.6.2 mobile-experience@1.1.0 mobile-status-bar@1.1.0 @@ -72,11 +72,11 @@ modern-browsers@0.1.5 modules@0.16.0 modules-runtime@0.12.0 momentjs:moment@2.29.1 -mongo@1.11.0 +mongo@1.11.1 mongo-decimal@0.1.2 mongo-dev-server@1.1.0 -mongo-id@1.0.7 -npm-bcrypt@0.9.3 +mongo-id@1.0.8 +npm-bcrypt@0.9.4 npm-mongo@3.9.0 oauth@1.3.2 oauth2@1.3.0 @@ -98,7 +98,7 @@ promise@0.11.2 raix:eventemitter@1.0.0 random@1.2.0 rate-limit@1.0.9 -react-fast-refresh@0.1.0 +react-fast-refresh@0.1.1 reactive-dict@1.3.0 reactive-var@1.0.11 reload@1.3.1 @@ -112,18 +112,18 @@ shell-server@0.5.0 simple:json-routes@2.1.0 simple:rest@1.1.1 simple:rest-method-mixin@1.0.1 -socket-stream-client@0.3.1 -spacebars-compiler@1.2.1 +socket-stream-client@0.3.3 +spacebars-compiler@1.3.0 srp@1.1.0 -standard-minifier-js@2.6.0 -static-html@1.3.0 +standard-minifier-js@2.6.1 +static-html@1.3.2 templates:array@1.0.3 -templating-tools@1.2.0 -tmeasday:check-npm-versions@1.0.1 +templating-tools@1.2.1 +tmeasday:check-npm-versions@1.0.2 tracker@1.2.0 typescript@4.2.2 underscore@1.0.10 -url@1.3.1 +url@1.3.2 webapp@1.10.1 webapp-hashing@1.1.0 zer0th:meteor-vuetify-loader@0.1.30 diff --git a/app/client/head.html b/app/client/head.html index 4984d688..85ef16df 100644 --- a/app/client/head.html +++ b/app/client/head.html @@ -1,5 +1,6 @@ - + + diff --git a/app/imports/api/creature/Parties.js b/app/imports/api/creature/Parties.js deleted file mode 100644 index e0f90c60..00000000 --- a/app/imports/api/creature/Parties.js +++ /dev/null @@ -1,28 +0,0 @@ -import SimpleSchema from 'simpl-schema'; - -let Parties = new Mongo.Collection('parties'); - -let partySchema = new SimpleSchema({ - name: { - type: String, - defaultValue: 'New Party', - trim: false, - optional: true, - }, - creatures: { - type: Array, - defaultValue: [], - }, - 'creatures.$': { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, - owner: { - type: String, - regEx: SimpleSchema.RegEx.Id, - }, -}); - -Parties.attachSchema(partySchema); - -export default Parties; diff --git a/app/imports/api/creature/actions/applyProperties.js b/app/imports/api/creature/actions/applyProperties.js index fadde1ae..e4e72c47 100644 --- a/app/imports/api/creature/actions/applyProperties.js +++ b/app/imports/api/creature/actions/applyProperties.js @@ -62,6 +62,7 @@ function applyPropertyAndWalkChildren({prop, children, targets, ...options}){ export default function applyProperties({ forest, targets, ...options}){ forest.forEach(node => { let prop = node.node; + options.actionContext[`#${prop.type}`] = prop; let children = node.children; if (shouldSplit(prop) && targets.length){ targets.forEach(target => { diff --git a/app/imports/api/creature/actions/castSpellWithSlot.js b/app/imports/api/creature/actions/castSpellWithSlot.js index 1a84e84c..1b1408ff 100644 --- a/app/imports/api/creature/actions/castSpellWithSlot.js +++ b/app/imports/api/creature/actions/castSpellWithSlot.js @@ -2,12 +2,13 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { doActionWork } from '/imports/api/creature/actions/doAction.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; +import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js'; const castSpellWithSlot = new ValidatedMethod({ name: 'creatureProperties.castSpellWithSlot', @@ -61,9 +62,11 @@ const castSpellWithSlot = new ValidatedMethod({ value: 1, }); } + let actionContext = getAncestorContext(spell); + doActionWork({ action: spell, - context: {slotLevel}, + actionContext: {slotLevel, ...actionContext}, creature, targets: target ? [target] : [], method: this, diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/creature/actions/doAction.js index 2b8209b5..27d560db 100644 --- a/app/imports/api/creature/actions/doAction.js +++ b/app/imports/api/creature/actions/doAction.js @@ -2,15 +2,16 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { nodesToTree } from '/imports/api/parenting/parenting.js'; import applyProperties from '/imports/api/creature/actions/applyProperties.js'; import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; +import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doAction', @@ -36,6 +37,10 @@ const doAction = new ValidatedMethod({ let action = CreatureProperties.findOne(actionId); // Check permissions let creature = getRootCreatureAncestor(action); + + // Build ancestor context + let actionContext = getAncestorContext(action); + assertEditPermission(creature, this.userId); let targets = []; targetIds.forEach(targetId => { @@ -43,7 +48,7 @@ const doAction = new ValidatedMethod({ assertEditPermission(target, this.userId); targets.push(target); }); - doActionWork({action, creature, targets, method: this}); + doActionWork({action, creature, targets, actionContext, method: this}); // The acting creature might have used ammo recomputeInventory(creature._id); @@ -64,7 +69,7 @@ export function doActionWork({ action, creature, targets, - context = {}, + actionContext = {}, method }){ // Create the log @@ -83,7 +88,7 @@ export function doActionWork({ }]; applyProperties({ forest: startingForest, - actionContext: context, + actionContext, creature, targets, log, diff --git a/app/imports/api/creature/actions/doCheck.js b/app/imports/api/creature/actions/doCheck.js index 76e421a3..410462a7 100644 --- a/app/imports/api/creature/actions/doCheck.js +++ b/app/imports/api/creature/actions/doCheck.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/Creatures.js'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import roll from '/imports/parser/roll.js'; const doCheck = new ValidatedMethod({ diff --git a/app/imports/api/creature/actions/getAncestorContext.js b/app/imports/api/creature/actions/getAncestorContext.js new file mode 100644 index 00000000..9508107b --- /dev/null +++ b/app/imports/api/creature/actions/getAncestorContext.js @@ -0,0 +1,15 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; + +export default function getAncestorContext(prop){ + // Build ancestor context + const actionContext = {}; + let ancestorIds = prop.ancestors.map(ref => ref.id); + CreatureProperties.find({ + _id: {$in: ancestorIds} + }, { + sort: {order: 1}, + }).forEach(ancestor => { + actionContext[`#${ancestor.type}`] = ancestor; + }); + return actionContext; +} diff --git a/app/imports/api/creature/archive/ArchivedCreatures.js b/app/imports/api/creature/archive/ArchivedCreatures.js new file mode 100644 index 00000000..4a572482 --- /dev/null +++ b/app/imports/api/creature/archive/ArchivedCreatures.js @@ -0,0 +1,57 @@ +import SimpleSchema from 'simpl-schema'; + +// Archived creatures is an immutable collection of creatures that are no longer +// in use and can be safely archived by the mongoDB hosting service. +// It keeps the working datasets like creatureProperties much smaller +// than they would otherwise be. +let ArchivedCreatures = new Mongo.Collection('archivedCreatures'); + +// We use blackbox objects for everything: +// - saves time checking every object against a schema +// - doesn't accidentaly create indices defined in subschemas +// - The objects we are archiving have already been checked against their +// own schemas +let ArchivedCreatureSchema = new SimpleSchema({ + owner: { + type: String, + regEx: SimpleSchema.RegEx.Id, + // The primary index on this collection + index: 1, + }, + archiveDate: { + type: Date, + // Indexed so the archiving system can archive documents when they + // get to a certain age + index: 1, + }, + creature: { + type: Object, + blackbox: true, + }, + properties: { + type: Array, + }, + 'properties.$': { + type: Object, + blackbox: true, + }, + experiences: { + type: Array, + }, + 'experiences.$': { + type: Object, + blackbox: true, + }, + logs: { + type: Array, + }, + 'logs.$': { + type: Object, + blackbox: true, + }, +}); + +ArchivedCreatures.attachSchema(ArchivedCreatureSchema); + +import '/imports/api/creature/archive/methods/index.js'; +export default ArchivedCreatures; diff --git a/app/imports/api/creature/archive/methods/archiveCreatures.js b/app/imports/api/creature/archive/methods/archiveCreatures.js new file mode 100644 index 00000000..304cce11 --- /dev/null +++ b/app/imports/api/creature/archive/methods/archiveCreatures.js @@ -0,0 +1,66 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; +import Experiences from '/imports/api/creature/experience/Experiences.js'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; +import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; + +function archiveCreature(creatureId){ + // Build the archive document + const creature = Creatures.findOne(creatureId); + const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch(); + const experiences = Experiences.find({creatureId}).fetch(); + const logs = CreatureLogs.find({creatureId}).fetch(); + let archiveCreature = { + owner: creature.owner, + archiveDate: new Date(), + creature, + properties, + experiences, + logs, + }; + + // Insert it + let id = ArchivedCreatures.insert(archiveCreature); + + // Remove the original creature + removeCreatureWork(creatureId); + + return id; +} + +const archiveCreatures = new ValidatedMethod({ + name: 'Creatures.methods.archiveCreatures', + validate: new SimpleSchema({ + creatureIds: { + type: Array, + max: 10, + }, + 'creatureIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({creatureIds}) { + for (let id of creatureIds){ + assertOwnership(id, this.userId) + } + let archivedIds = []; + for (let id of creatureIds){ + let archivedId = archiveCreature(id); + archivedIds.push(archivedId); + } + return archivedIds; + }, +}); + +export default archiveCreatures; diff --git a/app/imports/api/creature/archive/methods/index.js b/app/imports/api/creature/archive/methods/index.js new file mode 100644 index 00000000..62d8bd8d --- /dev/null +++ b/app/imports/api/creature/archive/methods/index.js @@ -0,0 +1,2 @@ +import '/imports/api/creature/archive/methods/archiveCreatures.js'; +import '/imports/api/creature/archive/methods/restoreCreatures.js'; diff --git a/app/imports/api/creature/archive/methods/restoreCreatures.js b/app/imports/api/creature/archive/methods/restoreCreatures.js new file mode 100644 index 00000000..b4ec2c44 --- /dev/null +++ b/app/imports/api/creature/archive/methods/restoreCreatures.js @@ -0,0 +1,77 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; +import Experiences from '/imports/api/creature/experience/Experiences.js'; +import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; +import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js'; + +function restoreCreature(archiveId){ + // Get the archive + const archivedCreature = ArchivedCreatures.findOne(archiveId); + + // Insert the creature sub documents + // They still have their original _id's + Creatures.insert(archivedCreature.creature); + try { + // Add all the properties + if (archivedCreature.properties && archivedCreature.properties.length){ + CreatureProperties.batchInsert(archivedCreature.properties); + } + if (archivedCreature.experiences && archivedCreature.experiences.length){ + Experiences.batchInsert(archivedCreature.experiences); + } + if (archivedCreature.logs && archivedCreature.logs.length){ + CreatureLogs.batchInsert(archivedCreature.logs); + } + // Remove the archived creature + ArchivedCreatures.remove(archiveId); + } catch (e) { + // If the above fails, delete the inserted creature + removeCreatureWork(archivedCreature.creature._id); + throw e; + } + + // Do not recompute. The creature was in a computed and ordered state when + // we archived it, just restore everything as-is + + return archivedCreature.creature._id; +} + +const restoreCreatures = new ValidatedMethod({ + name: 'Creatures.methods.restoreCreatures', + validate: new SimpleSchema({ + archiveIds: { + type: Array, + max: 10, + }, + 'archiveIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({archiveIds}) { + for (let id of archiveIds){ + let archivedCreature = ArchivedCreatures.findOne(id, { + fields: {owner: 1} + }); + assertOwnership(archivedCreature, this.userId) + } + let creatureIds = []; + for (let id of archiveIds){ + let creatureId = restoreCreature(id); + creatureIds.push(creatureId); + } + return creatureIds; + }, +}); + +export default restoreCreatures; diff --git a/app/imports/api/creature/creatureComputation.test.js b/app/imports/api/creature/computation/creatureComputation.test.js similarity index 100% rename from app/imports/api/creature/creatureComputation.test.js rename to app/imports/api/creature/computation/creatureComputation.test.js diff --git a/app/imports/api/creature/computation/engine/EffectAggregator.js b/app/imports/api/creature/computation/engine/EffectAggregator.js index 649d6e31..83e30f32 100644 --- a/app/imports/api/creature/computation/engine/EffectAggregator.js +++ b/app/imports/api/creature/computation/engine/EffectAggregator.js @@ -1,6 +1,6 @@ export default class EffectAggregator{ constructor(){ - this.base = 0; + this.base = undefined; this.add = 0; this.mul = 1; this.min = Number.NEGATIVE_INFINITY; @@ -20,7 +20,13 @@ export default class EffectAggregator{ switch(effect.operation){ case 'base': // Take the largest base value - this.base = result > this.base ? result : this.base; + if (Number.isFinite(result)){ + if(Number.isFinite(this.base)){ + this.base = Math.max(this.base, result); + } else { + this.base = result; + } + } break; case 'add': // Add all adds together diff --git a/app/imports/api/creature/computation/engine/combineStat.js b/app/imports/api/creature/computation/engine/combineStat.js index 23225a42..53e48cd4 100644 --- a/app/imports/api/creature/computation/engine/combineStat.js +++ b/app/imports/api/creature/computation/engine/combineStat.js @@ -14,7 +14,14 @@ export default function combineStat(stat, aggregator, memo){ } function getAggregatorResult(stat, aggregator){ - let base = Math.max(aggregator.base, stat.baseValue || 0); + let base; + if (!Number.isFinite(aggregator.base)){ + base = stat.baseValue || 0; + } else if (!Number.isFinite(stat.baseValue)){ + base = aggregator.base || 0; + } else { + base = Math.max(aggregator.base, stat.baseValue); + } let result = (base + aggregator.add) * aggregator.mul; if (result < aggregator.min) { result = aggregator.min; @@ -137,7 +144,8 @@ function combineSkill(stat, aggregator, memo){ } // Combine everything to get the final result - let result = (aggregator.base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul; + let base = aggregator.base || 0; + let result = (base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul; if (result < aggregator.min) result = aggregator.min; if (result > aggregator.max) result = aggregator.max; if (aggregator.set !== undefined) { diff --git a/app/imports/api/creature/computation/engine/computeStat.js b/app/imports/api/creature/computation/engine/computeStat.js index e2812dc2..3b411e8d 100644 --- a/app/imports/api/creature/computation/engine/computeStat.js +++ b/app/imports/api/creature/computation/engine/computeStat.js @@ -102,24 +102,31 @@ export default function computeStat(stat, memo){ prop: statInstance, memo }); - statInstance.baseValue = +result.value; + result.value = +result.value; + if (!isNaN(result.value)){ + statInstance.baseValue = result.value; + } else { + statInstance.baseValue = undefined; + } statInstance.dependencies = union(statInstance.dependencies, dependencies); if (context.errors.length){ statInstance.baseValueErrors = context.errors; } // Apply all the base values - effects.push({ - operation: 'base', - calculation: statInstance.baseValueCalculation, - result: statInstance.baseValue, - stats: [statInstance.variableName], - dependencies: statInstance.overridden ? + if (Number.isFinite(statInstance.baseValue)){ + effects.push({ + operation: 'base', + calculation: statInstance.baseValueCalculation, + result: statInstance.baseValue, + stats: [statInstance.variableName], + dependencies: statInstance.overridden ? union(statInstance.dependencies, [statInstance._id]) : [], - computationDetails: { - computed: true, - }, - }); + computationDetails: { + computed: true, + }, + }); + } } }); diff --git a/app/imports/api/creature/computation/engine/writeCreatureVariables.js b/app/imports/api/creature/computation/engine/writeCreatureVariables.js index 11749005..c8ccb7d8 100644 --- a/app/imports/api/creature/computation/engine/writeCreatureVariables.js +++ b/app/imports/api/creature/computation/engine/writeCreatureVariables.js @@ -1,5 +1,5 @@ import { pick, forOwn } from 'lodash'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import VERSION from '/imports/constants/VERSION.js'; export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) { diff --git a/app/imports/api/creature/computation/methods/recomputeCreature.js b/app/imports/api/creature/computation/methods/recomputeCreature.js index 74e78ba5..781304ae 100644 --- a/app/imports/api/creature/computation/methods/recomputeCreature.js +++ b/app/imports/api/creature/computation/methods/recomputeCreature.js @@ -1,7 +1,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js'; import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js'; import computeMemo from '/imports/api/creature/computation/engine/computeMemo.js'; @@ -11,7 +11,7 @@ import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalis import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; export const recomputeCreature = new ValidatedMethod({ diff --git a/app/imports/api/creature/creatureFolders/CreatureFolders.js b/app/imports/api/creature/creatureFolders/CreatureFolders.js new file mode 100644 index 00000000..3d00b3a9 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/CreatureFolders.js @@ -0,0 +1,37 @@ +import SimpleSchema from 'simpl-schema'; + +let CreatureFolders = new Mongo.Collection('creatureFolders'); + +let creatureFolderSchema = new SimpleSchema({ + name: { + type: String, + trim: false, + optional: true, + }, + creatures: { + type: Array, + defaultValue: [], + }, + 'creatures.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + owner: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, + archived: { + type: Boolean, + optional: true, + }, + order: { + type: Number, + defaultValue: 0, + }, +}); + +CreatureFolders.attachSchema(creatureFolderSchema); + +import '/imports/api/creature/creatureFolders/methods.js/index.js'; +export default CreatureFolders; diff --git a/app/imports/api/creature/creatureFolders/methods.js/index.js b/app/imports/api/creature/creatureFolders/methods.js/index.js new file mode 100644 index 00000000..26782576 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/index.js @@ -0,0 +1,4 @@ +import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js'; +import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js'; +import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js'; +import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js'; diff --git a/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js new file mode 100644 index 00000000..8df44d40 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js @@ -0,0 +1,46 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const insertCreatureFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.insert', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run() { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.insert.denied', + 'You need to be logged in to insert a folder'); + } + // Limit folders to 50 per user + let existingFolders = CreatureFolders.find({ + owner: userId + }, { + fields: {order: 1}, + sort: {order :-1} + }); + if (existingFolders.count() >= 50){ + throw new Meteor.Error('creatureFolders.methods.insert.denied', + 'You can not have more than 50 folders'); + } + // Make the new folder the last in the order + let order = 0; + let lastFolder = existingFolders.fetch()[0]; + if (lastFolder){ + order = (lastFolder.order || 0) + 1; + } + // Insert + return CreatureFolders.insert({ + name: 'Folder', + owner: userId, + order, + }); + }, +}); + +export default insertCreatureFolder; diff --git a/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js b/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js new file mode 100644 index 00000000..46f9b696 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js @@ -0,0 +1,45 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const moveCreatureToFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.moveCreatureToFolder', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({creatureId, folderId}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'You need to be logged in to remove a folder'); + } + // Check that this folder is owned by the user + if (folderId){ + let existingFolder = CreatureFolders.findOne(folderId); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'This folder does not belong to you'); + } + } + // Remove from other folders + CreatureFolders.update({ + owner: userId + }, { + $pull: {creatures: creatureId}, + }, { + multi: true, + }); + if (folderId){ + // Add to this folder + CreatureFolders.update(folderId, { + $addToSet: {creatures: creatureId}, + }); + } + }, +}); + +export default moveCreatureToFolder; diff --git a/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js new file mode 100644 index 00000000..71006cb7 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js @@ -0,0 +1,31 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const removeCreatureFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.remove', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'You need to be logged in to remove a folder'); + } + // Check that this folder is owned by the user + let existingFolder = CreatureFolders.findOne(_id); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'This folder does not belong to you'); + } + // Remove + return CreatureFolders.remove(_id); + }, +}); + +export default removeCreatureFolder; \ No newline at end of file diff --git a/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js new file mode 100644 index 00000000..92ecb418 --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/reorderCreatureFolder.js @@ -0,0 +1,43 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const reorderCreatureFolder = new ValidatedMethod({ + name: 'creatureFolders.methods.reorder', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, order}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.reorder.denied', + 'You need to be logged in to reorder a folder'); + } + // Check that this folder is owned by the user + let existingFolder = CreatureFolders.findOne(_id); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.reorder.denied', + 'This folder does not belong to you'); + } + // First give it the new order, it should end in 0.5 putting it between two other docs + CreatureFolders.update(_id, {$set: {order}}); + this.unblock(); + // Reorder all the folders with integer numbers in this new order + CreatureFolders.find({ + owner: userId + }, { + fields: {order: 1,}, + sort: {order: -1} + }).forEach((folder, index) => { + if (folder.order !== index){ + CreatureFolders.update(_id, {$set: {order: index}}) + } + }); + }, +}); + +export default reorderCreatureFolder; diff --git a/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js new file mode 100644 index 00000000..d798e59a --- /dev/null +++ b/app/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js @@ -0,0 +1,31 @@ +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const updateCreatureFolderName = new ValidatedMethod({ + name: 'creatureFolders.methods.updateName', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, name}) { + // Ensure logged in + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'You need to be logged in to update a folder'); + } + // Check that this folder is owned by the user + let existingFolder = CreatureFolders.findOne(_id); + if (existingFolder.owner !== userId){ + throw new Meteor.Error('creatureFolders.methods.updateName.denied', + 'This folder does not belong to you'); + } + // Update + return CreatureFolders.update(_id, {$set: {name}}); + }, +}); + +export default updateCreatureFolderName; \ No newline at end of file diff --git a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js index d1768254..c4e2c862 100644 --- a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js +++ b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js @@ -1,4 +1,4 @@ -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; export default function getRootCreatureAncestor(property){ return Creatures.findOne(property.ancestors[0].id); diff --git a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js index 302fc8ce..2a67299c 100644 --- a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js +++ b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js @@ -2,7 +2,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js'; diff --git a/app/imports/api/creature/creatureProperties/methods/dealDamage.js b/app/imports/api/creature/creatureProperties/methods/dealDamage.js index 270221b9..ea3f3607 100644 --- a/app/imports/api/creature/creatureProperties/methods/dealDamage.js +++ b/app/imports/api/creature/creatureProperties/methods/dealDamage.js @@ -2,7 +2,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js'; diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 52a858c1..3af576c8 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -192,6 +192,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ return true; }); + // TODO: Force the referencedNode to take the old id of the reference + // such that the reference's children can be kept + // Give the new referenced sub-tree new ids renewDocIds({ docArray: addedNodes, diff --git a/app/imports/api/creature/Creatures.js b/app/imports/api/creature/creatures/Creatures.js similarity index 54% rename from app/imports/api/creature/Creatures.js rename to app/imports/api/creature/creatures/Creatures.js index 422d2f7c..8863dbb0 100644 --- a/app/imports/api/creature/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.js @@ -1,16 +1,7 @@ -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js' import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; import SharingSchema from '/imports/api/sharing/SharingSchema.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js'; -import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; -import defaultCharacterProperties from '/imports/api/creature/defaultCharacterProperties.js'; -import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; -import '/imports/api/creature/removeCreature.js'; -import '/imports/api/creature/restCreature.js'; //set up the collection for creatures let Creatures = new Mongo.Collection('creatures'); @@ -176,92 +167,8 @@ CreatureSchema.extend(SharingSchema); Creatures.attachSchema(CreatureSchema); -const insertCreature = new ValidatedMethod({ - name: 'creatures.insertCreature', - - validate: null, - - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - - run() { - if (!this.userId) { - throw new Meteor.Error('Creatures.methods.insert.denied', - 'You need to be logged in to insert a creature'); - } - assertUserHasPaidBenefits(this.userId); - - // Create the creature document - let creatureId = Creatures.insert({ - owner: this.userId, - }); - - // Insert the default properties - // Not batchInsert because we want the properties cleaned by the schema - let baseId; - defaultCharacterProperties(creatureId).forEach(prop => { - let id = CreatureProperties.insert(prop); - if (prop.name === 'Ruleset'){ - baseId = id; - } - }); - - if (Meteor.isServer){ - // Insert the 5e ruleset as the default base - insertPropertyFromLibraryNode.call({ - nodeId: 'iHbhfcg3AL5isSWbw', - parentRef: {id: baseId, collection: 'creatureProperties'}, - order: 0.5, - }); - } - - this.unblock(); - return creatureId; - }, -}); - -const updateCreature = new ValidatedMethod({ - name: 'creatures.update', - validate({_id, path}){ - if (!_id) return false; - // Allowed fields - let allowedFields = [ - 'name', - 'alignment', - 'gender', - 'picture', - 'avatarPicture', - 'color', - 'settings', - ]; - if (!allowedFields.includes(path[0])){ - throw new Meteor.Error('Creatures.methods.update.denied', - 'This field can\'t be updated using this method'); - } - }, - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - run({_id, path, value}) { - let creature = Creatures.findOne(_id); - assertEditPermission(creature, this.userId); - if (value === undefined || value === null){ - Creatures.update(_id, { - $unset: {[path.join('.')]: 1}, - }); - } else { - Creatures.update(_id, { - $set: {[path.join('.')]: value}, - }); - } - }, -}); +import '/imports/api/creature/creatures/methods/index.js'; export default Creatures; -export { CreatureSchema, insertCreature, updateCreature }; +export { CreatureSchema }; diff --git a/app/imports/api/creature/creaturePermissions.js b/app/imports/api/creature/creatures/creaturePermissions.js similarity index 92% rename from app/imports/api/creature/creaturePermissions.js rename to app/imports/api/creature/creatures/creaturePermissions.js index 8496d519..380a5258 100644 --- a/app/imports/api/creature/creaturePermissions.js +++ b/app/imports/api/creature/creatures/creaturePermissions.js @@ -1,4 +1,4 @@ -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { assertEditPermission as editPermission, assertViewPermission as viewPermission, diff --git a/app/imports/api/creature/defaultCharacterProperties.js b/app/imports/api/creature/creatures/defaultCharacterProperties.js similarity index 100% rename from app/imports/api/creature/defaultCharacterProperties.js rename to app/imports/api/creature/creatures/defaultCharacterProperties.js diff --git a/app/imports/api/creature/creatures/methods/index.js b/app/imports/api/creature/creatures/methods/index.js new file mode 100644 index 00000000..2cd1b752 --- /dev/null +++ b/app/imports/api/creature/creatures/methods/index.js @@ -0,0 +1,5 @@ +import '/imports/api/creature/creatures/methods/insertCreature.js'; +import '/imports/api/creature/creatures/methods/removeCreature.js'; +import '/imports/api/creature/creatures/methods/restCreature.js'; +import '/imports/api/creature/creatures/methods/transferCreatureOwnership.js'; +import '/imports/api/creature/creatures/methods/updateCreature.js'; diff --git a/app/imports/api/creature/creatures/methods/insertCreature.js b/app/imports/api/creature/creatures/methods/insertCreature.js new file mode 100644 index 00000000..41a8c83f --- /dev/null +++ b/app/imports/api/creature/creatures/methods/insertCreature.js @@ -0,0 +1,70 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js'; +import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; + +const insertCreature = new ValidatedMethod({ + + name: 'creatures.insertCreature', + + validate: null, + + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + + run() { + if (!this.userId) { + throw new Meteor.Error('Creatures.methods.insert.denied', + 'You need to be logged in to insert a creature'); + } + let tier = getUserTier(this.userId); + + let currentCharacterCount = Creatures.find({ + owner: this.userId, + }, { + fields: {_id: 1}, + }).count(); + + if ( + tier.characterSlots !== -1 && + currentCharacterCount >= tier.characterSlots + ){ + throw new Meteor.Error('Creatures.methods.insert.denied', + `You are already at your limit of ${tier.characterSlots} characters`) + } + + // Create the creature document + let creatureId = Creatures.insert({ + owner: this.userId, + }); + + // Insert the default properties + // Not batchInsert because we want the properties cleaned by the schema + let baseId; + defaultCharacterProperties(creatureId).forEach(prop => { + let id = CreatureProperties.insert(prop); + if (prop.name === 'Ruleset'){ + baseId = id; + } + }); + + if (Meteor.isServer){ + // Insert the 5e ruleset as the default base + insertPropertyFromLibraryNode.call({ + nodeId: 'iHbhfcg3AL5isSWbw', + parentRef: {id: baseId, collection: 'creatureProperties'}, + order: 0.5, + }); + } + + return creatureId; + }, +}); + +export default insertCreature; diff --git a/app/imports/api/creature/removeCreature.js b/app/imports/api/creature/creatures/methods/removeCreature.js similarity index 88% rename from app/imports/api/creature/removeCreature.js rename to app/imports/api/creature/creatures/methods/removeCreature.js index c8542343..66eaa0b4 100644 --- a/app/imports/api/creature/removeCreature.js +++ b/app/imports/api/creature/creatures/methods/removeCreature.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import { assertOwnership } from '/imports/api/creature/creaturePermissions.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import Experiences from '/imports/api/creature/experience/Experiences.js'; diff --git a/app/imports/api/creature/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js similarity index 97% rename from app/imports/api/creature/restCreature.js rename to app/imports/api/creature/creatures/methods/restCreature.js index c4c814ed..a0464a00 100644 --- a/app/imports/api/creature/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -1,9 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; const restCreature = new ValidatedMethod({ diff --git a/app/imports/api/creature/creatures/methods/transferCreatureOwnership.js b/app/imports/api/creature/creatures/methods/transferCreatureOwnership.js new file mode 100644 index 00000000..c7012d03 --- /dev/null +++ b/app/imports/api/creature/creatures/methods/transferCreatureOwnership.js @@ -0,0 +1,55 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers.js'; + +const transferCreatureOwnership = new ValidatedMethod({ + + name: 'creatures.methods.transferOwnership', + + validate: new SimpleSchema({ + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + userId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + + run({creatureId, userId}) { + assertOwnership(creatureId, this.userId); + + let tier = getUserTier(userId); + let currentCharacterCount = Creatures.find({ + owner: userId, + }, { + fields: {_id: 1}, + }).count(); + + if ( + tier.characterSlots !== -1 && + currentCharacterCount >= tier.characterSlots + ){ + throw new Meteor.Error('Creatures.methods.transferOwnership.denied', + 'The new owner is already at their character limit') + } + + Creatures.update(creatureId, { + $set: {owner: userId}, + }); + + return creatureId; + }, +}); + +export default transferCreatureOwnership; diff --git a/app/imports/api/creature/creatures/methods/updateCreature.js b/app/imports/api/creature/creatures/methods/updateCreature.js new file mode 100644 index 00000000..6530ccae --- /dev/null +++ b/app/imports/api/creature/creatures/methods/updateCreature.js @@ -0,0 +1,45 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js'; + +const updateCreature = new ValidatedMethod({ + name: 'creatures.update', + validate({_id, path}){ + if (!_id) return false; + // Allowed fields + let allowedFields = [ + 'name', + 'alignment', + 'gender', + 'picture', + 'avatarPicture', + 'color', + 'settings', + ]; + if (!allowedFields.includes(path[0])){ + throw new Meteor.Error('Creatures.methods.update.denied', + 'This field can\'t be updated using this method'); + } + }, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, path, value}) { + let creature = Creatures.findOne(_id); + assertEditPermission(creature, this.userId); + if (value === undefined || value === null){ + Creatures.update(_id, { + $unset: {[path.join('.')]: 1}, + }); + } else { + Creatures.update(_id, { + $set: {[path.join('.')]: value}, + }); + } + }, +}); + +export default updateCreature; diff --git a/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js b/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js index ccaed171..b31b1ece 100644 --- a/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js +++ b/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; export const recomputeDamageMultipliers = new ValidatedMethod({ diff --git a/app/imports/api/creature/denormalise/recomputeInventory.js b/app/imports/api/creature/denormalise/recomputeInventory.js index dd7f7237..ae3871ec 100644 --- a/app/imports/api/creature/denormalise/recomputeInventory.js +++ b/app/imports/api/creature/denormalise/recomputeInventory.js @@ -1,5 +1,5 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { nodesToTree } from '/imports/api/parenting/parenting.js'; export default function recomputeInventory(creatureId){ diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index 3584bc93..fe16563f 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -2,8 +2,8 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { getUserTier } from '/imports/api/users/patreon/tiers.js'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; let Experiences = new Mongo.Collection('experiences'); diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index aec3a4c8..62489cde 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -1,9 +1,9 @@ import SimpleSchema from 'simpl-schema'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js'; +import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js'; import { parse, CompilationContext, diff --git a/app/imports/api/creature/mixins/creaturePermissionMixin.js b/app/imports/api/creature/mixins/creaturePermissionMixin.js index 887879b2..18b96fbd 100644 --- a/app/imports/api/creature/mixins/creaturePermissionMixin.js +++ b/app/imports/api/creature/mixins/creaturePermissionMixin.js @@ -2,7 +2,7 @@ import { assertEditPermission, assertViewPermission, assertOwnership, -} from '/imports/api/creature/creaturePermissions.js'; +} from '/imports/api/creature/creatures/creaturePermissions.js'; // Checks if the method has permission to run on the document. If the document // has a charId, that creature is checked, otherwise if it has an _id and the diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index 94b42355..16831209 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -1,6 +1,5 @@ import { _ } from 'meteor/underscore'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; -import { getUserTier } from '/imports/api/users/patreon/tiers.js'; function assertIdValid(userId){ if (!userId || typeof userId !== 'string'){ @@ -48,13 +47,6 @@ export function assertEditPermission(doc, userId) { return true; } - // Ensure the user is of a tier with paid benefits - let tier = getUserTier(user); - if (!tier.paidBenefits){ - throw new Meteor.Error('Edit permission denied', - `The ${tier.name} tier does not allow you to edit this document`); - } - // Ensure the user is authorized for this specific document if ( doc.owner === userId || diff --git a/app/imports/api/tabletop/Messages.js b/app/imports/api/tabletop/Messages.js index badf61e6..b2dc0280 100644 --- a/app/imports/api/tabletop/Messages.js +++ b/app/imports/api/tabletop/Messages.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Tabletops, { assertUserInTabletop } from '/imports/api/tabletop/Tabletops.js'; let Messages = new Mongo.Collection('messages'); diff --git a/app/imports/api/tabletop/Tabletops.js b/app/imports/api/tabletop/Tabletops.js index 29c3f04d..cf991b55 100644 --- a/app/imports/api/tabletop/Tabletops.js +++ b/app/imports/api/tabletop/Tabletops.js @@ -2,7 +2,7 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; let Tabletops = new Mongo.Collection('tabletops'); diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index 9bd75ac7..7b784909 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import '/imports/api/users/deleteMyAccount.js'; +const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; const userSchema = new SimpleSchema({ username: { @@ -63,7 +64,7 @@ const userSchema = new SimpleSchema({ }, subscribedLibraries: { type: Array, - defaultValue: [], + defaultValue: defaultLibraries, max: 100, }, 'subscribedLibraries.$': { diff --git a/app/imports/api/users/deleteMyAccount.js b/app/imports/api/users/deleteMyAccount.js index 6a5aa8c1..d887b8c0 100644 --- a/app/imports/api/users/deleteMyAccount.js +++ b/app/imports/api/users/deleteMyAccount.js @@ -1,8 +1,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js'; -import Creatures from '/imports/api/creature/Creatures.js'; -import {removeCreatureWork} from '/imports/api/creature/removeCreature.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import {removeCreatureWork} from '/imports/api/creature/creatures/methods/removeCreature.js'; Meteor.users.deleteMyAccount = new ValidatedMethod({ name: 'users.deleteMyAccount', diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js index 8b16bfec..7f9ad0d6 100644 --- a/app/imports/api/users/patreon/tiers.js +++ b/app/imports/api/users/patreon/tiers.js @@ -1,58 +1,78 @@ import { findLast } from 'lodash'; import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js'; import Invites from '/imports/api/users/Invites.js'; +const patreonDisabled = !!process.env.DISABLE_PATREON; -const TIERS = [ +const TIERS = Object.freeze([ { name: 'Commoner', minimumEntitledCents: 0, invites: 0, + characterSlots: 0, //5, paidBenefits: false, }, { name: 'Dreamer', minimumEntitledCents: 100, invites: 0, + characterSlots: 0, //5, paidBenefits: false, }, { name: 'Wanderer', minimumEntitledCents: 300, invites: 0, + characterSlots: 0, //5, paidBenefits: false, }, { //cost per user $5 name: 'Adventurer', minimumEntitledCents: 500, invites: 0, + characterSlots: -1, //20, paidBenefits: true, }, { //cost per user $3.33 name: 'Hero', minimumEntitledCents: 1000, invites: 2, + characterSlots: -1, //50, paidBenefits: true, }, { //cost per user $3.333 name: 'Legend', minimumEntitledCents: 2000, invites: 5, + characterSlots: -1, //120, paidBenefits: true, }, { //cost per user $3.125 name: 'Paragon', minimumEntitledCents: 5000, invites: 15, + characterSlots: -1, // Unlimited characters paidBenefits: true, }, -]; +]); -const GUEST_TIER = { +// Companion tier should be equivalent to the Adventurer tier +const GUEST_TIER = Object.freeze({ name: 'Companion', guest: true, invites: 0, + characterSlots: 20, paidBenefits: true, -} +}); + +// When patreon features are disabled, give all the users the same tier +// with no limitations +const PATREON_DISABLED_TIER = Object.freeze({ + name: 'Outlander', + invites: 0, + characterSlots: -1, // Can have infinitely many characters + paidBenefits: true, +}); export function getTierByEntitledCents(entitledCents = 0){ + if (patreonDisabled) return PATREON_DISABLED_TIER; return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents); } @@ -66,6 +86,7 @@ export function getUserTier(user){ }); if (!user) throw 'User not found'; } + if (patreonDisabled) return PATREON_DISABLED_TIER; const entitledCents = getEntitledCents(user); const tier = getTierByEntitledCents(entitledCents); if (tier.paidBenefits) return tier; diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index bb60d32e..f06cc7b2 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -24,11 +24,11 @@ const PROPERTIES = Object.freeze({ name: 'Class level' }, constant: { - icon: 'anchor', + icon: 'mdi-anchor', name: 'Constant' }, container: { - icon: 'work', + icon: 'mdi-bag-personal-outline', name: 'Container' }, damage: { @@ -44,23 +44,23 @@ const PROPERTIES = Object.freeze({ name: 'Effect' }, feature: { - icon: 'subject', + icon: 'mdi-text-subject', name: 'Feature' }, folder: { - icon: 'folder', + icon: 'mdi-folder-outline', name: 'Folder' }, item: { - icon: '$vuetify.icons.item', + icon: 'mdi-cube-outline', name: 'Item' }, note: { - icon: 'note', + icon: 'mdi-note-outline', name: 'Note' }, proficiency: { - icon: 'radio_button_checked', + icon: 'mdi-brightness-1', name: 'Proficiency' }, roll: { @@ -68,7 +68,7 @@ const PROPERTIES = Object.freeze({ name: 'Roll' }, reference: { - icon: 'link', + icon: 'mdi-vector-link', name: 'Reference', libraryOnly: true, }, @@ -81,11 +81,11 @@ const PROPERTIES = Object.freeze({ name: 'Skill' }, propertySlot: { - icon: 'tab_unselected', + icon: 'mdi-power-socket-eu', name: 'Slot' }, slotFiller: { - icon: 'picture_in_picture', + icon: 'mdi-power-plug-outline', name: 'Slot filler' }, spellList: { diff --git a/app/imports/constants/SVG_ICONS.js b/app/imports/constants/SVG_ICONS.js index d988a2ee..00ab7353 100644 --- a/app/imports/constants/SVG_ICONS.js +++ b/app/imports/constants/SVG_ICONS.js @@ -35,10 +35,6 @@ const SVG_ICONS = Object.freeze({ name: 'cash', shape:'M251.813 20.5c-.78-.008-1.558.003-2.344.03-11.005.39-22.285 5.142-32.376 17.814l-5.156 6.468-7.063-4.343c-13.67-8.436-30.948-11.566-45.5-8.75-14.552 2.814-26.03 10.716-31.344 25.374l-3.624 9.968L115 62.22c-16.243-8.34-24.717-8.468-31.75-5.314-5.606 2.515-11.456 8.555-18.094 17.375L147.594 138c12.92 4.168 31.79 4.086 50.75-.813 18.95-4.895 37.863-14.44 51.625-27.406l31.124-76.905c-6.596-6.452-16.42-11.52-26.938-12.28-.778-.058-1.563-.087-2.344-.095zM265 121.28l-.594.595c-16.8 16.497-39.04 27.636-61.375 33.406-6.883 1.78-13.784 3.075-20.56 3.814-3.682 6.112-6.727 12.262-9.19 18.47l.532-.033c1.4 24.178 11.835 42.98 26.75 60.876l-14.343 11.97c-8.29-9.947-15.738-20.762-21.282-32.814-.92 23.227 4.222 47.218 12.78 72.22l-17.687 6.062c-15.615-45.618-20.942-92.383 1-136.375-7.386-.588-14.426-1.96-20.905-4.25l-.344-.126c-4.26 4.08-8.35 8.368-12.25 12.875-31.64 36.583-49.03 85.3-39.936 123.25 4.87 20.324 13.632 39.513 26.156 55.874 3.653-.298 7.256-.49 10.78-.53 11.585-.138 22.4 1.094 32.032 3.623 3.04.798 5.98 1.776 8.813 2.875 7.484-.797 15.245-1.218 23.22-1.218 28.474 0 54.303 5.297 73.843 14.47 7.586 3.56 14.44 7.848 20 12.81-.047-.742-.063-1.493-.063-2.25 0-4.568.85-8.905 2.438-13-8.938-8.382-14.532-18.546-14.532-30.187 0-16.12 10.75-29.346 26.22-39 .012-4.624.893-9.02 2.53-13.156-10.165-8.73-16.655-19.555-16.655-32.092 0-7.917 2.594-15.142 7.063-21.594-4.47-6.452-7.063-13.677-7.063-21.594 0-3.813.598-7.456 1.72-10.938-9.688-8.603-15.814-19.182-15.814-31.375 0-12.108 6.054-22.594 15.626-31.156-5.94-4.6-12.232-8.505-18.906-11.5zm111.438 2.345c-26.022 0-49.507 5.433-65.688 13.563-16.18 8.13-23.78 18.062-23.78 26.75 0 8.687 7.6 18.65 23.78 26.78 16.18 8.13 39.666 13.532 65.688 13.532 11.222 0 21.978-1.018 31.875-2.813v-17.03c13.862-2.068 25.41-5.9 32.812-10.782v17.563c.328-.16.678-.307 1-.47 16.18-8.13 23.813-18.093 23.813-26.78 0-8.687-7.632-18.62-23.813-26.75-16.18-8.13-39.666-13.563-65.688-13.563zm96.5 67.625c-3.334 3.582-7.283 6.86-11.657 9.844l.25 29.03c12.545-7.523 18.5-16.197 18.5-23.874 0-4.788-2.31-9.965-7.092-15zm-171.844 15.47c.265 8.576 7.887 18.325 23.78 26.31 16.182 8.13 39.667 13.532 65.688 13.532 16.316 0 31.636-2.13 44.782-5.718l.625-27.28c-17.166 6.025-37.58 9.374-59.533 9.374-28.558 0-54.474-5.69-74.062-15.532-.435-.218-.852-.464-1.28-.687zm1.97 34.81c-1.345 2.694-2.002 5.352-2.002 7.907 0 8.688 7.632 18.652 23.813 26.782 16.18 8.13 39.666 13.53 65.688 13.53 13.194 0 25.73-1.384 37-3.813v-24.062c-11.556 2.212-24.002 3.375-37 3.375-28.56 0-54.506-5.69-74.094-15.53-4.828-2.426-9.34-5.17-13.408-8.19zm174.967 0c-4.067 3.02-8.58 5.764-13.405 8.19-1.382.693-2.806 1.346-4.25 2V274c13.335-7.677 19.656-16.654 19.656-24.563 0-2.548-.662-5.218-2-7.906zm7.5 36.75c-2.03 2.028-4.236 3.956-6.624 5.783v32.468c10.314-7.004 15.25-14.77 15.25-21.75 0-5.266-2.816-10.994-8.625-16.5zm-170.124 14.064c-.13.817-.22 1.64-.22 2.437.002 8.69 7.633 18.62 23.814 26.75 16.18 8.13 39.666 13.564 65.688 13.564 14.953 0 29.064-1.775 41.437-4.844v-29.875c-16.304 5.196-35.265 8.063-55.563 8.063-28.558 0-54.505-5.69-74.093-15.532-.36-.18-.708-.378-1.064-.562zm-13.594 21.312c-8.665 6.538-12.843 13.626-12.843 20.03 0 8.69 7.63 18.653 23.81 26.783 16.182 8.13 39.667 13.53 65.69 13.53 9.54 0 18.72-.71 27.342-2.03v-18.19c-.377.003-.747 0-1.125 0-28.558 0-54.505-5.688-74.093-15.53-12.46-6.26-22.86-14.558-28.78-24.594zm162.532 30.688c-7.93 2.796-16.566 5.006-25.72 6.594V363c1.92-.813 3.78-1.65 5.532-2.53 9.82-4.935 16.47-10.537 20.188-16.126zM473.5 362.5c-3.77 3.77-8.21 7.184-13.125 10.25v28.688c13.335-7.678 19.656-16.655 19.656-24.563 0-4.59-2.138-9.537-6.53-14.375zm-348.72 2.813c-10.14.115-21.222 1.413-32.624 4.062-22.803 5.3-42.462 15.006-55.25 25.688-12.788 10.68-17.74 21.388-16.28 29.125 1.46 7.736 9.393 14.833 24.78 18.875 15.388 4.04 36.854 4.204 59.657-1.094 9.19-2.136 17.857-4.97 25.78-8.314V413.03c14.53-6.486 25.467-14.375 30.376-21.905V415.5c12.102-10.422 16.83-20.828 15.405-28.375-1.46-7.735-9.425-14.802-24.813-18.844-7.693-2.02-16.89-3.083-27.03-2.967zm64.657 5.218c2.71 3.895 4.61 8.28 5.532 13.158 1.128 5.983.634 11.85-1.126 17.53 9.822 2.56 18.812 6.073 26.656 10.47 4.39 2.46 8.47 5.268 12.125 8.343 9.022-2.163 16.573-5.152 21.938-8.686v20.53c16.183-7.61 23.5-16.89 23.5-24.312 0-7.43-7.334-16.726-23.563-24.343-16.054-7.537-39.295-12.58-65.063-12.69zm111.72 4.94c-.044.467-.094.944-.094 1.405 0 8.688 7.63 18.65 23.812 26.78 16.18 8.13 39.666 13.533 65.688 13.533 13.194 0 25.73-1.384 37-3.813V386.53c-14.77 4-31.43 6.19-49.094 6.19-28.56 0-54.507-5.72-74.095-15.564-1.086-.546-2.165-1.11-3.22-1.687zM304 412.81c-1.958 3.257-2.938 6.484-2.938 9.563 0 8.688 7.632 18.65 23.813 26.78 16.18 8.13 39.666 13.533 65.688 13.533 13.194 0 25.73-1.384 37-3.813V432.5c-11.556 2.212-24.002 3.375-37 3.375-28.56 0-54.506-5.69-74.094-15.53-4.47-2.246-8.643-4.777-12.47-7.533zm173.125 0c-3.832 2.762-8.025 5.283-12.5 7.532-1.382.694-2.806 1.347-4.25 2v24.594c13.335-7.678 19.656-16.655 19.656-24.563 0-3.077-.953-6.308-2.905-9.563zM184.5 418.22c-3.47 4.383-7.572 8.55-12.188 12.405-15.66 13.082-37.66 23.643-63 29.53-9.14 2.125-18.11 3.472-26.75 4.126 3.2 5.317 8.83 10.542 17.063 15.158 13.698 7.677 33.69 12.843 55.875 12.843 10.077 0 19.68-1.084 28.47-2.967v-20.844c13.858-2.068 25.41-5.903 32.81-10.783v18.407c10.023-6.95 14.845-14.89 14.845-22.375 0-8.723-6.552-18.043-20.25-25.72-7.35-4.12-16.532-7.518-26.875-9.78z', }, - 'cube': { - name: 'item', - shape: 'M256 24.585L51.47 118.989 256 213.394l204.53-94.405zM38.998 133.054v258.353L247 487.415V229.063zm434.004 0L265 229.062v258.353l208.002-96.008z', - }, 'electric': { name: 'action', shape: 'M376 211H256V16L136 301h120v195z', diff --git a/app/imports/server/publications/archivedCreatures.js b/app/imports/server/publications/archivedCreatures.js new file mode 100644 index 00000000..93043407 --- /dev/null +++ b/app/imports/server/publications/archivedCreatures.js @@ -0,0 +1,19 @@ +import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; + +Meteor.publish('archivedCreatures', function(){ + this.autorun(function (){ + var userId = this.userId; + if (!userId) { + return []; + } + return ArchivedCreatures.find({ + owner: userId, + }, { + fields: { + creature: 1, + owner: 1, + } + } + ); + }); +}); diff --git a/app/imports/server/publications/characterList.js b/app/imports/server/publications/characterList.js index 34bdd0da..660d10f2 100644 --- a/app/imports/server/publications/characterList.js +++ b/app/imports/server/publications/characterList.js @@ -1,5 +1,5 @@ -import Creatures from '/imports/api/creature/Creatures.js'; -import Parties from '/imports/api/creature/Parties.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js'; Meteor.publish('characterList', function(){ this.autorun(function (){ @@ -36,7 +36,7 @@ Meteor.publish('characterList', function(){ } } ), - Parties.find({owner: userId}), + CreatureFolders.find({owner: userId}), ]; }); }); diff --git a/app/imports/server/publications/experiences.js b/app/imports/server/publications/experiences.js index ef847bd2..ca01450c 100644 --- a/app/imports/server/publications/experiences.js +++ b/app/imports/server/publications/experiences.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Experiences from '/imports/api/creature/experience/Experiences.js'; -import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; let schema = new SimpleSchema({ creatureId: { diff --git a/app/imports/server/publications/index.js b/app/imports/server/publications/index.js index c11cb255..adff4e49 100644 --- a/app/imports/server/publications/index.js +++ b/app/imports/server/publications/index.js @@ -6,5 +6,6 @@ import '/imports/server/publications/experiences.js'; import '/imports/server/publications/users.js'; import '/imports/server/publications/icons.js'; import '/imports/server/publications/tabletops.js'; -import '/imports/server/publications/slotFillers.js' -import '/imports/server/publications/ownedDocuments.js' +import '/imports/server/publications/slotFillers.js'; +import '/imports/server/publications/ownedDocuments.js'; +import '/imports/server/publications/archivedCreatures.js'; diff --git a/app/imports/server/publications/ownedDocuments.js b/app/imports/server/publications/ownedDocuments.js index 317b4e83..2683977a 100644 --- a/app/imports/server/publications/ownedDocuments.js +++ b/app/imports/server/publications/ownedDocuments.js @@ -1,4 +1,4 @@ -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Libraries from '/imports/api/library/Libraries.js'; Meteor.publish('ownedDocuments', function(){ diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index 1ad7027b..105ffed2 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; -import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js'; import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import VERSION from '/imports/constants/VERSION.js'; diff --git a/app/imports/server/publications/tabletops.js b/app/imports/server/publications/tabletops.js index b6fc4ea1..30345f79 100644 --- a/app/imports/server/publications/tabletops.js +++ b/app/imports/server/publications/tabletops.js @@ -1,5 +1,5 @@ import Tabletops from '/imports/api/tabletop/Tabletops.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Messages from '/imports/api/tabletop/Messages.js'; Meteor.publish('tabletops', function(){ diff --git a/app/imports/ui/components/ColorPicker.vue b/app/imports/ui/components/ColorPicker.vue index eb385581..23b8463b 100644 --- a/app/imports/ui/components/ColorPicker.vue +++ b/app/imports/ui/components/ColorPicker.vue @@ -10,7 +10,7 @@ icon v-on="on" > - format_paint + mdi-format-paint @@ -30,7 +30,7 @@ v-if="kebabColor === colorOption" :class="{dark: isDark(colorOption, shade)}" > - check + mdi-check @@ -58,7 +58,7 @@ v-if="kebabShade === shadeOption" :class="isDark(color, shade) ? 'dark' : 'light'" > - check + mdi-check diff --git a/app/imports/ui/components/IncrementButton.vue b/app/imports/ui/components/IncrementButton.vue index 3189010c..3d7ad623 100644 --- a/app/imports/ui/components/IncrementButton.vue +++ b/app/imports/ui/components/IncrementButton.vue @@ -14,7 +14,7 @@ @click.stop > - add + mdi-plus diff --git a/app/imports/ui/components/IncrementMenu.vue b/app/imports/ui/components/IncrementMenu.vue index 24d46b91..b8f0ccb3 100644 --- a/app/imports/ui/components/IncrementMenu.vue +++ b/app/imports/ui/components/IncrementMenu.vue @@ -15,14 +15,14 @@ class="filled" @click="toggleAdd(); $nextTick(() => $refs.editInput.focus())" > - add + mdi-plus - remove + mdi-minus - done + mdi-check - close + mdi-close @@ -117,11 +117,11 @@ operationIcon(operation) { switch (operation) { case 'set': - return 'forward'; + return 'mdi-forward'; case 'add': - return 'add'; + return 'mdi-plus'; case 'subtract': - return 'remove'; + return 'mdi-minus'; } }, toggleAdd(){ diff --git a/app/imports/ui/components/SharedIcon.vue b/app/imports/ui/components/SharedIcon.vue new file mode 100644 index 00000000..568bd91c --- /dev/null +++ b/app/imports/ui/components/SharedIcon.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/app/imports/ui/components/global/DatePicker.vue b/app/imports/ui/components/global/DatePicker.vue index 808eb998..7b70e44a 100644 --- a/app/imports/ui/components/global/DatePicker.vue +++ b/app/imports/ui/components/global/DatePicker.vue @@ -11,7 +11,7 @@ - highlight_alt + mdi-select-search -
+
- - cancel - + clear
- arrow_back + mdi-arrow-left - more_vert + mdi-dots-vertical @@ -64,7 +64,7 @@ - file_copy + mdi-content-copy - send + mdi-send - delete + mdi-delete @@ -112,13 +112,13 @@ v-if="editing" key="doneIcon" > - done + mdi-check - create + mdi-pencil diff --git a/app/imports/ui/components/snackbars/SnackbarQueue.vue b/app/imports/ui/components/snackbars/SnackbarQueue.vue index 0cd0ecd6..78a6c550 100644 --- a/app/imports/ui/components/snackbars/SnackbarQueue.vue +++ b/app/imports/ui/components/snackbars/SnackbarQueue.vue @@ -32,7 +32,7 @@ v-bind="attrs" @click="closeSnackbar" > - close + mdi-close diff --git a/app/imports/ui/components/tree/TreeNode.vue b/app/imports/ui/components/tree/TreeNode.vue index 321347df..c535eb3e 100644 --- a/app/imports/ui/components/tree/TreeNode.vue +++ b/app/imports/ui/components/tree/TreeNode.vue @@ -18,7 +18,7 @@ @click.stop="expanded = !expanded" > - chevron_right + mdi-chevron-right
- drag_handle + mdi-drag diff --git a/app/imports/ui/creature/RestButton.vue b/app/imports/ui/creature/RestButton.vue index 81974af2..d7a887f7 100644 --- a/app/imports/ui/creature/RestButton.vue +++ b/app/imports/ui/creature/RestButton.vue @@ -7,14 +7,14 @@ @click="rest" > - {{ type === 'shortRest' ? 'snooze' : 'bedtime' }} + {{ type === 'shortRest' ? 'mdi-music-rest-quarter' : 'mdi-bed' }} {{ type === 'shortRest' ? 'Short Rest' : 'Long Rest' }} + + diff --git a/app/imports/ui/creature/character/CharacterDeleteDialog.vue b/app/imports/ui/creature/character/CharacterDeleteDialog.vue index 2e10de67..a8345316 100644 --- a/app/imports/ui/creature/character/CharacterDeleteDialog.vue +++ b/app/imports/ui/creature/character/CharacterDeleteDialog.vue @@ -31,9 +31,9 @@ + + diff --git a/app/imports/ui/creature/creatureList/CharacterListToolbarItems.vue b/app/imports/ui/creature/creatureList/CharacterListToolbarItems.vue new file mode 100644 index 00000000..5c678946 --- /dev/null +++ b/app/imports/ui/creature/creatureList/CharacterListToolbarItems.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/app/imports/ui/creature/creatureList/CreatureFolderHeader.vue b/app/imports/ui/creature/creatureList/CreatureFolderHeader.vue new file mode 100644 index 00000000..1b2d6bd3 --- /dev/null +++ b/app/imports/ui/creature/creatureList/CreatureFolderHeader.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/app/imports/ui/creature/creatureList/CreatureFolderList.vue b/app/imports/ui/creature/creatureList/CreatureFolderList.vue new file mode 100644 index 00000000..d98e2103 --- /dev/null +++ b/app/imports/ui/creature/creatureList/CreatureFolderList.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/app/imports/ui/creature/creatureList/CreatureList.vue b/app/imports/ui/creature/creatureList/CreatureList.vue new file mode 100644 index 00000000..1cdcdf96 --- /dev/null +++ b/app/imports/ui/creature/creatureList/CreatureList.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/app/imports/ui/creature/creatureList/CreatureListTile.vue b/app/imports/ui/creature/creatureList/CreatureListTile.vue new file mode 100644 index 00000000..2bb28101 --- /dev/null +++ b/app/imports/ui/creature/creatureList/CreatureListTile.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/imports/ui/creature/creatureProperties/Breadcrumbs.vue b/app/imports/ui/creature/creatureProperties/Breadcrumbs.vue index c7f85f46..2cd94af0 100644 --- a/app/imports/ui/creature/creatureProperties/Breadcrumbs.vue +++ b/app/imports/ui/creature/creatureProperties/Breadcrumbs.vue @@ -8,7 +8,7 @@ v-if="index !== 0" :key="index" > - chevron_right + mdi-chevron-right - add + mdi-plus Property @@ -82,7 +82,7 @@ import softRemoveProperty from '/imports/api/creature/creatureProperties/methods import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js'; import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js'; import duplicateProperty from '/imports/api/creature/creatureProperties/methods/duplicateProperty.js'; -import Creatures from '/imports/api/creature/Creatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; import PropertyToolbar from '/imports/ui/components/propertyToolbar.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import { getPropertyName } from '/imports/constants/PROPERTIES.js'; @@ -91,7 +91,7 @@ import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormI import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js'; import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue'; import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js'; -import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { get, findLast } from 'lodash'; import equipItem from '/imports/api/creature/creatureProperties/methods/equipItem.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js'; diff --git a/app/imports/ui/creature/experiences/ExperienceListDialog.vue b/app/imports/ui/creature/experiences/ExperienceListDialog.vue index 61e46ef1..b90450f7 100644 --- a/app/imports/ui/creature/experiences/ExperienceListDialog.vue +++ b/app/imports/ui/creature/experiences/ExperienceListDialog.vue @@ -10,13 +10,13 @@ data-id="experience-add-button" @click="addExperience" > - add + mdi-plus - refresh + mdi-refresh
- delete + mdi-delete diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index 29c98f91..5af45199 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -9,7 +9,7 @@ diff --git a/app/imports/ui/pages/Friends.vue b/app/imports/ui/pages/Friends.vue index ba98ea17..5a7abd9d 100644 --- a/app/imports/ui/pages/Friends.vue +++ b/app/imports/ui/pages/Friends.vue @@ -10,7 +10,7 @@ right fab > - add + mdi-plus
diff --git a/app/imports/ui/pages/Home.vue b/app/imports/ui/pages/Home.vue index 3a516202..1b638bc8 100644 --- a/app/imports/ui/pages/Home.vue +++ b/app/imports/ui/pages/Home.vue @@ -37,7 +37,7 @@ x-large class="ma-2" > - money_off + mdi-currency-usd-off

Free, open source, community funded @@ -55,7 +55,7 @@ x-large class="ma-2" > - ballot + mdi-ballot-outline

Character sheets optimised for one ruleset @@ -73,7 +73,7 @@ x-large class="ma-2" > - scatter_plot + mdi-file-tree-outline

Inventory manager diff --git a/app/imports/ui/pages/Tabletops.vue b/app/imports/ui/pages/Tabletops.vue index 8cd5c4ad..24f68157 100644 --- a/app/imports/ui/pages/Tabletops.vue +++ b/app/imports/ui/pages/Tabletops.vue @@ -28,7 +28,7 @@ :loading="addTabletopLoading" @click="addTabletop" > - add + mdi-plus diff --git a/app/imports/ui/properties/components/actions/ItemConsumedView.vue b/app/imports/ui/properties/components/actions/ItemConsumedView.vue index 5552ba75..ae6b50e7 100644 --- a/app/imports/ui/properties/components/actions/ItemConsumedView.vue +++ b/app/imports/ui/properties/components/actions/ItemConsumedView.vue @@ -53,7 +53,7 @@

- arrow_drop_down + mdi-menu-down
diff --git a/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue b/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue index c8dc4b06..9b8440e7 100644 --- a/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue +++ b/app/imports/ui/properties/components/attributes/HealthBarCardContainer.vue @@ -12,7 +12,7 @@ diff --git a/app/imports/ui/properties/forms/AttributesConsumedListForm.vue b/app/imports/ui/properties/forms/AttributesConsumedListForm.vue index 4c32001a..d86652bc 100644 --- a/app/imports/ui/properties/forms/AttributesConsumedListForm.vue +++ b/app/imports/ui/properties/forms/AttributesConsumedListForm.vue @@ -19,7 +19,7 @@ class="ma-3" @click="$emit('pull', {path: [i]})" > - delete + mdi-delete diff --git a/app/imports/ui/properties/forms/EffectForm.vue b/app/imports/ui/properties/forms/EffectForm.vue index 6157b74e..8200a4a4 100644 --- a/app/imports/ui/properties/forms/EffectForm.vue +++ b/app/imports/ui/properties/forms/EffectForm.vue @@ -9,7 +9,7 @@ /> - delete + mdi-delete diff --git a/app/imports/ui/properties/forms/ReferenceForm.vue b/app/imports/ui/properties/forms/ReferenceForm.vue index 27df3b78..fde536d6 100644 --- a/app/imports/ui/properties/forms/ReferenceForm.vue +++ b/app/imports/ui/properties/forms/ReferenceForm.vue @@ -13,8 +13,8 @@ " :hint="model.cache.library && model.cache.library.name" :error-messages="model.cache.error || errors.ref" - prepend-inner-icon="link" - append-icon="refresh" + prepend-inner-icon="mdi-vector-link" + append-icon="mdi-refresh" data-id="change-ref" @click="changeReference" @click:prepend-inner="changeReference" diff --git a/app/imports/ui/properties/forms/ResourcesForm.vue b/app/imports/ui/properties/forms/ResourcesForm.vue index f56c63d2..201915e4 100644 --- a/app/imports/ui/properties/forms/ResourcesForm.vue +++ b/app/imports/ui/properties/forms/ResourcesForm.vue @@ -34,7 +34,7 @@ outlined v-on="on" > - add + mdi-plus diff --git a/app/imports/ui/properties/forms/shared/CalculationErrorList.vue b/app/imports/ui/properties/forms/shared/CalculationErrorList.vue index 29574ad7..e993706e 100644 --- a/app/imports/ui/properties/forms/shared/CalculationErrorList.vue +++ b/app/imports/ui/properties/forms/shared/CalculationErrorList.vue @@ -51,11 +51,11 @@ export default { methods: { errorIcon(type){ if (type === 'subsitution'){ - return 'info'; + return 'mdi-information'; } else if (type === 'evaluation'){ - return 'warning'; + return 'mdi-alert-circle'; } else { - return 'error' + return 'mdi-alert' } }, errorColor(type){ diff --git a/app/imports/ui/properties/forms/shared/ProficiencySelect.vue b/app/imports/ui/properties/forms/shared/ProficiencySelect.vue index 8183c00b..51e1dc4c 100644 --- a/app/imports/ui/properties/forms/shared/ProficiencySelect.vue +++ b/app/imports/ui/properties/forms/shared/ProficiencySelect.vue @@ -1,6 +1,6 @@