diff --git a/app/.meteor/packages b/app/.meteor/packages index 8535b3b5..6bfb61c4 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -3,26 +3,25 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -accounts-password@2.2.0 +accounts-password@2.3.1 random@1.2.0 underscore@1.0.10 dburles:mongo-collection-instances accounts-google@1.4.0 -email@2.2.0 +email@2.2.1 meteor-base@1.5.1 mobile-experience@1.1.0 -mongo@1.14.6 +mongo@1.15.0 session@1.2.0 tracker@1.2.0 logging@1.3.1 reload@1.3.1 -ejson@1.1.1 +ejson@1.1.2 check@1.3.1 standard-minifier-js@2.8.0 shell-server@0.5.0 -ecmascript@0.16.1 +ecmascript@0.16.2 es5-shim@4.8.0 -percolate:synced-cron service-configuration@1.3.0 dynamic-import@0.7.2 ddp-rate-limiter@1.1.0 @@ -47,4 +46,5 @@ meteortesting:mocha ostrio:files simple:rest-bearer-token-parser simple:rest-json-error-handler +littledata:synced-cron mdg:meteor-apm-agent diff --git a/app/.meteor/release b/app/.meteor/release index b1b0cceb..66dd7b66 100644 --- a/app/.meteor/release +++ b/app/.meteor/release @@ -1 +1 @@ -METEOR@2.6.1 +METEOR@2.7.3 diff --git a/app/.meteor/versions b/app/.meteor/versions index 8195b39a..dbabbb8b 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -1,7 +1,7 @@ -accounts-base@2.2.1 +accounts-base@2.2.3 accounts-google@1.4.0 -accounts-oauth@1.4.0 -accounts-password@2.2.0 +accounts-oauth@1.4.1 +accounts-password@2.3.1 accounts-patreon@0.1.0 akryum:npm-check@0.1.2 akryum:vue-component@0.15.2 @@ -12,13 +12,13 @@ aldeed:collection2@3.5.0 aldeed:schema-index@3.0.0 allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.8.1 -babel-runtime@1.5.0 +babel-compiler@7.9.0 +babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 -blaze-tools@1.1.2 +blaze-tools@1.1.3 boilerplate-generator@1.7.1 -bozhao:link-accounts@2.4.0 +bozhao:link-accounts@2.6.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 callback-hook@1.4.0 @@ -33,24 +33,25 @@ ddp-rate-limiter@1.1.0 ddp-server@2.5.0 diff-sequence@1.1.1 dynamic-import@0.7.2 -ecmascript@0.16.1 +ecmascript@0.16.2 ecmascript-runtime@0.8.0 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 -ejson@1.1.1 -email@2.2.0 +ejson@1.1.2 +email@2.2.1 es5-shim@4.8.0 fetch@0.1.1 geojson-utils@1.0.10 -google-oauth@1.4.1 +google-oauth@1.4.2 hot-code-push@1.0.4 -html-tools@1.1.2 +html-tools@1.1.3 htmljs@1.1.1 http@2.0.0 id-map@1.1.1 inter-process-messaging@0.1.1 lai:collection-extensions@0.3.0 launch-screen@1.3.0 +littledata:synced-cron@1.5.1 livedata@1.0.18 localstorage@1.2.0 logging@1.3.1 @@ -63,23 +64,23 @@ meteortesting:mocha@2.0.3 meteortesting:mocha-core@8.1.2 mikowals:batch-insert@1.3.0 minifier-css@1.6.0 -minifier-js@2.7.3 +minifier-js@2.7.4 minimongo@1.8.0 mobile-experience@1.1.0 mobile-status-bar@1.1.0 -modern-browsers@0.1.7 +modern-browsers@0.1.8 modules@0.18.0 -modules-runtime@0.12.0 -mongo@1.14.6 -mongo-decimal@0.1.2 +modules-runtime@0.13.0 +mongo@1.15.0 +mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 mongo-livedata@1.0.12 npm-mongo@4.3.1 -oauth@2.1.1 +oauth@2.1.2 oauth2@1.3.1 ordered-dict@1.1.0 -ostrio:cookies@2.7.0 +ostrio:cookies@2.7.2 ostrio:files@2.0.1 patreon-oauth@0.1.0 peerlibrary:assert@0.3.0 @@ -93,12 +94,11 @@ peerlibrary:reactive-publish@0.10.0 peerlibrary:server-autorun@0.8.0 peerlibrary:subscription-data@0.8.0 percolate:migrations@1.0.3 -percolate:synced-cron@1.3.2 promise@0.12.0 raix:eventemitter@1.0.0 random@1.2.0 rate-limit@1.0.9 -react-fast-refresh@0.2.2 +react-fast-refresh@0.2.3 reactive-dict@1.3.0 reactive-var@1.0.11 reload@1.3.1 @@ -114,14 +114,14 @@ simple:rest@1.2.1 simple:rest-bearer-token-parser@1.1.1 simple:rest-json-error-handler@1.1.1 simple:rest-method-mixin@1.1.0 -socket-stream-client@0.4.0 -spacebars-compiler@1.3.0 +socket-stream-client@0.5.0 +spacebars-compiler@1.3.1 standard-minifier-js@2.8.0 static-html@1.3.2 -templating-tools@1.2.1 +templating-tools@1.2.2 tmeasday:check-npm-versions@1.0.2 tracker@1.2.0 -typescript@4.4.1 +typescript@4.5.4 underscore@1.0.10 url@1.3.2 webapp@1.13.1 diff --git a/app/imports/api/creature/archive/ArchiveCreatureFiles.js b/app/imports/api/creature/archive/ArchiveCreatureFiles.js index 99f4f7f2..1b699c0b 100644 --- a/app/imports/api/creature/archive/ArchiveCreatureFiles.js +++ b/app/imports/api/creature/archive/ArchiveCreatureFiles.js @@ -1,4 +1,8 @@ import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js'; +import SimpleSchema from 'simpl-schema'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; +import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js'; const ArchiveCreatureFiles = createS3FilesCollection({ collectionName: 'archiveCreatureFiles', @@ -11,7 +15,38 @@ const ArchiveCreatureFiles = createS3FilesCollection({ if (!/json/i.test(file.extension)){ return 'Please upload only a JSON file'; } + return true; + }, + onAfterUpload(file) { + if (Meteor.isServer) incrementFileStorageUsed(file.userId, file.size); } }); +let archiveSchema = new SimpleSchema({ + meta: { + type: Object, + blackbox: true, + }, + creature: CreatureSchema, + properties: { + type: Array, + }, + 'properties.$': CreaturePropertySchema, + experiences: { + type: Array, + }, + 'experiences.$': { + type: Object, + blackbox: true, + }, + logs: { + type: Array, + }, + 'logs.$': { + type: Object, + blackbox: true, + }, +}); + export default ArchiveCreatureFiles; +export { archiveSchema }; diff --git a/app/imports/api/creature/archive/ArchivedCreatures.js b/app/imports/api/creature/archive/ArchivedCreatures.js deleted file mode 100644 index 4a572482..00000000 --- a/app/imports/api/creature/archive/ArchivedCreatures.js +++ /dev/null @@ -1,57 +0,0 @@ -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/archiveCreatureToFile.js b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js index 7599299c..92b6f3bd 100644 --- a/app/imports/api/creature/archive/methods/archiveCreatureToFile.js +++ b/app/imports/api/creature/archive/methods/archiveCreatureToFile.js @@ -68,7 +68,7 @@ const archiveCreatureToFile = new ValidatedMethod({ async run({creatureId}) { assertOwnership(creatureId, this.userId); if (Meteor.isServer){ - archiveCreature(creatureId); + archiveCreature(creatureId, this.userId); } else { removeCreatureWork(creatureId); } diff --git a/app/imports/api/creature/archive/methods/index.js b/app/imports/api/creature/archive/methods/index.js index d3784bde..491ba038 100644 --- a/app/imports/api/creature/archive/methods/index.js +++ b/app/imports/api/creature/archive/methods/index.js @@ -1,4 +1,3 @@ -// import '/imports/api/creature/archive/methods/archiveCreatures.js'; import '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; -import '/imports/api/creature/archive/methods/restoreCreatures.js'; import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js'; +import '/imports/api/creature/archive/methods/removeArchiveCreature.js'; diff --git a/app/imports/api/creature/archive/methods/removeArchiveCreature.js b/app/imports/api/creature/archive/methods/removeArchiveCreature.js new file mode 100644 index 00000000..f4ba608b --- /dev/null +++ b/app/imports/api/creature/archive/methods/removeArchiveCreature.js @@ -0,0 +1,40 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; + +const removeArchiveCreature = new ValidatedMethod({ + name: 'ArchiveCreatureFiles.methods.removeArchiveCreature', + validate: new SimpleSchema({ + 'fileId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + async run({ fileId }) { + // fetch the file + const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get(); + if (!file) { + throw new Meteor.Error('File not found', + 'The requested creature archive does not exist'); + } + // Assert ownership + const userId = file?.userId; + if (!userId || userId !== this.userId) { + throw new Meteor.Error('Permission denied', + 'You can only restore creatures you own'); + } + //Remove the archive once the restore succeeded + ArchiveCreatureFiles.remove({ _id: fileId }); + // Update the user's file storage limits + incrementFileStorageUsed(userId, -file.size); + }, +}); + +export default removeArchiveCreature; diff --git a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js index 986bb9ca..9479b0c7 100644 --- a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js +++ b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js @@ -8,12 +8,16 @@ 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 ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; +import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js'; +import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js'; + let migrateArchive; if (Meteor.isServer){ migrateArchive = require('/imports/migrations/server/migrateArchive.js').default; } -function restoreCreature(archive){ +function restoreCreature(archive, userId){ if (SCHEMA_VERSION < archive.meta.schemaVersion){ throw new Meteor.Error('Incompatible', 'The archive file is from a newer version. Update required to read.') @@ -22,6 +26,19 @@ function restoreCreature(archive){ // Migrate and verify the archive meets the current schema migrateArchive(archive); + // Asset that the archive is safe + verifyArchiveSafety(archive); + + // Don't upload creatures twice + const existingCreature = Creatures.findOne(archive.creature._id, { + fields: { _id: 1 } + }); + if (existingCreature) throw new Meteor.Error('Already exists', + 'The creature you are trying to restore already exists.') + + // Ensure the user owns the restored creature + archive.creature.owner = userId; + // Insert the creature sub documents // They still have their original _id's Creatures.insert(archive.creature); @@ -69,13 +86,18 @@ const restoreCreaturefromFile = new ValidatedMethod({ throw new Meteor.Error('Permission denied', 'You can only restore creatures you own'); } + + assertHasCharactersSlots(this.userId); + if (Meteor.isServer){ // Read the file data const archive = await ArchiveCreatureFiles.readJSONFile(file); - restoreCreature(archive); + restoreCreature(archive, this.userId); } //Remove the archive once the restore succeeded - ArchiveCreatureFiles.remove({_id: fileId}); + ArchiveCreatureFiles.remove({ _id: fileId }); + // Update the user's file storage limits + incrementFileStorageUsed(userId, -file.size); }, }); diff --git a/app/imports/api/creature/archive/methods/restoreCreatures.js b/app/imports/api/creature/archive/methods/restoreCreatures.js deleted file mode 100644 index 5da270e7..00000000 --- a/app/imports/api/creature/archive/methods/restoreCreatures.js +++ /dev/null @@ -1,77 +0,0 @@ -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'; - -export 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/archive/methods/verifyArchiveSafety.js b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js new file mode 100644 index 00000000..734b2582 --- /dev/null +++ b/app/imports/api/creature/archive/methods/verifyArchiveSafety.js @@ -0,0 +1,28 @@ +import { slice } from 'lodash'; +import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs.js'; + +export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }){ + const creatureId = creature._id; + + // Check lengths of arrays + if (logs.length > PER_CREATURE_LOG_LIMIT) { + logs = slice(logs, 0, PER_CREATURE_LOG_LIMIT); + } + + // Check that everything belongs to the right creature + logs.forEach(log => { + if (log.creatureId !== creatureId) { + throw new Meteor.Error('Malicious log', 'Log contains an entry for the wrong creature'); + } + }); + experiences.forEach(experience => { + if (experience.creatureId !== creatureId) { + throw new Meteor.Error('Malicious experience', 'Experiences contains an entry for the wrong creature'); + } + }); + properties.forEach(prop => { + if (prop.ancestors[0].id !== creatureId) { + throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature'); + } + }); +} diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index b70d96d5..e4cc728e 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -82,6 +82,13 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ index: 1, removeBeforeCompute: true, }, + // When this is true on any property, the creature needs to be recomputed + dirty: { + type: Boolean, + // Default to true because new properties cause a recomputation + defaultValue: true, + optional: true, + }, }); CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); @@ -98,10 +105,6 @@ for (let key in propertySchemasIndex){ }); } -import '/imports/api/creature/creatureProperties/methods/index.js'; -//import '/imports/api/creature/actions/doAction.js'; -//import '/imports/api/creature/actions/castSpellWithSlot.js'; - export default CreatureProperties; export { DenormalisedOnlyCreaturePropertySchema, diff --git a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js index b9958fcb..06ca6459 100644 --- a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js +++ b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js @@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const adjustQuantity = new ValidatedMethod({ name: 'creatureProperties.adjustQuantity', @@ -29,10 +28,6 @@ const adjustQuantity = new ValidatedMethod({ // Do work adjustQuantityWork({property, operation, value}); - - // Changing quantity does not change dependencies, but recomputing the - // inventory changes many deps at once, so recompute fully - computeCreature(rootCreature._id); }, }); @@ -47,7 +42,7 @@ export function adjustQuantityWork({property, operation, value}){ } if (operation === 'set'){ CreatureProperties.update(property._id, { - $set: {quantity: value} + $set: {quantity: value, dirty: true} }, { selector: property }); @@ -57,7 +52,8 @@ export function adjustQuantityWork({property, operation, value}){ let currentQuantity = property.quantity; if (currentQuantity + value < 0) value = -currentQuantity; CreatureProperties.update(property._id, { - $inc: {quantity: value} + $inc: { quantity: value }, + $set: { dirty: true } }, { selector: property }); diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js index 573e004a..315b6f2c 100644 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js @@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js'; const damageProperty = new ValidatedMethod({ name: 'creatureProperties.damage', @@ -37,9 +36,7 @@ const damageProperty = new ValidatedMethod({ `Property of type "${property.type}" can't be damaged` ); } - let result = damagePropertyWork({property, operation, value}); - // Dependencies can't be changed through damage, only recompute deps - computeCreatureDependencyGroup(property); + let result = damagePropertyWork({ property, operation, value }); return result; }, }); @@ -70,7 +67,7 @@ export function damagePropertyWork({property, operation, value}){ // Write the results CreatureProperties.update(property._id, { - $set: {damage, value: newValue} + $set: {damage, value: newValue, dirty: true} }, { selector: property }); diff --git a/app/imports/api/creature/creatureProperties/methods/dealDamage.js b/app/imports/api/creature/creatureProperties/methods/dealDamage.js index b93aa495..ddb3dcc1 100644 --- a/app/imports/api/creature/creatureProperties/methods/dealDamage.js +++ b/app/imports/api/creature/creatureProperties/methods/dealDamage.js @@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur 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 computeCreature from '/imports/api/engine/computeCreature.js'; const dealDamage = new ValidatedMethod({ name: 'creatureProperties.dealDamage', @@ -33,7 +32,6 @@ const dealDamage = new ValidatedMethod({ assertEditPermission(creature, this.userId); const totalDamage = dealDamageWork({creature, damageType, amount}) - computeCreature(creatureId); return totalDamage; }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 03c505c2..3e640714 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -9,7 +9,6 @@ import { renewDocIds } from '/imports/api/parenting/parenting.js'; import { reorderDocs } from '/imports/api/parenting/order.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; var snackbar; if (Meteor.isClient){ snackbar = require( @@ -77,6 +76,9 @@ const duplicateProperty = new ValidatedMethod({ // Order the root node property.order += 0.5; + + // Mark the sheet as needing recompute + property.dirty = true; // Insert the properties CreatureProperties.batchInsert([property, ...nodes]); @@ -87,9 +89,6 @@ const duplicateProperty = new ValidatedMethod({ ancestorId: property.ancestors[0].id, }); - // Inserting a creature property invalidates dependencies: full recompute - computeCreature(creature._id); - return propertyId; }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/equipItem.js b/app/imports/api/creature/creatureProperties/methods/equipItem.js index 24cb51fa..a8bbb911 100644 --- a/app/imports/api/creature/creatureProperties/methods/equipItem.js +++ b/app/imports/api/creature/creatureProperties/methods/equipItem.js @@ -4,7 +4,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { organizeDoc } from '/imports/api/parenting/organizeMethods.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; @@ -29,7 +28,7 @@ const equipItem = new ValidatedMethod({ let creature = getRootCreatureAncestor(item); assertEditPermission(creature, this.userId); CreatureProperties.update(_id, { - $set: {equipped}, + $set: { equipped, dirty: true }, }, { selector: {type: 'item'}, }); @@ -46,8 +45,6 @@ const equipItem = new ValidatedMethod({ order: Number.MAX_SAFE_INTEGER, skipRecompute: true, }); - - computeCreature(creature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/flipToggle.js b/app/imports/api/creature/creatureProperties/methods/flipToggle.js index e39d0c75..df563cdc 100644 --- a/app/imports/api/creature/creatureProperties/methods/flipToggle.js +++ b/app/imports/api/creature/creatureProperties/methods/flipToggle.js @@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const flipToggle = new ValidatedMethod({ name: 'creatureProperties.flipToggle', @@ -36,12 +35,10 @@ const flipToggle = new ValidatedMethod({ CreatureProperties.update(_id, {$set: { enabled: !currentValue, disabled: currentValue, + dirty: true, }}, { selector: {type: 'toggle'}, }); - - // Updating a toggle is likely to change the whole tree, do a full recompute - computeCreature(rootCreature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js index 1bba9421..c80f7fa3 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js @@ -15,9 +15,26 @@ export default function getSlotFillFilter({slot, libraryIds}){ slotFillerType: slot.slotType, }] }); + } else if (slot.type === 'class') { + filter.$and.push({ + $or: [{ + type: 'classLevel', + },{ + type: 'slotFiller', + slotFillerType: 'classLevel', + }] + }); + filter.variableName = slot.variableName; + + // Only search for levels the class needs + if (slot.missingLevels && slot.missingLevels.length) { + filter.level = {$in: slot.missingLevels}; + } else { + filter.level = (slot.level || 0) + 1; + } } let tagsOr = []; - let tagsNor = []; + let tagsNin = []; if (slot.slotTags && slot.slotTags.length){ tagsOr.push({tags: {$all: slot.slotTags}}); } @@ -27,15 +44,15 @@ export default function getSlotFillFilter({slot, libraryIds}){ if (extra.operation === 'OR'){ tagsOr.push({tags: {$all: extra.tags}}); } else if (extra.operation === 'NOT'){ - tagsNor.push({tags: {$all: extra.tags}}); + tagsNin.push(...extra.tags); } }); } if (tagsOr.length){ - filter.$and.push({$or: tagsOr}); + filter.$or = tagsOr; } - if (tagsNor.length){ - filter.$and.push({$nor: tagsNor}); + if (tagsNin.length){ + filter.$and.push({tags: {$nin: tagsNin}}); } if (!filter.$and.length){ delete filter.$and; diff --git a/app/imports/api/creature/creatureProperties/methods/insertProperty.js b/app/imports/api/creature/creatureProperties/methods/insertProperty.js index 4535e1fe..5d3567b0 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/insertProperty.js @@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge import SimpleSchema from 'simpl-schema'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { reorderDocs } from '/imports/api/parenting/order.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import { getAncestry } from '/imports/api/parenting/parenting.js'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; @@ -132,14 +131,13 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({ export function insertPropertyWork({property, creature}){ delete property._id; + property.dirty = true; let _id = CreatureProperties.insert(property); // Tree structure changed by insert, reorder the tree reorderDocs({ collection: CreatureProperties, ancestorId: creature._id, }); - // Inserting a creature property invalidates dependencies: full recompute - computeCreature(creature._id); return _id; } diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index e73cbe33..10de2b4a 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { setLineageOfDocs, @@ -71,9 +70,6 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ collection: CreatureProperties, ancestorId: rootCreature._id, }); - - // Inserting a creature property invalidates dependencies: full recompute - computeCreature(rootCreature._id); // Return the docId of the last property, the inserted root property return rootId; }, @@ -135,12 +131,14 @@ function insertPropertyFromNode(nodeId, ancestors, order){ node.order = order; } + // Mark all nodes as dirty + dirtyNodes(nodes); + // Insert the creature properties CreatureProperties.batchInsert(nodes); return node; } - function storeLibraryNodeReferences(nodes){ nodes.forEach(node => { if (node.libraryNodeId) return; @@ -148,6 +146,12 @@ function storeLibraryNodeReferences(nodes){ }); } +function dirtyNodes(nodes) { + nodes.forEach(node => { + node.dirty = true; + }); +} + // Covert node references into actual nodes // TODO: check permissions for each library a reference node references function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ diff --git a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js index fa2acf78..3d8f99ea 100644 --- a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js @@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const pullFromProperty = new ValidatedMethod({ name: 'creatureProperties.pull', @@ -21,15 +20,12 @@ const pullFromProperty = new ValidatedMethod({ // Do work CreatureProperties.update(_id, { - $pull: {[path.join('.')]: {_id: itemId}}, + $pull: { [path.join('.')]: { _id: itemId } }, + $set: { dirty: true } }, { selector: {type: property.type}, getAutoValues: false, }); - - // TODO figure out if this method can change deps or not - computeCreature(rootCreature._id); - // recomputePropertyDependencies(property); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js index e730065c..0ec4719c 100644 --- a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js @@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import { get } from 'lodash'; const pushToProperty = new ValidatedMethod({ @@ -39,13 +38,11 @@ const pushToProperty = new ValidatedMethod({ // Do work CreatureProperties.update(_id, { - $push: {[joinedPath]: value}, + $push: { [joinedPath]: value }, + $set: { dirty: true }, }, { selector: {type: property.type}, }); - - // TODO figure out if this method can change deps or not - computeCreature(rootCreature._id); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js index b612e6c7..02ed7637 100644 --- a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js @@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { restore } from '/imports/api/parenting/softRemove.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const restoreProperty = new ValidatedMethod({ name: 'creatureProperties.restore', @@ -24,10 +23,13 @@ const restoreProperty = new ValidatedMethod({ assertEditPermission(rootCreature, this.userId); // Do work - restore({_id, collection: CreatureProperties}); - - // Changes dependency tree by restoring children - computeCreature(rootCreature._id); + restore({ + _id, + collection: CreatureProperties, + extraUpdates: { + $set: { dirty: true } + }, + }); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js index c1e4baa3..4f460287 100644 --- a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js +++ b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js @@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const selectAmmoItem = new ValidatedMethod({ name: 'creatureProperties.selectAmmoItem', @@ -37,15 +36,10 @@ const selectAmmoItem = new ValidatedMethod({ } let path = `resources.itemsConsumed.${itemConsumedIndex}.itemId`; CreatureProperties.update(actionId, { - $set: {[path]: itemId} + $set: { [path]: itemId, dirty: true } }, { selector: action, }); - - // Changing the linked item does change the dependency tree - // TODO: We can predict exactly which deps will be affected instead of - // recomputing the entire creature - computeCreature(rootCreature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js index 86df9247..96110e8e 100644 --- a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js @@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { softRemove } from '/imports/api/parenting/softRemove.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const softRemoveProperty = new ValidatedMethod({ name: 'creatureProperties.softRemove', @@ -25,9 +24,6 @@ const softRemoveProperty = new ValidatedMethod({ // Do work softRemove({_id, collection: CreatureProperties}); - - // Changes dependency tree by removing children - computeCreature(rootCreature._id); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js index d53ddc6e..d6bf733c 100644 --- a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js @@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; const updateCreatureProperty = new ValidatedMethod({ name: 'creatureProperties.update', @@ -37,17 +36,13 @@ const updateCreatureProperty = new ValidatedMethod({ let modifier; // unset empty values if (value === null || value === undefined){ - modifier = {$unset: {[pathString]: 1}}; + modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } }; } else { - modifier = {$set: {[pathString]: value}}; + modifier = { $set: {[pathString]: value, dirty: true } }; } CreatureProperties.update(_id, modifier, { selector: {type: property.type}, }); - - // Updating a property is likely to change dependencies, do a full recompute - // denormalised stats might change, so fetch the creature again - computeCreature(rootCreature._id); }, }); diff --git a/app/imports/api/creature/creatures/CreatureVariables.js b/app/imports/api/creature/creatures/CreatureVariables.js new file mode 100644 index 00000000..27fe0af7 --- /dev/null +++ b/app/imports/api/creature/creatures/CreatureVariables.js @@ -0,0 +1,21 @@ +//set up the collection for creature variables +let CreatureVariables = new Mongo.Collection('creatureVariables'); + +// Unique index on _creatureId +if (Meteor.isServer) { + CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true }) +} + +/** No schema because the structure isn't known until compute time + * Expect documents to looke like: + * { + * _id: "nE8Ngd6K4L4jSxLY2", + * _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature + * explicitlyDefinedVariableName: {...some creatureProperty} + * implicitVariableName: {value: 10}, + * undefinedVariableName: {}, + * } + * Where top level fields that don't start with `_` are variables on the sheet +**/ + +export default CreatureVariables; diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.js index 1c118fee..311a013e 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.js @@ -80,6 +80,27 @@ let CreatureSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.url, }, + + // Libraries + allowedLibraries: { + type: Array, + optional: true, + maxCount: 100, + }, + 'allowedLibraries.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + allowedLibraryCollections: { + type: Array, + optional: true, + maxCount: 100, + }, + 'allowedLibraryCollections.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Mechanics deathSave: { type: deathSaveSchema, @@ -100,6 +121,11 @@ let CreatureSchema = new SimpleSchema({ type: SimpleSchema.Integer, defaultValue: 0, }, + // Does the character need a recompute? + dirty: { + type: Boolean, + optional: true, + }, // Version of computation engine that was last used to compute this creature computeVersion: { type: String, @@ -160,8 +186,8 @@ CreatureSchema.extend(SharingSchema); Creatures.attachSchema(CreatureSchema); -import '/imports/api/creature/creatures/methods/index.js'; -import '/imports/api/engine/actions/doAction.js'; export default Creatures; export { CreatureSchema }; + +import '/imports/api/engine/actions/doAction.js'; diff --git a/app/imports/api/creature/creatures/defaultCharacterProperties.js b/app/imports/api/creature/creatures/defaultCharacterProperties.js index dd3d5499..e7f05a11 100644 --- a/app/imports/api/creature/creatures/defaultCharacterProperties.js +++ b/app/imports/api/creature/creatures/defaultCharacterProperties.js @@ -10,7 +10,7 @@ export default function defaultCharacterProperties(creatureId){ { type: 'propertySlot', name: 'Ruleset', - description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'}, + description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.'}, slotTags: ['base'], tags: [], quantityExpected: {calculation: '1'}, diff --git a/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js b/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js new file mode 100644 index 00000000..5b368e5f --- /dev/null +++ b/app/imports/api/creature/creatures/methods/assertHasCharacterSlots.js @@ -0,0 +1,22 @@ +import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; + +export default function assertHasCharactersSlots(userId) { + if (characterSlotsRemaining(userId) <= 0) { + throw new Meteor.Error('characterSlotLimit', + 'No character slots left') + } +} + +export function characterSlotsRemaining(userId) { + let tier = getUserTier(userId); + const currentCharacterCount = Creatures.find({ + owner: userId, + }, { + fields: { _id: 1 }, + }).count(); + if (tier.characterSlots === -1) { + return Number.POSITIVE_INFINITY; + } + return tier.characterSlots - currentCharacterCount; +} diff --git a/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js b/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js new file mode 100644 index 00000000..29057f55 --- /dev/null +++ b/app/imports/api/creature/creatures/methods/changeAllowedLibraries.js @@ -0,0 +1,90 @@ +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'; +import SimpleSchema from 'simpl-schema'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; + +const changeAllowedLibraries = new ValidatedMethod({ + name: 'creatures.changeAllowedLibraries', + mixins: [RateLimiterMixin, simpleSchemaMixin], + schema: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + allowedLibraries: { + type: Array, + optional: true, + maxCount: 100, + }, + 'allowedLibraries.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + allowedLibraryCollections: { + type: Array, + optional: true, + maxCount: 100, + }, + 'allowedLibraryCollections.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }), + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + run({_id, allowedLibraries, allowedLibraryCollections}) { + let creature = Creatures.findOne(_id); + assertEditPermission(creature, this.userId); + let $set; + if (allowedLibraries) { + $set = { allowedLibraries } + } + if (allowedLibraryCollections) { + if (!$set) $set = {}; + $set.allowedLibraryCollections = allowedLibraryCollections; + } + if (!$set) return; + Creatures.update(_id, {$set}); + }, +}); + +const toggleAllUserLibraries = new ValidatedMethod({ + name: 'creatures.removeLibraryLimits', + mixins: [RateLimiterMixin, simpleSchemaMixin], + schema: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + value: { + type: Boolean, + }, + }), + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + run({_id, value}) { + if (value) { + Creatures.update(_id, { + $unset: { + allowedLibraryCollections: 1, + allowedLibraries: 1, + }, + }); + } else { + Creatures.update(_id, { + $set: { + allowedLibraryCollections: [], + allowedLibraries: [], + }, + }); + } + }, +}); + +export {changeAllowedLibraries, toggleAllUserLibraries}; diff --git a/app/imports/api/creature/creatures/methods/index.js b/app/imports/api/creature/creatures/methods/index.js index 42e9c99e..85aefc77 100644 --- a/app/imports/api/creature/creatures/methods/index.js +++ b/app/imports/api/creature/creatures/methods/index.js @@ -2,3 +2,4 @@ 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/updateCreature.js'; +import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js'; diff --git a/app/imports/api/creature/creatures/methods/insertCreature.js b/app/imports/api/creature/creatures/methods/insertCreature.js index 79086eb3..da72f6ed 100644 --- a/app/imports/api/creature/creatures/methods/insertCreature.js +++ b/app/imports/api/creature/creatures/methods/insertCreature.js @@ -1,70 +1,104 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; +import Creatures, { CreatureSchema } 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'; +import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'; +import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js'; +import SimpleSchema from 'simpl-schema'; const insertCreature = new ValidatedMethod({ - name: 'creatures.insertCreature', - - validate: null, - - mixins: [RateLimiterMixin], + mixins: [RateLimiterMixin, simpleSchemaMixin], + schema: CreatureSchema.pick( + 'name', + 'gender', + 'alignment', + 'allowedLibraries', + 'allowedLibraryCollections', + ).extend({ + 'startingLevel': { + type: SimpleSchema.Integer, + min: 0, + }, + }), rateLimit: { numRequests: 5, timeInterval: 5000, }, - run() { - if (!this.userId) { + run({ name, gender, alignment, startingLevel, + allowedLibraries, allowedLibraryCollections }) { + const userId = this.userId + if (!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`) + 'You need to be logged in to insert a creature'); } - // Create the creature document + assertHasCharactersSlots(userId); + + // Create the creature document let creatureId = Creatures.insert({ - owner: this.userId, - }); + owner: userId, + name, + gender, + alignment, + allowedLibraries, + allowedLibraryCollections, + }); + + // Insert experience to get character to starting level + if (startingLevel) { + insertExperienceForCreature({ + experience: { + name: 'Starting level', + levels: startingLevel, + creatureId + }, + creatureId, + userId, + }); + } // Insert the default properties // Not batchInsert because we want the properties cleaned by the schema - let baseId; + let baseId, rulesetSlot; defaultCharacterProperties(creatureId).forEach(prop => { let id = CreatureProperties.insert(prop); if (prop.name === 'Ruleset'){ baseId = id; + rulesetSlot = prop; } }); - if (Meteor.isServer){ - // Insert the 5e ruleset as the default base - insertPropertyFromLibraryNode.call({ - nodeIds: ['iHbhfcg3AL5isSWbw'], - parentRef: {id: baseId, collection: 'creatureProperties'}, - order: 0.5, - }); + // If the user only has a single ruleset subscribed, use it by default + if (Meteor.isServer) { + insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot); } return creatureId; }, }); +// If the user only has a single ruleset subscribed, insert it by default +function insertDefaultRuleset(creatureId, baseId, userId, slot) { + const libraryIds = getCreatureLibraryIds(creatureId, userId); + const filter = getSlotFillFilter({ slot, libraryIds }); + const fillCursor = LibraryNodes.find(filter, { fields: { _id: 1 } }); + const numRulesets = fillCursor.count(); + if (numRulesets === 1) { + const ruleset = fillCursor.fetch()[0] + insertPropertyFromLibraryNode.call({ + nodeIds: [ruleset._id], + parentRef: {id: baseId, collection: 'creatureProperties'}, + order: 0.5, + }); + } +} + export default insertCreature; diff --git a/app/imports/api/creature/creatures/methods/removeCreature.js b/app/imports/api/creature/creatures/methods/removeCreature.js index 66eaa0b4..d157f5d7 100644 --- a/app/imports/api/creature/creatures/methods/removeCreature.js +++ b/app/imports/api/creature/creatures/methods/removeCreature.js @@ -3,11 +3,13 @@ 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 CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.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'; function removeRelatedDocuments(creatureId){ + CreatureVariables.remove({_creatureId: creatureId}); CreatureProperties.remove({'ancestors.id': creatureId}); CreatureLogs.remove({creatureId}); Experiences.remove({creatureId}); diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index 07a607ee..2f630a5b 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -1,13 +1,17 @@ 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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; +import { groupBy, remove, rest, union } from 'lodash'; +import { + getCreature, getVariables, getPropertiesOfType +} from '/imports/api/engine/loadCreatures.js'; +import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; +import { applyTrigger } from '/imports/api/engine/actions/applyTriggers.js'; const restCreature = new ValidatedMethod({ - name: 'creature.methods.longRest', + name: 'creature.methods.rest', validate: new SimpleSchema({ creatureId: { type: String, @@ -24,93 +28,142 @@ const restCreature = new ValidatedMethod({ timeInterval: 5000, }, run({creatureId, restType}) { - let creature = Creatures.findOne(creatureId, { - fields: { - owner: 1, - writers: 1, - settings: 1, - } - }) ; - // Need edit permissions + // Check permissions + let creature = getCreature(creatureId); assertEditPermission(creature, this.userId); - // Long rests reset short rest properties as well - let resetFilter; - if (restType === 'shortRest'){ - resetFilter = 'shortRest' - } else { - resetFilter = {$in: ['shortRest', 'longRest']} + // Add the variables to the creature document + const variables = getVariables(creatureId); + delete variables._id; + delete variables._creatureId; + creature.variables = variables; + const scope = creature.variables; + + // Get the triggers + let triggers = getPropertiesOfType(creatureId, 'trigger'); + remove(triggers, trigger => + trigger.event !== 'anyRest' && + trigger.event !== 'longRest' && + trigger.event !== 'shortRest' + ); + triggers = groupBy(triggers, 'event'); + for (let type in triggers) { + triggers[type] = groupBy(triggers[type], 'timing') } - // Only apply to active properties - let filter = { - 'ancestors.id': creatureId, - reset: resetFilter, - removed: {$ne: true}, - inactive: {$ne: true}, - }; - // update all attribute's damage - filter.type = 'attribute'; - CreatureProperties.update(filter, { - $set: {damage: 0} - }, { - selector: {type: 'attribute'}, - multi: true, + + // Create the log + const log = CreatureLogSchema.clean({ + creatureId: creature._id, + creatureName: creature.name, }); - // Update all action-like properties' usesUsed - filter.type = {$in: [ - 'action', - 'attack', - 'spell' - ]}; - CreatureProperties.update(filter, { - $set: {usesUsed: 0} - }, { - selector: {type: 'action'}, - multi: true, - }); - // Reset half hit dice on a long rest, starting with the highest dice - if (restType === 'longRest'){ - let hitDice = CreatureProperties.find({ - 'ancestors.id': creatureId, - type: 'attribute', - attributeType: 'hitDice', - removed: {$ne: true}, - inactive: {$ne: true}, - }, { - fields: { - hitDiceSize: 1, - damage: 1, - value: 1, - } - }).fetch(); - // Use a collator to do sorting in natural order - let collator = new Intl.Collator('en', { - numeric: true, sensitivity: 'base' - }); - // Get the hit dice in decending order of hitDiceSize - let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) - hitDice.sort(compare); - // Get the total number of hit dice that can be recovered this rest - let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0); - let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5; - let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1); - // recover each hit dice in turn until the recoverable amount is used up - let amountToRecover, resultingDamage; - hitDice.forEach(hd => { - if (!recoverableHd) return; - amountToRecover = Math.min(recoverableHd, hd.damage || 0); - if (!amountToRecover) return; - recoverableHd -= amountToRecover; - resultingDamage = hd.damage - amountToRecover; - CreatureProperties.update(hd._id, { - $set: {damage: resultingDamage} - }, { - selector: {type: 'attribute'}, - }); - }); - } - computeCreature(creatureId); + + const targets = [creature]; + + applyTriggers(triggers, restType, 'before', { creature, targets, scope, log }); + doRestWork(creature, restType); + applyTriggers(triggers, restType, 'after', { creature, targets, scope, log }); + + insertCreatureLogWork({log, creature, method: this}); }, }); +function applyTriggers(triggers, restType, timing, opts) { + // Get matching triggers + let selectedTriggers = triggers[restType]?.[timing] || []; + // Get any rest triggers as well + selectedTriggers = union(selectedTriggers, triggers['anyRest']?.[timing]); + selectedTriggers.sort((a, b) => a.order - b.order); + // Apply the triggers + selectedTriggers.forEach(trigger => { + applyTrigger(trigger, opts) + }); +} + +function doRestWork(creature, restType) { + // Long rests reset short rest properties as well + let resetFilter; + if (restType === 'shortRest'){ + resetFilter = 'shortRest' + } else { + resetFilter = {$in: ['shortRest', 'longRest']} + } + // Only apply to active properties + let filter = { + 'ancestors.id': creature._id, + reset: resetFilter, + removed: { $ne: true }, + inactive: { $ne: true }, + }; + // update all attribute's damage + filter.type = 'attribute'; + CreatureProperties.update(filter, { + $set: { + damage: 0, + dirty: true, + } + }, { + selector: {type: 'attribute'}, + multi: true, + }); + // Update all action-like properties' usesUsed + filter.type = {$in: [ + 'action', + 'attack', + 'spell' + ]}; + CreatureProperties.update(filter, { + $set: { + usesUsed: 0, + dirty: true, + } + }, { + selector: {type: 'action'}, + multi: true, + }); + // Reset half hit dice on a long rest, starting with the highest dice + if (restType === 'longRest'){ + let hitDice = CreatureProperties.find({ + 'ancestors.id': creature._id, + type: 'attribute', + attributeType: 'hitDice', + removed: {$ne: true}, + inactive: {$ne: true}, + }, { + fields: { + hitDiceSize: 1, + damage: 1, + value: 1, + } + }).fetch(); + // Use a collator to do sorting in natural order + let collator = new Intl.Collator('en', { + numeric: true, sensitivity: 'base' + }); + // Get the hit dice in decending order of hitDiceSize + let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) + hitDice.sort(compare); + // Get the total number of hit dice that can be recovered this rest + let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0); + let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5; + let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1); + // recover each hit dice in turn until the recoverable amount is used up + let amountToRecover, resultingDamage; + hitDice.forEach(hd => { + if (!recoverableHd) return; + amountToRecover = Math.min(recoverableHd, hd.damage || 0); + if (!amountToRecover) return; + recoverableHd -= amountToRecover; + resultingDamage = hd.damage - amountToRecover; + CreatureProperties.update(hd._id, { + $set: { + damage: resultingDamage, + dirty: true, + } + }, { + selector: {type: 'attribute'}, + }); + }); + } +} + export default restCreature; diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index 7c0bb002..58b1aeed 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -3,7 +3,6 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; let Experiences = new Mongo.Collection('experiences'); @@ -48,20 +47,20 @@ let ExperienceSchema = new SimpleSchema({ Experiences.attachSchema(ExperienceSchema); const insertExperienceForCreature = function({experience, creatureId, userId}){ - assertEditPermission(creatureId, userId); if (experience.xp){ - Creatures.update(creatureId, {$inc: { - 'denormalizedStats.xp': experience.xp - }}); + Creatures.update(creatureId, { + $inc: { 'denormalizedStats.xp': experience.xp }, + $set: { dirty: true }, + }); } if (experience.levels) { - Creatures.update(creatureId, {$inc: { - 'denormalizedStats.milestoneLevels': experience.levels - }}); + Creatures.update(creatureId, { + $inc: { 'denormalizedStats.milestoneLevels': experience.levels }, + $set: { dirty: true }, + }); } experience.creatureId = creatureId; let id = Experiences.insert(experience); - computeCreature(creatureId); return id; }; @@ -93,6 +92,7 @@ const insertExperience = new ValidatedMethod({ } let insertedIds = []; creatureIds.forEach(creatureId => { + assertEditPermission(creatureId, userId); let id = insertExperienceForCreature({experience, creatureId, userId}); insertedIds.push(id); }); @@ -124,18 +124,19 @@ const removeExperience = new ValidatedMethod({ let creatureId = experience.creatureId assertEditPermission(creatureId, userId); if (experience.xp){ - Creatures.update(creatureId, {$inc: { - 'denormalizedStats.xp': -experience.xp - }}); + Creatures.update(creatureId, { + $inc: { 'denormalizedStats.xp': -experience.xp }, + $set: { dirty: true }, + }); } if (experience.levels) { - Creatures.update(creatureId, {$inc: { - 'denormalizedStats.milestoneLevels': -experience.levels - }}); + Creatures.update(creatureId, { + $inc: { 'denormalizedStats.milestoneLevels': -experience.levels }, + $set: { dirty: true }, + }); } experience.creatureId = creatureId; let numRemoved = Experiences.remove(experienceId); - computeCreature(creatureId); return numRemoved; }, }); @@ -173,11 +174,11 @@ const recomputeExperiences = new ValidatedMethod({ }); Creatures.update(creatureId, {$set: { 'denormalizedStats.xp': xp, - 'denormalizedStats.milestoneLevels': milestoneLevels + 'denormalizedStats.milestoneLevels': milestoneLevels, + dirty: true, }}); - computeCreature(creatureId); }, }); export default Experiences; -export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences }; +export { ExperienceSchema, insertExperience, insertExperienceForCreature, removeExperience, recomputeExperiences }; diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index 1cab572f..de7777b7 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; @@ -154,7 +155,6 @@ const logRoll = new ValidatedMethod({ }).validator(), run({roll, creatureId}){ const creature = Creatures.findOne(creatureId, {fields: { - variables: 1, readers: 1, writers: 1, owner: 1, @@ -163,6 +163,7 @@ const logRoll = new ValidatedMethod({ avatarPicture: 1, }}); assertEditPermission(creature, this.userId); + const variables = CreatureVariables.findOne({ _creatureId: creatureId }); let logContent = [] let parsedResult = undefined; try { @@ -175,7 +176,7 @@ const logRoll = new ValidatedMethod({ let { result: compiled, context - } = resolve('compile', parsedResult, creature.variables); + } = resolve('compile', parsedResult, variables); const compiledString = toString(compiled); if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({ value: roll @@ -183,12 +184,12 @@ const logRoll = new ValidatedMethod({ logContent.push({ value: compiledString }); - let {result: rolled} = resolve('roll', compiled, creature.variables, context); + let {result: rolled} = resolve('roll', compiled, variables, context); let rolledString = toString(rolled); if (rolledString !== compiledString) logContent.push({ value: rolledString }); - let {result} = resolve('reduce', rolled, creature.variables, context); + let {result} = resolve('reduce', rolled, variables, context); let resultString = toString(result); if (resultString !== rolledString) logContent.push({ value: resultString @@ -210,4 +211,4 @@ const logRoll = new ValidatedMethod({ }); export default CreatureLogs; -export { CreatureLogSchema, insertCreatureLog, logRoll}; +export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT}; diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index 32542390..5930a93d 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -7,6 +7,7 @@ import note from './applyPropertyByType/applyNote.js'; import roll from './applyPropertyByType/applyRoll.js'; import savingThrow from './applyPropertyByType/applySavingThrow.js'; import toggle from './applyPropertyByType/applyToggle.js'; +import applyTriggers from '/imports/api/engine/actions/applyTriggers.js'; const applyPropertyByType = { action, @@ -21,7 +22,9 @@ const applyPropertyByType = { toggle, }; -export default function applyProperty(node, opts, ...rest){ +export default function applyProperty(node, opts, ...rest) { + applyTriggers(node, opts, 'before'); opts.scope[`#${node.node.type}`] = node.node; - return applyPropertyByType[node.node.type]?.(node, opts, ...rest); + applyPropertyByType[node.node.type]?.(node, opts, ...rest); + applyTriggers(node, opts, 'after'); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index 0978d07a..f8b6eb8b 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -18,10 +18,9 @@ export default function applyDamage(node, { const prop = node.node; // Skip if there is no parse node to work with - if (!prop.amount.parseNode) return; + if (!prop.amount?.parseNode) return; // Choose target - let damageTargets = prop.target === 'self' ? [creature] : targets; // Determine if the hit is critical let criticalHit = scope['$criticalHit']?.value && @@ -69,6 +68,15 @@ export default function applyDamage(node, { // Round the damage to a whole number damage = Math.floor(damage); + // Convert extra damage into the stored type + if (prop.damageType === 'extra' && scope['$lastDamageType']) { + prop.damageType = scope['$lastDamageType']; + } + // Store current damage type + if (prop.damageType !== 'healing') { + scope['$lastDamageType'] = prop.damageType; + } + // Memoise the damage suffix for the log let suffix = (criticalHit ? ' critical ' : ' ') + prop.damageType + diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js new file mode 100644 index 00000000..30b1790e --- /dev/null +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -0,0 +1,98 @@ +import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js'; +import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js'; +import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; +import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import applyProperty from '/imports/api/engine/actions/applyProperty.js'; +import { difference, intersection } from 'lodash'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; + +export default function applyTriggers(node, { creature, targets, scope, log }, timing) { + const prop = node.node; + const type = prop.type; + if (creature.triggers?.[type]?.[timing]) { + creature.triggers[type][timing].forEach(trigger => { + // Tags + if (!triggerMatchTags(trigger, prop)) return; + // Condition + if (trigger.condition?.parseNode) { + recalculateCalculation(trigger.condition, scope, log); + if (!trigger.condition.value) return; + } + // Apply + applyTrigger(trigger, { creature, targets, scope, log }); + }); + } +} + +function triggerMatchTags(trigger, prop) { + let matched = false; + const propTags = getEffectivePropTags(prop); + // Check the target tags + if ( + !trigger.targetTags?.length || + difference(trigger.targetTags, propTags).length === 0 + ) { + matched = true; + } + // Check the extra tags + trigger.extraTags?.forEach(extra => { + if (extra.operation === 'OR') { + if (matched) return; + if ( + !extra.tags.length || + difference(extra.tags, propTags).length === 0 + ) { + matched = true; + } + } else if (extra.operation === 'NOT') { + if ( + extra.tags.length && + intersection(extra.tags, propTags) + ) { + return false; + } + } + }); + return matched; +} + +export function applyTrigger(trigger, { creature, targets, scope, log }) { + if (trigger.firing) { + /* + log.content.push({ + name: trigger.name || 'Trigger', + value: 'Trigger can\'t fire itself', + inline: true, + }); + */ + return; + } + trigger.firing = true; + + // Fire the trigger + const content = { + name: trigger.name || 'Trigger', + value: trigger.summary, + inline: false, + } + if (trigger.summary?.text){ + recalculateInlineCalculations(trigger.summary, scope, log); + content.value = trigger.summary.value; + } + log.content.push(content); + + // Get all the trigger's properties and apply them + const properties = getPropertyDecendants(creature._id, trigger._id); + properties.sort((a, b) => a.order - b.order); + const propertyForest = nodeArrayToTree(properties); + propertyForest.forEach(node => { + applyProperty(node, { + creature, + targets, + scope, + log, + }); + }); + + trigger.firing = false; +} diff --git a/app/imports/api/engine/actions/doAction.js b/app/imports/api/engine/actions/doAction.js index e72e43bc..75a4112c 100644 --- a/app/imports/api/engine/actions/doAction.js +++ b/app/imports/api/engine/actions/doAction.js @@ -1,14 +1,16 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import { + getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType +} from '/imports/api/engine/loadCreatures.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import applyProperty from './applyProperty.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; +import { groupBy, remove } from 'lodash'; const doAction = new ValidatedMethod({ name: 'creatureProperties.doAction', @@ -38,48 +40,46 @@ const doAction = new ValidatedMethod({ run({actionId, targetIds = [], scope}) { let action = CreatureProperties.findOne(actionId); // Check permissions - let creature = getRootCreatureAncestor(action); - + const creatureId = action.ancestors[0].id; + let creature = getCreature(action.ancestors[0].id); assertEditPermission(creature, this.userId); + // Add the variables to the creature document + const variables = getVariables(creatureId); + delete variables._id; + delete variables._creatureId; + creature.variables = variables; + // Get all the targets and make sure we can edit them let targets = []; targetIds.forEach(targetId => { - let target = Creatures.findOne(targetId); + let target = getCreature(targetId); assertEditPermission(target, this.userId); + + // add the variables to the target documents + const variables = getVariables(creatureId); + delete variables._id; + delete variables._creatureId; + target.variables = variables; + targets.push(target); }); - // Fetch all the action's ancestor creatureProperties - const ancestorIds = []; - action.ancestors.forEach(ref => { - if (ref.collection === 'creatureProperties') { - ancestorIds.push(ref.id); - } - }); + const ancestors = getProperyAncestors(creatureId, action._id); + ancestors.sort((a, b) => a.order - b.order); - // Get cursor of ancestors - const ancestors = CreatureProperties.find({ - _id: {$in: ancestorIds}, - }, { - sort: {order: 1}, - }); - - // Get cursor of the properties - const properties = CreatureProperties.find({ - $or: [{_id: action._id}, {'ancestors.id': action._id}], - removed: {$ne: true}, - }, { - sort: {order: 1}, - }); + const properties = getPropertyDecendants(creatureId, action._id); + properties.push(action); + properties.sort((a, b) => a.order - b.order); // Do the action doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope}); // Recompute all involved creatures - computeCreature(creature._id); - targets.forEach(target => { - computeCreature(target._id); + Creatures.update({ + _id: { $in: [creature._id, ...targetIds] } + }, { + $set: {dirty: true}, }); }, }); @@ -96,6 +96,14 @@ export function doActionWork({ throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`); } + // Get the triggers + const triggers = getPropertiesOfType(creature._id, 'trigger'); + remove(triggers, trigger => trigger.event !== 'doActionProperty'); + creature.triggers = groupBy(triggers, 'actionPropertyType'); + for (let type in creature.triggers) { + creature.triggers[type] = groupBy(creature.triggers[type], 'timing') + } + // Create the log if (!log) log = CreatureLogSchema.clean({ creatureId: creature._id, diff --git a/app/imports/api/engine/actions/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js index 890c4879..e403e271 100644 --- a/app/imports/api/engine/actions/doCastSpell.js +++ b/app/imports/api/engine/actions/doCastSpell.js @@ -7,7 +7,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { doActionWork } from '/imports/api/engine/actions/doAction.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js'; const doAction = new ValidatedMethod({ @@ -129,12 +128,12 @@ const doAction = new ValidatedMethod({ } // Do the action - doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope, log}); - - // Recompute all involved creatures - computeCreature(creature._id); - targets.forEach(target => { - computeCreature(target._id); + doActionWork({ creature, targets, properties, ancestors, method: this, methodScope: scope, log }); + + Creatures.update({ + _id: { $in: [creature._id, ...targetIds] } + }, { + $set: { dirty: true }, }); }, }); diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index ce66db02..9f139f2c 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; import rollDice from '/imports/parser/rollDice.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; @@ -32,9 +31,6 @@ const doCheck = new ValidatedMethod({ // Do the check doCheckWork({creature, prop, method: this, methodScope: scope}); - - // Recompute all involved creatures - computeCreature(creature._id); }, }); diff --git a/app/imports/api/engine/computation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js index 278b3276..1fe14769 100644 --- a/app/imports/api/engine/computation/CreatureComputation.js +++ b/app/imports/api/engine/computation/CreatureComputation.js @@ -1,8 +1,9 @@ import { EJSON } from 'meteor/ejson'; import createGraph from 'ngraph.graph'; +import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; export default class CreatureComputation { - constructor(properties, creature){ + constructor(properties, creature, variables){ // Set up fields this.originalPropsById = {}; this.propsById = {}; @@ -12,6 +13,7 @@ export default class CreatureComputation { this.dependencyGraph = createGraph(); this.errors = []; this.creature = creature; + this.variables = variables; // Store properties for easy access later properties.forEach(prop => { @@ -21,28 +23,15 @@ export default class CreatureComputation { // Store by id this.propsById[prop._id] = prop; - // Store tags - const storePropOnTag = (prop, tag) => { + // Store sets of ids in each tag + getEffectivePropTags(prop).forEach(tag => { if (!tag) return; - if (this.propsWithTag[tag]){ + if (this.propsWithTag[tag]) { this.propsWithTag[tag].push(prop._id); } else { this.propsWithTag[tag] = [prop._id]; } - } - // Store sets of ids in each tag - if (prop.tags){ - prop.tags.forEach(tag => { - storePropOnTag(prop, tag); - }); - } - // Store tags for the property type - storePropOnTag(prop, `#${prop.type}`); - // Store tags for some string properties - storePropOnTag(prop, prop.damageType); - storePropOnTag(prop, prop.skillType); - storePropOnTag(prop, prop.attributeType); - storePropOnTag(prop, prop.reset); + }); // Store the prop in the dependency graph this.dependencyGraph.addNode(prop._id, prop); diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 848cf1d3..5e907acc 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -1,4 +1,4 @@ -import { get, intersection, difference } from 'lodash'; +import { get, intersection, difference, union } from 'lodash'; const linkDependenciesByType = { action: linkAction, @@ -128,10 +128,21 @@ function linkEffects(dependencyGraph, prop, computation){ if (prop.targetByTags){ getEffectTagTargets(prop, computation).forEach(targetId => { const targetProp = computation.propsById[targetId]; - const key = prop.targetField || getDefaultCalculationField(targetProp); - const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation){ - dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect'); + if ( + (targetProp.type === 'attribute' || targetProp.type === 'skill') + && targetProp.variableName + && !prop.targetField + ) { + // If the field wasn't specified and we're targeting an attribute or + // skill, just treat it like a normal effect on its variable name + dependencyGraph.addLink(targetProp.variableName, prop._id, 'effect'); + } else { + // Otherwise target a field on that property + const key = prop.targetField || getDefaultCalculationField(targetProp); + const calcObj = get(targetProp, key); + if (calcObj && calcObj.calculation){ + dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect'); + } } }); } else { @@ -144,16 +155,18 @@ function linkEffects(dependencyGraph, prop, computation){ // Returns an array of IDs of the properties the effect targets function getEffectTagTargets(effect, computation){ - const targets = getTargetListFromTags(effect.targetTags, computation); - const notIds = []; + let targets = getTargetListFromTags(effect.targetTags, computation); + let notIds = []; if (effect.extraTags){ effect.extraTags.forEach(ex => { - if (ex.operation === 'OR'){ - targets.push(...getTargetListFromTags(ex.tags, computation)); + if (ex.operation === 'OR') { + targets = union(targets, getTargetListFromTags(ex.tags, computation)); } else if (ex.operation === 'NOT'){ ex.tags.forEach(tag => { const idList = computation.propsWithTag[tag]; - if (idList) notIds.push(...computation.propsWithTag[tag]) + if (idList) { + notIds = union(notIds, computation.propsWithTag[tag]); + } }); } }); diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index ebf06518..d99d9290 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -1,8 +1,7 @@ import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import CreatureProperties, - { DenormalisedOnlyCreaturePropertySchema as denormSchema } +import { DenormalisedOnlyCreaturePropertySchema as denormSchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures.js'; import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; import linkInventory from './buildComputation/linkInventory.js'; @@ -32,29 +31,15 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; export default function buildCreatureComputation(creatureId){ const creature = getCreature(creatureId); + const variables = getVariables(creatureId); const properties = getProperties(creatureId); - const computation = buildComputationFromProps(properties, creature); + const computation = buildComputationFromProps(properties, creature, variables); return computation; } -function getProperties(creatureId){ - return CreatureProperties.find({ - 'ancestors.id': creatureId, - 'removed': {$ne: true}, - }, { - sort: {order: 1} - }).fetch(); -} +export function buildComputationFromProps(properties, creature, variables){ -function getCreature(creatureId){ - return Creatures.findOne(creatureId, { - denormalizedStats: 1, - }); -} - -export function buildComputationFromProps(properties, creature){ - - const computation = new CreatureComputation(properties, creature); + const computation = new CreatureComputation(properties, creature, variables); // Dependency graph where edge(a, b) means a depends on b // The graph includes all dependencies even of inactive properties // such that any properties changing without changing their dependencies @@ -81,8 +66,10 @@ export function buildComputationFromProps(properties, creature){ // Process the properties one by one properties.forEach(prop => { + // The prop has been processed, it's no longer dirty + delete prop.dirty; - let computedSchema = computedOnlySchemas[prop.type]; + const computedSchema = computedOnlySchemas[prop.type]; removeSchemaFields([computedSchema, denormSchema], prop); // Add a place to store all the computation details @@ -114,5 +101,6 @@ export function buildComputationFromProps(properties, creature){ linkTypeDependencies(dependencyGraph, prop, computation); linkCalculationDependencies(dependencyGraph, prop, computation); }); + return computation; } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index 6b9c3222..828059c0 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -71,6 +71,8 @@ export default function computeVariableAsSkill(computation, node, prop){ prop.fail = aggregator.fail; // Rollbonus prop.rollBonuses = aggregator.rollBonus; + // Store effects + prop.effects = node.data.effects; } function aggregateAbilityEffects({computation, skillNode, abilityNode}){ diff --git a/app/imports/api/engine/computation/utility/getEffectivePropTags.js b/app/imports/api/engine/computation/utility/getEffectivePropTags.js new file mode 100644 index 00000000..e3e52bf2 --- /dev/null +++ b/app/imports/api/engine/computation/utility/getEffectivePropTags.js @@ -0,0 +1,17 @@ +export default function getEffectivePropTags(prop) { + if (!prop.tags) return []; + const tags = [...prop.tags]; + // Tags for the property type, separate #damage from #healing + if (prop.type === 'damage' && prop.damageType === 'healing') { + tags.push('#healing'); + } else { + tags.push(`#${prop.type}`); + } + + // Tags for some string properties + if (prop.damageType) tags.push(prop.damageType); + if (prop.skillType) tags.push(prop.skillType); + if (prop.attributeType) tags.push(prop.attributeType); + if (prop.reset) tags.push(prop.reset); + return tags; +} diff --git a/app/imports/api/engine/computation/utility/walkdown.js b/app/imports/api/engine/computation/utility/walkdown.js index e0c4728a..0efad425 100644 --- a/app/imports/api/engine/computation/utility/walkdown.js +++ b/app/imports/api/engine/computation/utility/walkdown.js @@ -2,7 +2,7 @@ export default function walkDown(tree, callback){ let stack = [...tree]; while(stack.length){ let node = stack.pop(); - callback(node); + callback(node, stack); stack.push(...node.children); } } diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index cd0372cb..fcad024c 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -21,6 +21,7 @@ export default function writeAlteredProperties(computation){ 'deactivatedByAncestor', 'deactivatedByToggle', 'damage', + 'dirty', ...schema.objectKeys(), ]; op = addChangedKeysToOp(op, keys, original, changed); @@ -28,13 +29,14 @@ export default function writeAlteredProperties(computation){ bulkWriteOperations.push(op); } }); - writePropertiesSequentially(bulkWriteOperations); + bulkWriteProperties(bulkWriteOperations); + //if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`); } function addChangedKeysToOp(op, keys, original, changed) { // Loop through all keys that can be changed by computation // and compile an operation that sets all those keys - for (let key of keys){ + for (let key of keys) { if (!EJSON.equals(original[key], changed[key])){ if (!op) op = newOperation(original._id, changed.type); let value = changed[key]; @@ -79,10 +81,10 @@ function addUnsetOp(op, key){ } } -// We use this instead of bulkWriteProperties because it functions with latency -// compensation without needing to roll back changes, which causes multiple -// expensive redraws of the character sheet -function writePropertiesSequentially(bulkWriteOps){ +// If we re-enable client-side sheet recalculation, this needs to be run on +// both client and server to preserve latency compensation. Bulkwrite breaks +// latency compensation and causes flickering +function writePropertiesSequentially(bulkWriteOps) { bulkWriteOps.forEach(op => { let updateOneOrMany = op.updateOne || op.updateMany; CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, { @@ -101,7 +103,7 @@ function writePropertiesSequentially(bulkWriteOps){ function bulkWriteProperties(bulkWriteOps){ if (!bulkWriteOps.length) return; // bulkWrite is only available on the server - if (Meteor.isServer){ + if (Meteor.isServer) { CreatureProperties.rawCollection().bulkWrite( bulkWriteOps, {ordered : false}, diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index c318fb9e..d96c2bfe 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -1,10 +1,20 @@ +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { EJSON } from 'meteor/ejson'; export default function writeScope(creatureId, computation) { + if (!creatureId) throw 'creatureId is required'; const scope = computation.scope; - const variables = computation.creature.variables || {}; - let $set; + let variables = computation.variables; + if (!variables) { + CreatureVariables.insert({ _creatureId: creatureId }); + variables = {}; + } + delete variables._id; + delete variables._creatureId; + + let $set, $unset; + for (const key in scope){ // Remove large properties that aren't likely to be accessed delete scope[key].parent; @@ -20,12 +30,32 @@ export default function writeScope(creatureId, computation) { // Only update changed fields if (!EJSON.equals(variables[key], scope[key])) { if (!$set) $set = {}; - + /* Log detailed diffs + const diff = omitBy(variables[key], (v, k) => EJSON.equals(scope[key][k], v)); + for (let subkey in diff) { + console.log(`${key}.${subkey}: ${variables[key][subkey]} => ${scope[key][subkey]}`) + } + */ // Set the changed key in the creature variables - $set[`variables.${key}`] = scope[key]; + $set[key] = scope[key]; } } - if ($set) { - Creatures.update(creatureId, {$set}); + + // Remove all the keys that no longer exist in scope + for (const key in variables) { + if (!scope[key]) { + if (!$unset) $unset = {}; + $unset[key] = 1; + } + } + + if ($set || $unset) { + const update = {}; + if ($set) update.$set = $set; + if ($unset) update.$unset = $unset; + CreatureVariables.update({_creatureId: creatureId}, update); + } + if (computation.creature?.dirty) { + Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }}); } } diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index cc3c0da9..d0a3d469 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -6,7 +6,12 @@ import writeErrors from './computation/writeComputation/writeErrors.js'; export default function computeCreature(creatureId){ if (Meteor.isClient) return; + // console.log('compute ' + creatureId); const computation = buildCreatureComputation(creatureId); + computeComputation(computation, creatureId); +} + +function computeComputation(computation, creatureId) { try { computeCreatureComputation(computation); writeAlteredProperties(computation); @@ -15,24 +20,18 @@ export default function computeCreature(creatureId){ const errorText = e.reason || e.message || e.toString(); computation.errors.push({ type: 'crash', - details: {error: errorText}, + details: { error: errorText }, }); const logError = { creatureId, computeError: errorText, }; - if (e.stack){ + if (e.stack) { logError.location = e.stack.split('\n')[1]; } console.error(logError); + throw e; } finally { writeErrors(creatureId, computation.errors); } } - -// For now just recompute the whole creature, TODO only recompute a single -// connected section of the depdendency graph -export function computeCreatureDependencyGroup(property){ - let creatureId = property.ancestors[0].id; - computeCreature(creatureId); -} diff --git a/app/imports/api/engine/loadCreatures.js b/app/imports/api/engine/loadCreatures.js new file mode 100644 index 00000000..9823f892 --- /dev/null +++ b/app/imports/api/engine/loadCreatures.js @@ -0,0 +1,302 @@ +import { debounce } from 'lodash'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import computeCreature from './computeCreature'; + +const COMPUTE_DEBOUNCE_TIME = 100; // ms +export const loadedCreatures = new Map(); // creatureId => {creature, properties, etc.} + +export function loadCreature(creatureId, subscription) { + if (!creatureId) throw 'creatureId is required'; + let creature = loadedCreatures.get(creatureId); + if (loadedCreatures.has(creatureId)) { + creature.subs.add(subscription); + } else { + creature = new LoadedCreature(subscription, creatureId); + loadedCreatures.set(creatureId, creature); + } + subscription.onStop(() => { + unloadCreature(creatureId, subscription); + }); +} + +function unloadCreature(creatureId, subscription) { + if (!creatureId) throw 'creatureId is required'; + const creature = loadedCreatures.get(creatureId); + if (!creature) return; + creature.subs.delete(subscription); + if (creature.subs.size === 0) { + creature.stop(); + loadedCreatures.delete(creatureId); + } +} + +export function getSingleProperty(creatureId, propertyId) { + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const property = creature.properties.get(propertyId); + const cloneProp = EJSON.clone(property); + return cloneProp; + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const prop = CreatureProperties.findOne({ + _id: propertyId, + 'ancestors.id': creatureId, + 'removed': {$ne: true}, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return prop; +} + +export function getProperties(creatureId) { + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const props = Array.from(creature.properties.values()); + const cloneProps = EJSON.clone(props); + return cloneProps + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'ancestors.id': creatureId, + 'removed': {$ne: true}, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +export function getPropertiesOfType(creatureId, propType) { + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const props = [] + for (const prop of creature.properties.values()){ + if (prop.type === propType) { + props.push(prop); + } + } + const cloneProps = EJSON.clone(props); + return cloneProps + } + // console.time(`Cache miss on creature properties: ${creatureId}`) + const props = CreatureProperties.find({ + 'ancestors.id': creatureId, + 'removed': { $ne: true }, + 'type': propType, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }).fetch(); + // console.timeEnd(`Cache miss on creature properties: ${creatureId}`); + return props; +} + +export function getCreature(creatureId) { + if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + const creature = loadedCreature.creature; + if (creature) return creature; + } + // console.time(`Cache miss on Creature: ${creatureId}`); + const creature = Creatures.findOne(creatureId, { + denormalizedStats: 1, + variables: 1, + dirty: 1, + }); + // console.timeEnd(`Cache miss on Creature: ${creatureId}`); + return creature; +} + +export function getVariables(creatureId) { + if (loadedCreatures.has(creatureId)) { + const loadedCreature = loadedCreatures.get(creatureId); + const variables = loadedCreature.variables; + if (variables) return variables; + } + // console.time(`Cache miss on variables: ${creatureId}`); + const variables = CreatureVariables.findOne({_creatureId: creatureId}); + // console.timeEnd(`Cache miss on variables: ${creatureId}`); + return variables; +} + +export function getProperyAncestors(creatureId, propertyId) { + const prop = getSingleProperty(creatureId, propertyId); + if (!prop) return []; + const ancestorIds = []; + prop.ancestors.forEach(ref => { + if (ref.collection === 'creatureProperties') { + ancestorIds.push(ref.id); + } + }); + if (loadedCreatures.has(creatureId)) { + // Get the ancestor properties from the cache + const creature = loadedCreatures.get(creatureId); + const props = []; + ancestorIds.forEach(id => { + const prop = creature.properties.get(id); + if (prop) { + props.push(prop); + } + }); + const cloneProps = EJSON.clone(props); + return cloneProps + } else { + // Fetch from database + return CreatureProperties.find({ + _id: { $in: ancestorIds }, + }, { + sort: { order: 1 }, + }).fetch(); + } +} + +export function getPropertyDecendants(creatureId, propertyId) { + const property = getSingleProperty(creatureId, propertyId); + if (!property) return []; + // This prop will always appear at the same position in the ancestor array + // of its decendants, so only check there + const expectedAncestorPostition = property.ancestors.length; + if (loadedCreatures.has(creatureId)) { + const creature = loadedCreatures.get(creatureId); + const props = []; + for(const prop of creature.properties.values()){ + if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) { + props.push(prop); + } + } + const cloneProps = EJSON.clone(props); + return cloneProps + } else { + return CreatureProperties.find({ + 'ancestors.id': propertyId, + removed: { $ne: true }, + }).fetch(); + } +} + +class LoadedCreature { + constructor(sub, creatureId) { + // This may be called from a subscription, but we don't want the observers + // to be destroyed with it, so use a non-reactive context to observe + // the required documents + const self = this; + Tracker.nonreactive(() => { + self.subs = new Set([sub]); + + const compute = debounce(Meteor.bindEnvironment(() => { + computeCreature(creatureId); + }), COMPUTE_DEBOUNCE_TIME); + + self.properties = new Map(); + // Observe all creature properties which are needed for computation + self.propertyObserver = CreatureProperties.find({ + 'ancestors.id': creatureId, + removed: { $ne: true }, + }, { + sort: { order: 1 }, + fields: { icon: 0 }, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addProperty(fields); + if (fields.dirty) compute(); + }, + changed(id, fields) { + self.changeProperty(id, fields); + if (fields.dirty) compute(); + }, + removed(id) { + self.removeProperty(id); + compute(); + }, + }); + + // Observe the creature itself + self.creatureObserver = Creatures.find({ + _id: creatureId, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addCreature(fields) + if (fields.dirty) compute(); + }, + changed(id, fields) { + self.changeCreature(id, fields); + if (fields.dirty) compute(); + }, + removed(id) { + self.removeCreature(id); + }, + }); + + // Observe the creature's variables + self.variablesObserver = CreatureVariables.find({ + _creatureId: creatureId, + }, { + fields: { _creatureId: 0}, + }).observeChanges({ + added(id, fields) { + fields._id = id; + self.addVariables(fields) + }, + changed(id, fields) { + self.changeVariables(id, fields); + }, + removed(id) { + self.removeVariables(id); + }, + }); + }); + } + stop() { + this.propertyObserver.stop(); + this.creatureObserver.stop(); + this.variablesObserver.stop(); + } + addProperty(prop) { + this.properties.set(prop._id, prop); + } + changeProperty(id, fields) { + LoadedCreature.changeMap(id, fields, this.properties); + } + removeProperty(id) { + this.properties.delete(id) + } + addCreature(creature) { + this.creature = creature; + } + changeCreature(id, fields) { + LoadedCreature.changeDoc(this.creature, fields); + } + removeCreature() { + delete this.creature; + } + addVariables(variables) { + this.variables = variables; + } + changeVariables(id, fields) { + LoadedCreature.changeDoc(this.variables, fields); + } + removeVariables() { + delete this.variables; + } + static changeMap(id, fields, map) { + const doc = map.get(id); + LoadedCreature.changeDoc(doc, fields); + } + static changeDoc(doc, fields) { + if (!doc) return; + for (let key in fields) { + if (key === undefined) { + delete doc[key]; + } else { + doc[key] = fields[key]; + } + } + } +} diff --git a/app/imports/api/files/UserImages.js b/app/imports/api/files/UserImages.js new file mode 100644 index 00000000..fad4576b --- /dev/null +++ b/app/imports/api/files/UserImages.js @@ -0,0 +1,19 @@ +import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js'; + +const UserImages = createS3FilesCollection({ + collectionName: 'userImages', + storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages', + onBeforeUpload(file) { + // Allow upload files under 10MB + if (file.size > 10485760) { + return 'Please upload with size equal or less than 10MB'; + } + // Allow common image extensions + if (!/gif|png|jpe?g|webp/i.test(file.extension || '')) { + return 'Please upload an image file only'; + } + return true + } +}); + +export default UserImages; diff --git a/app/imports/api/files/s3FileStorage.js b/app/imports/api/files/s3FileStorage.js index e26e6ac5..c23bf198 100644 --- a/app/imports/api/files/s3FileStorage.js +++ b/app/imports/api/files/s3FileStorage.js @@ -47,6 +47,7 @@ if (Meteor.isServer && Meteor.settings.useS3) { collectionName, storagePath, onBeforeUpload, + onAfterUpload, debug = Meteor.isProduction, allowClientCode = false, }){ @@ -54,7 +55,10 @@ if (Meteor.isServer && Meteor.settings.useS3) { collectionName, storagePath, onBeforeUpload, - onAfterUpload(fileRef){ + onAfterUpload(fileRef) { + // Call the provided afterUpload hook first + onAfterUpload?.(fileRef); + // Start moving files to AWS:S3 // after fully received by the Meteor server @@ -217,6 +221,7 @@ if (Meteor.isServer && Meteor.settings.useS3) { collectionName, storagePath, onBeforeUpload, + onAfterUpload, debug = Meteor.isProduction, allowClientCode = false, }){ @@ -224,11 +229,12 @@ if (Meteor.isServer && Meteor.settings.useS3) { collectionName, storagePath, onBeforeUpload, + onAfterUpload, debug, allowClientCode, }); - if (Meteor.isServer){ + if (Meteor.isServer) { // Use the normal file system to read files collection.readJSONFile = async function(file){ const fileString = await fsp.readFile(file.path, 'utf8'); diff --git a/app/imports/api/library/Libraries.js b/app/imports/api/library/Libraries.js index 358489ea..95444bc3 100644 --- a/app/imports/api/library/Libraries.js +++ b/app/imports/api/library/Libraries.js @@ -24,6 +24,11 @@ let LibrarySchema = new SimpleSchema({ type: String, max: STORAGE_LIMITS.name, }, + description: { + type: String, + optional: true, + max: STORAGE_LIMITS.summary, + }, }); LibrarySchema.extend(SharingSchema); @@ -76,6 +81,29 @@ const updateLibraryName = new ValidatedMethod({ }, }); +const updateLibraryDescription = new ValidatedMethod({ + name: 'libraries.updateDescription', + validate: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.id + }, + description: { + type: String, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, description}){ + let library = Libraries.findOne(_id); + assertEditPermission(library, this.userId); + Libraries.update(_id, {$set: {description}}); + }, +}); + const removeLibrary = new ValidatedMethod({ name: 'libraries.remove', validate: new SimpleSchema({ @@ -102,4 +130,4 @@ export function removeLibaryWork(libraryId){ LibraryNodes.remove({'ancestors.id': libraryId}); } -export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary }; +export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary }; diff --git a/app/imports/api/library/LibraryCollections.js b/app/imports/api/library/LibraryCollections.js new file mode 100644 index 00000000..aec4d351 --- /dev/null +++ b/app/imports/api/library/LibraryCollections.js @@ -0,0 +1,135 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import SimpleSchema from 'simpl-schema'; +import SharingSchema from '/imports/api/sharing/SharingSchema.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; +import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js'; +import { getUserTier } from '/imports/api/users/patreon/tiers.js' +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +/** + * LibraryCollections are groups of libraries that are subscribed together at once + */ +const LibraryCollections = new Mongo.Collection('libraryCollections'); + +const LibraryCollectionSchema = new SimpleSchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + description: { + type: String, + optional: true, + max: STORAGE_LIMITS.summary, + }, + libraries: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.libraryCollectionCount, + }, + 'libraries.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, +}); + +LibraryCollectionSchema.extend(SharingSchema); +LibraryCollections.attachSchema(LibraryCollectionSchema); + +export default LibraryCollections; + +const insertLibraryCollection = new ValidatedMethod({ + name: 'libraryCollections.insert', + mixins: [ + simpleSchemaMixin, + ], + schema: LibraryCollectionSchema.omit('owner'), + run(libraryCollection) { + if (!this.userId) { + throw new Meteor.Error('LibraryCollections.methods.insert.denied', + 'You need to be logged in to insert a library'); + } + let tier = getUserTier(this.userId); + if (!tier.paidBenefits){ + throw new Meteor.Error('LibraryCollections.methods.insert.denied', + `The ${tier.name} tier does not allow you to insert a library collection`); + } + libraryCollection.owner = this.userId; + return LibraryCollections.insert(libraryCollection); + }, +}); + +const updateLibraryCollection = new ValidatedMethod({ + name: 'libraryCollections.update', + mixins: [ + simpleSchemaMixin, + ], + schema: { + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + update: { + type: LibraryCollectionSchema + .pick('name', 'description', 'libraries') + .extend({ //make libraries optional + libraries: { + optional: true, + defaultValue: undefined, + }, + }), + } + }, + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id, update}){ + const libraryCollection = LibraryCollections.findOne(_id, { + fields: { + owner: 1, + writers: 1, + } + }); + assertEditPermission(libraryCollection, this.userId); + return LibraryCollections.update(_id, {$set: update}); + }, +}); + +const removeLibraryCollection = new ValidatedMethod({ + name: 'libraryCollections.remove', + validate: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.id + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({_id}){ + const libraryCollection = LibraryCollections.findOne(_id, { + fields: { + owner: 1, + } + }); + assertOwnership(libraryCollection, this.userId); + return LibraryCollections.remove(_id); + } +}); + +function getLibraryIdsByCollectionId(libraryCollectionId) { + const libraryCollection = LibraryCollections.findOne(libraryCollectionId) + return libraryCollection?.libraries || []; +} + +export { + LibraryCollectionSchema, + insertLibraryCollection, + updateLibraryCollection, + removeLibraryCollection, + getLibraryIdsByCollectionId, +}; diff --git a/app/imports/api/library/getCreatureLibraryIds.js b/app/imports/api/library/getCreatureLibraryIds.js new file mode 100644 index 00000000..beb9c7d3 --- /dev/null +++ b/app/imports/api/library/getCreatureLibraryIds.js @@ -0,0 +1,39 @@ +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import getUserLibraryIds from './getUserLibraryIds'; +import { intersection, union } from 'lodash'; + +export default function getCreatureLibraryIds(creature, userId) { + if (!userId) return []; + + // Get the ids of libraries the user is permitted to view + const userLibIds = getUserLibraryIds(userId); + + // If given a creature Id, get the creature document + if (typeof creature === 'string') { + creature = Creatures.findOne(creature, { + fields: { + allowedLibraries: 1, + allowedLibraryCollections: 1, + } + }); + if (!creature) return []; + } + + // If the creature does not restrict the libraries, let it use them all + if (!creature.allowedLibraryCollections && !creature.allowedLibraries) { + return userLibIds; + } + + // Get the ids of the libraries that the creature allows + const allowedCollections = creature.allowedLibraryCollections || []; + let creatureLibIds = creature.allowedLibraries || []; + LibraryCollections.find({ + _id: { $in: allowedCollections } + }, { fields: { libraries: 1 } }).forEach(collection => { + creatureLibIds = union(creatureLibIds, collection.libraries); + }); + + // return all the ids that the creature allows and the user can view + return intersection(userLibIds, creatureLibIds); +} \ No newline at end of file diff --git a/app/imports/api/library/getUserLibraryIds.js b/app/imports/api/library/getUserLibraryIds.js new file mode 100644 index 00000000..55f7498d --- /dev/null +++ b/app/imports/api/library/getUserLibraryIds.js @@ -0,0 +1,31 @@ +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; +import Libraries from '/imports/api/library/Libraries.js'; +import { union } from 'lodash'; + +export default function getUserLibraryIds(userId) { + if (!userId) return []; + const user = Meteor.users.findOne(userId); + let subbedIds = user?.subscribedLibraries || []; + const subCollections = user?.subscribedLibraryCollections || []; + LibraryCollections.find({ + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: subCollections }, public: true }, + ] + }, { fields: { libraries: 1 } }).forEach(collection => { + subbedIds = union(subbedIds, collection.libraries); + }); + const libraryIds = Libraries.find({ + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: subbedIds }, public: true }, + ] + }, { + fields: { _id: 1 } + }).map(lib => lib._id); + return libraryIds; +} \ No newline at end of file diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js index f104154c..88500085 100644 --- a/app/imports/api/parenting/organizeMethods.js +++ b/app/imports/api/parenting/organizeMethods.js @@ -8,7 +8,7 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import computeCreature from '/imports/api/engine/computeCreature.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; const organizeDoc = new ValidatedMethod({ name: 'organize.organizeDoc', @@ -57,10 +57,11 @@ const organizeDoc = new ValidatedMethod({ let parentCreatures = getCreatureAncestors(parent); if (!skipRecompute){ let creaturesToRecompute = union(docCreatures, parentCreatures); - // Recompute the creatures - creaturesToRecompute.forEach(id => { - // Some Dependencies depend on ancestry, so a full recompute is needed - computeCreature(id); + // Mark the creatures for recompute + Creatures.update({ + _id: { $in: creaturesToRecompute } + }, { + $set: { dirty: true }, }); } }, @@ -85,9 +86,14 @@ const reorderDoc = new ValidatedMethod({ assertDocEditPermission(doc, this.userId); safeUpdateDocOrder({docRef, order}); // Recompute the affected creatures - getCreatureAncestors(doc).forEach(id => { - computeCreature(id); - }); + const ancestors = getCreatureAncestors(doc); + if (ancestors.length) { + Creatures.update({ + _id: { $in: ancestors } + }, { + $set: { dirty: true }, + }); + } }, }); diff --git a/app/imports/api/parenting/softRemove.js b/app/imports/api/parenting/softRemove.js index 9ef16f8c..099abfc2 100644 --- a/app/imports/api/parenting/softRemove.js +++ b/app/imports/api/parenting/softRemove.js @@ -40,17 +40,22 @@ const restoreError = function(){ ); }; -export function restore({_id, collection}){ +export function restore({ _id, collection, extraUpdates}){ if (typeof collection === 'string') { collection = getCollectionByName(collection); } + const update = { + $unset: { + removed: 1, + removedAt: 1, + }, + ...extraUpdates + } + let numUpdated = collection.update({ _id, removedWith: {$exists: false} - }, { $unset: { - removed: 1, - removedAt: 1, - }}, { + }, update , { selector: {type: 'any'}, },); if (numUpdated === 0) restoreError(); diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index ccda82d1..53e57afa 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -5,9 +5,6 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; /* * Actions are things a character can do - * Any rolls that are children of actions will be rolled when taking the action - * Any actions that are children of this action will be considered alternatives - * to this action */ let ActionSchema = createPropertySchema({ name: { diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 5c83a660..4b4537c0 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -145,6 +145,15 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ optional: true, removeBeforeCompute: true, }, + // A list of effect ids targeting this attribute + effects: { + type: Array, + optional: true, + }, + 'effects.$': { + type: Object, + blackbox: true, + }, }); const ComputedAttributeSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Classes.js b/app/imports/api/properties/Classes.js index ee631246..e6e6471f 100644 --- a/app/imports/api/properties/Classes.js +++ b/app/imports/api/properties/Classes.js @@ -1,34 +1,95 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import { SlotSchema, ComputedOnlySlotSchema } from './Slots.js'; // Classes are like slots, except they only take class levels and enforce that // lower levels are taken before higher levels let ClassSchema = createPropertySchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, // Only `classLevel`s with the same variable name can fill the class variableName: { type: String, optional: true, max: STORAGE_LIMITS.variableName, }, -}).extend(SlotSchema); + slotTags: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'slotTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + extraTags: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.extraTagsCount, + }, + 'extraTags.$': { + type: Object, + }, + 'extraTags.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'extraTags.$.operation': { + type: String, + allowedValues: ['OR', 'NOT'], + defaultValue: 'OR', + }, + 'extraTags.$.tags': { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'extraTags.$.tags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + slotCondition: { + type: 'fieldToCompute', + optional: true, + }, +}); const ComputedOnlyClassSchema = createPropertySchema({ - level: { - type: SimpleSchema.Integer, - optional: true, - removeBeforeCompute: true, - }, - missingLevels: { - type: Array, - optional: true, - removeBeforeCompute: true, - }, - 'missingLevels.$': { - type: SimpleSchema.Integer, - }, - }).extend(ComputedOnlySlotSchema); + // Computed fields + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, + slotCondition: { + type: 'computedOnlyField', + optional: true, + }, + + // Denormalised fields + level: { + type: SimpleSchema.Integer, + optional: true, + removeBeforeCompute: true, + }, + missingLevels: { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + 'missingLevels.$': { + type: SimpleSchema.Integer, + }, +}); const ComputedClassSchema = new SimpleSchema() .extend(ClassSchema) diff --git a/app/imports/api/properties/PointBuys.js b/app/imports/api/properties/PointBuys.js new file mode 100644 index 00000000..95cc1634 --- /dev/null +++ b/app/imports/api/properties/PointBuys.js @@ -0,0 +1,96 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; + +/* + * PointBuys are reason-value attached to skills and abilities + * that modify their final value or presentation in some way + */ +let PointBuySchema = createPropertySchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + variableName: { + type: String, + optional: true, + regEx: VARIABLE_NAME_REGEX, + min: 2, + max: STORAGE_LIMITS.variableName, + }, + ignored: { + type: Boolean, + optional: true, + }, + 'values': { + type: Array, + defaultValue: [], + }, + 'values.$': { + type: Object, + }, + 'values.$.name': { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + 'values.$.variableName': { + type: String, + optional: true, + regEx: VARIABLE_NAME_REGEX, + min: 2, + max: STORAGE_LIMITS.variableName, + }, + 'values.$.value': { + type: Number, + optional: true, + }, + min: { + type: 'fieldToCompute', + optional: true, + }, + max: { + type: 'fieldToCompute', + optional: true, + }, + total: { + type: 'fieldToCompute', + optional: true, + }, + cost: { + type: 'fieldToCompute', + optional: true, + }, +}); + +const ComputedOnlyPointBuySchema = createPropertySchema({ + min: { + type: 'computedOnlyField', + optional: true, + }, + max: { + type: 'computedOnlyField', + optional: true, + }, + total: { + type: 'computedOnlyField', + optional: true, + }, + cost: { + type: 'computedOnlyField', + optional: true, + }, + spent: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, +}); + +const ComputedPointBuySchema = new SimpleSchema() + .extend(ComputedOnlyPointBuySchema) + .extend(PointBuySchema); + +export { PointBuySchema, ComputedPointBuySchema, ComputedOnlyPointBuySchema }; diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index 281fe985..47ba2a76 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -131,6 +131,15 @@ let ComputedOnlySkillSchema = createPropertySchema({ optional: true, removeBeforeCompute: true, }, + // A list of effect ids targeting this skill + effects: { + type: Array, + optional: true, + }, + 'effects.$': { + type: Object, + blackbox: true, + }, }) const ComputedSkillSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Triggers.js b/app/imports/api/properties/Triggers.js new file mode 100644 index 00000000..3b9b0d13 --- /dev/null +++ b/app/imports/api/properties/Triggers.js @@ -0,0 +1,136 @@ +import SimpleSchema from 'simpl-schema'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const eventOptions = { + doActionProperty: 'Do action', + // receiveActionProperty: 'Receiving action property', + // flipToggle: 'Toggle changed', + // adjustProperty: 'Attribute adjusted', + anyRest: 'Short or long rest', + longRest: 'Long rest', + shortRest: 'Short rest', +} + +const timingOptions = { + before: 'Before', + after: 'After', +} + +const actionPropertyTypeOptions = { + action: 'Action', + adjustment: 'Attribute damage', + branch: 'Branch', + buff: 'Buff', + damage: 'Damage', + note: 'Note', + roll: 'Roll', + savingThrow: 'Saving throw', + toggle: 'Toggle', +} + +/* + * Triggers are like actions that fire themselves when certain things happen on + * the sheet. Either during another action or as its own action after a sheet + * event. The same trigger can't fire twice in the same action step. + */ +let TriggerSchema = createPropertySchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + summary: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + event: { + type: String, + allowedValues: Object.keys(eventOptions), + defaultValue: 'doActionProperty', + }, + // Action type + actionPropertyType: { + type: String, + allowedValues: Object.keys(actionPropertyTypeOptions), + optional: true, + }, + timing: { + type: String, + allowedValues: Object.keys(timingOptions), + defaultValue: 'after', + }, + condition: { + type: 'fieldToCompute', + optional: true, + parseLevel: 'compile', + }, + // Which tags the trigger is applied to + targetTags: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.tagCount, + }, + 'targetTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + extraTags: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.extraTagsCount, + }, + 'extraTags.$': { + type: Object, + }, + 'extraTags.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'extraTags.$.operation': { + type: String, + allowedValues: ['OR', 'NOT'], + defaultValue: 'OR', + }, + 'extraTags.$.tags': { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'extraTags.$.tags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, +}); + +const ComputedOnlyTriggerSchema = createPropertySchema({ + summary: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, + condition: { + type: 'computedOnlyField', + optional: true, + parseLevel: 'compile', + }, +}); + +const ComputedTriggerSchema = new SimpleSchema() + .extend(TriggerSchema) + .extend(ComputedOnlyTriggerSchema); + +export { + TriggerSchema, ComputedOnlyTriggerSchema, ComputedTriggerSchema, + eventOptions, timingOptions, actionPropertyTypeOptions +}; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index d47d560d..d490e4e3 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -25,6 +25,7 @@ import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFiller import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js'; import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js'; import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js'; +import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers.js'; const propertySchemasIndex = { action: ComputedOnlyActionSchema, @@ -53,6 +54,7 @@ const propertySchemasIndex = { spellList: ComputedOnlySpellListSchema, spell: ComputedOnlySpellSchema, toggle: ComputedOnlyToggleSchema, + trigger: ComputedOnlyTriggerSchema, any: new SimpleSchema({}), }; diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 1fa8c9d8..9084e3b7 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -25,6 +25,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js'; import { ComputedSpellSchema } from '/imports/api/properties/Spells.js'; import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js'; import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js'; +import { ComputedTriggerSchema } from '/imports/api/properties/Triggers.js'; const propertySchemasIndex = { action: ComputedActionSchema, @@ -51,6 +52,7 @@ const propertySchemasIndex = { spellList: ComputedSpellListSchema, spell: ComputedSpellSchema, toggle: ComputedToggleSchema, + trigger: ComputedTriggerSchema, container: ComputedContainerSchema, item: ComputedItemSchema, any: new SimpleSchema({}), diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 0cba80a0..cc4b77ee 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -23,6 +23,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js'; import { SpellListSchema } from '/imports/api/properties/SpellLists.js'; import { SpellSchema } from '/imports/api/properties/Spells.js'; import { ToggleSchema } from '/imports/api/properties/Toggles.js'; +import { TriggerSchema } from '/imports/api/properties/Triggers.js'; import { ContainerSchema } from '/imports/api/properties/Containers.js'; import { ItemSchema } from '/imports/api/properties/Items.js'; @@ -51,6 +52,7 @@ const propertySchemasIndex = { spellList: SpellListSchema, spell: SpellSchema, toggle: ToggleSchema, + trigger: TriggerSchema, container: ContainerSchema, item: ItemSchema, any: new SimpleSchema({}), diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index 6f48ed4b..58629c62 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -35,7 +35,7 @@ export function assertOwnership(doc, userId){ export function assertEditPermission(doc, userId) { assertIdValid(userId); assertdocExists(doc); - let user = Meteor.users.findOne(userId, { + const user = Meteor.users.findOne(userId, { fields: { 'services.patreon': 1, 'roles': 1, @@ -83,6 +83,7 @@ export function assertViewPermission(doc, userId) { assertdocExists(doc); if (doc.public) return true; assertIdValid(userId); + if ( doc.owner === userId || _.contains(doc.readers, userId) || @@ -90,6 +91,17 @@ export function assertViewPermission(doc, userId) { ){ return true; } else { + + // Admin override + const user = Meteor.users.findOne(userId, { + fields: { + 'roles': 1, + } + }); + if (user.roles && user.roles.includes('admin')){ + return true; + } + throw new Meteor.Error('View permission denied', 'You do not have permission to view this document'); } diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index be3fd3a8..b3152af8 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -4,9 +4,11 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import '/imports/api/users/methods/deleteMyAccount.js'; import '/imports/api/users/methods/addEmail.js'; import '/imports/api/users/methods/removeEmail.js'; +import '/imports/api/users/methods/updateFileStorageUsed.js'; import { some } from 'lodash'; const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; +const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || []; const userSchema = new SimpleSchema({ username: { @@ -69,10 +71,19 @@ const userSchema = new SimpleSchema({ subscribedLibraries: { type: Array, defaultValue: defaultLibraries, - max: 100, + maxCount: 100, }, 'subscribedLibraries.$': { type: String, + regEx: SimpleSchema.RegEx.Id, + }, + subscribedLibraryCollections: { + type: Array, + defaultValue: defaultLibraryCollections, + maxCount: 100, + }, + 'subscribedLibraryCollections.$': { + type: String, regEx: SimpleSchema.RegEx.Id, }, subscribedCharacters: { @@ -84,6 +95,10 @@ const userSchema = new SimpleSchema({ type: String, regEx: SimpleSchema.RegEx.Id, }, + fileStorageUsed: { + type: Number, + optional: true, + }, profile: { type: Object, blackbox: true, @@ -265,6 +280,36 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({ } }); +Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({ + name: 'users.subscribeToLibraryCollection', + validate: new SimpleSchema({ + libraryCollectionId:{ + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + subscribe: { + type: Boolean, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({libraryCollectionId, subscribe}){ + if (!this.userId) throw 'Can only subscribe if logged in'; + if (subscribe){ + return Meteor.users.update(this.userId, { + $addToSet: {subscribedLibraryCollections: libraryCollectionId}, + }); + } else { + return Meteor.users.update(this.userId, { + $pullAll: {subscribedLibraryCollections: libraryCollectionId}, + }); + } + } +}); + Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({ name: 'users.findUserByUsernameOrEmail', validate: new SimpleSchema({ diff --git a/app/imports/api/users/methods/updateFileStorageUsed.js b/app/imports/api/users/methods/updateFileStorageUsed.js new file mode 100644 index 00000000..4fad2feb --- /dev/null +++ b/app/imports/api/users/methods/updateFileStorageUsed.js @@ -0,0 +1,71 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; +import UserImages from '/imports/api/files/UserImages.js'; +const fileCollections = [ArchiveCreatureFiles, UserImages]; + +const updateFileStorageUsed = new ValidatedMethod({ + name: 'users.recalculateFileStorageUsed', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run() { + const userId = Meteor.userId(); + if (!userId) throw new Meteor.Error('No user', + 'You must be logged in to recalculate your file use'); + const user = Meteor.users.findOne(userId); + if (!user) { + throw new Meteor.Error('noUser', 'User not found'); + } + updateFileStorageUsedWork(userId); + } +}); + +export default updateFileStorageUsed; + +export function updateFileStorageUsedWork(userId) { + if (!userId) { + throw new Meteor.Error('idRequired', + 'No user ID was provided to update file storage used') + } + + let sum = 0; + fileCollections.forEach(collection => { + collection.find({ userId }, { fields: { size: 1 } }).forEach(file => { + sum += file.size; + }); + }); + + Meteor.users.update(userId, { + $set: { + fileStorageUsed: sum, + } + }); +} + +export function incrementFileStorageUsed(userId, amount) { + if (!userId) { + throw new Meteor.Error('idRequired', + 'No user ID was provided to update file storage used') + } + + const user = Meteor.users.findOne(userId); + if (!user) { + throw new Meteor.Error('noUser', 'User not found'); + } + + if (user.fileStorageUsed === undefined) { + // The user doesn't have a current value for storage used, calculate it + // from scratch + updateFileStorageUsedWork(userId); + } else { + Meteor.users.update(userId, { + $inc: { + fileStorageUsed: amount, + } + }); + } +} diff --git a/app/imports/api/users/patreon/tiers.js b/app/imports/api/users/patreon/tiers.js index 32f0465d..2d789993 100644 --- a/app/imports/api/users/patreon/tiers.js +++ b/app/imports/api/users/patreon/tiers.js @@ -9,18 +9,21 @@ const TIERS = Object.freeze([ minimumEntitledCents: 0, invites: 0, characterSlots: 5, + fileStorage: 50, paidBenefits: false, }, { name: 'Dreamer', minimumEntitledCents: 100, invites: 0, characterSlots: 5, + fileStorage: 50, paidBenefits: false, }, { name: 'Wanderer', minimumEntitledCents: 300, invites: 0, characterSlots: 5, + fileStorage: 50, paidBenefits: false, }, { //cost per user $5 @@ -28,6 +31,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 500, invites: 0, characterSlots: 20, + fileStorage: 200, paidBenefits: true, }, { //cost per user $3.33 @@ -35,6 +39,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 1000, invites: 2, characterSlots: 50, + fileStorage: 500, paidBenefits: true, }, { //cost per user $3.333 @@ -42,6 +47,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 2000, invites: 5, characterSlots: 120, + fileStorage: 1000, paidBenefits: true, }, { //cost per user $3.125 @@ -49,6 +55,7 @@ const TIERS = Object.freeze([ minimumEntitledCents: 5000, invites: 15, characterSlots: -1, // Unlimited characters + fileStorage: 2000, paidBenefits: true, }, ]); @@ -59,6 +66,7 @@ const GUEST_TIER = Object.freeze({ guest: true, invites: 0, characterSlots: 20, + fileStorage: 200, paidBenefits: true, }); @@ -68,6 +76,7 @@ const PATREON_DISABLED_TIER = Object.freeze({ name: 'Outlander', invites: 0, characterSlots: -1, // Can have infinitely many characters + fileStorage: 1000000, // 1TB file storage paidBenefits: true, }); diff --git a/app/imports/constants/DAMAGE_TYPES.js b/app/imports/constants/DAMAGE_TYPES.js index 9e544266..beb02518 100644 --- a/app/imports/constants/DAMAGE_TYPES.js +++ b/app/imports/constants/DAMAGE_TYPES.js @@ -13,6 +13,7 @@ const DAMAGE_TYPES = Object.freeze([ 'psychic', 'radiant', 'thunder', + 'extra', ]); export default DAMAGE_TYPES; diff --git a/app/imports/constants/MAINTENANCE_MODE.js b/app/imports/constants/MAINTENANCE_MODE.js index f81e01e4..42045410 100644 --- a/app/imports/constants/MAINTENANCE_MODE.js +++ b/app/imports/constants/MAINTENANCE_MODE.js @@ -6,6 +6,7 @@ if (Meteor.isServer){ const dbVersion = Migrations.getVersion(); if ( !Meteor.settings.public.maintenanceMode && + dbVersion !== undefined && SCHEMA_VERSION !== dbVersion ){ Meteor.settings.public.maintenanceMode = { diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index 4c35c038..c2aa3be1 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -138,7 +138,7 @@ const PROPERTIES = Object.freeze({ slotFiller: { icon: 'mdi-power-plug-outline', name: 'Slot filler', - helpText: 'A slot filler allows for more advanced logic when it attemptst to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.', + helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.', suggestedParents: ['propertySlot'], }, spellList: { @@ -159,6 +159,12 @@ const PROPERTIES = Object.freeze({ helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.', suggestedParents: [], }, + trigger: { + icon: 'mdi-electric-switch', + name: 'Trigger', + helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage', + suggestedParents: [], + }, }); export default PROPERTIES; diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index f2bd1e1f..6e116b07 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -31,6 +31,7 @@ const STORAGE_LIMITS = Object.freeze({ statsToTarget: 64, tagCount: 64, writersCount: 20, + libraryCollectionCount: 32, }); export default STORAGE_LIMITS; diff --git a/app/imports/migrations/server/dbv1/dbv1.js b/app/imports/migrations/server/dbv1/dbv1.js index b265cd9d..8ecfef81 100644 --- a/app/imports/migrations/server/dbv1/dbv1.js +++ b/app/imports/migrations/server/dbv1/dbv1.js @@ -1,9 +1,6 @@ import { Migrations } from 'meteor/percolate:migrations'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js'; -import { restoreCreature } from '/imports/api/creature/archive/methods/restoreCreatures.js'; -import { archiveCreature } from '/imports/api/creature/archive/methods/archiveCreatureToFile.js'; import transformFields from '/imports/migrations/server/transformFields.js'; import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; @@ -22,34 +19,11 @@ Migrations.add({ }); function migrate({reversed} = {}){ - console.log('restoring all characters from database archive'); - const restoredIds = restoreAllCreatures(); - console.log('migrating creature properties'); migrateCollection({collection: CreatureProperties, reversed}); console.log('migrating library nodes') migrateCollection({collection: LibraryNodes, reversed}); - - console.log('archiving characters to file system archive'); - rearchiveAllCreatures(restoredIds); -} - -function restoreAllCreatures(){ - const ids = []; - ArchivedCreatures.find({}, { - fields: {_id: 1} - }).forEach(archive => { - const id = restoreCreature(archive._id); - ids.push(id); - }); - return ids; -} - -function rearchiveAllCreatures(ids){ - ids.forEach(id => { - archiveCreature(id); - }); } function migrateCollection({collection, reversed}){ diff --git a/app/imports/migrations/v1Migration/migrateCharacter.js b/app/imports/migrations/v1Migration/migrateCharacter.js deleted file mode 100644 index f4246d19..00000000 --- a/app/imports/migrations/v1Migration/migrateCharacter.js +++ /dev/null @@ -1,12 +0,0 @@ -import { fetch } from 'meteor/fetch' - -export default function importCharacter(url){ - // Using v1's JSON API to fetch the character data in a usable format - // url -> https://dicecloud.com/character//json?key= - fetch(url) - .then(response => response.json()) - .then(data => { - let character = data.characters[0]; - console.log(character.name + ' fetched successfuly') - }); -} diff --git a/app/imports/parser/TextField.vue b/app/imports/parser/TextField.vue deleted file mode 100644 index 9c9f0919..00000000 --- a/app/imports/parser/TextField.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js index 64ef1ba9..920e1e6b 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.js @@ -1,4 +1,5 @@ import constant from './constant.js'; +// import array from './array.js'; import { toString } from '../resolve.js'; const accessor = { @@ -9,18 +10,18 @@ const accessor = { name, }; }, - compile(node, scope, context){ + compile(node, scope, context) { let value = scope && scope[node.name]; // For objects, get their value node.path.forEach(name => { if (value === undefined) return; value = value[name]; }); - let valueType = typeof value; + let valueType = Array.isArray(value) ? 'array' : typeof value; // If the accessor returns an objet, get the object's value instead while (valueType === 'object'){ value = value.value; - valueType = typeof value; + valueType = Array.isArray(value) ? 'array' : typeof value; } // Return a parse node based on the type returned if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){ @@ -31,7 +32,21 @@ const accessor = { }), context, }; - } else if (valueType === 'undefined'){ + } + /* Can't access #object.tags until this is fixed + * If we activate this, the array node expects values to be an array of + * parse nodes, so it will break unless the values are coerced here or at + * in the array node's code to be parse nodes, not raw js + else if (valueType === 'array') { + return { + result: array.create({ + values: value, + }), + context, + }; + } + */ + else if (valueType === 'undefined') { return { result: accessor.create({ name: node.name, @@ -40,8 +55,7 @@ const accessor = { context, }; } else { - context.error(`${node.name} returned an unexpected type`); - context.error(JSON.stringify(value, null, 2)); + context.error(`Accessing ${accessor.toString(node)} is not supported yet`); return { result: accessor.create({ name: node.name, diff --git a/app/imports/server/config/SyncedCronConfig.js b/app/imports/server/config/SyncedCronConfig.js index 8ef48fec..21ab1abd 100644 --- a/app/imports/server/config/SyncedCronConfig.js +++ b/app/imports/server/config/SyncedCronConfig.js @@ -1,4 +1,4 @@ -import { SyncedCron } from 'meteor/percolate:synced-cron'; +import { SyncedCron } from 'meteor/littledata:synced-cron'; SyncedCron.config({ // Log job run details to console diff --git a/app/imports/server/cron/deleteSoftRemovedDocuments.js b/app/imports/server/cron/deleteSoftRemovedDocuments.js index 3758aa27..835e3773 100644 --- a/app/imports/server/cron/deleteSoftRemovedDocuments.js +++ b/app/imports/server/cron/deleteSoftRemovedDocuments.js @@ -1,7 +1,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; -import { SyncedCron } from 'meteor/percolate:synced-cron'; +import { SyncedCron } from 'meteor/littledata:synced-cron'; Meteor.startup(() => { const collections = [ diff --git a/app/imports/server/imageProcessing/createThumbnail.js b/app/imports/server/imageProcessing/createThumbnail.js new file mode 100644 index 00000000..4837133d --- /dev/null +++ b/app/imports/server/imageProcessing/createThumbnail.js @@ -0,0 +1,8 @@ +import * as sharp from 'sharp'; + +export default async function createThumbnail(image) { + await sharp(image) + .resize(320, 240) + .png() + .toBuffer(); +} diff --git a/app/imports/server/publications/archivedCreatures.js b/app/imports/server/publications/archivedCreatures.js deleted file mode 100644 index 93043407..00000000 --- a/app/imports/server/publications/archivedCreatures.js +++ /dev/null @@ -1,19 +0,0 @@ -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/index.js b/app/imports/server/publications/index.js index 3bdc770a..eba626eb 100644 --- a/app/imports/server/publications/index.js +++ b/app/imports/server/publications/index.js @@ -8,6 +8,6 @@ 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/archivedCreatures.js'; import '/imports/server/publications/searchLibraryNodes.js'; import '/imports/server/publications/archiveFiles.js'; +import '/imports/server/publications/userImages.js'; diff --git a/app/imports/server/publications/library.js b/app/imports/server/publications/library.js index 19211a55..217f9b00 100644 --- a/app/imports/server/publications/library.js +++ b/app/imports/server/publications/library.js @@ -1,27 +1,133 @@ import SimpleSchema from 'simpl-schema'; import Libraries from '/imports/api/library/Libraries.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js'; +import { union } from 'lodash'; -Meteor.publish('libraries', function(){ - this.autorun(function (){ +const LIBRARY_NODE_TREE_FIELDS = { + _id: 1, + name: 1, + type: 1, + icon: 1, + color: 1, + order: 1, + parent: 1, + ancestors: 1, + tags: 1, + slotFillerCondition: 1, + // SlotFillers + slotQuantityFilled: 1, + // Effect + operation: 1, + targetTags: 1, + stats: 1, + // Item + quantity: 1, + plural: 1, + equipped: 1, + // Branch + branchType: 1, + // Damage: + damageType: 1, + stat: 1, + amount: 1, + // Class level + level: 1, + variableName: 1, + // Proficiency + value: 1, + // Reference + cache: 1, + // Saving throw + dc: 1, +} + +export { LIBRARY_NODE_TREE_FIELDS }; + +Meteor.publish('libraryCollection', function (libraryCollectionId) { + this.autorun(function () { + let userId = this.userId; + if (!userId) return []; + this.autorun(function () { + const libraryCollectionCursor = LibraryCollections.find({ + _id: libraryCollectionId, + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { public: true }, + ] + }); + const libraryCollection = libraryCollectionCursor.fetch()[0]; + if (!libraryCollection) return [ libraryCollectionCursor ]; + this.autorun(function () { + const libraryCursor = Libraries.find({ + _id: {$in: libraryCollection.libraries}, + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { public: true }, + ] + }, { + sort: { name: 1 } + }); + return [ libraryCollectionCursor, libraryCursor ]; + }); + }); + }) +}); + +Meteor.publish('libraries', function () { + this.autorun(function () { let userId = this.userId; if (!userId) { return []; } const user = Meteor.users.findOne(userId, { - fields: {subscribedLibraries: 1} + fields: { subscribedLibraries: 1, subscribedLibraryCollections: 1 } }); - const subs = user && user.subscribedLibraries || []; - return Libraries.find({ - $or: [ - {owner: this.userId}, - {writers: this.userId}, - {readers: this.userId}, - { _id: {$in: subs}, public: true }, - ] - }, { - sort: {name: 1} + + this.autorun(function () { + // Get the collections the user is subscribed to + const subCollections = user && user.subscribedLibraryCollections || []; + const libraryCollectionsCursor = LibraryCollections.find({ + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: subCollections }, public: true }, + ] + }, { + sort: { name: 1 } + }); + + // Collate all the libraryIds in those collections + let collectionLibIds = []; + libraryCollectionsCursor.forEach(libCollection => { + collectionLibIds = union(collectionLibIds, libCollection.libraries); + }); + + // Get the libraries the user is subscribed to directly + const subs = user && user.subscribedLibraries || []; + + // Combine all the library Ids + const libIds = union(collectionLibIds, subs); + + this.autorun(function () { + const librariesCursor = Libraries.find({ + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: libIds }, public: true }, + ] + }, { + sort: { name: 1 } + }); + return [librariesCursor, libraryCollectionsCursor]; + }); }); }); }); @@ -64,37 +170,7 @@ Meteor.publish('libraryNodes', function(libraryId){ 'ancestors.id': libraryId, }, { sort: { order: 1 }, - fields: { - _id: 1, - name: 1, - type: 1, - icon: 1, - color: 1, - order: 1, - parent: 1, - ancestors: 1, - removed: 1, - // Effect - operation: 1, - targetTags: 1, - stats: 1, - // Item - quantity: 1, - plural: 1, - equipped: 1, - // Branch - branchType: 1, - // Damage: - damageType: 1, - stat: 1, - amount: 1, - // Class level - level: 1, - // Proficiency - value: 1, - // Reference - cache: 1, - } + fields: LIBRARY_NODE_TREE_FIELDS, }), ]; }); diff --git a/app/imports/server/publications/searchLibraryNodes.js b/app/imports/server/publications/searchLibraryNodes.js index ddbbf4ba..cedbc7a8 100644 --- a/app/imports/server/publications/searchLibraryNodes.js +++ b/app/imports/server/publications/searchLibraryNodes.js @@ -1,6 +1,8 @@ import { check } from 'meteor/check'; import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js'; +import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js'; import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js'; Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){ @@ -37,7 +39,7 @@ Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){ })]; }); -Meteor.publish('searchLibraryNodes', function(){ +Meteor.publish('searchLibraryNodes', function(creatureId){ let self = this; this.autorun(function (){ let type = self.data('type'); @@ -49,23 +51,12 @@ Meteor.publish('searchLibraryNodes', function(){ } // Get all the ids of libraries the user can access - const user = Meteor.users.findOne(userId, { - fields: {subscribedLibraries: 1} - }); - if (!user) return []; - - const subs = user.subscribedLibraries || []; - let libraries = Libraries.find({ - $or: [ - {owner: this.userId}, - {writers: this.userId}, - {readers: this.userId}, - {_id: {$in: subs}}, - ] - }, { - fields: {_id: 1, name: 1}, - }); - let libraryIds = libraries.map(lib => lib._id); + let libraryIds; + if (creatureId) { + libraryIds = getCreatureLibraryIds(creatureId, userId) + } else { + libraryIds = getUserLibraryIds(userId) + } // Build a filter for nodes in those libraries that match the type let filter = { @@ -122,6 +113,7 @@ Meteor.publish('searchLibraryNodes', function(){ }); let cursor = LibraryNodes.find(filter, options); + const libraries = Libraries.find({ _id: { $in: libraryIds } }); Mongo.Collection._publishCursor(libraries, self, 'libraries'); diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index d27be56f..28d2da6f 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -1,10 +1,12 @@ import SimpleSchema from 'simpl-schema'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import computeCreature from '/imports/api/engine/computeCreature.js'; import VERSION from '/imports/constants/VERSION.js'; +import { loadCreature } from '/imports/api/engine/loadCreatures.js'; let schema = new SimpleSchema({ creatureId: { @@ -13,28 +15,24 @@ let schema = new SimpleSchema({ }, }); -Meteor.publish('singleCharacter', function(creatureId){ +Meteor.publish('singleCharacter', function (creatureId) { + const self = this; try { schema.validate({ creatureId }); } catch (e){ this.error(e); } this.autorun(function (computation){ - const userId = this.userId; - const creature = Creatures.findOne({ + let userId = this.userId; + let permissionCreature = Creatures.findOne({ _id: creatureId, }, { - fields: { - owner: 1, - readers: 1, - writers: 1, - public: 1, - computeVersion: 1, - } + fields: { owner: 1, readers: 1, writers: 1, public: 1, computeVersion: 1 } }); - try { assertViewPermission(creature, userId) } - catch(e){ return [] } - if (creature.computeVersion !== VERSION && computation.firstRun){ + try { assertViewPermission(permissionCreature, userId) } + catch (e) { return [] } + loadCreature(creatureId, self); + if (permissionCreature.computeVersion !== VERSION && computation.firstRun){ try { computeCreature(creatureId) } @@ -44,6 +42,9 @@ Meteor.publish('singleCharacter', function(creatureId){ Creatures.find({ _id: creatureId, }), + CreatureVariables.find({ + _creatureId: creatureId, + }), CreatureProperties.find({ 'ancestors.id': creatureId, }), diff --git a/app/imports/server/publications/slotFillers.js b/app/imports/server/publications/slotFillers.js index c5b3397f..30dadc9f 100644 --- a/app/imports/server/publications/slotFillers.js +++ b/app/imports/server/publications/slotFillers.js @@ -3,6 +3,8 @@ import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js' +import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js'; +import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js'; Meteor.publish('slotFillers', function(slotId, searchTerm){ if (searchTerm) check(searchTerm, String); @@ -20,21 +22,18 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ } // Get all the ids of libraries the user can access - const user = Meteor.users.findOne(userId, { - fields: {subscribedLibraries: 1} - }); - const subs = user && user.subscribedLibraries || []; - let libraries = Libraries.find({ + const creatureId = slot.ancestors[0].id; + const libraryIds = getCreatureLibraryIds(creatureId, userId); + const libraries = Libraries.find({ $or: [ - {owner: this.userId}, - {writers: this.userId}, - {readers: this.userId}, - {_id: {$in: subs}}, + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: libraryIds }, public: true }, ] }, { - fields: {_id: 1, name: 1}, + sort: { name: 1 } }); - let libraryIds = libraries.map(lib => lib._id); // Build a filter for nodes in those libraries that match the slot let filter = getSlotFillFilter({slot, libraryIds}); @@ -50,7 +49,8 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ options = { // relevant documents have a higher score. fields: { - _score: { $meta: 'textScore' } + _score: { $meta: 'textScore' }, + ...LIBRARY_NODE_TREE_FIELDS, }, sort: { // `score` property specified in the projection fields above. @@ -61,10 +61,13 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ } } else { delete filter.$text - options = {sort: { - name: 1, - order: 1, - }}; + options = { + sort: { + name: 1, + order: 1, + }, + fields: LIBRARY_NODE_TREE_FIELDS, + }; } options.limit = limit; @@ -72,7 +75,68 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ self.setData('countAll', LibraryNodes.find(filter).count()); }); self.autorun(function () { - Meteor._sleepForMs(1000); + return [ + LibraryNodes.find(filter, options), + libraries + ]; + }); + }); + }); +}); + +Meteor.publish('classFillers', function(classId){ + let self = this; + if (!classId) return []; + + this.autorun(function (){ + let userId = this.userId; + if (!userId) { + return []; + } + // Get the class + let classProp = CreatureProperties.findOne(classId); + if (!classProp){ + return []; + } + + // Get all the ids of libraries the user can access + const user = Meteor.users.findOne(userId, { + fields: {subscribedLibraries: 1} + }); + const subs = user && user.subscribedLibraries || []; + let libraries = Libraries.find({ + $or: [ + {owner: userId}, + {writers: userId}, + {readers: userId}, + {_id: {$in: subs}}, + ] + }, { + fields: {_id: 1, name: 1}, + }); + let libraryIds = libraries.map(lib => lib._id); + + // Build a filter for nodes in those libraries that match the slot + let filter = getSlotFillFilter({slot: classProp, libraryIds}); + + this.autorun(function(){ + // Get the limit of the documents the user can fetch + var limit = self.data('limit') || 50; + check(limit, Number); + + let options = { + sort: { + name: 1, + order: 1, + }, + fields: LIBRARY_NODE_TREE_FIELDS, + limit, + }; + + self.autorun(function () { + self.setData('countAll', LibraryNodes.find(filter).count()); + }); + self.autorun(function () { return [LibraryNodes.find(filter, options), libraries]; }); }); diff --git a/app/imports/server/publications/userImages.js b/app/imports/server/publications/userImages.js new file mode 100644 index 00000000..765b607a --- /dev/null +++ b/app/imports/server/publications/userImages.js @@ -0,0 +1,7 @@ +import UserImages from '/imports/api/files/UserImages.js'; + +Meteor.publish('userImages', function () { + return UserImages.find({ + userId: this.userId, + }).cursor; +}); diff --git a/app/imports/server/publications/users.js b/app/imports/server/publications/users.js index c3e0c101..f096ccd2 100644 --- a/app/imports/server/publications/users.js +++ b/app/imports/server/publications/users.js @@ -10,6 +10,8 @@ Meteor.publish('user', function(){ apiKey: 1, darkMode: 1, subscribedLibraries: 1, + subscribedLibraryCollections: 1, + fileStorageUsed: 1, profile: 1, preferences: 1, 'services.patreon.id': 1, diff --git a/app/imports/ui/components/CardHighlight.vue b/app/imports/ui/components/CardHighlight.vue index 42a25fcc..714cdb28 100644 --- a/app/imports/ui/components/CardHighlight.vue +++ b/app/imports/ui/components/CardHighlight.vue @@ -18,7 +18,7 @@ props: { active: Boolean, dark: Boolean, - } + }, } diff --git a/app/imports/ui/components/ImageUploadInput.vue b/app/imports/ui/components/ImageUploadInput.vue new file mode 100644 index 00000000..c93235b4 --- /dev/null +++ b/app/imports/ui/components/ImageUploadInput.vue @@ -0,0 +1,77 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/components/SharedIcon.vue b/app/imports/ui/components/SharedIcon.vue index 568bd91c..936f01d0 100644 --- a/app/imports/ui/components/SharedIcon.vue +++ b/app/imports/ui/components/SharedIcon.vue @@ -1,6 +1,6 @@ diff --git a/app/imports/ui/creature/CreatureFormDialog.vue b/app/imports/ui/creature/CreatureFormDialog.vue index 7f680da9..9a80adce 100644 --- a/app/imports/ui/creature/CreatureFormDialog.vue +++ b/app/imports/ui/creature/CreatureFormDialog.vue @@ -1,5 +1,5 @@ diff --git a/app/imports/ui/properties/forms/FeatureForm.vue b/app/imports/ui/properties/forms/FeatureForm.vue index 77f3a77b..cb900962 100644 --- a/app/imports/ui/properties/forms/FeatureForm.vue +++ b/app/imports/ui/properties/forms/FeatureForm.vue @@ -35,6 +35,13 @@ :error-messages="errors.tags" @change="change('tags', ...arguments)" /> + + + diff --git a/app/imports/ui/properties/forms/FolderForm.vue b/app/imports/ui/properties/forms/FolderForm.vue index 8f55fe54..84f29b84 100644 --- a/app/imports/ui/properties/forms/FolderForm.vue +++ b/app/imports/ui/properties/forms/FolderForm.vue @@ -9,19 +9,27 @@ :error-messages="errors.name" @change="change('name', ...arguments)" /> - - - + + + + + + + + + diff --git a/app/imports/ui/properties/forms/ItemForm.vue b/app/imports/ui/properties/forms/ItemForm.vue index 08a9e586..ced109d9 100644 --- a/app/imports/ui/properties/forms/ItemForm.vue +++ b/app/imports/ui/properties/forms/ItemForm.vue @@ -100,47 +100,56 @@ $emit('change', {path: ['description', ...path], value, ack})" /> - - - - - -
- -
-
-
+ + + + + + + + + + +
+ +
+
+
+
diff --git a/app/imports/ui/properties/forms/NoteForm.vue b/app/imports/ui/properties/forms/NoteForm.vue index a328de80..ba9eeafd 100644 --- a/app/imports/ui/properties/forms/NoteForm.vue +++ b/app/imports/ui/properties/forms/NoteForm.vue @@ -35,6 +35,13 @@ :value="model.tags" @change="change('tags', ...arguments)" /> + + + diff --git a/app/imports/ui/properties/forms/ProficiencyForm.vue b/app/imports/ui/properties/forms/ProficiencyForm.vue index 41961c69..87b35b4a 100644 --- a/app/imports/ui/properties/forms/ProficiencyForm.vue +++ b/app/imports/ui/properties/forms/ProficiencyForm.vue @@ -37,6 +37,13 @@ :value="model.tags" @change="change('tags', ...arguments)" /> + + + diff --git a/app/imports/ui/properties/forms/ResourcesForm.vue b/app/imports/ui/properties/forms/ResourcesForm.vue index 886ba8c4..5c3e92d9 100644 --- a/app/imports/ui/properties/forms/ResourcesForm.vue +++ b/app/imports/ui/properties/forms/ResourcesForm.vue @@ -24,35 +24,33 @@ @push="({path, value, ack}) => $emit('push', {path: ['itemsConsumed', ...path], value, ack})" @pull="({path, ack}) => $emit('pull', {path: ['itemsConsumed', ...path], ack})" /> -
- - - - - Add Resource - - - Add Ammo - - - -
+ + + + + Add Resource + + + Add Ammo + + + diff --git a/app/imports/ui/properties/forms/RollForm.vue b/app/imports/ui/properties/forms/RollForm.vue index 0355018f..26432790 100644 --- a/app/imports/ui/properties/forms/RollForm.vue +++ b/app/imports/ui/properties/forms/RollForm.vue @@ -35,6 +35,13 @@ $emit('change', {path: ['roll', ...path], value, ack})" /> + + + + + + + diff --git a/app/imports/ui/properties/forms/SkillForm.vue b/app/imports/ui/properties/forms/SkillForm.vue index 0bfe5ebb..2854013e 100644 --- a/app/imports/ui/properties/forms/SkillForm.vue +++ b/app/imports/ui/properties/forms/SkillForm.vue @@ -45,36 +45,44 @@ $emit('change', {path: ['description', ...path], value, ack})" /> - - -
- + + + + + + - -
-
+
+ + +
+
+
diff --git a/app/imports/ui/properties/forms/SlotFillerForm.vue b/app/imports/ui/properties/forms/SlotFillerForm.vue index d0f84567..64ce1c51 100644 --- a/app/imports/ui/properties/forms/SlotFillerForm.vue +++ b/app/imports/ui/properties/forms/SlotFillerForm.vue @@ -1,20 +1,37 @@ diff --git a/app/imports/ui/properties/forms/SlotForm.vue b/app/imports/ui/properties/forms/SlotForm.vue index df857437..6c001c2c 100644 --- a/app/imports/ui/properties/forms/SlotForm.vue +++ b/app/imports/ui/properties/forms/SlotForm.vue @@ -72,11 +72,14 @@ @change="change('slotTags', ...arguments)" /> - +
- -
- + + + + + +
+ + +
+ - -
- -
+ +
diff --git a/app/imports/ui/properties/forms/SpellForm.vue b/app/imports/ui/properties/forms/SpellForm.vue index 7fafb173..db36d5b0 100644 --- a/app/imports/ui/properties/forms/SpellForm.vue +++ b/app/imports/ui/properties/forms/SpellForm.vue @@ -238,7 +238,7 @@ $emit('change', {path: ['description', ...path], value, ack})" /> - + + @@ -291,6 +292,14 @@ @change="change('reset', ...arguments)" /> + + + + + + + + + diff --git a/app/imports/ui/properties/forms/ToggleForm.vue b/app/imports/ui/properties/forms/ToggleForm.vue index a10de795..f3d01b0f 100644 --- a/app/imports/ui/properties/forms/ToggleForm.vue +++ b/app/imports/ui/properties/forms/ToggleForm.vue @@ -84,6 +84,14 @@ :value="model.tags" @change="change('tags', ...arguments)" /> + + + + diff --git a/app/imports/ui/properties/forms/TriggerForm.vue b/app/imports/ui/properties/forms/TriggerForm.vue new file mode 100644 index 00000000..59e7f479 --- /dev/null +++ b/app/imports/ui/properties/forms/TriggerForm.vue @@ -0,0 +1,206 @@ + + + diff --git a/app/imports/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/ui/properties/forms/shared/propertyFormIndex.js index 2e28b01c..1aa8f42e 100644 --- a/app/imports/ui/properties/forms/shared/propertyFormIndex.js +++ b/app/imports/ui/properties/forms/shared/propertyFormIndex.js @@ -1,9 +1,9 @@ const ActionForm = () => import('/imports/ui/properties/forms/ActionForm.vue'); const AdjustmentForm = () => import('/imports/ui/properties/forms/AdjustmentForm.vue'); -const AttackForm = () => import('/imports/ui/properties/forms/AttackForm.vue'); const AttributeForm = () => import('/imports/ui/properties/forms/AttributeForm.vue'); const BuffForm = () => import('/imports/ui/properties/forms/BuffForm.vue'); const BranchForm = () => import('/imports/ui/properties/forms/BranchForm.vue'); +const ClassForm = () => import('/imports/ui/properties/forms/ClassForm.vue'); const ClassLevelForm = () => import('/imports/ui/properties/forms/ClassLevelForm.vue'); const ConstantForm = () => import('/imports/ui/properties/forms/ConstantForm.vue'); const ContainerForm = () => import('/imports/ui/properties/forms/ContainerForm.vue'); @@ -24,18 +24,18 @@ const SlotFillerForm = () => import('/imports/ui/properties/forms/SlotFillerForm const SpellListForm = () => import('/imports/ui/properties/forms/SpellListForm.vue'); const SpellForm = () => import('/imports/ui/properties/forms/SpellForm.vue'); const ToggleForm = () => import('/imports/ui/properties/forms/ToggleForm.vue'); +const TriggerForm = () => import('/imports/ui/properties/forms/TriggerForm.vue'); export default { action: ActionForm, adjustment: AdjustmentForm, - attack: AttackForm, attribute: AttributeForm, buff: BuffForm, branch: BranchForm, constant: ConstantForm, container: ContainerForm, + class: ClassForm, classLevel: ClassLevelForm, - class: SlotForm, damage: DamageForm, damageMultiplier: DamageMultiplierForm, effect: EffectForm, @@ -53,4 +53,5 @@ export default { spellList: SpellListForm, spell: SpellForm, toggle: ToggleForm, + trigger: TriggerForm, }; diff --git a/app/imports/ui/properties/forms/shared/propertyFormMixin.js b/app/imports/ui/properties/forms/shared/propertyFormMixin.js index e9eed2f4..172db58f 100644 --- a/app/imports/ui/properties/forms/shared/propertyFormMixin.js +++ b/app/imports/ui/properties/forms/shared/propertyFormMixin.js @@ -1,10 +1,13 @@ import ComputedField from '/imports/ui/properties/forms/shared/ComputedField.vue'; import InlineComputationField from '/imports/ui/properties/forms/shared/InlineComputationField.vue'; +import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue'; export default { components: { ComputedField, InlineComputationField, + FormSection, + FormSections, }, props: { model: { diff --git a/app/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue b/app/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue new file mode 100644 index 00000000..10767f99 --- /dev/null +++ b/app/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue @@ -0,0 +1,36 @@ + + + diff --git a/app/imports/ui/properties/treeNodeViews/TreeNodeView.vue b/app/imports/ui/properties/treeNodeViews/TreeNodeView.vue index c2a04b65..65a18e0e 100644 --- a/app/imports/ui/properties/treeNodeViews/TreeNodeView.vue +++ b/app/imports/ui/properties/treeNodeViews/TreeNodeView.vue @@ -6,6 +6,7 @@ :class="{ 'inactive': model.inactive, }" + v-bind="$attrs" /> diff --git a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js index 26d52ca0..1f9772e8 100644 --- a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js +++ b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js @@ -7,6 +7,7 @@ import EffectTreeNode from '/imports/ui/properties/treeNodeViews/EffectTreeNode. import ClassLevelTreeNode from '/imports/ui/properties/treeNodeViews/ClassLevelTreeNode.vue'; import ProficiencyTreeNode from '/imports/ui/properties/treeNodeViews/ProficiencyTreeNode.vue'; import ReferenceTreeNode from '/imports/ui/properties/treeNodeViews/ReferenceTreeNode.vue'; +import SavingThrowTreeNode from '/imports/ui/properties/treeNodeViews/SavingThrowTreeNode.vue'; export default { default: DefaultTreeNode, @@ -18,4 +19,5 @@ export default { item: ItemTreeNode, proficiency: ProficiencyTreeNode, reference: ReferenceTreeNode, + savingThrow: SavingThrowTreeNode, } diff --git a/app/imports/ui/properties/viewers/AttributeViewer.vue b/app/imports/ui/properties/viewers/AttributeViewer.vue index 8e9cc4a4..6a2844c8 100644 --- a/app/imports/ui/properties/viewers/AttributeViewer.vue +++ b/app/imports/ui/properties/viewers/AttributeViewer.vue @@ -232,18 +232,10 @@ return []; } }, - effects(){ - if (this.context.creatureId && this.model.variableName){ - let creatureId = this.context.creatureId; - return CreatureProperties.find({ - 'ancestors.id': creatureId, - 'stats': this.model.variableName, - removed: {$ne: true}, - inactive: {$ne: true}, - }); - } else { - return []; - } + effects() { + return CreatureProperties.find({ + _id: { $in: this.model.effects?.map(e => e._id) || [] } + }); }, }, } diff --git a/app/imports/ui/properties/viewers/ClassViewer.vue b/app/imports/ui/properties/viewers/ClassViewer.vue new file mode 100644 index 00000000..a03d3431 --- /dev/null +++ b/app/imports/ui/properties/viewers/ClassViewer.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/app/imports/ui/properties/viewers/EffectViewer.vue b/app/imports/ui/properties/viewers/EffectViewer.vue index 772a02fd..ef72d816 100644 --- a/app/imports/ui/properties/viewers/EffectViewer.vue +++ b/app/imports/ui/properties/viewers/EffectViewer.vue @@ -101,12 +101,13 @@ case 'mul': return 'Multiply'; case 'min': return 'Minimum'; case 'max': return 'Maximum'; + case 'set': return 'Set'; case 'advantage': return 'Advantage'; case 'disadvantage': return 'Disadvantage'; case 'passiveAdd': return 'Passive bonus'; case 'fail': return 'Always fail'; case 'conditional': return 'Conditional benefit' ; - default: return ''; + default: return this.model.operation; } }, displayedValue(){ diff --git a/app/imports/ui/properties/viewers/SkillViewer.vue b/app/imports/ui/properties/viewers/SkillViewer.vue index 580be486..5cc7479e 100644 --- a/app/imports/ui/properties/viewers/SkillViewer.vue +++ b/app/imports/ui/properties/viewers/SkillViewer.vue @@ -126,7 +126,7 @@ import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue'; import SkillProficiency from '/imports/ui/properties/components/skills/SkillProficiency.vue'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js'; import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js'; export default { @@ -185,6 +185,9 @@ export default { }, }, meteor: { + variables(){ + return CreatureVariables.findOne({_creatureId: this.context.creatureId}) || {}; + }, baseEffects(){ if (this.context.creatureId){ let creatureId = this.context.creatureId; @@ -207,19 +210,10 @@ export default { return []; } }, - effects(){ - if (this.context.creatureId && this.model.variableName){ - let creatureId = this.context.creatureId; - return CreatureProperties.find({ - 'ancestors.id': creatureId, - stats: this.model.variableName, - type: 'effect', - removed: {$ne: true}, - inactive: {$ne: true}, - }).fetch(); - } else { - return []; - } + effects() { + return CreatureProperties.find({ + _id: { $in: this.model.effects?.map(e => e._id) || [] } + }); }, baseProficiencies(){ if (this.context.creatureId){ @@ -280,10 +274,8 @@ export default { proficiencyBonus(){ let creatureId = this.context.creatureId; if (!creatureId) return; - let creature = Creatures.findOne(creatureId) - return creature && - creature.variables.proficiencyBonus && - creature.variables.proficiencyBonus.value; + return this.variables.proficiencyBonus && + this.variables.proficiencyBonus.value; }, }, } diff --git a/app/imports/ui/properties/viewers/SlotViewer.vue b/app/imports/ui/properties/viewers/SlotViewer.vue index bdd07cd5..36e432f1 100644 --- a/app/imports/ui/properties/viewers/SlotViewer.vue +++ b/app/imports/ui/properties/viewers/SlotViewer.vue @@ -16,7 +16,7 @@ /> + + + + + mdi-plus + + Fill Slot + + - diff --git a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js index 8845fac6..572bfad0 100644 --- a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js +++ b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js @@ -4,6 +4,7 @@ const AttributeViewer = () => import ('/imports/ui/properties/viewers/AttributeV const BuffViewer = () => import ('/imports/ui/properties/viewers/BuffViewer.vue'); const BranchViewer = () => import ('/imports/ui/properties/viewers/BranchViewer.vue'); const ContainerViewer = () => import ('/imports/ui/properties/viewers/ContainerViewer.vue'); +const ClassViewer = () => import ('/imports/ui/properties/viewers/ClassViewer.vue'); const ClassLevelViewer = () => import ('/imports/ui/properties/viewers/ClassLevelViewer.vue'); const ConstantViewer = () => import ('/imports/ui/properties/viewers/ConstantViewer.vue'); const DamageViewer = () => import ('/imports/ui/properties/viewers/DamageViewer.vue'); @@ -23,6 +24,7 @@ const SlotFillerViewer = () => import ('/imports/ui/properties/viewers/SlotFille const SpellListViewer = () => import ('/imports/ui/properties/viewers/SpellListViewer.vue'); const SpellViewer = () => import ('/imports/ui/properties/viewers/SpellViewer.vue'); const ToggleViewer = () => import ('/imports/ui/properties/viewers/ToggleViewer.vue'); +const TriggerViewer = () => import ('/imports/ui/properties/viewers/TriggerViewer.vue'); export default { action: ActionViewer, @@ -31,7 +33,7 @@ export default { buff: BuffViewer, branch: BranchViewer, container: ContainerViewer, - class: SlotViewer, + class: ClassViewer, classLevel: ClassLevelViewer, constant: ConstantViewer, damage: DamageViewer, @@ -51,4 +53,5 @@ export default { spellList: SpellListViewer, spell: SpellViewer, toggle: ToggleViewer, + trigger: TriggerViewer, }; diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js index f8fe9442..37d8e562 100644 --- a/app/imports/ui/router.js +++ b/app/imports/ui/router.js @@ -7,7 +7,8 @@ const About = () => import('/imports/ui/pages/About.vue'); const CharacterList = () => import('/imports/ui/pages/CharacterList.vue'); const CharacterListToolbarItems = () => import('/imports/ui/creature/creatureList/CharacterListToolbarItems.vue'); const Library = () => import('/imports/ui/pages/Library.vue'); -const SingleLibraryToolbar = () => import('/imports/ui/library/SingleLibraryToolbar.vue'); +const LibraryCollection = () => import('/imports/ui/pages/LibraryCollection.vue'); +const LibraryCollectionToolbar = () => import('/imports/ui/library/LibraryCollectionToolbar.vue'); const CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue'); const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue'); const CharacterSheetRightDrawer = () => import('/imports/ui/creature/character/CharacterSheetRightDrawer.vue'); @@ -24,12 +25,15 @@ const EmailVerificationError = () => import('/imports/ui/pages/EmailVerification const ResetPassword = () => import('/imports/ui/pages/ResetPassword.vue' ); const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue'); const PatreonLevelTooLow = () => import('/imports/ui/pages/PatreonLevelTooLow.vue'); +const SingleLibrary = () => import('/imports/ui/pages/SingleLibrary.vue'); +const SingleLibraryToolbar = () => import('/imports/ui/library/SingleLibraryToolbar.vue'); const Tabletops = () => import('/imports/ui/pages/Tabletops.vue'); const Tabletop = () => import('/imports/ui/pages/Tabletop.vue'); const TabletopToolbar = () => import('/imports/ui/tabletop/TabletopToolbar.vue'); const TabletopRightDrawer = () => import('/imports/ui/tabletop/TabletopRightDrawer.vue'); const Admin = () => import('/imports/ui/pages/Admin.vue'); const Maintenance = () => import('/imports/ui/pages/Maintenance.vue'); +const Files = () => import('/imports/ui/pages/Files.vue'); // Not found const NotFound = () => import('/imports/ui/pages/NotFound.vue'); @@ -119,7 +123,8 @@ RouterFactory.configure(router => { title: 'Home', }, },{ - path: '/characterList', + path: '/character-list', + alias: '/characterList', components: { default: CharacterList, toolbarItems: CharacterListToolbarItems, @@ -128,7 +133,8 @@ RouterFactory.configure(router => { title: 'Character List', }, beforeEnter: ensureLoggedIn, - },{ + }, { + name: 'library', path: '/library', components: { default: Library, @@ -141,13 +147,24 @@ RouterFactory.configure(router => { name: 'singleLibrary', path: '/library/:id', components: { - default: Library, + default: SingleLibrary, toolbar: SingleLibraryToolbar, }, meta: { title: 'Library', }, },{ + name: 'libraryCollection', + path: '/library-collection/:id', + components: { + default: LibraryCollection, + toolbar: LibraryCollectionToolbar, + }, + meta: { + title: 'Library Collection', + }, + }, { + name: 'characterSheet', path: '/character/:id', alias: '/character/:id/:urlName', components: { @@ -199,8 +216,8 @@ RouterFactory.configure(router => { meta: { title: 'Register', }, - },{ - path: '/account', + }, { + path: '/account', components: { default: Account, }, @@ -208,7 +225,16 @@ RouterFactory.configure(router => { title: 'Account', }, beforeEnter: ensureLoggedIn, - },{ + }, { + path: '/my-files', + components: { + default: Files, + }, + meta: { + title: 'Files', + }, + beforeEnter: ensureLoggedIn, + }, { path: '/feedback', components: { default: Feedback, diff --git a/app/imports/ui/sharing/ShareDialog.vue b/app/imports/ui/sharing/ShareDialog.vue index 728688e6..181b23fe 100644 --- a/app/imports/ui/sharing/ShareDialog.vue +++ b/app/imports/ui/sharing/ShareDialog.vue @@ -92,7 +92,7 @@ mdi-signature - Transfer Onwership + Transfer Ownership diff --git a/app/imports/ui/styles/cardTitles.css b/app/imports/ui/styles/cardTitles.css new file mode 100644 index 00000000..b45ce973 --- /dev/null +++ b/app/imports/ui/styles/cardTitles.css @@ -0,0 +1,3 @@ +.v-card__title { + word-break: normal !important; +} \ No newline at end of file diff --git a/app/imports/ui/styles/stylesIndex.js b/app/imports/ui/styles/stylesIndex.js index a49f5cd0..383664b1 100644 --- a/app/imports/ui/styles/stylesIndex.js +++ b/app/imports/ui/styles/stylesIndex.js @@ -1,4 +1,5 @@ import './cardColors.css'; +import './cardTitles.css'; import './centeredInputs.css'; import './denseLists.css'; import './fitAvatars.css'; diff --git a/app/package-lock.json b/app/package-lock.json index 90de0f9d..3ce518a8 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -50,9 +50,9 @@ } }, "@babel/runtime": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", - "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.3.tgz", + "integrity": "sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -279,25 +279,25 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, "aws-sdk": { - "version": "2.1059.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1059.0.tgz", - "integrity": "sha512-Q+6T9kpO6aobUNboTOk9MVAmWbs/KK0pxgCNFK0M8YO+7EWUFkNOLHM9tdYOP5vsJK5pLz6D2t2w3lHQjKzGlg==", + "version": "2.1148.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1148.0.tgz", + "integrity": "sha512-FUYAyveKmS5eqIiGQgrGVsLZwwtI+K6S6Gz8oJf56pgypZCo9dV+cXO4aaS+vN0+LSmGh6dSKc6G8h8FYASIJg==", "requires": { "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", - "jmespath": "0.15.0", + "jmespath": "0.16.0", "querystring": "0.2.0", "sax": "1.2.1", "url": "0.10.3", - "uuid": "3.3.2", + "uuid": "8.0.0", "xml2js": "0.4.19" }, "dependencies": { "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" } } }, @@ -367,6 +367,37 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -417,15 +448,16 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", "deep-eql": "^3.0.1", "get-func-name": "^2.0.0", + "loupe": "^2.3.1", "pathval": "^1.1.1", "type-detect": "^4.0.5" } @@ -484,13 +516,13 @@ "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", "dev": true }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -556,13 +588,37 @@ "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -575,8 +631,16 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "combined-stream": { "version": "1.0.8", @@ -661,6 +725,14 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -670,6 +742,11 @@ "type-detect": "^4.0.0" } }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -726,9 +803,9 @@ } }, "dompurify": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.4.tgz", - "integrity": "sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==" + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.3.8.tgz", + "integrity": "sha512-eVhaWoVibIzqdGYjwsBWodIQIaXFSB+cKDf4cfxLMsK0xiud6SE+/WCVx/Xw/UwQsa4cS3T2eITcdtmTg2UKcw==" }, "ecc-jsbn": { "version": "0.1.2", @@ -749,6 +826,14 @@ "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==" }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -983,7 +1068,12 @@ "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, "extend": { "version": "3.0.2", @@ -1069,6 +1159,11 @@ "mime-types": "^2.1.12" } }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1118,7 +1213,7 @@ "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", "dev": true }, "getpass": { @@ -1129,6 +1224,11 @@ "assert-plus": "^1.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -1234,9 +1334,9 @@ "integrity": "sha1-tJ7yJ0va/NikiAqWa/440aC/RnE=" }, "immutable": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", - "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz", + "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==", "dev": true }, "import-fresh": { @@ -1269,6 +1369,16 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1329,9 +1439,9 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, "jmespath": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", - "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" }, "js-tokens": { "version": "4.0.0", @@ -1431,7 +1541,7 @@ "lodash._reinterpolate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" }, "lodash.merge": { "version": "4.6.2", @@ -1467,6 +1577,15 @@ "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1500,9 +1619,9 @@ } }, "marked": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz", - "integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==" + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.16.tgz", + "integrity": "sha512-wahonIQ5Jnyatt2fn8KqF/nIqZM8mh3oRu2+l5EANGMhu6RFjiSG52QNE2eWzFMI94HqYSgN184NurgNG6CztA==" }, "mem": { "version": "6.1.1", @@ -1523,13 +1642,13 @@ } }, "meteor-node-stubs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.1.0.tgz", - "integrity": "sha512-YvMQb4zcfWA82wFdRVTyxq28GO+Us7GSdtP+bTtC/mV35yipKnWo4W4665O57AmLVFnz4zR+WIZW11b4sfCtJw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/meteor-node-stubs/-/meteor-node-stubs-1.2.3.tgz", + "integrity": "sha512-2kyYFh45428+q8EjydBhyHqPO30CG09yQ6xRNHMJSiFLqHaVoRJE1tWr7QrBKstjy8HkNH4UuKSp5S11HeZv/w==", "requires": { "assert": "^2.0.0", "browserify-zlib": "^0.2.0", - "buffer": "^6.0.3", + "buffer": "^5.7.1", "console-browserify": "^1.2.0", "constants-browserify": "^1.0.0", "crypto-browserify": "^3.12.0", @@ -1540,7 +1659,7 @@ "os-browserify": "^0.3.0", "path-browserify": "^1.0.0", "process": "^0.11.10", - "punycode": "^2.1.1", + "punycode": "^1.4.1", "querystring-es3": "^0.2.1", "readable-stream": "^3.6.0", "stream-browserify": "^3.0.0", @@ -1657,11 +1776,11 @@ } }, "buffer": { - "version": "6.0.3", + "version": "5.7.1", "bundled": true, "requires": { "base64-js": "^1.3.1", - "ieee754": "^1.2.1" + "ieee754": "^1.1.13" } }, "buffer-xor": { @@ -2110,7 +2229,7 @@ } }, "punycode": { - "version": "2.1.1", + "version": "1.4.1", "bundled": true }, "querystring": { @@ -2324,6 +2443,11 @@ "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", "dev": true }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + }, "minify-css-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/minify-css-string/-/minify-css-string-1.0.0.tgz", @@ -2337,6 +2461,11 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, "minipass": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", @@ -2359,6 +2488,11 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "mongo-object": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/mongo-object/-/mongo-object-0.1.4.tgz", @@ -2374,6 +2508,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -2409,6 +2548,14 @@ "resolved": "https://registry.npmjs.org/ngraph.path/-/ngraph.path-1.4.0.tgz", "integrity": "sha512-yJZay4tP0wcjqkkf8zlMQ/T+JOgU+EWfdE4w4TG8OS94B12J/+Z44UOYxVJErE8E6/wFunX1hMZEB1/GHsBYHg==" }, + "node-abi": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.22.0.tgz", + "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", + "requires": { + "semver": "^7.3.5" + } + }, "node-addon-api": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", @@ -2558,12 +2705,44 @@ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" }, + "prebuild-install": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz", + "integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==", + "requires": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "pretty-bytes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz", + "integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==" + }, "prism-media": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.1.tgz", @@ -2585,6 +2764,15 @@ "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -2609,7 +2797,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==" }, "railroad-diagrams": { "version": "1.0.0", @@ -2625,6 +2813,24 @@ "ret": "~0.1.10" } }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + } + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -2738,9 +2944,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.48.0.tgz", - "integrity": "sha512-hQi5g4DcfjcipotoHZ80l7GNJHGqQS5LwMBjVYB/TaT0vcSSpbgM8Ad7cgfsB2M0MinbkEQQPO9+sjjSiwxqmw==", + "version": "1.52.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.2.tgz", + "integrity": "sha512-mfHB2VSeFS7sZlPv9YohB9GB7yWIgQNTGniQwfQ04EoQN0wsQEv7SwpCwy/x48Af+Z3vDeFXz+iuXM3HK/phZQ==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -2771,6 +2977,41 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, + "sharp": { + "version": "0.30.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.6.tgz", + "integrity": "sha512-lSdVxFxcndzcXggDrak6ozdGJgmIgES9YVZWtAFrwi+a/H5vModaf51TghBtMPw+71sLxUsTy2j+aB7qLIODQg==", + "requires": { + "color": "^4.2.3", + "detect-libc": "^2.0.1", + "node-addon-api": "^5.0.0", + "prebuild-install": "^7.1.0", + "semver": "^7.3.7", + "simple-get": "^4.0.1", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, + "node-addon-api": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.0.0.tgz", + "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2792,15 +3033,38 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simpl-schema": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/simpl-schema/-/simpl-schema-1.12.0.tgz", - "integrity": "sha512-lzXC3L8jJbPhNXGR3cjlyIauqqrC5WUJS4O34Ym/wLIvb8K3ZieK+1OfTzs4mBpDc3Y8u53gQFAr1X37DmTcEg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/simpl-schema/-/simpl-schema-1.12.2.tgz", + "integrity": "sha512-FaisAjfJEt7Ie7K39wNqb/0F7FQ1q7yXmZcNa5JEBiPA9hIt4MpWMouL9mLqNB89alGpZAEiU7U9BelDxRqCVg==", "requires": { "clone": "^2.1.2", "message-box": "^0.2.7", "mongo-object": "^0.1.4" } }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, "slice-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", @@ -2855,9 +3119,9 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "dev": true }, "source-map-support": { @@ -3030,6 +3294,48 @@ "yallist": "^4.0.0" } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + } + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3094,9 +3400,9 @@ "dev": true }, "underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" }, "uri-js": { "version": "4.2.2", @@ -3118,7 +3424,7 @@ "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" } } }, @@ -3207,9 +3513,9 @@ "integrity": "sha512-hx2JtRPRvne9NY4s1r7ASsCaO8CIby30qwC1kGQRxsrWApO3he+rziGOzTDSfvmr852zWMb11n6qAwHCz6C/vw==" }, "vue-router": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.3.tgz", - "integrity": "sha512-FUlILrW3DGitS2h+Xaw8aRNvGTwtuaxrRkNSHWTizOfLUie7wuYwezeZ50iflRn8YPV5kxmU2LQuu3nM/b3Zsg==" + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.4.tgz", + "integrity": "sha512-x+/DLAJZv2mcQ7glH2oV9ze8uPwcI+H+GgTgTmb5I55bCgY3+vXWIsqbYUzbBSZnwFHEJku4eoaH/x98veyymQ==" }, "vuedraggable": { "version": "2.24.3", @@ -3220,9 +3526,9 @@ } }, "vuetify": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.2.tgz", - "integrity": "sha512-nx3uZkO8MZNMshUEh1xKaQ1hQYepNwWFn3FVxKt+XBVf7ZFscd0GS/a3KZo4B3baXQmziCQAZKNIQF5IWeaIUw==" + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.6.tgz", + "integrity": "sha512-H4KtxDFmDN8QiTRiGfBySyjMhVaHAJTKB0llGGKZT5jKxtnx9gvEtMWXKtVuRP0NJJP0H6xBPJHNOH7nT18qiQ==" }, "vuetify-upload-button": { "version": "2.0.2", diff --git a/app/package.json b/app/package.json index 79f6de12..bc90f96d 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,8 @@ }, "author": "Stefan Zermatten", "scripts": { - "run": "meteor --once", + "run": "meteor", + "debug": "meteor --inspect", "test": "meteor test --driver-package meteortesting:mocha --port 3001" }, "engines": { @@ -17,11 +18,11 @@ "npm": "6.13.x" }, "dependencies": { - "@babel/runtime": "^7.16.7", + "@babel/runtime": "^7.18.3", "@chenfengyuan/vue-countdown": "^1.1.5", "@tozd/vue-observer-utils": "^0.5.0", "animejs": "^2.2.0", - "aws-sdk": "^2.1059.0", + "aws-sdk": "^2.1148.0", "bcrypt": "^5.0.0", "chroma-js": "^2.4.2", "core-js": "^2.6.11", @@ -29,40 +30,42 @@ "date-fns": "^1.30.1", "ddp-rate-limiter-mixin": "^1.1.10", "discord.js": "^12.5.3", - "dompurify": "^2.3.4", + "dompurify": "^2.3.8", "ignore": "^5.2.0", "ignore-styles": "^5.0.1", "lodash": "^4.17.20", - "marked": "^4.0.10", - "meteor-node-stubs": "^1.1.0", + "marked": "^4.0.16", + "meteor-node-stubs": "^1.2.3", "minify-css-string": "^1.0.0", "moo": "^0.5.1", "nearley": "^2.19.1", "ngraph.graph": "^19.1.0", "ngraph.path": "^1.4.0", + "pretty-bytes": "^6.0.0", "qrcode": "^1.5.0", "request": "^2.88.2", - "simpl-schema": "^1.12.0", + "sharp": "^0.30.4", + "simpl-schema": "^1.12.2", "source-map-support": "^0.5.21", "speakingurl": "^14.0.1", "styles": "^0.2.1", - "underscore": "^1.13.2", + "underscore": "^1.13.4", "vue": "2.6.10", "vue-meteor-tracker": "^2.0.0-beta.5", "vue-reactive-provide": "^0.3.0", - "vue-router": "^3.5.3", + "vue-router": "^3.5.4", "vuedraggable": "^2.23.2", - "vuetify": "^2.6.2", + "vuetify": "^2.6.6", "vuetify-upload-button": "^2.0.2", "vuex": "^3.1.3" }, "devDependencies": { - "chai": "^4.3.4", + "chai": "^4.3.6", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.20.0", "eslint-plugin-vuetify": "^1.1.0", "mem": "^6.1.1", - "sass": "^1.48.0" + "sass": "^1.52.2" }, "eslintConfig": { "extends": [ @@ -113,4 +116,4 @@ "vuetify/no-deprecated-classes": "error" } } -} +} \ No newline at end of file diff --git a/app/public/images/paragons/dai.png b/app/public/images/paragons/dai.png new file mode 100644 index 00000000..4ec8fcfb Binary files /dev/null and b/app/public/images/paragons/dai.png differ diff --git a/app/server/main.js b/app/server/main.js index f377abe9..642113e0 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -13,3 +13,6 @@ import '/imports/api/engine/actions/index.js'; import '/imports/migrations/server/index.js'; import '/imports/migrations/methods/index.js' import '/imports/constants/MAINTENANCE_MODE.js'; +import '/imports/api/creature/creatureProperties/methods/index.js'; +import '/imports/api/creature/archive/methods/index.js'; +import '/imports/api/creature/creatures/methods/index.js';