diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..509c092d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "app/packages/redis-oplog"] + path = app/packages/redis-oplog + url = https://github.com/ramezrafla/redis-oplog.git diff --git a/README.md b/README.md index 3a50ea99..447d08b4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ DiceCloud ======== -This is the repo for [DiceCloud](dicecloud.com). +This is the repo for [DiceCloud](https://dicecloud.com). DiceCloud is a free, auditable, real-time character sheet for D&D 5e. diff --git a/app/.meteor/packages b/app/.meteor/packages index ff36c66b..06788bcc 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -3,29 +3,30 @@ # 'meteor add' and 'meteor remove' will edit this file for you, # but you can also edit it by hand. -accounts-password@2.3.1 +zegenie:redis-oplog +accounts-password@2.3.4 random@1.2.1 -underscore@1.0.11 +underscore@1.0.13 dburles:mongo-collection-instances accounts-google@1.4.0 -email@2.2.2 +email@2.2.5 meteor-base@1.5.1 mobile-experience@1.1.0 -mongo@1.16.1 +mongo@1.16.6 session@1.2.1 -tracker@1.2.1 -logging@1.3.1 +tracker@1.3.2 +logging@1.3.2 reload@1.3.1 ejson@1.1.3 check@1.3.2 standard-minifier-js@2.8.1 shell-server@0.5.0 -ecmascript@0.16.3 +ecmascript@0.16.7 es5-shim@4.8.0 service-configuration@1.3.1 -dynamic-import@0.7.2 -ddp-rate-limiter@1.1.1 -rate-limit@1.0.9 +dynamic-import@0.7.3 +ddp-rate-limiter@1.2.0 +rate-limit@1.1.1 mdg:validated-method static-html@1.3.2 aldeed:collection2 @@ -38,7 +39,7 @@ simple:rest-method-mixin mikowals:batch-insert peerlibrary:subscription-data zer0th:meteor-vuetify-loader -akryum:vue-component +akryum:vue-component@0.15.2 akryum:vue-router2 percolate:migrations meteortesting:mocha @@ -46,6 +47,7 @@ ostrio:files simple:rest-bearer-token-parser simple:rest-json-error-handler littledata:synced-cron -mdg:meteor-apm-agent -typescript@4.5.4 +#mdg:meteor-apm-agent +typescript@4.9.4 seba:minifiers-autoprefixer +mixmax:smart-disconnect diff --git a/app/.meteor/release b/app/.meteor/release index 42890181..e8cfc7ec 100644 --- a/app/.meteor/release +++ b/app/.meteor/release @@ -1 +1 @@ -METEOR@2.8.1 +METEOR@2.12 diff --git a/app/.meteor/versions b/app/.meteor/versions index 3f632f0b..578fce81 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -1,7 +1,7 @@ -accounts-base@2.2.5 +accounts-base@2.2.8 accounts-google@1.4.0 -accounts-oauth@1.4.1 -accounts-password@2.3.1 +accounts-oauth@1.4.2 +accounts-password@2.3.4 accounts-patreon@0.1.0 akryum:npm-check@0.1.2 akryum:vue-component@0.15.2 @@ -12,7 +12,7 @@ aldeed:collection2@3.5.0 aldeed:schema-index@3.0.0 allow-deny@1.1.1 autoupdate@1.8.0 -babel-compiler@7.9.2 +babel-compiler@7.10.4 babel-runtime@1.5.1 base64@1.0.12 binary-heap@1.0.11 @@ -21,7 +21,7 @@ boilerplate-generator@1.7.1 bozhao:link-accounts@2.6.1 caching-compiler@1.2.2 caching-html-compiler@1.2.1 -callback-hook@1.4.0 +callback-hook@1.5.1 check@1.3.2 coffeescript@2.4.1 coffeescript-compiler@2.4.1 @@ -29,20 +29,20 @@ dburles:mongo-collection-instances@0.3.6 ddp@1.4.1 ddp-client@2.6.1 ddp-common@1.4.0 -ddp-rate-limiter@1.1.1 -ddp-server@2.6.0 +ddp-rate-limiter@1.2.0 +ddp-server@2.6.1 diff-sequence@1.1.2 -dynamic-import@0.7.2 -ecmascript@0.16.3 -ecmascript-runtime@0.8.0 +dynamic-import@0.7.3 +ecmascript@0.16.7 +ecmascript-runtime@0.8.1 ecmascript-runtime-client@0.12.1 ecmascript-runtime-server@0.11.0 ejson@1.1.3 -email@2.2.2 +email@2.2.5 es5-shim@4.8.0 -fetch@0.1.2 +fetch@0.1.3 geojson-utils@1.0.11 -google-oauth@1.4.2 +google-oauth@1.4.3 hot-code-push@1.0.4 html-tools@1.1.3 htmljs@1.1.1 @@ -52,36 +52,34 @@ 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 -mdg:meteor-apm-agent@3.5.1 -mdg:validated-method@1.2.0 -meteor@1.10.2 +logging@1.3.2 +mdg:validated-method@1.3.0 +meteor@1.11.2 meteor-base@1.5.1 -meteortesting:browser-tests@1.3.5 -meteortesting:mocha@2.0.3 +meteortesting:browser-tests@1.4.2 +meteortesting:mocha@2.1.0 meteortesting:mocha-core@8.1.2 mikowals:batch-insert@1.3.0 -minifier-css@1.6.1 +minifier-css@1.6.4 minifier-js@2.7.5 -minimongo@1.9.0 +minimongo@1.9.3 +mixmax:smart-disconnect@0.0.5 mobile-experience@1.1.0 mobile-status-bar@1.1.0 modern-browsers@0.1.9 modules@0.19.0 modules-runtime@0.13.1 -mongo@1.16.1 +mongo@1.16.6 mongo-decimal@0.1.3 mongo-dev-server@1.1.0 mongo-id@1.0.8 -mongo-livedata@1.0.12 -npm-mongo@4.11.0 -oauth@2.1.2 -oauth2@1.3.1 +npm-mongo@4.16.0 +oauth@2.2.0 +oauth2@1.3.2 ordered-dict@1.1.0 ostrio:cookies@2.7.2 -ostrio:files@2.3.2 +ostrio:files@2.3.3 patreon-oauth@0.1.0 peerlibrary:assert@0.3.0 peerlibrary:check-extension@0.7.0 @@ -94,11 +92,11 @@ peerlibrary:reactive-publish@0.10.0 peerlibrary:server-autorun@0.8.0 peerlibrary:subscription-data@0.8.0 percolate:migrations@1.1.0 -promise@0.12.1 +promise@0.12.2 raix:eventemitter@1.0.0 random@1.2.1 -rate-limit@1.0.9 -react-fast-refresh@0.2.3 +rate-limit@1.1.1 +react-fast-refresh@0.2.7 reactive-dict@1.3.1 reactive-var@1.0.12 reload@1.3.1 @@ -114,16 +112,17 @@ 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.5.0 +socket-stream-client@0.5.1 spacebars-compiler@1.3.1 standard-minifier-js@2.8.1 static-html@1.3.2 templating-tools@1.2.2 tmeasday:check-npm-versions@1.0.2 -tracker@1.2.1 -typescript@4.5.4 -underscore@1.0.11 +tracker@1.3.2 +typescript@4.9.4 +underscore@1.0.13 url@1.3.2 -webapp@1.13.2 +webapp@1.13.5 webapp-hashing@1.1.1 +zegenie:redis-oplog@2.0.16 zer0th:meteor-vuetify-loader@0.1.41 diff --git a/app/imports/api/creature/actions/Actions.js b/app/imports/api/creature/actions/Actions.js new file mode 100644 index 00000000..947c779b --- /dev/null +++ b/app/imports/api/creature/actions/Actions.js @@ -0,0 +1,27 @@ +import SimpleSchema from 'simpl-schema'; + +// Actions are creature actions that have been partially executed and not yet resolved +// They require some user input to progress +let Actions = new Mongo.Collection('actions'); + +let CreaturePropertySchema = new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Which creature is taking the action + _creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // The user who began taking the action + user: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // The property that is about to be applied + property: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, +}); diff --git a/app/imports/api/creature/archive/ArchiveCreatureFiles.js b/app/imports/api/creature/archive/ArchiveCreatureFiles.js index c0d1094e..044e6158 100644 --- a/app/imports/api/creature/archive/ArchiveCreatureFiles.js +++ b/app/imports/api/creature/archive/ArchiveCreatureFiles.js @@ -1,8 +1,14 @@ -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'; +let createS3FilesCollection; +if (Meteor.isServer) { + createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection +} else { + createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection +} const ArchiveCreatureFiles = createS3FilesCollection({ collectionName: 'archiveCreatureFiles', diff --git a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js index 9479b0c7..112ea06d 100644 --- a/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js +++ b/app/imports/api/creature/archive/methods/restoreCreatureFromFile.js @@ -13,12 +13,12 @@ import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileS import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js'; let migrateArchive; -if (Meteor.isServer){ - migrateArchive = require('/imports/migrations/server/migrateArchive.js').default; +if (Meteor.isServer) { + migrateArchive = require('/imports/migrations/archive/migrateArchive.js').default; } -function restoreCreature(archive, userId){ - if (SCHEMA_VERSION < archive.meta.schemaVersion){ +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.') } @@ -35,7 +35,7 @@ function restoreCreature(archive, userId){ }); 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; @@ -44,13 +44,13 @@ function restoreCreature(archive, userId){ Creatures.insert(archive.creature); try { // Add all the properties - if (archive.properties && archive.properties.length){ + if (archive.properties && archive.properties.length) { CreatureProperties.batchInsert(archive.properties); } - if (archive.experiences && archive.experiences.length){ + if (archive.experiences && archive.experiences.length) { Experiences.batchInsert(archive.experiences); } - if (archive.logs && archive.logs.length){ + if (archive.logs && archive.logs.length) { CreatureLogs.batchInsert(archive.logs); } } catch (e) { @@ -73,23 +73,23 @@ const restoreCreaturefromFile = new ValidatedMethod({ numRequests: 10, timeInterval: 5000, }, - async run({fileId}) { + async run({ fileId }) { // fetch the file - const file = ArchiveCreatureFiles.findOne({_id: fileId}).get(); - if (!file){ + const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get(); + if (!file) { throw new Meteor.Error('File not found', - 'The requested creature archive does not exist'); + 'The requested creature archive does not exist'); } // Assert ownership const userId = file?.userId; - if (!userId || userId !== this.userId){ + if (!userId || userId !== this.userId) { throw new Meteor.Error('Permission denied', - 'You can only restore creatures you own'); + 'You can only restore creatures you own'); } assertHasCharactersSlots(this.userId); - if (Meteor.isServer){ + if (Meteor.isServer) { // Read the file data const archive = await ArchiveCreatureFiles.readJSONFile(file); restoreCreature(archive, this.userId); diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index ff301a79..b451dd1d 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -46,6 +46,12 @@ let CreaturePropertySchema = new SimpleSchema({ regEx: SimpleSchema.RegEx.Id, optional: true, }, + // Fill more than one quantity in a slot, like feats and ability score + // improvements, filtered out of UI if there isn't space in quantityExpected + slotQuantityFilled: { + type: SimpleSchema.Integer, + optional: true, // Undefined implies 1 + }, }); const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ @@ -82,6 +88,12 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ index: 1, removeBeforeCompute: true, }, + deactivatingToggleId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + removeBeforeCompute: true, + }, // When this is true on any property, the creature needs to be recomputed dirty: { type: Boolean, diff --git a/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js new file mode 100644 index 00000000..46e7d024 --- /dev/null +++ b/app/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js @@ -0,0 +1,189 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; +import { + assertEditPermission, + assertDocEditPermission, + assertCopyPermission +} from '/imports/api/sharing/sharingPermissions.js'; +import { + setLineageOfDocs, + getAncestry, + renewDocIds +} from '/imports/api/parenting/parenting.js'; +import { reorderDocs } from '/imports/api/parenting/order.js'; +import { setDocToLastOrder } from '/imports/api/parenting/order.js'; +import Libraries from '/imports/api/library/Libraries.js'; +const DUPLICATE_CHILDREN_LIMIT = 500; + +const copyPropertyToLibrary = new ValidatedMethod({ + name: 'creatureProperties.copyPropertyToLibrary', + validate: new SimpleSchema({ + propId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + parentRef: { + type: RefSchema, + }, + order: { + type: Number, + optional: true, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({ propId, parentRef, order }) { + // get the new ancestry for the properties + let { parentDoc, ancestors } = getAncestry({ parentRef }); + + // Check permission to edit the destination + let rootLibrary; + if (parentRef.collection === 'libraries') { + rootLibrary = parentDoc; + } else if (parentRef.collection === 'libraryNodes') { + rootLibrary = Libraries.findOne(parentDoc.ancestors[0].id) + } else { + throw `${parentRef.collection} is not a valid parent collection` + } + assertEditPermission(rootLibrary, this.userId); + + const insertedRootNode = insertNodeFromProperty(propId, ancestors, order, this); + + // Tree structure changed by inserts, reorder the tree + reorderDocs({ + collection: LibraryNodes, + ancestorId: rootLibrary._id, + }); + + // Return the docId of the inserted root property + return insertedRootNode?._id; + }, +}); + +function insertNodeFromProperty(propId, ancestors, order, method) { + // Fetch the property and its descendants, provided they have not been + // removed + let prop = CreatureProperties.findOne({ + _id: propId, + removed: { $ne: true }, + }); + if (!prop) { + if (Meteor.isClient) return; + else { + throw new Meteor.Error( + 'Insert property from library failed', + `No property with id '${propId}' was found` + ); + } + } + + // Make sure we can edit this property + assertDocEditPermission(prop, method.userId); + + let oldParent = prop.parent; + const propCursor = CreatureProperties.find({ + 'ancestors.id': propId, + removed: { $ne: true }, + }); + + // Make sure there aren't too many descendants + if (propCursor.count() > DUPLICATE_CHILDREN_LIMIT) { + throw new Meteor.Error('Copy children limit', + `The property has over ${DUPLICATE_CHILDREN_LIMIT} descendants and cannot be copied`); + } + + let props = propCursor.fetch(); + + // The root prop is first in the array of props + // It must get the first generated ID to prevent flickering + props = [prop, ...props]; + + // If the docs came from a library, that library must consent to this user copying their + // properties + assertSourceLibraryCopyPermission(props, method); + + // re-map all the ancestors + setLineageOfDocs({ + docArray: props, + newAncestry: ancestors, + oldParent, + }); + + // Give the docs new IDs without breaking internal references + renewDocIds({ + docArray: props, + collectionMap: { 'creatureProperties': 'libraryNodes' } + }); + + // Order the root node + if (order === undefined) { + setDocToLastOrder({ + collection: LibraryNodes, + doc: prop, + }); + } else { + prop.order = order; + } + + // Insert the props as library nodes + LibraryNodes.batchInsert(props); + return prop; +} + +/** + * + * @param {[Property]} props The properties to check + * @param {String} userId The userId trying to copy these properties to a library + * Checks that every property can be copied out of the library that originated it by this user + */ +function assertSourceLibraryCopyPermission(props, method) { + // Skip on the client + if (method.isSimulation) return; + + // Get all the library node ids that are sources for these properties + const libraryNodeIds = []; + props.forEach(prop => { + if (prop.libraryNodeId) libraryNodeIds.push(prop.libraryNodeId); + }); + if (!libraryNodeIds.length) return; + + // Get the actual library Ids that each of these source nodes came from + const sourceLibIds = new Set(); + LibraryNodes.find({ + _id: { $in: libraryNodeIds } + }, { + fields: { ancestors: 1 } + }).forEach(node => { + sourceLibIds.add(node.ancestors?.[0]?.id); + }); + + // Assert copy permission on each of those libraries + Libraries.find({ + _id: { $in: Array.from(sourceLibIds) } + }, { + fields: { + name: 1, + owner: 1, + readers: 1, + writers: 1, + public: 1, + readersCanCopy: 1, + } + }).forEach(lib => { + try { + assertCopyPermission(lib, method.userId); + } catch (e) { + throw new Meteor.Error('Copy permission denied', + `One of the properties you are copying comes from ${lib.name}, which you do not have permission to copy from`); + } + }); +} + +export default copyPropertyToLibrary; diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js index f129a562..a8cd0696 100644 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js @@ -64,12 +64,12 @@ export function damagePropertyWork({ prop, operation, value, actionContext, logF // Save the value to the scope before applying the before triggers if (operation === 'increment') { if (value >= 0) { - actionContext.scope['$damage'] = value; + actionContext.scope['~damage'] = { value }; } else { - actionContext.scope['$healing'] = -value; + actionContext.scope['~healing'] = { value: -value }; } } else { - actionContext.scope['$set'] = value; + actionContext.scope['~set'] = { value }; } applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext); @@ -77,12 +77,12 @@ export function damagePropertyWork({ prop, operation, value, actionContext, logF // fetch the value from the scope after the before triggers, in case they changed them if (operation === 'increment') { if (value >= 0) { - value = actionContext.scope['$damage']; + value = actionContext.scope['~damage']?.value; } else { - value = -actionContext.scope['$healing']; + value = -actionContext.scope['~healing']?.value; } } else { - value = actionContext.scope['$set']; + value = actionContext.scope['~set']?.value; } let damage, newValue, increment; diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 9bb40898..25044a7d 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -42,6 +42,11 @@ const duplicateProperty = new ValidatedMethod({ let propertyId = randomSrc.id(); property._id = propertyId; + // Change the variableName so it isn't immediately overridden + if (property.variableName) { + property.variableName += 'Copy' + } + // Get all the descendants let nodes = CreatureProperties.find({ 'ancestors.id': _id, diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js index d4670c95..9558ff55 100644 --- a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js @@ -1,17 +1,19 @@ -export default function getSlotFillFilter({slot, libraryIds}){ +export default function getSlotFillFilter({ slot, libraryIds }) { + + if (!slot) throw 'Slot is required for getSlotFillFilter'; + if (!libraryIds) throw 'LibraryIds is required for getSlotFillFilter'; + let filter = { - removed: {$ne: true}, + fillSlots: true, + removed: { $ne: true }, $and: [] }; - if (libraryIds){ - filter['ancestors.id'] = {$in: libraryIds}; - } - if (slot.slotType){ + filter['ancestors.id'] = { $in: libraryIds }; + if (slot.slotType) { filter.$and.push({ $or: [{ type: slot.slotType - },{ - type: 'slotFiller', + }, { slotFillerType: slot.slotType, }] }); @@ -19,44 +21,43 @@ export default function getSlotFillFilter({slot, libraryIds}){ filter.$and.push({ $or: [{ type: 'classLevel', - },{ - type: 'slotFiller', + }, { slotFillerType: 'classLevel', }] }); - if (slot.variableName) { + if (slot.variableName) { filter.variableName = slot.variableName; } // Only search for levels the class needs if (slot.missingLevels && slot.missingLevels.length) { - filter.level = {$in: slot.missingLevels}; + filter.level = { $in: slot.missingLevels }; } else { - filter.level = (slot.level || 0) + 1; + filter.level = { $gt: slot.level || 0 }; } } let tagsOr = []; let tagsNin = []; - if (slot.slotTags && slot.slotTags.length){ - tagsOr.push({tags: {$all: slot.slotTags}}); + if (slot.slotTags && slot.slotTags.length) { + tagsOr.push({ libraryTags: { $all: slot.slotTags } }); } - if (slot.extraTags && slot.extraTags.length){ + if (slot.extraTags && slot.extraTags.length) { slot.extraTags.forEach(extra => { if (!extra.tags || !extra.tags.length) return; - if (extra.operation === 'OR'){ - tagsOr.push({tags: {$all: extra.tags}}); - } else if (extra.operation === 'NOT'){ + if (extra.operation === 'OR') { + tagsOr.push({ libraryTags: { $all: extra.tags } }); + } else if (extra.operation === 'NOT') { tagsNin.push(...extra.tags); } }); } - if (tagsOr.length){ + if (tagsOr.length) { filter.$or = tagsOr; } - if (tagsNin.length){ - filter.$and.push({tags: {$nin: tagsNin}}); + if (tagsNin.length) { + filter.$and.push({ libraryTags: { $nin: tagsNin } }); } - if (!filter.$and.length){ + if (!filter.$and.length) { delete filter.$and; } return filter; diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js new file mode 100644 index 00000000..d7b33cc0 --- /dev/null +++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.test.js @@ -0,0 +1,85 @@ +import { assert } from 'chai'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'; + +describe('Slot fill filter', function () { + + it('Gives error if arguments aren\'t provided', function () { + assert.throws( + () => getSlotFillFilter(undefined), + null, null, 'Passing undefined should give an error' + ); + assert.throws( + () => getSlotFillFilter({ + slot: { slotTags: ['tag1'] }, + }), + null, null, 'Passing no libraryIds should give an error' + ); + assert.throws( + () => getSlotFillFilter({ + libraryIds: ['libraryId1'], + }), + null, null, 'Passing no slot should give an error' + ); + }); + + it('filters using basic slot tags', function () { + const filter = getSlotFillFilter({ + slot: { + slotTags: ['tag1', 'tag2'] + }, + libraryIds: ['libraryId1', 'libraryId2'], + }); + assert.deepStrictEqual(filter, { + $or: [{ + libraryTags: { $all: ['tag1', 'tag2'] } + }], + 'ancestors.id': { $in: ['libraryId1', 'libraryId2'] }, + removed: { $ne: true }, + fillSlots: true, + }); + }); + + it('filters using slot type', function () { + const filter = getSlotFillFilter({ + slot: { + slotTags: ['tag1', 'tag2'], + slotType: 'feature', + }, + libraryIds: ['libraryId1', 'libraryId2'] + }); + assert.deepStrictEqual(filter.$and, [{ + $or: [{ + type: 'feature' + }, { + slotFillerType: 'feature', + }], + }]); + }); + + it('filters using extra tags', function () { + const filter = getSlotFillFilter({ + slot: { + slotTags: ['tag1', 'tag2'], + extraTags: [ + { operation: 'OR', tags: ['tag3', 'tag4'] }, + { operation: 'NOT', tags: ['tag5', 'tag6'] }, + { operation: 'NOT', tags: ['tag7', 'tag8'] }, + ], + }, + libraryIds: ['libraryId1', 'libraryId2'], + }); + assert.deepStrictEqual(filter, { + $or: [ + { libraryTags: { $all: ['tag1', 'tag2'] } }, + { libraryTags: { $all: ['tag3', 'tag4'] } }, + ], + $and: [ + { libraryTags: { $nin: ['tag5', 'tag6', 'tag7', 'tag8'] } }, + ], + 'ancestors.id': { $in: ['libraryId1', 'libraryId2'] }, + removed: { $ne: true }, + fillSlots: true, + }); + }); + +}); \ No newline at end of file diff --git a/app/imports/api/creature/creatureProperties/methods/index.js b/app/imports/api/creature/creatureProperties/methods/index.js index b71515e9..94bedae3 100644 --- a/app/imports/api/creature/creatureProperties/methods/index.js +++ b/app/imports/api/creature/creatureProperties/methods/index.js @@ -1,4 +1,5 @@ import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; +import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary.js'; import '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js'; import '/imports/api/creature/creatureProperties/methods/equipItem.js'; diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 641a9cfb..deb55bc7 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -98,13 +98,14 @@ function insertPropertyFromNode(nodeId, ancestors, order) { removed: { $ne: true }, }).fetch(); - // Convert all references into actual nodes - nodes = reifyNodeReferences(nodes); // The root node is first in the array of nodes // It must get the first generated ID to prevent flickering nodes = [node, ...nodes]; + // Convert all references into actual nodes + nodes = reifyNodeReferences(nodes); + // set libraryNodeIds storeLibraryNodeReferences(nodes); diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index 7e7edb1e..bb351c03 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -83,13 +83,13 @@ export function resetProperties(creatureId, resetFilter, actionContext) { const attributeFilter = { ...filter, type: 'attribute', - damage: { $ne: 0 }, + damage: { $nin: [0, undefined] }, } CreatureProperties.find(attributeFilter).forEach(prop => { damagePropertyWork({ prop, operation: 'increment', - value: -prop.damage, + value: -prop.damage ?? 0, actionContext, logFunction(increment) { actionContext.addLog({ @@ -105,7 +105,7 @@ export function resetProperties(creatureId, resetFilter, actionContext) { type: { $in: ['action', 'spell'] }, - usesUsed: { $ne: 0 }, + usesUsed: { $nin: [0, undefined] }, }; CreatureProperties.find(actionFilter, { fields: { name: 1, usesUsed: 1 } diff --git a/app/imports/api/creature/log/CreatureLogs.js b/app/imports/api/creature/log/CreatureLogs.js index 522d1f2e..a37569bd 100644 --- a/app/imports/api/creature/log/CreatureLogs.js +++ b/app/imports/api/creature/log/CreatureLogs.js @@ -9,9 +9,10 @@ import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/table import { parse, prettifyParseError } from '/imports/parser/parser.js'; import resolve, { toString } from '/imports/parser/resolve.js'; -const PER_CREATURE_LOG_LIMIT = 100; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +const PER_CREATURE_LOG_LIMIT = 100; + if (Meteor.isServer) { var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature; } @@ -78,10 +79,21 @@ function logToMessageData(log) { let embed = { fields: [], }; - log.content.forEach(field => { + log.content.forEach((field, index) => { + // Empty character for blank names if (!field.name) field.name = '\u200b'; if (!field.value) field.value = '\u200b'; - embed.fields.push(field); + // Enforce Discord field character limits + if (field.name?.length > 256) { + field.name = field.name.substring(0, 255); + } + if (field.value?.length > 1024) { + field.value = field.value.substring(0, 1024 - 3) + '...'; + } + // Enforce Discord 25 field limit + if (index < 25) { + embed.fields.push(field); + } }); return { embeds: [embed] }; } @@ -150,8 +162,16 @@ export function insertCreatureLogWork({ log, creature, method }) { log = { content: [{ value: log }] }; } if (!log.content?.length) return; + + // Truncate the string lengths to fit the log content schema + log.content.forEach((logItem) => { + if (logItem.value?.length > STORAGE_LIMITS.summary) { + logItem.value = logItem.value.substring(0, STORAGE_LIMITS.summary - 3) + '...'; + } + }); log.date = new Date(); if (creature) log.tabletopId = creature.tabletop; + // Insert it let id = CreatureLogs.insert(log); if (Meteor.isServer) { diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index d749990d..3aa87df9 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -8,6 +8,8 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met import numberToSignedString from '/imports/api/utility/numberToSignedString.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js'; +import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js'; +import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; export default function applyAction(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); @@ -51,11 +53,11 @@ export default function applyAction(node, actionContext) { } function applyAttackWithoutTarget({ attack, actionContext }) { - delete actionContext.scope['$attackHit']; - delete actionContext.scope['$attackMiss']; - delete actionContext.scope['$criticalHit']; - delete actionContext.scope['$criticalMiss']; - delete actionContext.scope['$attackRoll']; + delete actionContext.scope['~attackHit']; + delete actionContext.scope['~attackMiss']; + delete actionContext.scope['~criticalHit']; + delete actionContext.scope['~criticalMiss']; + delete actionContext.scope['~attackRoll']; recalculateCalculation(attack, actionContext); const scope = actionContext.scope; @@ -66,16 +68,16 @@ function applyAttackWithoutTarget({ attack, actionContext }) { criticalMiss, } = rollAttack(attack, scope); let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit'; - if (scope['$attackAdvantage'] === 1) { + if (scope['~attackAdvantage']?.value === 1) { name += ' (Advantage)'; - } else if (scope['$attackAdvantage'] === -1) { + } else if (scope['~attackAdvantage']?.value === -1) { name += ' (Disadvantage)'; } if (!criticalMiss) { - scope['$attackHit'] = { value: true } + scope['~attackHit'] = { value: true } } if (!criticalHit) { - scope['$attackMiss'] = { value: true }; + scope['~attackMiss'] = { value: true }; } actionContext.addLog({ @@ -87,12 +89,12 @@ function applyAttackWithoutTarget({ attack, actionContext }) { function applyAttackToTarget({ attack, target, actionContext }) { const scope = actionContext.scope; - delete scope['$attackHit']; - delete scope['$attackMiss']; - delete scope['$criticalHit']; - delete scope['$criticalMiss']; - delete scope['$attackDiceRoll']; - delete scope['$attackRoll']; + delete scope['~attackHit']; + delete scope['~attackMiss']; + delete scope['~criticalHit']; + delete scope['~criticalMiss']; + delete scope['~attackDiceRoll']; + delete scope['~attackRoll']; recalculateCalculation(attack, actionContext); @@ -109,9 +111,9 @@ function applyAttackToTarget({ attack, target, actionContext }) { let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : result > armor ? 'Hit!' : 'Miss!'; - if (scope['$attackAdvantage'] === 1) { + if (scope['~attackAdvantage']?.value === 1) { name += ' (Advantage)'; - } else if (scope['$attackAdvantage'] === -1) { + } else if (scope['~attackAdvantage']?.value === -1) { name += ' (Disadvantage)'; } @@ -121,9 +123,9 @@ function applyAttackToTarget({ attack, target, actionContext }) { inline: true, }); if (criticalMiss || result < armor) { - scope['$attackMiss'] = { value: true }; + scope['~attackMiss'] = { value: true }; } else { - scope['$attackHit'] = { value: true }; + scope['~attackHit'] = { value: true }; } } else { actionContext.addLog({ @@ -141,7 +143,7 @@ function applyAttackToTarget({ attack, target, actionContext }) { function rollAttack(attack, scope) { const rollModifierText = numberToSignedString(attack.value, true); let value, resultPrefix; - if (scope['$attackAdvantage'] === 1) { + if (scope['~attackAdvantage']?.value === 1) { const [a, b] = rollDice(2, 20); if (a >= b) { value = a; @@ -150,7 +152,7 @@ function rollAttack(attack, scope) { value = b; resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; } - } else if (scope['$attackAdvantage'] === -1) { + } else if (scope['~attackAdvantage']?.value === -1) { const [a, b] = rollDice(2, 20); if (a <= b) { value = a; @@ -163,23 +165,23 @@ function rollAttack(attack, scope) { value = rollDice(1, 20)[0]; resultPrefix = `1d20 [${value}] ${rollModifierText}` } - scope['$attackDiceRoll'] = { value }; + scope['~attackDiceRoll'] = { value }; const result = value + attack.value; - scope['$attackRoll'] = { result }; + scope['~attackRoll'] = { value: result }; const { criticalHit, criticalMiss } = applyCrits(value, scope); return { resultPrefix, result, value, criticalHit, criticalMiss }; } function applyCrits(value, scope) { - let criticalHitTarget = scope.criticalHitTarget?.value || 20; + const criticalHitTarget = scope['~criticalHitTarget']?.value || 20; let criticalHit = value >= criticalHitTarget; let criticalMiss; if (criticalHit) { - scope['$criticalHit'] = { value: true }; + scope['~criticalHit'] = { value: true }; } else { criticalMiss = value === 1; if (criticalMiss) { - scope['$criticalMiss'] = { value: true }; + scope['~criticalMiss'] = { value: true }; } } return { criticalHit, criticalMiss }; @@ -211,6 +213,7 @@ function spendResources(prop, actionContext) { let itemQuantityAdjustments = []; let spendLog = []; let gainLog = []; + let ammoChildren = []; try { prop.resources.itemsConsumed.forEach(itemConsumed => { recalculateCalculation(itemConsumed.quantity, actionContext); @@ -221,11 +224,8 @@ function spendResources(prop, actionContext) { if (!item || item.ancestors[0].id !== prop.ancestors[0].id) { throw 'The prop\'s ammo was not found on the creature'; } - if (!item.equipped) { - throw 'The selected ammo is not equipped'; - } if ( - !itemConsumed.quantity.value || + !itemConsumed?.quantity?.value || !isFinite(itemConsumed.quantity.value) ) return; itemQuantityAdjustments.push({ @@ -242,12 +242,14 @@ function spendResources(prop, actionContext) { } else if (itemConsumed.quantity.value < 0) { gainLog.push(logName + ': ' + -itemConsumed.quantity.value); } + ammoChildren.push(...getItemChildren(item, actionContext, prop)); }); } catch (e) { actionContext.addLog({ name: 'Error', - value: e, + value: e.toString(), }); + console.error(e); return true; } // No more errors should be thrown after this line @@ -303,4 +305,36 @@ function spendResources(prop, actionContext) { value: spendLog.join('\n'), inline: true, }); + + // Apply the ammo children + ammoChildren.forEach(prop => { + applyProperty(prop, actionContext); + }); +} + +function getItemChildren(item, actionContext, prop) { + // Skip if the prop or the item are ancestors of one another, otherwise infinite loop + if (hasAncestorRelationship(item, prop)) return []; + // Get the item children + const itemProperties = getPropertyDecendants(actionContext.creature._id, item._id); + // Tree them up + const propertyForest = nodeArrayToTree(itemProperties); + return propertyForest +} + +function hasAncestorRelationship(a, b) { + let top, bottom; + if (a.ancestors.length === b.ancestors.length) { + // Can't be ancestors of one another if they have the same number of ancestors + return false; + } else if (a.ancestors.length > b.ancestors.length) { + // longer ancestor list goes on the bottom + top = b; + bottom = a; + } else { + top = a; + bottom = b; + } + const expectedAncestorPosition = top.ancestors.length; + return bottom.ancestors[expectedAncestorPosition]?.id === top._id; } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js index ce918c4a..d48ebc01 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBranch.js @@ -3,22 +3,22 @@ import recalculateCalculation from './shared/recalculateCalculation.js'; import rollDice from '/imports/parser/rollDice.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyBranch(node, actionContext){ +export default function applyBranch(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); - const applyChildren = function(){ + const applyChildren = function () { applyNodeTriggers(node, 'after', actionContext); node.children.forEach(child => applyProperty(child, actionContext)); }; const scope = actionContext.scope; const targets = actionContext.targets; const prop = node.node; - switch(prop.branchType){ + switch (prop.branchType) { case 'if': recalculateCalculation(prop.condition, actionContext); if (prop.condition?.value) applyChildren(); break; case 'index': - if (node.children.length){ + if (node.children.length) { recalculateCalculation(prop.condition, actionContext); if (!isFinite(prop.condition?.value)) { actionContext.addLog({ @@ -35,31 +35,31 @@ export default function applyBranch(node, actionContext){ } break; case 'hit': - if (scope['$attackHit']?.value){ - if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'}); + if (scope['~attackHit']?.value) { + if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On hit**' }); applyChildren(); } break; case 'miss': - if (scope['$attackMiss']?.value){ - if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'}); + if (scope['~attackMiss']?.value) { + if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On miss**' }); applyChildren(); } break; case 'failedSave': - if (scope['$saveFailed']?.value){ - if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'}); + if (scope['~saveFailed']?.value) { + if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On failed save**' }); applyChildren(); } break; case 'successfulSave': - if (scope['$saveSucceeded']?.value){ - if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',}); + if (scope['~saveSucceeded']?.value) { + if (!targets.length && !prop.silent) actionContext.addLog({ value: '**On save**', }); applyChildren(); } break; case 'random': - if (node.children.length){ + if (node.children.length) { let index = rollDice(1, node.children.length)[0] - 1; applyNodeTriggers(node, 'after', actionContext); applyProperty(node.children[index], actionContext); diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js index 4ee2cd92..a755a001 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyBuff.js @@ -21,7 +21,10 @@ export default function applyBuff(node, actionContext) { const prop = node.node; let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; - // Then copy the decendants of the buff to the targets + // Mark the buff as dirty for recalculation + prop.dirty = true; + + // Then copy the descendants of the buff to the targets let propList = [prop]; function addChildrenToPropList(children, { skipCrystalize } = {}) { children.forEach(child => { @@ -97,7 +100,7 @@ function copyNodeListToTarget(propList, target, oldParent) { /** * Replaces all variables with their resolved values - * except variables of the form `$target.thing.total` become `thing.total` + * except variables of the form `~target.thing.total` become `thing.total` */ function crystalizeVariables({ propList, actionContext }) { propList.forEach(prop => { @@ -116,8 +119,8 @@ function crystalizeVariables({ propList, actionContext }) { node.parseType !== 'accessor' && node.parseType !== 'symbol' ) return node; // Handle variables - if (node.name === '$target') { - // strip $target + if (node.name === '~target') { + // strip ~target if (node.parseType === 'accessor') { node.name = node.path.shift(); if (!node.path.length) { @@ -127,7 +130,7 @@ function crystalizeVariables({ propList, actionContext }) { // Can't strip symbols actionContext.addLog({ name: 'Error', - value: 'Variable `$target` should not be used without a property: $target.property', + value: 'Variable `~target` should not be used without a property: ~target.property', }); } return node; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index f05b7351..a622989e 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -27,7 +27,7 @@ export default function applyDamage(node, actionContext) { // Choose target let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; // Determine if the hit is critical - let criticalHit = scope['$criticalHit']?.value && + let criticalHit = scope['~criticalHit']?.value && prop.damageType !== 'healing' // Can't critically heal ; // Double the damage rolls if the hit is critical @@ -73,12 +73,12 @@ export default function applyDamage(node, actionContext) { damage = Math.floor(damage); // Convert extra damage into the stored type - if (prop.damageType === 'extra' && scope['$lastDamageType']) { - prop.damageType = scope['$lastDamageType']; + if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) { + prop.damageType = scope['~lastDamageType']?.value; } // Store current damage type if (prop.damageType !== 'healing') { - scope['$lastDamageType'] = prop.damageType; + scope['~lastDamageType'] = { value: prop.damageType }; } // Memoise the damage suffix for the log @@ -193,14 +193,18 @@ function dealDamage({ target, damageType, amount, actionContext }) { let healthBars = getPropertiesOfType(target._id, 'attribute'); // Keep only the healthbars that can take damage/healing - remove(healthBars, (bar) => - bar.attributeType !== 'healthBar' || - bar.inactive || - bar.removed || - bar.overridden || - (amount >= 0 && bar.healthBarNoDamage) || - (amount < 0 && bar.healthBarNoHealing) - ); + healthBars = healthBars.filter((bar) => { + if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) { + return false; + } + if (damageType === 'healing' && bar.healthBarNoHealing) { + return false; + } + if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) { + return false; + } + return true; + }); // Sort healthbars by damage/healing order or tree order as a fallback healthBars.sort((a, b) => { diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js index 7d860948..68892231 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyRoll.js @@ -4,22 +4,22 @@ import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/appl import resolve, { toString } from '/imports/parser/resolve.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; -export default function applyRoll(node, actionContext){ +export default function applyRoll(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); const prop = node.node; - const applyChildren = function(){ + const applyChildren = function () { applyNodeTriggers(node, 'after', actionContext); node.children.forEach(child => applyProperty(child, actionContext)); }; - if (prop.roll?.calculation){ + if (prop.roll?.calculation) { const logValue = []; // roll the dice only and store that string applyEffectsToCalculationParseNode(prop.roll, actionContext); - const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope); - if (rolled.parseType !== 'constant'){ + const { result: rolled, context } = resolve('roll', prop.roll.parseNode, actionContext.scope); + if (rolled.parseType !== 'constant') { logValue.push(toString(rolled)); } logErrors(context.errors, actionContext); @@ -28,28 +28,28 @@ export default function applyRoll(node, actionContext){ context.errors = []; // Resolve the roll to a final value - const {result: reduced} = resolve('reduce', rolled, actionContext.scope, context); + const { result: reduced } = resolve('reduce', rolled, actionContext.scope, context); logErrors(context.errors, actionContext); // Store the result - if (reduced.parseType === 'constant'){ + if (reduced.parseType === 'constant') { prop.roll.value = reduced.value; - } else if (reduced.parseType === 'error'){ + } else if (reduced.parseType === 'error') { prop.roll.value = null; } else { prop.roll.value = toString(reduced); } - // If we didn't end up with a constant of finite amount, give up - if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){ + // If we didn't end up with a constant or a number of finite value, give up + if (reduced?.parseType !== 'constant' || (reduced.valueType === 'number' && !isFinite(reduced.value))) { return applyChildren(); } const value = reduced.value; - actionContext.scope[prop.variableName] = value; + actionContext.scope[prop.variableName] = { value }; logValue.push(`**${value}**`); - if (!prop.silent){ + if (!prop.silent) { actionContext.addLog({ name: prop.name, value: logValue.join('\n'), diff --git a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js index ca760d7a..e0803109 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js @@ -8,6 +8,7 @@ import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js'; export default function applySavingThrow(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); const prop = node.node; + const originalTargets = actionContext.targets; let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; @@ -31,22 +32,22 @@ export default function applySavingThrow(node, actionContext) { // If there are no save targets, apply all children as if the save both // succeeeded and failed if (!saveTargets?.length) { - scope['$saveFailed'] = { value: true }; - scope['$saveSucceeded'] = { value: true }; + scope['~saveFailed'] = { value: true }; + scope['~saveSucceeded'] = { value: true }; applyNodeTriggers(node, 'after', actionContext); return node.children.forEach(child => applyProperty(child, actionContext)); } // Each target makes the saving throw saveTargets.forEach(target => { - delete scope['$saveFailed']; - delete scope['$saveSucceeded']; - delete scope['$saveDiceRoll']; - delete scope['$saveRoll']; + delete scope['~saveFailed']; + delete scope['~saveSucceeded']; + delete scope['~saveDiceRoll']; + delete scope['~saveRoll']; const applyChildren = function () { - applyNodeTriggers(node, 'after', actionContext); actionContext.targets = [target] + applyNodeTriggers(node, 'after', actionContext); node.children.forEach(child => applyProperty(child, actionContext)); }; @@ -90,14 +91,14 @@ export default function applySavingThrow(node, actionContext) { value = values[0]; resultPrefix = `1d20 [ ${value} ] ${rollModifierText}` } - scope['$saveDiceRoll'] = { value }; + scope['~saveDiceRoll'] = { value }; const result = value + rollModifier || 0; - scope['$saveRoll'] = { value: result }; + scope['~saveRoll'] = { value: result }; const saveSuccess = result >= dc; if (saveSuccess) { - scope['$saveSucceeded'] = { value: true }; + scope['~saveSucceeded'] = { value: true }; } else { - scope['$saveFailed'] = { value: true }; + scope['~saveFailed'] = { value: true }; } if (!prop.silent) actionContext.addLog({ name: saveSuccess ? 'Successful save' : 'Failed save', @@ -106,4 +107,6 @@ export default function applySavingThrow(node, actionContext) { }); return applyChildren(); }); + // reset the targets after the save to each child + actionContext.targets = originalTargets; } diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js index 8fd6a536..cbaf2108 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -35,7 +35,7 @@ export function applyTrigger(trigger, prop, actionContext) { if (trigger.inactive) { return; } - + // Prevent triggers from firing if their condition is false if (trigger.condition?.parseNode) { recalculateCalculation(trigger.condition, actionContext); @@ -61,11 +61,11 @@ export function applyTrigger(trigger, prop, actionContext) { value: trigger.description, inline: false, } - if (trigger.description?.text){ + if (trigger.description?.text) { recalculateInlineCalculations(trigger.description, actionContext); content.value = trigger.description.value; } - if(!trigger.silent) actionContext.addLog(content); + if (!trigger.silent) actionContext.addLog(content); // Get all the trigger's properties and apply them const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); @@ -78,7 +78,7 @@ export function applyTrigger(trigger, prop, actionContext) { trigger.firing = false; } -function triggerMatchTags(trigger, prop) { +export function triggerMatchTags(trigger, prop) { let matched = false; const propTags = getEffectivePropTags(prop); // Check the target tags @@ -89,23 +89,26 @@ function triggerMatchTags(trigger, prop) { 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; + if (trigger.extraTags) { + for (const extra of trigger.extraTags) { + if (extra.operation === 'OR') { + if (matched) break; + 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).length > 0 + ) { + matched = false; + break; + } } } - }); + } return matched; } diff --git a/app/imports/api/engine/actions/applyTriggers.testFn.js b/app/imports/api/engine/actions/applyTriggers.testFn.js new file mode 100644 index 00000000..f2955afb --- /dev/null +++ b/app/imports/api/engine/actions/applyTriggers.testFn.js @@ -0,0 +1,67 @@ +import { triggerMatchTags } from '/imports/api/engine/actions/applyTriggers.js'; +import clean from '/imports/api/engine/computation/utility/cleanProp.testFn.js'; +import { assert } from 'chai'; + +export default function () { + const prop = clean({ + id: 'propWithTags', + type: 'action', + tags: ['yes1', 'notUsed', 'no1', 'yes2', 'no2', 'or1', 'or2'], + }); + const positiveProp = clean({ + id: 'propWithTags', + type: 'action', + tags: ['yes1', 'notUsed', 'yes2', 'or1', 'or2'], + }); + assert.isTrue( + triggerMatchTags(clean({ + type: 'trigger', + targetTags: ['yes1'], + }), prop), + 'Trigger matches on a single target tag' + ); + assert.isTrue( + triggerMatchTags(clean({ + type: 'trigger', + targetTags: ['yes1', 'yes2'], + }), prop), + 'Trigger matches on a multiple target tags' + ); + assert.isFalse( + triggerMatchTags(clean({ + type: 'trigger', + targetTags: ['yes1'], + extraTags: [{ operation: 'NOT', tags: ['no1'] }] + }), prop), + 'Trigger correctly fails to match when not tags are present' + ); + assert.isFalse( + triggerMatchTags(clean({ + type: 'trigger', + extraTags: [{ operation: 'NOT', tags: ['no1'] }] + }), prop), + 'Trigger correctly fails to match when only not tags are present' + ); + assert.isTrue( + triggerMatchTags(clean({ + type: 'trigger', + extraTags: [{ operation: 'NOT', tags: ['no1'] }] + }), positiveProp), + 'Trigger matches when only not tags are present' + ); + assert.isTrue( + triggerMatchTags(clean({ + type: 'trigger', + extraTags: [{ operation: 'OR', tags: ['or1'] }] + }), positiveProp), + 'Trigger matches when OR tags are present' + ); + assert.isTrue( + triggerMatchTags(clean({ + type: 'trigger', + targetTags: ['missing1'], + extraTags: [{ operation: 'OR', tags: ['or1'] }] + }), positiveProp), + 'Trigger matches when only OR tags are present' + ); +} \ No newline at end of file diff --git a/app/imports/api/engine/actions/doAction.test.js b/app/imports/api/engine/actions/doAction.test.js index 56c365cb..b9fdfa23 100644 --- a/app/imports/api/engine/actions/doAction.test.js +++ b/app/imports/api/engine/actions/doAction.test.js @@ -1,16 +1,17 @@ import '/imports/api/simpleSchemaConfig.js'; //import testTypes from './testTypes/index.js'; +import applyTriggers from '/imports/api/engine/actions/applyTriggers.testFn.js'; import { doActionWork } from './doAction.js'; import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; -function cleanProp(prop){ +function cleanProp(prop) { let schema = CreatureProperties.simpleSchema(prop); return schema.clean(prop); } -function cleanCreature(creature){ +function cleanCreature(creature) { let schema = Creatures.simpleSchema(creature); return schema.clean(creature); } @@ -28,7 +29,7 @@ const testActionContext = { }), scope: {}, addLog(content) { - if (content.name || content.value){ + if (content.name || content.value) { this.log.content.push(content); } }, @@ -40,8 +41,8 @@ const action = cleanProp({ }); const actionAncestors = []; -describe('Do Action', function(){ - it('Does an empty action', function(){ +describe('Do Action', function () { + it('Does an empty action', function () { doActionWork({ properties: [action], ancestors: actionAncestors, @@ -51,3 +52,7 @@ describe('Do Action', function(){ }); //testTypes.forEach(test => it(test.text, test.fn)); }); + +describe('Action utility functions', function () { + it('Triggers match tags', applyTriggers); +}) diff --git a/app/imports/api/engine/actions/doCastSpell.js b/app/imports/api/engine/actions/doCastSpell.js index f512e4ed..4ea03630 100644 --- a/app/imports/api/engine/actions/doCastSpell.js +++ b/app/imports/api/engine/actions/doCastSpell.js @@ -117,7 +117,8 @@ const doAction = new ValidatedMethod({ } } - actionContext.scope['slotLevel'] = slotLevel; + actionContext.scope['slotLevel'] = { value: slotLevel }; + actionContext.scope['~slotLevel'] = { value: slotLevel }; // Do the action doActionWork({ diff --git a/app/imports/api/engine/actions/doCheck.js b/app/imports/api/engine/actions/doCheck.js index 5d9b4164..452c32eb 100644 --- a/app/imports/api/engine/actions/doCheck.js +++ b/app/imports/api/engine/actions/doCheck.js @@ -81,7 +81,7 @@ function rollCheck(prop, actionContext) { rollModifier += effectBonus; let value, values, resultPrefix; - if (scope['$checkAdvantage'] === 1) { + if (scope['~checkAdvantage']?.value === 1) { logName += ' (Advantage)'; const [a, b] = rollDice(2, 20); if (a >= b) { @@ -91,7 +91,7 @@ function rollCheck(prop, actionContext) { value = b; resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `; } - } else if (scope['$checkAdvantage'] === -1) { + } else if (scope['~checkAdvantage']?.value === -1) { logName += ' (Disadvantage)'; const [a, b] = rollDice(2, 20); if (a <= b) { @@ -107,9 +107,9 @@ function rollCheck(prop, actionContext) { resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = ` } const result = (value + rollModifier) || 0; - scope['$checkDiceRoll'] = value; - scope['$checkRoll'] = result; - scope['$checkModifier'] = rollModifier; + scope['~checkDiceRoll'] = { value }; + scope['~checkRoll'] = { value: result }; + scope['~checkModifier'] = { value: rollModifier }; actionContext.addLog({ name: logName, value: `${resultPrefix} **${result}**`, diff --git a/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js index e3adabc8..e9e4a287 100644 --- a/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js @@ -2,7 +2,7 @@ * Only computes `totalFilled`, need to compute `quantityExpected.value` * before `spacesLeft` can be computed */ -export default function computeSlotQuantityFilled(node, dependencyGraph){ +export default function computeSlotQuantityFilled(node, dependencyGraph) { let slot = node.node; if (slot.type !== 'propertySlot') return; slot.totalFilled = 0; @@ -10,9 +10,8 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){ let childProp = child.node; dependencyGraph.addLink(slot._id, childProp._id, 'slotFill'); if ( - childProp.type === 'slotFiller' && Number.isFinite(childProp.slotQuantityFilled) - ){ + ) { slot.totalFilled += childProp.slotQuantityFilled; } else { slot.totalFilled++; diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js index b7a98595..3d9bf564 100644 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js @@ -1,16 +1,31 @@ import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; +import { getEffectTagTargets } from '/imports/api/engine/computation/buildComputation/linkTypeDependencies.js'; -export default function computeToggleDependencies(node, dependencyGraph){ +export default function computeToggleDependencies(node, dependencyGraph, computation, forest) { const prop = node.node; - // Only for toggles that aren't inactive and aren't set to enabled or disabled - if ( - prop.inactive || - prop.type !== 'toggle' || - prop.disabled || - prop.enabled - ) return; + // Only for toggles + if (prop.type !== 'toggle') return; + + if (prop.targetByTags) { + // Find all the props targeted by tags, and disable them and their children + getEffectTagTargets(prop, computation).forEach(targetId => { + const target = forest.nodeIndex[targetId]; + if (!target) return; + target.node._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(target.node._id, prop._id, 'toggle'); + walkDown(target.children, child => { + // The child nodes depend on the toggle + child.node._computationDetails.toggleAncestors.push(prop); + dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); + }); + }); + } + + // We don't need to link direct children of static toggles, it's already done + if (prop.disabled || prop.enabled) return; + walkDown(node.children, child => { - // The child nodes depend on the toggle condition compuation + // The child nodes depend on the toggle child.node._computationDetails.toggleAncestors.push(prop); dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 7ed69d19..4a83ab62 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -29,9 +29,9 @@ export default function linkTypeDependencies(dependencyGraph, prop, computation) function dependOnCalc({ dependencyGraph, prop, key }) { let calc = get(prop, key); - if (!calc) return; + if (!calc?.type) return; if (calc.type !== '_calculation') { - throw `Expected calculation got ${calc.type}` + throw `Failed to dependOnCal for prop: ${prop._id}, key: ${key}. Expected calculation got ${calc.type}` } dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation'); } @@ -164,7 +164,7 @@ function linkEffects(dependencyGraph, prop, computation) { } // Returns an array of IDs of the properties the effect targets -function getEffectTagTargets(effect, computation) { +export function getEffectTagTargets(effect, computation) { let targets = getTargetListFromTags(effect.targetTags, computation); let notIds = []; if (effect.extraTags) { @@ -218,7 +218,6 @@ function getDefaultCalculationField(prop) { case 'roll': return 'roll'; case 'savingThrow': return 'dc'; case 'skill': return 'baseValue'; - case 'slotFiller': return null; case 'slot': return 'quantityExpected'; case 'spellList': return 'attackRollBonus'; case 'spell': return null; @@ -268,20 +267,45 @@ function linkPointBuy(dependencyGraph, prop) { if (prop.inactive) return; } -function linkProficiencies(dependencyGraph, prop) { +function linkProficiencies(dependencyGraph, prop, computation) { // The stats depend on the proficiency if (prop.inactive) return; - prop.stats.forEach(statName => { - if (!statName) return; - dependencyGraph.addLink(statName, prop._id, prop.type); - }); + if (prop.targetByTags) { + // Tag targeted proficiencies depend on the creature's proficiencyBonus, + // since they add it directly to the targeted field + dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); + getEffectTagTargets(prop, computation).forEach(targetId => { + const targetProp = computation.propsById[targetId]; + 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 proficiency on its variable name + dependencyGraph.addLink(targetProp.variableName, prop._id, 'proficiency'); + } 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, 'proficiency'); + } + } + }); + } else { + prop.stats.forEach(statName => { + if (!statName) return; + dependencyGraph.addLink(statName, prop._id, 'proficiency'); + }); + } } function linkSavingThrow(dependencyGraph, prop) { dependOnCalc({ dependencyGraph, prop, key: 'dc' }); } -function linkSkill(dependencyGraph, prop) { +function linkSkill(dependencyGraph, prop, computation) { // Depends on base value dependOnCalc({ dependencyGraph, prop, key: 'baseValue' }); // Link dependents @@ -293,6 +317,20 @@ function linkSkill(dependencyGraph, prop) { } // Skills depend on the creature's proficiencyBonus dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); + + // Skills can apply their value as a proficiency bonus to calculations based on tag + if (prop.targetByTags) { + getEffectTagTargets(prop, computation).forEach(targetId => { + const targetProp = computation.propsById[targetId]; + // Always target a field on the target property, applying a skill to an attribute or + // other skill isn't supported + const key = prop.targetField || getDefaultCalculationField(targetProp); + const calcObj = get(targetProp, key); + if (calcObj && calcObj.calculation) { + dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); + } + }); + } } function linkSlot(dependencyGraph, prop) { diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 3de657fe..73217f80 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -5,14 +5,14 @@ import { get, unset } from 'lodash'; import errorNode from '/imports/parser/parseTree/error.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; -export default function parseCalculationFields(prop, schemas){ +export default function parseCalculationFields(prop, schemas) { discoverInlineCalculationFields(prop, schemas); parseAllCalculationFields(prop, schemas); } -function discoverInlineCalculationFields(prop, schemas){ +function discoverInlineCalculationFields(prop, schemas) { // For each key in the schema - schemas[prop.type].inlineCalculationFields().forEach( calcKey => { + schemas[prop.type]?.inlineCalculationFields?.()?.forEach(calcKey => { // That ends in .inlineCalculations applyFnToKey(prop, calcKey, (prop, key) => { const inlineCalcObj = get(prop, key); @@ -22,7 +22,7 @@ function discoverInlineCalculationFields(prop, schemas){ // Extract the calculations and store them on the property let string = inlineCalcObj.text; // If there is no text, delete the whole field - if (!string){ + if (!string) { unset(prop, calcKey); return; } @@ -32,7 +32,7 @@ function discoverInlineCalculationFields(prop, schemas){ // Has the text, if it matches the existing hash, stop const inlineCalcHash = cyrb53(inlineCalcObj.text); - if (inlineCalcHash === inlineCalcObj.hash){ + if (inlineCalcHash === inlineCalcObj.hash) { return; } inlineCalcObj.hash = inlineCalcHash; @@ -41,7 +41,7 @@ function discoverInlineCalculationFields(prop, schemas){ // It will be re set including the embedded calculation at the end of // the computation let matches = string.matchAll(INLINE_CALCULATION_REGEX); - for (let match of matches){ + for (let match of matches) { let calculation = match[1]; inlineCalcObj.inlineCalculations.push({ calculation, @@ -51,9 +51,9 @@ function discoverInlineCalculationFields(prop, schemas){ }); } -function parseAllCalculationFields(prop, schemas){ +function parseAllCalculationFields(prop, schemas) { // For each computed key in the schema - schemas[prop.type].computedFields().forEach( calcKey => { + schemas[prop.type]?.computedFields?.()?.forEach(calcKey => { // Determine the level the calculation should compute down to let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce'; @@ -66,7 +66,7 @@ function parseAllCalculationFields(prop, schemas){ const calcObj = get(prop, key); if (!calcObj) return; // Delete the whole calculation object if the calculation string isn't set - if (!calcObj.calculation){ + if (!calcObj.calculation) { unset(prop, calcKey); return; } @@ -84,10 +84,10 @@ function parseAllCalculationFields(prop, schemas){ }); } -function parseCalculation(calcObj){ +function parseCalculation(calcObj) { const calcHash = cyrb53(calcObj.calculation); // If the cached parse calculation is equal to the calculation, skip - if (calcHash === calcObj.hash){ + if (calcHash === calcObj.hash) { return; } calcObj.hash = calcHash; @@ -100,6 +100,6 @@ function parseCalculation(calcObj){ message: prettifyParseError(e), }; calcObj.parseError = error; - calcObj.parseNode = errorNode.create({error}); + calcObj.parseNode = errorNode.create({ error }); } } diff --git a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js index 9c783df0..d87fe3b3 100644 --- a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js +++ b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js @@ -1,9 +1,9 @@ import applyFnToKey from '../utility/applyFnToKey.js'; import { unset } from 'lodash'; -export default function removeSchemaFields(schemas, prop){ +export default function removeSchemaFields(schemas, prop) { schemas.forEach(schema => { - schema.removeBeforeComputeFields().forEach( + schema?.removeBeforeComputeFields?.().forEach( key => applyFnToKey(prop, key, unset) ); }); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js index 8a8af817..b2c80f9a 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js @@ -2,7 +2,7 @@ import { buildComputationFromProps } from '/imports/api/engine/computation/build import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; -export default function(){ +export default function () { const computation = buildComputationFromProps(testProperties); const totalFilled = computation.propsById['slotId'].totalFilled; assert.equal(totalFilled, 4); @@ -13,24 +13,24 @@ var testProperties = [ clean({ _id: 'slotId', type: 'propertySlot', - ancestors: [{id: 'charId'}], + ancestors: [{ id: 'charId' }], }), // Children clean({ _id: 'slotFillerId', - type: 'slotFiller', + type: 'folder', slotQuantityFilled: 3, slotFillerType: 'item', - ancestors: [{id: 'charId'}, {id: 'slotId'}], + ancestors: [{ id: 'charId' }, { id: 'slotId' }], }), clean({ _id: 'slotChildId', type: 'item', - ancestors: [{id: 'charId'}, {id: 'slotId'}], + ancestors: [{ id: 'charId' }, { id: 'slotId' }], }), clean({ _id: 'slotGrandchildId', type: 'effect', - ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}], + ancestors: [{ id: 'charId' }, { id: 'slotId' }, { id: 'slotChildId' }], }), ]; diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index a736354c..a97f5b0a 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -29,7 +29,7 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; * computed toggles */ -export default function buildCreatureComputation(creatureId){ +export default function buildCreatureComputation(creatureId) { const creature = getCreature(creatureId); const variables = getVariables(creatureId); const properties = getProperties(creatureId); @@ -37,7 +37,7 @@ export default function buildCreatureComputation(creatureId){ return computation; } -export function buildComputationFromProps(properties, creature, variables){ +export function buildComputationFromProps(properties, creature, variables) { const computation = new CreatureComputation(properties, creature, variables); // Dependency graph where edge(a, b) means a depends on b @@ -49,14 +49,14 @@ export function buildComputationFromProps(properties, creature, variables){ const dependencyGraph = computation.dependencyGraph; // Link the denormalizedStats from the creature - if (creature && creature.denormalizedStats){ - if (creature.denormalizedStats.xp){ + if (creature && creature.denormalizedStats) { + if (creature.denormalizedStats.xp) { dependencyGraph.addNode('xp', { baseValue: creature.denormalizedStats.xp, type: '_variable' }); } - if (creature.denormalizedStats.milestoneLevels){ + if (creature.denormalizedStats.milestoneLevels) { dependencyGraph.addNode('milestoneLevels', { baseValue: creature.denormalizedStats.milestoneLevels, type: '_variable' @@ -93,7 +93,7 @@ export function buildComputationFromProps(properties, creature, variables){ // Inactive status must be complete for the whole tree before toggle deps // are calculated walkDown(forest, node => { - computeToggleDependencies(node, dependencyGraph); + computeToggleDependencies(node, dependencyGraph, computation, forest); computeSlotQuantityFilled(node, dependencyGraph); }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index 0b5633e3..0f3d5436 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -6,6 +6,7 @@ import pointBuy from './computeByType/computePointBuy.js'; import propertySlot from './computeByType/computeSlot.js'; import container from './computeByType/computeContainer.js'; import spellList from './computeByType/computeSpellList.js'; +import toggle from './computeByType/computeToggle.js'; import _calculation from './computeByType/computeCalculation.js'; export default Object.freeze({ @@ -19,4 +20,5 @@ export default Object.freeze({ propertySlot, spell: action, spellList, + toggle, }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index 054fc7f9..f695b25d 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,12 +1,17 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js'; +import { getPropertyName } from '/imports/constants/PROPERTIES.js'; -export default function computeCalculation(computation, node){ +export default function computeCalculation(computation, node) { const calcObj = node.data; evaluateCalculation(calcObj, computation.scope); + if (calcObj.effects || calcObj.proficiencies) { + calcObj.baseValue = calcObj.value; + } aggregateCalculationEffects(node, computation); + aggregateCalculationProficiencies(node, computation); } -export function aggregateCalculationEffects(node, computation){ +function aggregateCalculationEffects(node, computation) { const calcObj = node.data; delete calcObj.effects; computation.dependencyGraph.forEachLinkedNode( @@ -34,15 +39,77 @@ export function aggregateCalculationEffects(node, computation){ }, true // enumerate only outbound links ); - if (calcObj.effects && typeof calcObj.value === 'number'){ - calcObj.baseValue = calcObj.value; + if (calcObj.effects && typeof calcObj.value === 'number') { calcObj.effects.forEach(effect => { if ( effect.operation === 'add' && effect.amount && typeof effect.amount.value === 'number' - ){ + ) { calcObj.value += effect.amount.value } }); } } + +function aggregateCalculationProficiencies(node, computation) { + const calcObj = node.data; + delete calcObj.proficiencies; + delete calcObj.proficiency; + let profBonus = computation.scope['proficiencyBonus']?.value || 0; + + // Go through all the links and collect them on the calculation + computation.dependencyGraph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + // Only proficiency links + if (link.data !== 'proficiency') return; + // That have data + if (!linkedNode.data) return; + // Ignoring inactive props + if (linkedNode.data.inactive) return; + // Compute the proficiency and value + let proficiency, value; + if (linkedNode.data.type === 'proficiency') { + proficiency = linkedNode.data.value || 0; + // Multiply the proficiency bonus by the actual proficiency + if (proficiency === 0.49) { + // Round down proficiency bonus in the special case + value = Math.floor(profBonus * 0.5); + } else { + value = Math.ceil(profBonus * proficiency); + } + } else if (linkedNode.data.type === 'skill') { + value = linkedNode.data.value || 0; + proficiency = linkedNode.data.proficiency || 0; + } + // Collate proficiencies + calcObj.proficiencies = calcObj.proficiencies || []; + calcObj.proficiencies.push({ + _id: linkedNode.data._id, + name: linkedNode.data.name, + type: linkedNode.data.type, + proficiency, + value, + }); + }, + true // enumerate only outbound links + ); + + // Apply the highest proficiency, marking all others as overridden + if (calcObj.proficiencies && typeof calcObj.value === 'number') { + calcObj.proficiency = 0; + calcObj.proficiencyBonus = 0; + let currentProf; + calcObj.proficiencies.forEach(prof => { + if (prof.value > calcObj.proficiencyBonus) { + if (currentProf) currentProf.overridden = true; + calcObj.proficiencyBonus = prof.value; + calcObj.proficiency = prof.proficiency; + currentProf = prof; + } else { + prof.overridden = true; + } + }); + calcObj.value += calcObj.proficiencyBonus; + } +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js index 9e9dc07b..8128719a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js @@ -1,6 +1,6 @@ -export default function computSlot(computation, node){ +export default function computeSlot(computation, node) { const prop = node.data; - if (prop.quantityExpected && prop.quantityExpected.value){ + if (prop.quantityExpected && prop.quantityExpected.value) { prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled; } } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js new file mode 100644 index 00000000..bba5c4ff --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeToggle.js @@ -0,0 +1,7 @@ +export default function computeToggle(computation, node) { + const prop = node.data; + if (!prop.enabled && !prop.disabled && prop.condition && !prop.condition.value) { + prop.inactive = true; + prop.deactivatedBySelf = true; + } +} diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 6ec5df2d..395619a9 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -1,16 +1,40 @@ import getAggregatorResult from './getAggregatorResult.js'; -export default function computeVariableAsAttribute(computation, node, prop){ +export default function computeVariableAsAttribute(computation, node, prop) { let result = getAggregatorResult(node) || 0; prop.total = result; + + // Apply damage in a way that respects the damage rules, modifying damage if need be + // Bound the damage + if (!prop.ignoreLowerLimit && prop.damage > prop.total) { + console.log(`reducing damage from ${prop.damage} to ${prop.total}`); + prop.damage = prop.total; + } + if (!prop.ignoreUpperLimit && prop.damage < 0) { + console.log(`increasing damage from ${prop.damage} to 0`); + prop.damage = 0; + } + // Apply damage prop.value = prop.total - (prop.damage || 0); // Proficiency prop.proficiency = node.data.proficiency; + // Advantage/disadvantage + const aggregator = node.data.effectAggregator; + if (aggregator) { + if (aggregator.advantage && !aggregator.disadvantage) { + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage) { + prop.advantage = -1; + } else { + prop.advantage = 0; + } + } + // Ability scores get modifiers - if (prop.attributeType === 'ability'){ + if (prop.attributeType === 'ability') { prop.modifier = Math.floor((prop.value - 10) / 2); } 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 01d34101..336a5ab7 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -1,12 +1,12 @@ import aggregate from './aggregate/index.js'; -export default function computeVariableAsSkill(computation, node, prop){ +export default function computeVariableAsSkill(computation, node, prop) { // Skills are based on some ability Modifier let ability = computation.scope[prop.ability]; prop.abilityMod = ability?.modifier || 0; // Inherit the ability's skill effects and proficiencies if skill is not a save - if (prop.skillType !== 'save' && ability){ + if (prop.skillType !== 'save' && ability) { aggregateAbilityEffects({ computation, skillNode: node, @@ -21,7 +21,7 @@ export default function computeVariableAsSkill(computation, node, prop){ let profBonus = computation.scope['proficiencyBonus']?.value || 0; // Multiply the proficiency bonus by the actual proficiency - if(prop.proficiency === 0.49){ + if (prop.proficiency === 0.49) { // Round down proficiency bonus in the special case profBonus = Math.floor(profBonus * 0.5); } else { @@ -37,7 +37,7 @@ export default function computeVariableAsSkill(computation, node, prop){ prop.effects = node.data.effects; // If there is no aggregator, determine if the prop can hide, then exit - if (!aggregator){ + if (!aggregator) { prop.hide = statBase === undefined && prop.proficiency == 0 || undefined; @@ -52,20 +52,32 @@ export default function computeVariableAsSkill(computation, node, prop){ if (aggregator.set !== undefined) { result = aggregator.set; } - if (Number.isFinite(result)){ + if (Number.isFinite(result)) { result = Math.floor(result); } prop.value = result; // Advantage/disadvantage - if (aggregator.advantage && !aggregator.disadvantage){ + if (aggregator.advantage && !aggregator.disadvantage) { prop.advantage = 1; - } else if (aggregator.disadvantage && !aggregator.advantage){ + } else if (aggregator.disadvantage && !aggregator.advantage) { prop.advantage = -1; } else { prop.advantage = 0; } // Passive bonus prop.passiveBonus = aggregator.passiveAdd; + // +/- 5 to passive bonus if the skill has advantage/disadvantage + if ( + prop.advantage === 1 + && Number.isFinite(prop.passiveBonus) + ) { + prop.passiveBonus += 5; + } else if ( + prop.advantage === -1 + && Number.isFinite(prop.passiveBonus) + ) { + prop.bassiveBonus -= 5; + } // conditional benefits prop.conditionalBenefits = aggregator.conditional; // Roll bonuses @@ -76,7 +88,8 @@ export default function computeVariableAsSkill(computation, node, prop){ prop.rollBonuses = aggregator.rollBonus; } -function aggregateAbilityEffects({computation, skillNode, abilityNode}){ +function aggregateAbilityEffects({ computation, skillNode, abilityNode }) { + if (!abilityNode?.id) return; computation.dependencyGraph.forEachLinkedNode( abilityNode.id, (linkedNode, link) => { @@ -85,15 +98,15 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){ if (linkedNode.data.inactive) return; // Check that the link is a valid effect/proficiency to pass on // to a skill from its ability - if (link.data === 'effect'){ + if (link.data === 'effect') { if (![ 'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional' - ].includes(linkedNode.data.operation)){ + ].includes(linkedNode.data.operation)) { return; } } // Apply the aggregations - let arg = {node: skillNode, linkedNode, link}; + let arg = { node: skillNode, linkedNode, link }; aggregate.effect(arg); aggregate.proficiency(arg); }, diff --git a/app/imports/api/engine/computation/computeComputation/computeToggles.js b/app/imports/api/engine/computation/computeComputation/computeToggles.js index caa310d2..657305d6 100644 --- a/app/imports/api/engine/computation/computeComputation/computeToggles.js +++ b/app/imports/api/engine/computation/computeComputation/computeToggles.js @@ -1,13 +1,16 @@ -export default function evaluateToggles(computation, node){ +export default function evaluateToggles(computation, node) { let prop = node.data; if (!prop) return; let toggles = prop._computationDetails?.toggleAncestors; if (!toggles) return; toggles.forEach(toggle => { - if (!toggle.condition) return; - if (!toggle.condition.value){ + if ( + (!toggle.enabled && !toggle.disabled && toggle.condition && !toggle.condition.value) + || (toggle.disabled) + ) { prop.inactive = true; prop.deactivatedByToggle = true; + prop.deactivatingToggleId = toggle._id; } }); } diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js new file mode 100644 index 00000000..b61395da --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/tests/computeProficiencies.testFn.js @@ -0,0 +1,64 @@ +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; +import { assert } from 'chai'; +import computeCreatureComputation from '../../computeCreatureComputation.js'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function () { + const computation = buildComputationFromProps(testProperties); + computeCreatureComputation(computation); + const prop = id => computation.propsById[id]; + assert.equal( + prop('strengthId').value, 8, + 'The proficiency bonus should not change the strength score' + ); + assert.equal( + prop('strengthId').modifier, -1, + 'The proficiency bonus should not change the strength modifier' + ); + assert.exists(prop('actionId').attackRoll.proficiencies, 'The proficiency aggregator should be here') + assert.exists(prop('actionId').attackRoll.proficiencies[0], 'The proficiency should be here') + // attack roll = strength.mod + proficiencyBonus/2 rounded down + // = -1 + 13/2 = -1 + 6 = 5 + assert.equal( + prop('actionId').attackRoll.value, 5, + 'The proficiency should apply correctly to modify the attack roll' + ); +} + +var testProperties = [ + clean({ + _id: 'strengthId', + variableName: 'strength', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '8' + }, + }), + clean({ + _id: 'actionId', + type: 'action', + ancestors: [{ id: 'charId' }], + attackRoll: { + calculation: 'strength.modifier', + }, + tags: ['rapier', 'martial weapon', 'weapon', 'attack'] + }), + clean({ + _id: 'profBonusId', + type: 'attribute', + variableName: 'proficiencyBonus', + ancestors: [{ id: 'charId' }], + baseValue: { + calculation: '13' + }, + }), + clean({ + _id: 'tagTargetedProficiency', + type: 'proficiency', + stats: ['strength'], // Should be ignored, we are targeting by tags + value: 0.49, + targetByTags: true, + targetTags: ['martial weapon'] + }), +]; diff --git a/app/imports/api/engine/computation/computeComputation/tests/index.js b/app/imports/api/engine/computation/computeComputation/tests/index.js index b9c8baf7..768fee2d 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/index.js +++ b/app/imports/api/engine/computation/computeComputation/tests/index.js @@ -6,29 +6,33 @@ import computeInventory from './computeInventory.testFn.js'; import computeDamageMultipliers from './computeDamageMultipliers.testFn.js'; import computeEffects from './computeEffects.testFn.js'; import computeSkills from './computeSkills.testFn.js'; +import computeProficiencies from './computeProficiencies.testFn.js'; export default [{ text: 'Computes actions', fn: computeAction, -},{ +}, { text: 'Computes attributes', fn: computeAttribute, -},{ +}, { text: 'Computes classes', fn: computeClasses, -},{ +}, { text: 'Computes constants', fn: computeConstants, -},{ +}, { text: 'Computes inventory', fn: computeInventory, -},{ +}, { text: 'Computes damage multipliers', fn: computeDamageMultipliers, -},{ +}, { text: 'Computes effects', fn: computeEffects, -},{ +}, { text: 'Computes skills', fn: computeSkills, +}, { + text: 'Computes proficiencies', + fn: computeProficiencies, }]; diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index fcad024c..0548fedd 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -3,12 +3,12 @@ import { EJSON } from 'meteor/ejson'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; -export default function writeAlteredProperties(computation){ +export default function writeAlteredProperties(computation) { let bulkWriteOperations = []; // Loop through all properties on the memo computation.props.forEach(changed => { let schema = propertySchemasIndex[changed.type]; - if (!schema){ + if (!schema) { console.warn('No schema for ' + changed.type); return; } @@ -20,12 +20,13 @@ export default function writeAlteredProperties(computation){ 'deactivatedBySelf', 'deactivatedByAncestor', 'deactivatedByToggle', + 'deactivatingToggleId', 'damage', 'dirty', ...schema.objectKeys(), ]; op = addChangedKeysToOp(op, keys, original, changed); - if (op){ + if (op) { bulkWriteOperations.push(op); } }); @@ -37,10 +38,10 @@ 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) { - if (!EJSON.equals(original[key], changed[key])){ + if (!EJSON.equals(original[key], changed[key])) { if (!op) op = newOperation(original._id, changed.type); let value = changed[key]; - if (value === undefined){ + if (value === undefined) { // Unset values that become undefined addUnsetOp(op, key); } else { @@ -52,32 +53,32 @@ function addChangedKeysToOp(op, keys, original, changed) { return op; } -function newOperation(_id, type){ +function newOperation(_id, type) { let newOp = { updateOne: { - filter: {_id}, + filter: { _id }, update: {}, } }; - if (Meteor.isClient){ + if (Meteor.isClient) { newOp.type = type; } return newOp; } -function addSetOp(op, key, value){ - if (op.updateOne.update.$set){ +function addSetOp(op, key, value) { + if (op.updateOne.update.$set) { op.updateOne.update.$set[key] = value; } else { - op.updateOne.update.$set = {[key]: value}; + op.updateOne.update.$set = { [key]: value }; } } -function addUnsetOp(op, key){ - if (op.updateOne.update.$unset){ +function addUnsetOp(op, key) { + if (op.updateOne.update.$unset) { op.updateOne.update.$unset[key] = 1; } else { - op.updateOne.update.$unset = {[key]: 1}; + op.updateOne.update.$unset = { [key]: 1 }; } } @@ -100,14 +101,14 @@ function writePropertiesSequentially(bulkWriteOps) { // in the UI because of incompatibility with latency compensation. If the // duplicate redraws can be fixed, this is a strictly better way of processing // writes -function bulkWriteProperties(bulkWriteOps){ +function bulkWriteProperties(bulkWriteOps) { if (!bulkWriteOps.length) return; // bulkWrite is only available on the server if (Meteor.isServer) { CreatureProperties.rawCollection().bulkWrite( bulkWriteOps, - {ordered : false}, - function(e){ + { ordered: false }, + function (e) { if (e) { console.error('Bulk write failed: '); console.error(e); diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index d96c2bfe..a25e9ba0 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -15,18 +15,21 @@ export default function writeScope(creatureId, computation) { let $set, $unset; - for (const key in scope){ + for (const key in scope) { + // Mongo can't handle keys that start with a dollar sign + if (key[0] === '$' || key[0] === '_') continue; + // Remove large properties that aren't likely to be accessed delete scope[key].parent; delete scope[key].ancestors; - + // Remove empty keys for (const subKey in scope[key]) { if (scope[key][subKey] === undefined) { delete scope[key][subKey] } } - + // Only update changed fields if (!EJSON.equals(variables[key], scope[key])) { if (!$set) $set = {}; @@ -53,9 +56,19 @@ export default function writeScope(creatureId, computation) { const update = {}; if ($set) update.$set = $set; if ($unset) update.$unset = $unset; - CreatureVariables.update({_creatureId: creatureId}, update); + CreatureVariables.update({ _creatureId: creatureId }, update); } if (computation.creature?.dirty) { - Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }}); + Creatures.update({ _id: creatureId }, { $unset: { dirty: 1 } }); } } +/* +function calculateSize(computation) { + const sizeEstimator = { + creature: computation.creature, + variables: computation.variables, + props: computation.originalPropsById, + }; + return MongoInternals.NpmModule.BSON.calculateObjectSize(sizeEstimator, { checkKeys: false }) +} +*/ diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index d0a3d469..b6dba147 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -4,7 +4,7 @@ import writeAlteredProperties from './computation/writeComputation/writeAlteredP import writeScope from './computation/writeComputation/writeScope.js'; import writeErrors from './computation/writeComputation/writeErrors.js'; -export default function computeCreature(creatureId){ +export default function computeCreature(creatureId) { if (Meteor.isClient) return; // console.log('compute ' + creatureId); const computation = buildCreatureComputation(creatureId); @@ -16,7 +16,7 @@ function computeComputation(computation, creatureId) { computeCreatureComputation(computation); writeAlteredProperties(computation); writeScope(creatureId, computation); - } catch (e){ + } catch (e) { const errorText = e.reason || e.message || e.toString(); computation.errors.push({ type: 'crash', @@ -32,6 +32,19 @@ function computeComputation(computation, creatureId) { console.error(logError); throw e; } finally { + checkPropertyCount(computation) writeErrors(creatureId, computation.errors); } } + +const MAX_PROPS = 1000; +function checkPropertyCount(computation) { + const count = computation.props.length; + if (count <= MAX_PROPS) return; + computation.errors.push({ + type: 'warning', + details: { + error: `This character sheet has too many properties and may perform poorly ( ${count} / ${MAX_PROPS} )` + }, + }); +} diff --git a/app/imports/api/files/UserImages.js b/app/imports/api/files/UserImages.js index 9638d2f0..057c48a0 100644 --- a/app/imports/api/files/UserImages.js +++ b/app/imports/api/files/UserImages.js @@ -1,4 +1,9 @@ -import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js'; +let createS3FilesCollection; +if (Meteor.isServer) { + createS3FilesCollection = require('/imports/api/files/server/s3FileStorage.js').createS3FilesCollection +} else { + createS3FilesCollection = require('/imports/api/files/client/s3FileStorage.js').createS3FilesCollection +} const UserImages = createS3FilesCollection({ collectionName: 'userImages', diff --git a/app/imports/api/files/client/s3FileStorage.js b/app/imports/api/files/client/s3FileStorage.js new file mode 100644 index 00000000..ce2777cc --- /dev/null +++ b/app/imports/api/files/client/s3FileStorage.js @@ -0,0 +1,24 @@ +// https://github.com/VeliovGroup/Meteor-Files/blob/master/docs/aws-s3-integration.md +import { FilesCollection } from 'meteor/ostrio:files'; + +const createS3FilesCollection = function ({ + collectionName, + storagePath, + onBeforeUpload, + onAfterUpload, + debug,// = !Meteor.isProduction, + allowClientCode = false, +}) { + const collection = new FilesCollection({ + collectionName, + storagePath, + onBeforeUpload, + onAfterUpload, + debug, + allowClientCode, + }); + + return collection; +} + +export { createS3FilesCollection }; diff --git a/app/imports/api/files/server/s3.js b/app/imports/api/files/server/s3.js deleted file mode 100644 index 83719fa6..00000000 --- a/app/imports/api/files/server/s3.js +++ /dev/null @@ -1,2 +0,0 @@ -import S3 from 'aws-sdk/clients/s3'; -export default S3; diff --git a/app/imports/api/files/s3FileStorage.js b/app/imports/api/files/server/s3FileStorage.js similarity index 95% rename from app/imports/api/files/s3FileStorage.js rename to app/imports/api/files/server/s3FileStorage.js index 1ef22730..27fbed67 100644 --- a/app/imports/api/files/s3FileStorage.js +++ b/app/imports/api/files/server/s3FileStorage.js @@ -4,9 +4,7 @@ import { each, clone } from 'lodash'; import { Random } from 'meteor/random'; import { FilesCollection } from 'meteor/ostrio:files'; import stream from 'stream'; -if (Meteor.isServer) { - import S3 from '/imports/api/files/server/s3.js'; -} +import S3 from 'aws-sdk/clients/s3'; /* See fs-extra and graceful-fs NPM packages */ /* For better i/o performance */ @@ -31,7 +29,7 @@ let createS3FilesCollection; /* Check settings existence in `Meteor.settings` */ /* This is the best practice for app security */ -if (Meteor.isServer && Meteor.settings.useS3) { +if (Meteor.settings.useS3) { // Create a new S3 object const s3 = new S3({ accessKeyId: s3Conf.key, @@ -236,13 +234,11 @@ if (Meteor.isServer && Meteor.settings.useS3) { allowClientCode, }); - if (Meteor.isServer) { - // Use the normal file system to read files - collection.readJSONFile = async function (file) { - const fileString = await fsp.readFile(file.path, 'utf8'); - return JSON.parse(fileString); - }; - } + // Use the normal file system to read files + collection.readJSONFile = async function (file) { + const fileString = await fsp.readFile(file.path, 'utf8'); + return JSON.parse(fileString); + }; return collection; } diff --git a/app/imports/api/library/Libraries.js b/app/imports/api/library/Libraries.js index 696f0001..1cbb723e 100644 --- a/app/imports/api/library/Libraries.js +++ b/app/imports/api/library/Libraries.js @@ -29,6 +29,16 @@ let LibrarySchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.summary, }, + showInMarket: { + index: 1, + type: Boolean, + optional: true, + }, + subscriberCount: { + index: 1, + type: Number, + optional: true, + }, }); LibrarySchema.extend(SharingSchema); @@ -104,6 +114,29 @@ const updateLibraryDescription = new ValidatedMethod({ }, }); +const updateLibraryShowInMarket = new ValidatedMethod({ + name: 'libraries.updateShowInMarket', + validate: new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.id + }, + value: { + type: Boolean, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({ _id, value }) { + let library = Libraries.findOne(_id); + assertEditPermission(library, this.userId); + Libraries.update(_id, { $set: { showInMarket: value } }); + }, +}); + const removeLibrary = new ValidatedMethod({ name: 'libraries.remove', validate: new SimpleSchema({ @@ -130,4 +163,4 @@ export function removeLibaryWork(libraryId) { LibraryNodes.remove({ 'ancestors.id': libraryId }); } -export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary }; +export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, updateLibraryShowInMarket, removeLibrary }; diff --git a/app/imports/api/library/LibraryCollections.js b/app/imports/api/library/LibraryCollections.js index aec4d351..aff9fe20 100644 --- a/app/imports/api/library/LibraryCollections.js +++ b/app/imports/api/library/LibraryCollections.js @@ -32,6 +32,16 @@ const LibraryCollectionSchema = new SimpleSchema({ type: String, regEx: SimpleSchema.RegEx.Id, }, + showInMarket: { + index: 1, + type: Boolean, + optional: true, + }, + subscriberCount: { + index: 1, + type: Number, + optional: true, + }, }); LibraryCollectionSchema.extend(SharingSchema); @@ -48,12 +58,12 @@ const insertLibraryCollection = new ValidatedMethod({ run(libraryCollection) { if (!this.userId) { throw new Meteor.Error('LibraryCollections.methods.insert.denied', - 'You need to be logged in to insert a library'); + 'You need to be logged in to insert a library'); } let tier = getUserTier(this.userId); - if (!tier.paidBenefits){ + if (!tier.paidBenefits) { throw new Meteor.Error('LibraryCollections.methods.insert.denied', - `The ${tier.name} tier does not allow you to insert a library collection`); + `The ${tier.name} tier does not allow you to insert a library collection`); } libraryCollection.owner = this.userId; return LibraryCollections.insert(libraryCollection); @@ -72,7 +82,7 @@ const updateLibraryCollection = new ValidatedMethod({ }, update: { type: LibraryCollectionSchema - .pick('name', 'description', 'libraries') + .pick('name', 'description', 'libraries', 'showInMarket') .extend({ //make libraries optional libraries: { optional: true, @@ -85,7 +95,7 @@ const updateLibraryCollection = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id, update}){ + run({ _id, update }) { const libraryCollection = LibraryCollections.findOne(_id, { fields: { owner: 1, @@ -93,7 +103,7 @@ const updateLibraryCollection = new ValidatedMethod({ } }); assertEditPermission(libraryCollection, this.userId); - return LibraryCollections.update(_id, {$set: update}); + return LibraryCollections.update(_id, { $set: update }); }, }); @@ -110,7 +120,7 @@ const removeLibraryCollection = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id}){ + run({ _id }) { const libraryCollection = LibraryCollections.findOne(_id, { fields: { owner: 1, diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index 50d97cd3..39264fb6 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -4,7 +4,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import SimpleSchema from 'simpl-schema'; import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; -import ChildSchema from '/imports/api/parenting/ChildSchema.js'; +import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js'; import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js'; import Libraries from '/imports/api/library/Libraries.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; @@ -15,6 +15,8 @@ import '/imports/api/library/methods/index.js'; import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import { restore } from '/imports/api/parenting/softRemove.js'; +import { getAncestry } from '/imports/api/parenting/parenting.js'; +import { reorderDocs } from '/imports/api/parenting/order.js'; let LibraryNodes = new Mongo.Collection('libraryNodes'); @@ -36,20 +38,66 @@ let LibraryNodeSchema = new SimpleSchema({ type: String, max: STORAGE_LIMITS.tagLength, }, + icon: { + type: storedIconsSchema, + optional: true, + max: STORAGE_LIMITS.icon, + }, + + // Library-specific properties, these can be stripped from the resulting + // creature properties + + // Will this property show up in the slot-fill dialog + fillSlots: { + type: Boolean, + optional: true, + index: 1, + }, + // Will this property show up in the insert-from-library dialog + searchable: { + type: Boolean, + optional: true, + index: 1, + }, libraryTags: { type: Array, - defaultValue: [], + optional: true, maxCount: STORAGE_LIMITS.tagCount, }, 'libraryTags.$': { type: String, max: STORAGE_LIMITS.tagLength, }, - icon: { - type: storedIconsSchema, + // Overrides the type when searching for properties + slotFillerType: { + type: String, optional: true, - max: STORAGE_LIMITS.icon, - } + max: STORAGE_LIMITS.variableName, + }, + // Image to display when filling the slot + slotFillImage: { + type: String, + optional: true, + max: STORAGE_LIMITS.url, + }, + // Fill more than one quantity in a slot, like feats and ability score + // improvements, filtered out of UI if there isn't space in quantityExpected + slotQuantityFilled: { + type: SimpleSchema.Integer, + optional: true, // Undefined implies 1 + }, + // Filters out of UI if condition isn't met, but isn't otherwise enforced + slotFillerCondition: { + type: String, + optional: true, + max: STORAGE_LIMITS.calculation, + }, + // Text to display if slot filler condition fails + slotFillerConditionNote: { + type: String, + optional: true, + max: STORAGE_LIMITS.calculation, + }, }); // Set up server side search index @@ -86,20 +134,56 @@ function assertNodeEditPermission(node, userId) { const insertNode = new ValidatedMethod({ name: 'libraryNodes.insert', - validate: null, + validate: new SimpleSchema({ + libraryNode: { + type: Object, + blackbox: true, + }, + parentRef: RefSchema, + }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, - run(libraryNode) { + run({ libraryNode, parentRef }) { + // get the new ancestry + let { parentDoc, ancestors } = getAncestry({ parentRef }); + + // Check permission to edit + let root; + if (parentRef.collection === 'libraries') { + root = parentDoc; + } else if (parentRef.collection === 'libraryNodes') { + root = Libraries.findOne(parentDoc.ancestors[0].id); + } else { + throw `${parentRef.collection} is not a valid parent collection` + } + assertEditPermission(root, this.userId); + + // Set the ancestry of the library node + libraryNode.parent = parentRef; + libraryNode.ancestors = ancestors; + // Remove its ID if it came with one to force a random one to be generated + // server-side delete libraryNode._id; - assertNodeEditPermission(libraryNode, this.userId); - let nodeId = LibraryNodes.insert(libraryNode); + + // Insert the node + const nodeId = LibraryNodes.insert(libraryNode); + + // Update the node if it was a reference node if (libraryNode.type == 'reference') { libraryNode._id = nodeId; updateReferenceNodeWork(libraryNode, this.userId); } + + // Tree structure changed by insert, reorder the tree + reorderDocs({ + collection: LibraryNodes, + ancestorId: root._id, + }); + + // Return the id of the inserted node return nodeId; }, }); @@ -119,7 +203,7 @@ const updateLibraryNode = new ValidatedMethod({ }, mixins: [RateLimiterMixin], rateLimit: { - numRequests: 5, + numRequests: 15, timeInterval: 5000, }, run({ _id, path, value }) { diff --git a/app/imports/api/library/getCreatureLibraryIds.js b/app/imports/api/library/getCreatureLibraryIds.js index beb9c7d3..a951a042 100644 --- a/app/imports/api/library/getCreatureLibraryIds.js +++ b/app/imports/api/library/getCreatureLibraryIds.js @@ -4,6 +4,7 @@ import getUserLibraryIds from './getUserLibraryIds'; import { intersection, union } from 'lodash'; export default function getCreatureLibraryIds(creature, userId) { + if (!userId) console.log('no userId, returning empty array'); if (!userId) return []; // Get the ids of libraries the user is permitted to view @@ -17,14 +18,14 @@ export default function getCreatureLibraryIds(creature, userId) { allowedLibraryCollections: 1, } }); - if (!creature) return []; + if (!creature) return userLibIds; } // 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 || []; diff --git a/app/imports/api/library/methods/getDefaultSlotFiller.js b/app/imports/api/library/methods/getDefaultSlotFiller.js new file mode 100644 index 00000000..aeb5917b --- /dev/null +++ b/app/imports/api/library/methods/getDefaultSlotFiller.js @@ -0,0 +1,18 @@ +export default function getDefaultSlotFiller(slot) { + if (typeof slot !== 'object') throw 'getDefaultSlotFiller requires a slot'; + if (slot.type !== 'propertySlot') throw 'provided slot must be a propertySlot'; + + let slotType = slot.slotType; + if (!slotType || slot.slotType === 'slotFiller') { + slotType = 'folder'; + } + + const filler = { + type: slotType, + libraryTags: slot.slotTags || [], + name: 'Custom ' + slot.name || 'slot filler', + parent: { collection: 'creatureProperties', id: slot._id }, + ancestors: [...slot.ancestors, { collection: 'creatureProperties', id: slot._id }], + }; + return filler; +} diff --git a/app/imports/api/library/methods/updateReferenceNode.js b/app/imports/api/library/methods/updateReferenceNode.js index f933d065..aa2a5407 100644 --- a/app/imports/api/library/methods/updateReferenceNode.js +++ b/app/imports/api/library/methods/updateReferenceNode.js @@ -21,7 +21,7 @@ const updateReferenceNode = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({_id}) { + run({ _id }) { let userId = this.userId; let node = LibraryNodes.findOne(_id); assertDocEditPermission(node, userId); @@ -29,15 +29,15 @@ const updateReferenceNode = new ValidatedMethod({ }, }); -function writeCache(_id, cache){ - LibraryNodes.update(_id, {$set: {cache}}, { - selector: {type: 'reference'}, +function writeCache(_id, cache) { + LibraryNodes.update(_id, { $set: { cache } }, { + selector: { type: 'reference' }, }); } -function updateReferenceNodeWork(node, userId){ +function updateReferenceNodeWork(node, userId) { let cache = {} - if (!node.ref){ + if (!node.ref?.collection || !node.ref?.id) { writeCache(node._id, cache); return; } @@ -45,20 +45,23 @@ function updateReferenceNodeWork(node, userId){ try { doc = fetchDocByRef(node.ref); if (doc.removed) throw 'Property has been deleted'; - if (doc.ancestors[0].id !== node.ancestors[0].id){ + if (doc.ancestors[0].id !== node.ancestors[0].id) { library = fetchDocByRef(doc.ancestors[0]); assertViewPermission(library, userId) } - } catch(e){ - cache = {error: e.reason || e.message || e.toString()} + } catch (e) { + cache = { error: e.reason || e.message || e.toString() } writeCache(node._id, cache); return; } cache = { node: doc, }; - if (library){ - cache.library = {name: library.name}; + if (library) { + cache.library = { + id: library._id, + name: library.name, + }; } writeCache(node._id, cache); } diff --git a/app/imports/api/parenting/nodesToTree.js b/app/imports/api/parenting/nodesToTree.js index 3e4b5733..86fbe1c1 100644 --- a/app/imports/api/parenting/nodesToTree.js +++ b/app/imports/api/parenting/nodesToTree.js @@ -1,4 +1,4 @@ -import { union, difference, sortBy, findLast } from 'lodash'; +import { union, difference, sortBy, findLast, intersection } from 'lodash'; export function nodeArrayToTree(nodes) { // Store a dict and list of all the nodes @@ -26,6 +26,7 @@ export function nodeArrayToTree(nodes) { forest.push(treeNode); } }); + forest.nodeIndex = nodeIndex; return forest; } @@ -82,9 +83,15 @@ export default function nodesToTree({ docs.forEach(doc => { ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id)); }); - // Remove the IDs of docs we have already found + // Get all the docs that are also ancestors and mark them + docs.forEach(doc => { + if (ancestorIds.includes(doc._id)) { + doc._ancestorOfMatchedDocument = true; + } + }); + // Remove the ancestor IDs of docs we have already found ancestorIds = difference(ancestorIds, docIds); - // Get the docs from the collection, don't worry about `removed` docs, + // Get the ancestor docs from the collection, don't worry about `removed` docs, // if their descendant was not removed, neither are they ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => { // Mark that the nodes are ancestors of the found nodes diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js index 88500085..54d5f1ba 100644 --- a/app/imports/api/parenting/organizeMethods.js +++ b/app/imports/api/parenting/organizeMethods.js @@ -23,13 +23,20 @@ const organizeDoc = new ValidatedMethod({ type: Boolean, optional: true, }, + skipClient: { + type: Boolean, + optional: true, + }, }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, timeInterval: 5000, }, - run({docRef, parentRef, order, skipRecompute}) { + run({ docRef, parentRef, order, skipRecompute, skipClient }) { + if (skipClient && this.isSimulation) { + return; + } let doc = fetchDocByRef(docRef); let collection = getCollectionByName(docRef.collection); // The user must be able to edit both the doc and its parent to move it @@ -39,23 +46,23 @@ const organizeDoc = new ValidatedMethod({ assertDocEditPermission(parent, this.userId); // Change the doc's parent - updateParent({docRef, parentRef}); + updateParent({ docRef, parentRef }); // Change the doc's order to be a half step ahead of its target location - collection.update(doc._id, {$set: {order}}, {selector: {type: 'any'}}); + collection.update(doc._id, { $set: { order } }, { selector: { type: 'any' } }); // Reorder both ancestors' documents let oldAncestorId = doc.ancestors[0].id; - reorderDocs({collection, ancestorId: oldAncestorId}); + reorderDocs({ collection, ancestorId: oldAncestorId }); let newAncestorId = getRootId(parent); - if (newAncestorId !== oldAncestorId){ - reorderDocs({collection, ancestorId: newAncestorId}); + if (newAncestorId !== oldAncestorId) { + reorderDocs({ collection, ancestorId: newAncestorId }); } // Figure out which creatures need to be recalculated after this move let docCreatures = getCreatureAncestors(doc); let parentCreatures = getCreatureAncestors(parent); - if (!skipRecompute){ + if (!skipRecompute) { let creaturesToRecompute = union(docCreatures, parentCreatures); // Mark the creatures for recompute Creatures.update({ @@ -81,10 +88,10 @@ const reorderDoc = new ValidatedMethod({ numRequests: 5, timeInterval: 5000, }, - run({docRef, order}) { + run({ docRef, order }) { let doc = fetchDocByRef(docRef); assertDocEditPermission(doc, this.userId); - safeUpdateDocOrder({docRef, order}); + safeUpdateDocOrder({ docRef, order }); // Recompute the affected creatures const ancestors = getCreatureAncestors(doc); if (ancestors.length) { @@ -97,22 +104,22 @@ const reorderDoc = new ValidatedMethod({ }, }); -function getRootId(doc){ - if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){ +function getRootId(doc) { + if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) { return doc.ancestors[0].id; } else { return doc._id; } } -function getCreatureAncestors(doc){ +function getCreatureAncestors(doc) { let ids = []; - if(doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster'){ + if (doc.type === 'pc' || doc.type === 'npc' || doc.type === 'monster') { ids.push(doc._id); } - if (doc.ancestors){ + if (doc.ancestors) { doc.ancestors.forEach(ancestorRef => { - if (ancestorRef.collection === 'creatures'){ + if (ancestorRef.collection === 'creatures') { ids.push(ancestorRef.id); } }); diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index d4a5c4b4..3de84914 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -50,7 +50,6 @@ let ActionSchema = createPropertySchema({ attackRoll: { type: 'fieldToCompute', optional: true, - defaultValue: 'strength.modifier + proficiencyBonus', }, // Calculation of how many times this action can be used uses: { diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 94e5d657..8835661a 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -173,6 +173,13 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ optional: true, removeBeforeCompute: true, }, + // Attributes with advantage grant it to all skills based on the attribute + advantage: { + type: SimpleSchema.Integer, + optional: true, + allowedValues: [-1, 0, 1], + removeBeforeCompute: true, + }, // The computed creature constitution modifier for hit dice constitutionMod: { type: Number, diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index bbd76d58..5fd8a702 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -26,12 +26,6 @@ const ClassLevelSchema = createPropertySchema({ defaultValue: 1, max: STORAGE_LIMITS.levelMax, }, - // Filters out of UI if condition isn't met, but isn't otherwise enforced - slotFillerCondition: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, }); const ComputedOnlyClassLevelSchema = createPropertySchema({ diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index e1ba0c37..77eadbeb 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -1,6 +1,7 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; /* * Effects are reason-value attached to skills and abilities @@ -50,57 +51,7 @@ let EffectSchema = createPropertySchema({ type: String, max: STORAGE_LIMITS.variableName, }, - // True when targeting by tags instead of stats - targetByTags: { - type: Boolean, - optional: true, - }, - // If targeting by tags, the field which will be targeted - targetField: { - type: String, - optional: true, - max: STORAGE_LIMITS.variableName, - }, - // Which tags the effect 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, - }, -}); +}).extend(TagTargetingSchema); const ComputedOnlyEffectSchema = createPropertySchema({ amount: { diff --git a/app/imports/api/properties/Folders.js b/app/imports/api/properties/Folders.js index 66a10355..626b07ed 100644 --- a/app/imports/api/properties/Folders.js +++ b/app/imports/api/properties/Folders.js @@ -1,14 +1,19 @@ +import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; // Folders organize a character sheet into a tree, particularly to group things // like 'race' and 'background' -let FolderSchema = new createPropertySchema({ +let FolderSchema = createPropertySchema({ name: { type: String, max: STORAGE_LIMITS.name, optional: true, }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, groupStats: { type: Boolean, optional: true, @@ -33,6 +38,15 @@ let FolderSchema = new createPropertySchema({ }, }); -const ComputedOnlyFolderSchema = new createPropertySchema({}); +const ComputedOnlyFolderSchema = createPropertySchema({ + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, +}); -export { FolderSchema, ComputedOnlyFolderSchema }; +const ComputedFolderSchema = new SimpleSchema() + .extend(FolderSchema) + .extend(ComputedOnlyFolderSchema); + +export { FolderSchema, ComputedFolderSchema, ComputedOnlyFolderSchema }; diff --git a/app/imports/api/properties/Proficiencies.js b/app/imports/api/properties/Proficiencies.js index e79276ff..cec32217 100644 --- a/app/imports/api/properties/Proficiencies.js +++ b/app/imports/api/properties/Proficiencies.js @@ -1,5 +1,6 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; let ProficiencySchema = new SimpleSchema({ name: { @@ -24,7 +25,7 @@ let ProficiencySchema = new SimpleSchema({ allowedValues: [0.49, 0.5, 1, 2], defaultValue: 1, }, -}); +}).extend(TagTargetingSchema); const ComputedOnlyProficiencySchema = new SimpleSchema({}); diff --git a/app/imports/api/properties/References.js b/app/imports/api/properties/References.js index 31ae8288..d6ece6e6 100644 --- a/app/imports/api/properties/References.js +++ b/app/imports/api/properties/References.js @@ -51,6 +51,10 @@ let ReferenceSchema = new SimpleSchema({ type: Object, optional: true, }, + 'cache.library.id': { + type: String, + optional: true, + }, 'cache.library.name': { type: String, optional: true, diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index a2083cdf..7f15df3a 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; /* * Skills are anything that results in a modifier to be added to a D20 @@ -59,7 +60,8 @@ let SkillSchema = createPropertySchema({ type: 'inlineCalculationFieldToCompute', optional: true, }, -}); + // Skills can apply their value to other calculations as a proficiency using tag targeting +}).extend(TagTargetingSchema); let ComputedOnlySkillSchema = createPropertySchema({ // Computed value of skill to be added to skill rolls diff --git a/app/imports/api/properties/SlotFillers.js b/app/imports/api/properties/SlotFillers.js deleted file mode 100644 index 185e7ba2..00000000 --- a/app/imports/api/properties/SlotFillers.js +++ /dev/null @@ -1,44 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -// SlotFiller fillers specifically fill a slot with a bit more control than -// other properties -let SlotFillerSchema = new SimpleSchema({ - name: { - type: String, - optional: true, - max: STORAGE_LIMITS.name, - }, - picture: { - type: String, - optional: true, - max: STORAGE_LIMITS.url, - }, - description: { - type: String, - optional: true, - max: STORAGE_LIMITS.description, - }, - // Overrides the type when searching for properties - slotFillerType: { - type: String, - optional: true, - max: STORAGE_LIMITS.variableName, - }, - // Fill more than one quantity in a slot, like feats and ability score - // improvements, filtered out of UI if there isn't space in quantityExpected - slotQuantityFilled: { - type: SimpleSchema.Integer, - defaultValue: 1, - }, - // Filters out of UI if condition isn't met, but isn't otherwise enforced - slotFillerCondition: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, -}); - -const ComputedOnlySlotFillerSchema = new SimpleSchema({}); - -export { SlotFillerSchema, ComputedOnlySlotFillerSchema }; diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index 755ed89b..c5babdc6 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -1,6 +1,7 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import TagTargetingSchema from '/imports/api/properties/subSchemas/TagTargetingSchema.js'; const ToggleSchema = createPropertySchema({ name: { @@ -31,7 +32,7 @@ const ToggleSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, -}); +}).extend(TagTargetingSchema); const ComputedOnlyToggleSchema = createPropertySchema({ condition: { diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 93f3793a..89cb21e4 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -23,7 +23,6 @@ import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js'; import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js'; -import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFillers.js'; import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js'; import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js'; import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js'; @@ -54,7 +53,6 @@ const propertySchemasIndex = { roll: ComputedOnlyRollSchema, savingThrow: ComputedOnlySavingThrowSchema, skill: ComputedOnlySkillSchema, - slotFiller: ComputedOnlySlotFillerSchema, spellList: ComputedOnlySpellListSchema, spell: ComputedOnlySpellSchema, toggle: ComputedOnlyToggleSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 7bdfb302..b6adaaa2 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -13,7 +13,7 @@ import { ComputedDamageSchema } from '/imports/api/properties/Damages.js'; import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; import { ComputedEffectSchema } from '/imports/api/properties/Effects.js'; import { ComputedFeatureSchema } from '/imports/api/properties/Features.js'; -import { FolderSchema } from '/imports/api/properties/Folders.js'; +import { ComputedFolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedItemSchema } from '/imports/api/properties/Items.js'; import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js'; @@ -23,7 +23,6 @@ import { ComputedRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { ComputedSkillSchema } from '/imports/api/properties/Skills.js'; import { ComputedSlotSchema } from '/imports/api/properties/Slots.js'; -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'; @@ -43,7 +42,7 @@ const propertySchemasIndex = { damageMultiplier: DamageMultiplierSchema, effect: ComputedEffectSchema, feature: ComputedFeatureSchema, - folder: FolderSchema, + folder: ComputedFolderSchema, note: ComputedNoteSchema, pointBuy: ComputedPointBuySchema, proficiency: ProficiencySchema, @@ -52,7 +51,6 @@ const propertySchemasIndex = { roll: ComputedRollSchema, savingThrow: ComputedSavingThrowSchema, skill: ComputedSkillSchema, - slotFiller: SlotFillerSchema, spellList: ComputedSpellListSchema, spell: ComputedSpellSchema, toggle: ComputedToggleSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index fdccedc9..ed0a3a22 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -21,7 +21,6 @@ import { RollSchema } from '/imports/api/properties/Rolls.js'; import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { SkillSchema } from '/imports/api/properties/Skills.js'; import { SlotSchema } from '/imports/api/properties/Slots.js'; -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'; @@ -52,7 +51,6 @@ const propertySchemasIndex = { roll: RollSchema, savingThrow: SavingThrowSchema, skill: SkillSchema, - slotFiller: SlotFillerSchema, spellList: SpellListSchema, spell: SpellSchema, toggle: ToggleSchema, diff --git a/app/imports/api/properties/subSchemas/TagTargetingSchema.js b/app/imports/api/properties/subSchemas/TagTargetingSchema.js new file mode 100644 index 00000000..8bbe1ddb --- /dev/null +++ b/app/imports/api/properties/subSchemas/TagTargetingSchema.js @@ -0,0 +1,57 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const TagTargetingSchema = new SimpleSchema({ + // True when targeting by tags instead of stats + targetByTags: { + type: Boolean, + optional: true, + }, + // If targeting by tags, the field which will be targeted + targetField: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + // Which tags the effect 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, + }, +}); + +export default TagTargetingSchema; diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index 54328914..b27edfca 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -1,11 +1,12 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import Libraries from '/imports/api/library/Libraries.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; 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(',') || []; @@ -142,12 +143,12 @@ Meteor.users.generateApiKey = new ValidatedMethod({ Meteor.users.setDarkMode = new ValidatedMethod({ name: 'users.setDarkMode', validate: new SimpleSchema({ - darkMode: { type: Boolean }, + darkMode: { type: Boolean, optional: true }, }).validator(), mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, - timeInterval: 5000, + timeInterval: 2000, }, run({ darkMode }) { if (!this.userId) return; @@ -250,6 +251,30 @@ Meteor.users.setPreference = new ValidatedMethod({ }, }); +if (Meteor.isServer) { + Accounts.onCreateUser((options, user) => { + if (defaultLibraries?.length) { + Libraries.update({ + _id: { $in: defaultLibraries } + }, { + $inc: { subscriberCount: 1 } + }, { + multi: true, + }, () => {/**/ }); + } + if (defaultLibraryCollections?.length) { + LibraryCollections.update({ + _id: { $in: defaultLibraryCollections } + }, { + $inc: { subscriberCount: 1 } + }, { + multi: true, + }, () => {/**/ }); + } + return user; + }); +} + Meteor.users.subscribeToLibrary = new ValidatedMethod({ name: 'users.subscribeToLibrary', validate: new SimpleSchema({ @@ -264,15 +289,17 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({ mixins: [RateLimiterMixin], rateLimit: { numRequests: 5, - timeInterval: 5000, + timeInterval: 2000, }, run({ libraryId, subscribe }) { if (!this.userId) throw 'Can only subscribe if logged in'; if (subscribe) { + Libraries.update({ _id: libraryId }, { $inc: { subscriberCount: 1 } }, () => {/**/ }); return Meteor.users.update(this.userId, { $addToSet: { subscribedLibraries: libraryId }, }); } else { + Libraries.update({ _id: libraryId }, { $inc: { subscriberCount: -1 } }, () => {/**/ }); return Meteor.users.update(this.userId, { $pullAll: { subscribedLibraries: libraryId }, }); @@ -299,10 +326,12 @@ Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({ run({ libraryCollectionId, subscribe }) { if (!this.userId) throw 'Can only subscribe if logged in'; if (subscribe) { + LibraryCollections.update({ _id: libraryCollectionId }, { $inc: { subscriberCount: 1 } }, () => {/**/ }); return Meteor.users.update(this.userId, { $addToSet: { subscribedLibraryCollections: libraryCollectionId }, }); } else { + LibraryCollections.update({ _id: libraryCollectionId }, { $inc: { subscriberCount: -1 } }, () => {/**/ }); return Meteor.users.update(this.userId, { $pullAll: { subscribedLibraryCollections: libraryCollectionId }, }); diff --git a/app/imports/api/utility/escapeRegex.js b/app/imports/api/utility/escapeRegex.js new file mode 100644 index 00000000..4f0be7de --- /dev/null +++ b/app/imports/api/utility/escapeRegex.js @@ -0,0 +1,3 @@ +export default function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} \ No newline at end of file diff --git a/app/imports/api/utility/numberToSignedString.js b/app/imports/api/utility/numberToSignedString.js index da6b124b..f60b3412 100644 --- a/app/imports/api/utility/numberToSignedString.js +++ b/app/imports/api/utility/numberToSignedString.js @@ -1,10 +1,11 @@ -export default function numberToSignedString(number, spaced){ +export default function numberToSignedString(number, spaced) { if (typeof number !== 'number') return number; - if (number === 0){ + if (number === 0) { return spaced ? '+ 0' : '+0'; - } else if (number > 0){ + } else if (number > 0) { return spaced ? `+ ${number}` : `+${number}`; } else { - return spaced ? `- ${Math.abs(number) || number}` : `${number}`; + // Uses the unicode minus sign '−' instead of a dash '-' to help line up numbers nicely + return spaced ? `− ${Math.abs(number) || number}` : `−${Math.abs(number) || number}`; } } diff --git a/app/imports/client/ui/components/ColorPicker.vue b/app/imports/client/ui/components/ColorPicker.vue index 92f304fe..ef73f90d 100644 --- a/app/imports/client/ui/components/ColorPicker.vue +++ b/app/imports/client/ui/components/ColorPicker.vue @@ -7,16 +7,18 @@ > + +
+ + + + {{ typeName }} + +
+
@@ -28,26 +47,26 @@ size="64" /> - - -

- This property can't be viewed yet. -

diff --git a/app/imports/client/ui/library/LibrarySecondTree.vue b/app/imports/client/ui/library/LibrarySecondTree.vue new file mode 100644 index 00000000..2233bc97 --- /dev/null +++ b/app/imports/client/ui/library/LibrarySecondTree.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/app/imports/client/ui/library/SingleLibraryToolbar.vue b/app/imports/client/ui/library/SingleLibraryToolbar.vue index 038dc867..40cd8ec8 100644 --- a/app/imports/client/ui/library/SingleLibraryToolbar.vue +++ b/app/imports/client/ui/library/SingleLibraryToolbar.vue @@ -10,7 +10,7 @@ mdi-arrow-left @@ -34,11 +34,20 @@ > mdi-cog + +
+ {{ formatNumber(library.subscriberCount) }} subscribers +
diff --git a/app/imports/client/ui/pages/About.vue b/app/imports/client/ui/pages/About.vue index e643dcd1..7eed0225 100644 --- a/app/imports/client/ui/pages/About.vue +++ b/app/imports/client/ui/pages/About.vue @@ -2,8 +2,8 @@

DiceCloud is a single-developer project started in 2014 with the aim of @@ -87,7 +87,12 @@ export default { name: 'Dai', title: 'A Kobold\'s Best Friend', avatar: 'dai' - },], + }, { + name: 'Vibes', + title: 'Kell of Nothing', + avatar: 'vibes' + } + ], }}, } diff --git a/app/imports/client/ui/pages/Account.vue b/app/imports/client/ui/pages/Account.vue index 5f196ce8..807428cd 100644 --- a/app/imports/client/ui/pages/Account.vue +++ b/app/imports/client/ui/pages/Account.vue @@ -20,15 +20,18 @@ - + Preferences - @@ -260,7 +263,7 @@ return user && user.emails; }, darkMode(){ - return this.user && this.tier.paidBenefits && this.user.darkMode; + return this.user && this.user.darkMode; }, invites(){ let usernames = {}; @@ -342,8 +345,16 @@ Meteor.logout(); router.push('/'); }, - setDarkMode(value, ack){ - Meteor.users.setDarkMode.call({darkMode: !!value}, ack); + setDarkMode(value, ack) { + let darkMode; + if (value === 'true') { + darkMode = true; + } else if (value === 'false') { + darkMode = false; + } else if (value === 'unset') { + darkMode = null; + } + Meteor.users.setDarkMode.call({darkMode}, ack); }, swapAbilityScoresAndModifiers(value, ack){ Meteor.users.setPreference.call({ diff --git a/app/imports/client/ui/pages/CharacterList.vue b/app/imports/client/ui/pages/CharacterList.vue index 20290aee..a30578df 100644 --- a/app/imports/client/ui/pages/CharacterList.vue +++ b/app/imports/client/ui/pages/CharacterList.vue @@ -4,7 +4,10 @@ style="height: 100%" > - + @@ -57,7 +58,7 @@ @@ -107,6 +108,7 @@ import ImageUploadInput from '/imports/client/ui/components/ImageUploadInput.vue import UserImageCard from '/imports/client/ui/files/UserImageCard.vue'; import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js'; import { archiveSchema } from '/imports/api/creature/archive/ArchiveCreatureFiles.js'; +import migrateArchive from '/imports/migrations/archive/migrateArchive.js'; export default { components: { @@ -120,8 +122,8 @@ export default { archiveFileError: undefined, archiveFile: undefined, archiveUploadInProgress: false, - archiveUploadProgress: undefined, - archiveUploadIndeterminate: false, + archiveUploadProgress: 0, + archiveUploadIndeterminate: true, }}, meteor: { $subscribe: { @@ -158,11 +160,21 @@ export default { }); } }, + watch: { + archiveUploadInProgress(val){ + if (val === false) { + this.archiveUploadProgress = 0; + this.archiveUploadIndeterminate = true; + } + }, + }, methods: { inputArchiveFile(){ this.archiveFile = undefined; this.archiveFileError = undefined; const file = this.$refs.archiveFileInput.files[0]; + // Reset the file input + this.$refs.archiveFileInput.value = null; if (!file) return; if (file.type !== 'application/json'){ this.archiveFileError = 'File must be .json'; @@ -174,6 +186,8 @@ export default { } this.archiveFile = file; this.archiveUploadIndeterminate = true; + this.archiveUploadInProgress = true; + this.archiveUploadProgress = undefined; const fr = new FileReader(); const self = this; @@ -184,14 +198,18 @@ export default { data = JSON.parse(fr.result); } catch (e){ self.archiveFileError = 'File could not be parsed'; + self.archiveUploadInProgress = false; console.error(e); return; } - try { - archiveSchema.validate(archiveSchema.clean(data)); + // Migrate, clean, and validate the archive + migrateArchive(data); + data = archiveSchema.clean(data); + archiveSchema.validate(data); } catch (e){ self.archiveFileError = 'File failed validation: ' + (e.reason || e.message || e.toString()); + self.archiveUploadInProgress = false; console.error(e); return; } @@ -210,7 +228,6 @@ export default { uploadInstance.on('start', function () { console.log('Starting'); self.archiveUploadIndeterminate = false; - self.archiveUploadInProgress = true; }); uploadInstance.on('end', function (error, fileObj) { @@ -226,7 +243,6 @@ export default { // Reset our state for the next file self.archiveUploadInProgress = false; - self.archiveUploadProgress = 0; }); uploadInstance.on('error', function (error, fileObj) { @@ -234,6 +250,7 @@ export default { const text = error.reason || error.message || error; snackbar({text}); self.archiveFileError = text; + self.archiveUploadInProgress = false; }); uploadInstance.on('progress', function (progress, fileObj) { diff --git a/app/imports/client/ui/pages/Home.vue b/app/imports/client/ui/pages/Home.vue index 72642172..5e1f4f14 100644 --- a/app/imports/client/ui/pages/Home.vue +++ b/app/imports/client/ui/pages/Home.vue @@ -1,29 +1,75 @@ diff --git a/app/imports/client/ui/pages/SignIn.vue b/app/imports/client/ui/pages/SignIn.vue index a1a0ba2e..8dfe63cc 100644 --- a/app/imports/client/ui/pages/SignIn.vue +++ b/app/imports/client/ui/pages/SignIn.vue @@ -39,7 +39,10 @@ > Reset Password -

+
{{ error }}
@@ -59,6 +62,13 @@ Register +
+

+ DiceCloud Version 2 requires a new account to use. +

+ Version 1 is still available at v1.dicecloud.com +

+
diff --git a/app/imports/client/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue b/app/imports/client/ui/properties/InsertPropertyDialog.vue similarity index 85% rename from app/imports/client/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue rename to app/imports/client/ui/properties/InsertPropertyDialog.vue index 32ae0e01..5ef9042b 100644 --- a/app/imports/client/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue +++ b/app/imports/client/ui/properties/InsertPropertyDialog.vue @@ -16,7 +16,7 @@ flat @change="propertyHelpChanged" /> - Create - + Library @@ -56,31 +59,43 @@ > - + - - + - Cancel + {{ tab === 1 ? "Discard" : "Cancel" }} [], + }, + hideLibraryTab: Boolean, + showLibraryOnlyProps: Boolean, }, reactiveProvide: { name: 'context', - include: ['debounceTime'], + include: ['debounceTime', 'isLibraryForm'], }, data(){return { selectedNodeIds: [], - type: this.forcedType || this.suggestedType, - model: { + type: this.forcedType || this.suggestedType || this.prop?.type || undefined, + model: this.prop || { type: this.type, + children: [], }, searchValue: undefined, debounceTime: 0, @@ -248,17 +279,22 @@ export default { const propDef = PROPERTIES[this.type]; return propDef && propDef.docsPath; }, + isLibraryForm() { + return this.collection === 'libraryNodes' || undefined; + }, }, watch: { type(newType){ this.changeType(newType); }, + prop(newProp) { + this.model = newProp + }, }, mounted(){ this.changeType(this.type); }, methods: { - propertyHelpChanged(value){ Meteor.users.setPreference.call({ preference: 'hidePropertySelectDialogHelp', @@ -291,17 +327,14 @@ export default { if (this.currentLimit >= this.countAll) return; this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32); }, - insert(){ - if (!this.selectedNodeIds.length) return; - this.$store.dispatch('popDialogStack', this.selectedNodeIds); - }, changeType(type){ this._subs.searchLibraryNodes.setData('type', type); if (!type) return; this.tab = 1; this.schema = propertySchemasIndex[type]; this.validationContext = this.schema.newContext(); - let model = this.schema.clean({}); + let model = this.model || {}; + model = this.schema.clean(model); model.type = type; this.model = model; }, @@ -355,4 +388,11 @@ export default { diff --git a/app/imports/client/ui/properties/PropertyForm.vue b/app/imports/client/ui/properties/PropertyForm.vue new file mode 100644 index 00000000..02dbb8ba --- /dev/null +++ b/app/imports/client/ui/properties/PropertyForm.vue @@ -0,0 +1,310 @@ + + + diff --git a/app/imports/client/ui/properties/components/actions/ActionCard.vue b/app/imports/client/ui/properties/components/actions/ActionCard.vue index 51f11ebd..0df8dc34 100644 --- a/app/imports/client/ui/properties/components/actions/ActionCard.vue +++ b/app/imports/client/ui/properties/components/actions/ActionCard.vue @@ -234,13 +234,13 @@ export default { actionId: this.model._id, targetIds: this.targets, scope: { - $attackAdvantage: advantage, + '~attackAdvantage': { value: advantage }, } }, error => { this.doActionLoading = false; if (error) { console.error(error); - snackbar({ text: error.reason }); + snackbar({ text: error.reason || error.message || error.toString() }); } }); }, diff --git a/app/imports/client/ui/properties/components/actions/EventButton.vue b/app/imports/client/ui/properties/components/actions/EventButton.vue index 27773e04..f7333739 100644 --- a/app/imports/client/ui/properties/components/actions/EventButton.vue +++ b/app/imports/client/ui/properties/components/actions/EventButton.vue @@ -53,7 +53,7 @@ export default { doAction.call({ actionId: this.model._id, scope: { - $attackAdvantage: advantage, + '~attackAdvantage': { value: advantage }, } }, error => { this.doActionLoading = false; diff --git a/app/imports/client/ui/properties/components/actions/SelectItemToConsume.vue b/app/imports/client/ui/properties/components/actions/SelectItemToConsume.vue index 5634958e..33b5b7e7 100644 --- a/app/imports/client/ui/properties/components/actions/SelectItemToConsume.vue +++ b/app/imports/client/ui/properties/components/actions/SelectItemToConsume.vue @@ -13,7 +13,7 @@ - No equipped items found with the tag "{{ itemConsumed.tag }}" + No active items found with the tag "{{ itemConsumed.tag }}" @@ -42,7 +42,6 @@ export default { return CreatureProperties.find({ 'ancestors.id': this.action.ancestors[0].id, type: 'item', - equipped: true, tags: this.itemConsumed.tag, removed: {$ne: true}, inactive: {$ne: true}, diff --git a/app/imports/client/ui/properties/components/attributes/AbilityListTile.vue b/app/imports/client/ui/properties/components/attributes/AbilityListTile.vue index 2cc2eb95..3a1d6866 100644 --- a/app/imports/client/ui/properties/components/attributes/AbilityListTile.vue +++ b/app/imports/client/ui/properties/components/attributes/AbilityListTile.vue @@ -46,6 +46,18 @@ {{ model.name }} + + mdi-chevron-double-up + + + mdi-chevron-double-down + @@ -89,7 +101,7 @@ export default { doCheck.call({ propId: this.model._id, scope: { - $checkAdvantage: advantage, + '~checkAdvantage': { value: advantage }, }, }, error => { this.checkLoading = false; diff --git a/app/imports/client/ui/properties/components/attributes/AttributeCard.vue b/app/imports/client/ui/properties/components/attributes/AttributeCard.vue index 2f96dd5f..088c94ff 100644 --- a/app/imports/client/ui/properties/components/attributes/AttributeCard.vue +++ b/app/imports/client/ui/properties/components/attributes/AttributeCard.vue @@ -1,6 +1,9 @@ @@ -75,7 +87,7 @@ export default { doCheck.call({ propId: this.model._id, scope: { - $checkAdvantage: advantage, + '~checkAdvantage': { value: advantage }, }, }, error => { this.checkLoading = false; diff --git a/app/imports/client/ui/properties/components/attributes/ResourceCard.vue b/app/imports/client/ui/properties/components/attributes/ResourceCard.vue index a46cede2..164599c5 100644 --- a/app/imports/client/ui/properties/components/attributes/ResourceCard.vue +++ b/app/imports/client/ui/properties/components/attributes/ResourceCard.vue @@ -2,6 +2,9 @@ import CardHighlight from '/imports/client/ui/components/CardHighlight.vue'; import ResourceCardContent from '/imports/client/ui/properties/components/attributes/ResourceCardContent.vue'; +import isDarkColor from '/imports/client/ui/utility/isDarkColor.js'; export default { components: { @@ -38,6 +42,12 @@ export default { hover: false, } }, + computed: { + isDark() { + if (!this.model.color) return; + return isDarkColor(this.model.color); + }, + } }; diff --git a/app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue b/app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue index 1f9de91f..325651be 100644 --- a/app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue +++ b/app/imports/client/ui/properties/components/attributes/ResourceCardContent.vue @@ -1,26 +1,28 @@ diff --git a/app/imports/client/ui/properties/components/buffs/BuffListItem.vue b/app/imports/client/ui/properties/components/buffs/BuffListItem.vue index b122f33c..ef2cb391 100644 --- a/app/imports/client/ui/properties/components/buffs/BuffListItem.vue +++ b/app/imports/client/ui/properties/components/buffs/BuffListItem.vue @@ -10,7 +10,7 @@ mdi-delete diff --git a/app/imports/client/ui/properties/components/folders/FolderGroupCard.vue b/app/imports/client/ui/properties/components/folders/FolderGroupCard.vue index 7729cd9c..b21a1a2b 100644 --- a/app/imports/client/ui/properties/components/folders/FolderGroupCard.vue +++ b/app/imports/client/ui/properties/components/folders/FolderGroupCard.vue @@ -15,8 +15,9 @@ :model="prop" :data-id="prop._id" @click="$emit('click-property', {_id: prop._id})" - @sub-click="_id => $emit('sub-click', _id)" - @remove="$emit('remove', prop._id)" + @click-property="e => $emit('click-property', e)" + @sub-click="e => $emit('sub-click', e)" + @remove="id => $emit('remove', id || prop._id)" />
@@ -27,15 +28,15 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import propComponents from '/imports/client/ui/properties/components/folders/propertyComponentIndex.js'; export default { - components: { - ...propComponents, - }, props: { model: { type: Object, required: true, } }, + beforeCreate() { + Object.assign(this.$options.components, propComponents); + }, meteor: { properties() { const props = []; @@ -48,8 +49,10 @@ export default { type: 'toggle', showUI: true, deactivatedByAncestor: { $ne: true }, + deactivatedByToggle: { $ne: true }, }, { + type: { $ne: 'toggle' }, inactive: { $ne: true } }, ], @@ -60,7 +63,7 @@ export default { }, { sort: { order: 1 }, }).forEach(prop => { - if (this.$options.components[prop.type]) { + if (propComponents[prop.type]) { props.push(prop); } }); diff --git a/app/imports/client/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue b/app/imports/client/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue index a2ef661d..2b485e3b 100644 --- a/app/imports/client/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue +++ b/app/imports/client/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue @@ -1,44 +1,55 @@ @@ -50,6 +61,7 @@ import SpellSlotListTile from '/imports/client/ui/properties/components/attribut import ResourceCardContent from '/imports/client/ui/properties/components/attributes/ResourceCardContent.vue'; import AttributeCardContent from '/imports/client/ui/properties/components/attributes/AttributeCardContent.vue'; import CardHighlight from '/imports/client/ui/components/CardHighlight.vue'; +import FolderGroupChildren from '/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue'; import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; @@ -62,12 +74,17 @@ export default { ResourceCardContent, AttributeCardContent, CardHighlight, + FolderGroupChildren, }, props: { model: { type: Object, required: true, }, + dataId: { + type: String, + required: true, + }, }, data() { return { @@ -81,6 +98,10 @@ export default { value: change.value }); }, + log({_id}) { + console.log(...arguments) + this.$emit('click-property', { _id }); + } } } diff --git a/app/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue b/app/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue new file mode 100644 index 00000000..bbb026b5 --- /dev/null +++ b/app/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue @@ -0,0 +1,67 @@ + + + diff --git a/app/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue b/app/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue new file mode 100644 index 00000000..13966bcd --- /dev/null +++ b/app/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue @@ -0,0 +1,82 @@ + + + \ No newline at end of file diff --git a/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js b/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js index de9ce052..8676d317 100644 --- a/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js +++ b/app/imports/client/ui/properties/components/folders/propertyComponentIndex.js @@ -17,14 +17,13 @@ import item from '/imports/client/ui/properties/components/inventory/ItemListTil import note from '/imports/client/ui/properties/components/persona/NoteCard.vue'; //import pointBuy from ''; //import proficiency from ''; -//import propertySlot from ''; +import propertySlot from '/imports/client/ui/properties/components/folders/folderGroupComponents/SlotBuildTree.vue'; //import reference from ''; //import roll from ''; //import savingThrow from ''; import skill from '/imports/client/ui/properties/components/skills/SkillListTile.vue'; -//import slotFiller from ''; -//import spellList from ''; -//import spell from ''; +import spellList from '/imports/client/ui/properties/components/spells/SpellListCard.vue'; +import spell from '/imports/client/ui/properties/components/spells/SpellListTile.vue'; import toggle from '/imports/client/ui/properties/components/toggles/ToggleCard.vue'; //import trigger from ''; @@ -43,19 +42,18 @@ export default { //damageMultiplier, //effect, feature, - //folder, + // folder // Like actions, we don't show sub-folders item, note, //pointBuy, //proficiency, - //propertySlot, + propertySlot, //reference, //roll, //savingThrow, skill, - //slotFiller, - //spellList, - //spell, + spellList, + spell, toggle, //trigger, }; diff --git a/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js b/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js index 5097143e..33dd5105 100644 --- a/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js +++ b/app/imports/client/ui/properties/components/folders/tabFoldersMixin.js @@ -11,6 +11,10 @@ function getFolders(creatureId, tab, location) { removed: { $ne: true }, tab, location, + }, { + sort: { + order: 1, + } }); } diff --git a/app/imports/client/ui/properties/components/inventory/ContainerCard.vue b/app/imports/client/ui/properties/components/inventory/ContainerCard.vue index b2d6de9f..fee97c5b 100644 --- a/app/imports/client/ui/properties/components/inventory/ContainerCard.vue +++ b/app/imports/client/ui/properties/components/inventory/ContainerCard.vue @@ -99,6 +99,7 @@ export default { removed: { $ne: true }, equipped: { $ne: true }, deactivatedByAncestor: { $ne: true }, + deactivatedByToggle: { $ne: true }, }, { sort: { order: 1 }, }); diff --git a/app/imports/client/ui/properties/components/proficiencies/InlineProficiency.vue b/app/imports/client/ui/properties/components/proficiencies/InlineProficiency.vue new file mode 100644 index 00000000..7af9155c --- /dev/null +++ b/app/imports/client/ui/properties/components/proficiencies/InlineProficiency.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/app/imports/client/ui/properties/components/skills/SkillListTile.vue b/app/imports/client/ui/properties/components/skills/SkillListTile.vue index ec436241..d10c4a78 100644 --- a/app/imports/client/ui/properties/components/skills/SkillListTile.vue +++ b/app/imports/client/ui/properties/components/skills/SkillListTile.vue @@ -111,7 +111,7 @@ export default { doCheck.call({ propId: this.model._id, scope: { - $checkAdvantage: advantage, + '~checkAdvantage': { value: advantage }, }, }, error => { this.checkLoading = false; diff --git a/app/imports/client/ui/properties/components/spells/SpellListCard.vue b/app/imports/client/ui/properties/components/spells/SpellListCard.vue index 4474d606..c31e5897 100644 --- a/app/imports/client/ui/properties/components/spells/SpellListCard.vue +++ b/app/imports/client/ui/properties/components/spells/SpellListCard.vue @@ -5,11 +5,21 @@ @toolbarclick="clickSpellList(model._id)" > - - -
- {{ numPrepared }}/{{ model.maxPrepared.value }} spells prepared -
- -
-
+ mdi-check + + +
- - - - - - + + + + + + - - - - + @@ -84,18 +87,12 @@ label="Description" hint="This text will be displayed in the log when the action is taken" :model="model.description" - :error-messages="errors.description" + :error-messages="errors['description.text']" @change="({path, value, ack}) => $emit('change', {path: ['description', ...path], value, ack})" /> - - - - + - - - + - - - + + + +
@@ -179,13 +156,11 @@ diff --git a/app/imports/client/ui/properties/forms/BuffRemoverForm.vue b/app/imports/client/ui/properties/forms/BuffRemoverForm.vue index abd05c62..6aa8e299 100644 --- a/app/imports/client/ui/properties/forms/BuffRemoverForm.vue +++ b/app/imports/client/ui/properties/forms/BuffRemoverForm.vue @@ -1,101 +1,58 @@ diff --git a/app/imports/client/ui/properties/forms/ConstantForm.vue b/app/imports/client/ui/properties/forms/ConstantForm.vue index c6c5e07a..316688bd 100644 --- a/app/imports/client/ui/properties/forms/ConstantForm.vue +++ b/app/imports/client/ui/properties/forms/ConstantForm.vue @@ -1,36 +1,33 @@ diff --git a/app/imports/client/ui/properties/forms/ContainerForm.vue b/app/imports/client/ui/properties/forms/ContainerForm.vue index 5cef5978..9d8556f3 100644 --- a/app/imports/client/ui/properties/forms/ContainerForm.vue +++ b/app/imports/client/ui/properties/forms/ContainerForm.vue @@ -1,14 +1,45 @@ diff --git a/app/imports/client/ui/properties/forms/DamageForm.vue b/app/imports/client/ui/properties/forms/DamageForm.vue index 23d00ad8..b7ed8881 100644 --- a/app/imports/client/ui/properties/forms/DamageForm.vue +++ b/app/imports/client/ui/properties/forms/DamageForm.vue @@ -8,7 +8,7 @@ - - - - - + + + + + +
diff --git a/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue b/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue index 130fbc85..41cfa862 100644 --- a/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue +++ b/app/imports/client/ui/properties/forms/DamageMultiplierForm.vue @@ -1,29 +1,23 @@ @@ -102,18 +96,6 @@ export default { data() { return { DAMAGE_TYPES, - values: [ - { - value: 0, - text: 'Immunity', - }, { - value: 0.5, - text: 'Resistance', - }, { - value: 2, - text: 'Vulnerability', - }, - ], damageTypeRules: [ value => { if (value && value.length) { diff --git a/app/imports/client/ui/properties/forms/EffectForm.vue b/app/imports/client/ui/properties/forms/EffectForm.vue index 1d386bf8..fbf57736 100644 --- a/app/imports/client/ui/properties/forms/EffectForm.vue +++ b/app/imports/client/ui/properties/forms/EffectForm.vue @@ -1,181 +1,119 @@ @@ -184,20 +122,18 @@ import getEffectIcon from '/imports/client/ui/utility/getEffectIcon.js'; import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js'; import attributeListMixin from '/imports/client/ui/properties/forms/shared/lists/attributeListMixin.js'; -import { EffectSchema } from '/imports/api/properties/Effects.js'; -import FormSection from '/imports/client/ui/properties/forms/shared/FormSection.vue'; +import TagTargeting from '/imports/client/ui/properties/forms/shared/TagTargeting.vue'; const ICON_SPIN_DURATION = 300; export default { components: { - FormSection, + TagTargeting, }, mixins: [propertyFormMixin, attributeListMixin], data() { return { displayedIcon: 'add', iconClass: '', - addExtraTagsLoading: false, oldOperation: undefined, operations: [ { value: 'base', text: 'Base Value' }, @@ -218,11 +154,6 @@ export default { radioGroup() { return this.model.targetByTags ? 'tags' : 'stats'; }, - extraTagsFull() { - if (!this.model.extraTags) return false; - let maxCount = EffectSchema.get('extraTags', 'maxCount'); - return this.model.extraTags.length >= maxCount; - }, needsValue() { switch (this.model.operation) { case 'base': return true; @@ -242,7 +173,7 @@ export default { operationHint() { switch (this.model.operation) { case 'base': return 'Stats take their largest base value, and then apply all other effects'; - case 'add': return 'Add this vaulue to the stat'; + case 'add': return 'Add this value to the stat'; case 'mul': return 'Multiply the stat by this value'; case 'min': return 'The stat will be at least this value'; case 'max': return 'The stat will not exceed this value'; @@ -279,32 +210,20 @@ export default { }, methods: { getEffectIcon, - changeTargetByTags(value) { + changeTargetByTags(value, ack) { if (value === 'stats') { - this.$emit('change', { path: ['targetByTags'], value: undefined }); + this.$emit('change', { path: ['targetByTags'], value: undefined, ack }); if (this.oldOperation && this.oldOperation !== this.model.operation) { this.$emit('change', { path: ['operation'], value: this.oldOperation }); } } else if (value === 'tags') { - this.$emit('change', { path: ['targetByTags'], value: true }); + this.$emit('change', { path: ['targetByTags'], value: true, ack }); if (this.model.operation !== 'add') { this.oldOperation = this.model.operation; this.$emit('change', { path: ['operation'], value: 'add' }); } } }, - addExtraTags() { - this.addExtraTagsLoading = true; - this.$emit('push', { - path: ['extraTags'], - value: { - _id: Random.id(), - operation: 'OR', - tags: [], - }, - ack: () => this.addExtraTagsLoading = false, - }); - }, } }; diff --git a/app/imports/client/ui/properties/forms/FeatureForm.vue b/app/imports/client/ui/properties/forms/FeatureForm.vue index 10aa74fd..acf86e2c 100644 --- a/app/imports/client/ui/properties/forms/FeatureForm.vue +++ b/app/imports/client/ui/properties/forms/FeatureForm.vue @@ -1,17 +1,10 @@ diff --git a/app/imports/client/ui/properties/forms/FolderForm.vue b/app/imports/client/ui/properties/forms/FolderForm.vue index f98d87ba..7263f1d7 100644 --- a/app/imports/client/ui/properties/forms/FolderForm.vue +++ b/app/imports/client/ui/properties/forms/FolderForm.vue @@ -1,76 +1,59 @@ diff --git a/app/imports/client/ui/properties/forms/ItemForm.vue b/app/imports/client/ui/properties/forms/ItemForm.vue index 45ef9e5c..3bbebdd4 100644 --- a/app/imports/client/ui/properties/forms/ItemForm.vue +++ b/app/imports/client/ui/properties/forms/ItemForm.vue @@ -1,14 +1,6 @@ diff --git a/app/imports/client/ui/properties/forms/NoteForm.vue b/app/imports/client/ui/properties/forms/NoteForm.vue index 36d693bb..43397b4f 100644 --- a/app/imports/client/ui/properties/forms/NoteForm.vue +++ b/app/imports/client/ui/properties/forms/NoteForm.vue @@ -1,18 +1,10 @@ diff --git a/app/imports/client/ui/properties/forms/PointBuyForm.vue b/app/imports/client/ui/properties/forms/PointBuyForm.vue index 4698559d..e366abf9 100644 --- a/app/imports/client/ui/properties/forms/PointBuyForm.vue +++ b/app/imports/client/ui/properties/forms/PointBuyForm.vue @@ -1,26 +1,13 @@ @@ -188,6 +185,9 @@ export default { PointBuySpendForm, }, mixins: [propertyFormMixin, attributeListMixin], + inject: { + context: { default: {} } + }, data() { return { addRowLoading: false, diff --git a/app/imports/client/ui/properties/forms/PointBuySpendForm.vue b/app/imports/client/ui/properties/forms/PointBuySpendForm.vue index 1eeb8ed8..5fbbaf28 100644 --- a/app/imports/client/ui/properties/forms/PointBuySpendForm.vue +++ b/app/imports/client/ui/properties/forms/PointBuySpendForm.vue @@ -118,6 +118,7 @@ export default { const currentSpent = this.model.spent; let newSpent = currentSpent - row.spent; const costFunction = EJSON.clone(row.cost || this.model.cost); + if (!costFunction?.parseNode) return; if (costFunction) costFunction.parseLevel = 'reduce'; evaluateCalculation(costFunction, { value }); if (Number.isFinite(costFunction.value)) { diff --git a/app/imports/client/ui/properties/forms/ProficiencyForm.vue b/app/imports/client/ui/properties/forms/ProficiencyForm.vue index a8a18d09..f66c7055 100644 --- a/app/imports/client/ui/properties/forms/ProficiencyForm.vue +++ b/app/imports/client/ui/properties/forms/ProficiencyForm.vue @@ -1,49 +1,86 @@ @@ -51,10 +88,12 @@ import ProficiencySelect from '/imports/client/ui/properties/forms/shared/ProficiencySelect.vue'; import skillListMixin from '/imports/client/ui/properties/forms/shared/lists/skillListMixin.js'; import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js'; +import TagTargeting from '/imports/client/ui/properties/forms/shared/TagTargeting.vue'; export default { components: { ProficiencySelect, + TagTargeting, }, mixins: [propertyFormMixin, skillListMixin], }; diff --git a/app/imports/client/ui/properties/forms/ReferenceForm.vue b/app/imports/client/ui/properties/forms/ReferenceForm.vue index 88032c03..1248f89a 100644 --- a/app/imports/client/ui/properties/forms/ReferenceForm.vue +++ b/app/imports/client/ui/properties/forms/ReferenceForm.vue @@ -1,34 +1,66 @@ @@ -36,12 +68,12 @@ import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue'; import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js'; import updateReferenceNode from '/imports/api/library/methods/updateReferenceNode.js'; - import PropertyField from '/imports/client/ui/properties/viewers/shared/PropertyField.vue'; + import OutlinedInput from '/imports/client/ui/properties/viewers/shared/OutlinedInput.vue'; export default { components: { TreeNodeView, - PropertyField, + OutlinedInput, }, mixins: [propertyFormMixin], data(){return { diff --git a/app/imports/client/ui/properties/forms/RollForm.vue b/app/imports/client/ui/properties/forms/RollForm.vue index 87b4664a..27b326c4 100644 --- a/app/imports/client/ui/properties/forms/RollForm.vue +++ b/app/imports/client/ui/properties/forms/RollForm.vue @@ -1,17 +1,6 @@ diff --git a/app/imports/client/ui/properties/forms/SavingThrowForm.vue b/app/imports/client/ui/properties/forms/SavingThrowForm.vue index b2bd4cf5..4b5aaf25 100644 --- a/app/imports/client/ui/properties/forms/SavingThrowForm.vue +++ b/app/imports/client/ui/properties/forms/SavingThrowForm.vue @@ -1,25 +1,13 @@ @@ -87,25 +63,5 @@ import propertyFormMixin from '/imports/client/ui/properties/forms/shared/proper export default { mixins: [saveListMixin, propertyFormMixin], - computed: { - targetOptions() { - return [ - { - text: 'Self', - value: 'self', - }, { - text: 'Target', - value: 'target', - }, - ]; - }, - targetOptionHint() { - let hints = { - self: 'The save will be applied to the character taking the action', - target: 'The save will be applied to the targets of the action', - }; - return hints[this.model.target]; - } - }, }; diff --git a/app/imports/client/ui/properties/forms/SkillForm.vue b/app/imports/client/ui/properties/forms/SkillForm.vue index e7c2ab54..7078ea59 100644 --- a/app/imports/client/ui/properties/forms/SkillForm.vue +++ b/app/imports/client/ui/properties/forms/SkillForm.vue @@ -1,85 +1,105 @@ @@ -89,11 +109,13 @@ import ProficiencySelect from '/imports/client/ui/properties/forms/shared/Profic import FormSection from '/imports/client/ui/properties/forms/shared/FormSection.vue'; import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js'; import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js'; +import TagTargeting from '/imports/client/ui/properties/forms/shared/TagTargeting.vue'; export default { components: { ProficiencySelect, FormSection, + TagTargeting, }, mixins: [propertyFormMixin], data() { diff --git a/app/imports/client/ui/properties/forms/SlotFillerForm.vue b/app/imports/client/ui/properties/forms/SlotFillerForm.vue deleted file mode 100644 index 88e0f350..00000000 --- a/app/imports/client/ui/properties/forms/SlotFillerForm.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - diff --git a/app/imports/client/ui/properties/forms/SlotForm.vue b/app/imports/client/ui/properties/forms/SlotForm.vue index e973932b..14fab234 100644 --- a/app/imports/client/ui/properties/forms/SlotForm.vue +++ b/app/imports/client/ui/properties/forms/SlotForm.vue @@ -1,200 +1,144 @@ @@ -203,19 +147,19 @@ import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js'; import FormSection from '/imports/client/ui/properties/forms/shared/FormSection.vue'; import PROPERTIES from '/imports/constants/PROPERTIES.js'; -import { SlotSchema } from '/imports/api/properties/Slots.js'; +import TagTargeting from '/imports/client/ui/properties/forms/shared/TagTargeting.vue'; +import OutlinedInput from '/imports/client/ui/properties/viewers/shared/OutlinedInput.vue'; export default { components: { FormSection, + TagTargeting, + OutlinedInput, }, mixins: [propertyFormMixin], inject: { context: { default: {} } }, - props: { - classForm: Boolean, - }, data() { let slotTypes = []; for (let key in PROPERTIES) { @@ -223,8 +167,6 @@ export default { } return { slotTypes, - addExtraTagsLoading: false, - extraTagOperations: ['OR', 'NOT'], uniqueOptions: [{ text: 'Each property inside this slot should be unique', value: 'uniqueInSlot', @@ -234,29 +176,7 @@ export default { }], }; }, - computed: { - extraTagsFull() { - if (!this.model.extraTags) return false; - let maxCount = SlotSchema.get('extraTags', 'maxCount'); - return this.model.extraTags.length >= maxCount; - } - }, methods: { - acknowledgeAddResult() { - this.addExtraTagsLoading = false; - }, - addExtraTags() { - this.addExtraTagsLoading = true; - this.$emit('push', { - path: ['extraTags'], - value: { - _id: Random.id(), - operation: 'OR', - tags: [], - }, - ack: this.acknowledgeAddResult, - }); - }, testSlot() { if (!this.context.isLibraryForm) return; this.$store.commit('pushDialogStack', { diff --git a/app/imports/client/ui/properties/forms/SpellForm.vue b/app/imports/client/ui/properties/forms/SpellForm.vue index 14c06817..34a6cce1 100644 --- a/app/imports/client/ui/properties/forms/SpellForm.vue +++ b/app/imports/client/ui/properties/forms/SpellForm.vue @@ -1,17 +1,5 @@ @@ -311,7 +287,6 @@ - - diff --git a/app/imports/client/ui/properties/forms/ToggleForm.vue b/app/imports/client/ui/properties/forms/ToggleForm.vue index 3a9f80fe..24cbd6d5 100644 --- a/app/imports/client/ui/properties/forms/ToggleForm.vue +++ b/app/imports/client/ui/properties/forms/ToggleForm.vue @@ -1,18 +1,6 @@ diff --git a/app/imports/client/ui/properties/forms/shared/IconColorMenu.vue b/app/imports/client/ui/properties/forms/shared/IconColorMenu.vue index aa58ed16..824f645c 100644 --- a/app/imports/client/ui/properties/forms/shared/IconColorMenu.vue +++ b/app/imports/client/ui/properties/forms/shared/IconColorMenu.vue @@ -1,32 +1,39 @@ diff --git a/app/imports/client/ui/properties/forms/shared/TagTargeting.vue b/app/imports/client/ui/properties/forms/shared/TagTargeting.vue new file mode 100644 index 00000000..fb9663a0 --- /dev/null +++ b/app/imports/client/ui/properties/forms/shared/TagTargeting.vue @@ -0,0 +1,141 @@ + + + \ No newline at end of file diff --git a/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js index 28cf4172..def2dba2 100644 --- a/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js +++ b/app/imports/client/ui/properties/forms/shared/propertyFormIndex.js @@ -22,7 +22,6 @@ import RollForm from '/imports/client/ui/properties/forms/RollForm.vue'; import SavingThrowForm from '/imports/client/ui/properties/forms/SavingThrowForm.vue'; import SkillForm from '/imports/client/ui/properties/forms/SkillForm.vue'; import SlotForm from '/imports/client/ui/properties/forms/SlotForm.vue'; -import SlotFillerForm from '/imports/client/ui/properties/forms/SlotFillerForm.vue'; import SpellListForm from '/imports/client/ui/properties/forms/SpellListForm.vue'; import SpellForm from '/imports/client/ui/properties/forms/SpellForm.vue'; import ToggleForm from '/imports/client/ui/properties/forms/ToggleForm.vue'; @@ -53,7 +52,6 @@ export default { roll: RollForm, savingThrow: SavingThrowForm, skill: SkillForm, - slotFiller: SlotFillerForm, spellList: SpellListForm, spell: SpellForm, toggle: ToggleForm, diff --git a/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js b/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js index 58c3ec28..84f6760e 100644 --- a/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js +++ b/app/imports/client/ui/properties/forms/shared/propertyFormMixin.js @@ -19,22 +19,12 @@ export default { default: () => ({}), }, }, - mounted(){ - // Don't autofocus on mobile, it brings up the on-screen keyboard - if (this.$vuetify.breakpoint.smAndDown) return; - - setTimeout(() => { - if (this.$refs.focusFirst && this.$refs.focusFirst.focus){ - this.$refs.focusFirst.focus() - } - }, 300); - }, methods: { - change(path, value, ack){ - if (!Array.isArray(path)){ + change(path, value, ack) { + if (!Array.isArray(path)) { path = [path]; } - this.$emit('change', {path, value, ack}); + this.$emit('change', { path, value, ack }); } }, } diff --git a/app/imports/client/ui/properties/shared/PropertySelectCard.vue b/app/imports/client/ui/properties/shared/PropertySelectCard.vue index 2e7f08f8..e8fab6fe 100644 --- a/app/imports/client/ui/properties/shared/PropertySelectCard.vue +++ b/app/imports/client/ui/properties/shared/PropertySelectCard.vue @@ -2,6 +2,8 @@ @@ -46,6 +47,7 @@ > @@ -72,6 +74,10 @@ export default { type: Array, default: undefined, }, + currentType: { + type: String, + default: undefined, + } }, data() { return { diff --git a/app/imports/client/ui/properties/shared/PropertyViewer.vue b/app/imports/client/ui/properties/shared/PropertyViewer.vue index 20a3aeed..de7777b0 100644 --- a/app/imports/client/ui/properties/shared/PropertyViewer.vue +++ b/app/imports/client/ui/properties/shared/PropertyViewer.vue @@ -1,11 +1,149 @@ \ No newline at end of file diff --git a/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js index 456bdbf5..29753094 100644 --- a/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js +++ b/app/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js @@ -22,7 +22,6 @@ import RollViewer from '/imports/client/ui/properties/viewers/RollViewer.vue'; import SkillViewer from '/imports/client/ui/properties/viewers/SkillViewer.vue'; import SavingThrowViewer from '/imports/client/ui/properties/viewers/SavingThrowViewer.vue'; import SlotViewer from '/imports/client/ui/properties/viewers/SlotViewer.vue'; -import SlotFillerViewer from '/imports/client/ui/properties/viewers/SlotFillerViewer.vue'; import SpellListViewer from '/imports/client/ui/properties/viewers/SpellListViewer.vue'; import SpellViewer from '/imports/client/ui/properties/viewers/SpellViewer.vue'; import ToggleViewer from '/imports/client/ui/properties/viewers/ToggleViewer.vue'; @@ -52,7 +51,6 @@ export default { roll: RollViewer, reference: ReferenceViewer, savingThrow: SavingThrowViewer, - slotFiller: SlotFillerViewer, skill: SkillViewer, spellList: SpellListViewer, spell: SpellViewer, diff --git a/app/imports/client/ui/router.js b/app/imports/client/ui/router.js index f26f720d..4f3fe2cc 100644 --- a/app/imports/client/ui/router.js +++ b/app/imports/client/ui/router.js @@ -9,10 +9,12 @@ const CharacterListToolbarItems = () => import('/imports/client/ui/creature/crea const Library = () => import('/imports/client/ui/pages/Library.vue'); const LibraryCollection = () => import('/imports/client/ui/pages/LibraryCollection.vue'); const LibraryCollectionToolbar = () => import('/imports/client/ui/library/LibraryCollectionToolbar.vue'); +const LibraryBrowser = () => import('/imports/client/ui/pages/LibraryBrowser.vue'); const CharacterSheetPage = () => import('/imports/client/ui/pages/CharacterSheetPage.vue'); const CharacterSheetToolbar = () => import('/imports/client/ui/creature/character/CharacterSheetToolbar.vue'); const CharacterSheetRightDrawer = () => import('/imports/client/ui/creature/character/CharacterSheetRightDrawer.vue'); const CharacterSheetPrinted = () => import('/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue'); +const CharacterSheetPrintedToolbar = () => import('/imports/client/ui/creature/character/printedCharacterSheet/CharacterSheetPrintedToolbar.vue'); const SignIn = () => import('/imports/client/ui/pages/SignIn.vue'); const Register = () => import('/imports/client/ui/pages/Register.vue'); const IconAdmin = () => import('/imports/client/ui/icons/IconAdmin.vue'); @@ -167,6 +169,15 @@ RouterFactory.configure(router => { meta: { title: 'Library Collection', }, + }, { + name: 'libraryBrowser', + path: '/community-libraries', + components: { + default: LibraryBrowser, + }, + meta: { + title: 'Community Libraries', + }, }, { name: 'characterSheet', path: '/character/:id', @@ -185,11 +196,14 @@ RouterFactory.configure(router => { alias: '/print-character/:id/:urlName', components: { default: CharacterSheetPrinted, + toolbar: CharacterSheetPrintedToolbar, }, meta: { title: 'Print Character Sheet', }, - }, { + }, + /* Not ready for prime time <3 + { path: '/tabletops', name: 'tabletops', component: Tabletops, @@ -203,7 +217,9 @@ RouterFactory.configure(router => { rightDrawer: TabletopRightDrawer, }, beforeEnter: ensureLoggedIn, - }, { + }, + */ + { path: '/friends', components: { default: NotImplemented, @@ -371,7 +387,11 @@ RouterFactory.configure(router => { function redirectIfMaintenance(to, from, next) { if (!MAINTENANCE_MODE) return next(); - if (to?.path === '/admin' || to?.path === '/maintenance' || to?.path === '/sign-in') return next(); + if ( + to?.path === '/admin' || + to?.path === '/maintenance' || + to?.path === '/sign-in' + ) return next(); Tracker.autorun((computation) => { if (userSubscription.ready()) { computation.stop(); diff --git a/app/imports/client/ui/sharing/ShareDialog.vue b/app/imports/client/ui/sharing/ShareDialog.vue index 7db4aa24..5b81c469 100644 --- a/app/imports/client/ui/sharing/ShareDialog.vue +++ b/app/imports/client/ui/sharing/ShareDialog.vue @@ -27,7 +27,7 @@ v-if="model.public && docRef.collection === 'libraries'" readonly label="Link" - :value="'https://beta.dicecloud.com' + $router.resolve({ + :value="'https://dicecloud.com' + $router.resolve({ name: 'singleLibrary', params: { id: model._id }, }).href" diff --git a/app/imports/client/ui/styles/body.css b/app/imports/client/ui/styles/body.css new file mode 100644 index 00000000..d70859e8 --- /dev/null +++ b/app/imports/client/ui/styles/body.css @@ -0,0 +1,37 @@ +html { + --scrollbarBG: #f0f0f0; + --thumbBG: #cdcdcd; + scrollbar-gutter: stable; + background-color: var(--scrollbarBG); +} + +html:has(#app.theme--dark) { + --scrollbarBG: #212121; + --thumbBG: #404040; +} + +#app.theme--dark { + --scrollbarBG: #212121; + --thumbBG: #404040; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--thumbBG) var(--scrollbarBG); +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background-color: var(--scrollbarBG); +} + +::-webkit-scrollbar-thumb { + background-color: var(--thumbBG); +} + +::-webkit-scrollbar-corner { + background-color: rgba(0, 0, 0, 0); +} \ No newline at end of file diff --git a/app/imports/client/ui/styles/stylesIndex.js b/app/imports/client/ui/styles/stylesIndex.js index f9a49d9c..add626e2 100644 --- a/app/imports/client/ui/styles/stylesIndex.js +++ b/app/imports/client/ui/styles/stylesIndex.js @@ -1,3 +1,4 @@ +import './body.css'; import './cardColors.css'; import './cardTitles.css'; import './centeredInputs.css'; diff --git a/app/imports/client/ui/utility/escapeRegex.js b/app/imports/client/ui/utility/escapeRegex.js deleted file mode 100644 index 8ed9c0b9..00000000 --- a/app/imports/client/ui/utility/escapeRegex.js +++ /dev/null @@ -1,3 +0,0 @@ -RegExp.escape = function(s) { - return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); -}; diff --git a/app/imports/client/ui/utility/numberFormatter.js b/app/imports/client/ui/utility/numberFormatter.js new file mode 100644 index 00000000..794498f3 --- /dev/null +++ b/app/imports/client/ui/utility/numberFormatter.js @@ -0,0 +1,3 @@ +const formatter = Intl.NumberFormat('en', { notation: 'compact' }); + +export default formatter; \ No newline at end of file diff --git a/app/imports/client/ui/vuexStore.js b/app/imports/client/ui/vuexStore.js index feaf1544..fec340cd 100644 --- a/app/imports/client/ui/vuexStore.js +++ b/app/imports/client/ui/vuexStore.js @@ -2,8 +2,8 @@ import Vue from 'vue'; import Vuex from 'vuex'; import dialogStackStore from '/imports/client/ui/dialogStack/dialogStackStore.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; -const tabs = ['stats', 'features', 'actions', 'spells', 'inventory', 'journal', 'build', 'tree']; -const tabsWithoutSpells = ['stats', 'features', 'actions', 'inventory', 'journal', 'build', 'tree']; +const tabs = ['stats', 'actions', 'spells', 'inventory', 'features', 'journal', 'build', 'tree']; +const tabsWithoutSpells = ['stats', 'actions', 'inventory', 'features', 'journal', 'build', 'tree']; Vue.use(Vuex); const store = new Vuex.Store({ @@ -17,6 +17,7 @@ const store = new Vuex.Store({ pageTitle: undefined, characterSheetTabs: {}, showDetailsDialog: false, + formExpansions: {}, }, getters: { tabById: (state) => (id) => { @@ -30,7 +31,10 @@ const store = new Vuex.Store({ } else { return tabs[tabNumber] } - } + }, + formExpansionByType: (state) => (type) => { + return state.formExpansions[type] || []; + }, }, mutations: { toggleDrawer(state) { @@ -50,11 +54,27 @@ const store = new Vuex.Store({ document.title = value; }, setTabForCharacterSheet(state, { tab, id }) { + // Convert tab names to tab numbers + if (typeof tab === 'string') { + const creature = Creatures.findOne(id); + if (creature?.settings?.hideSpellsTab) { + tab = tabsWithoutSpells.indexOf(tab); + } else { + tab = tabs.indexOf(tab); + } + if (!(tab > -1)) { + throw 'Could not find requested tab'; + } + console.log('resolved: ', tab); + } Vue.set(state.characterSheetTabs, id, tab); }, setShowDetailsDialog(state, value) { state.showDetailsDialog = value; }, + setFormExpansion(state, { type, value }) { + state.formExpansions[type] = value; + }, }, }); diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index f3db0cbe..4686082c 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -170,13 +170,6 @@ const PROPERTIES = Object.freeze({ helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot', suggestedParents: [], }, - slotFiller: { - icon: 'mdi-power-plug-outline', - name: 'Slot filler', - docsPath: 'property/slot-filler', - 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: { icon: '$vuetify.icons.spell_list', name: 'Spell list', @@ -209,14 +202,25 @@ const PROPERTIES = Object.freeze({ export default PROPERTIES; -export function getPropertyName(type){ - return type && PROPERTIES[type] && PROPERTIES[type].name; +export function getPropertyName(type) { + return (type && PROPERTIES[type] && PROPERTIES[type].name) || type; } -export function getPropertyIcon(type){ +export function getPropertyIcon(type) { return type && PROPERTIES[type] && PROPERTIES[type].icon; } +export function getSuggestedChildren(type) { + const suggestions = []; + for (const key in PROPERTIES) { + const prop = PROPERTIES[key]; + if (prop.suggestedParents.includes(type)) { + suggestions.push({ type: key, details: prop }); + } + } + return suggestions; +} + const propsByDocsPath = new Map(); for (const key in PROPERTIES) { diff --git a/app/imports/constants/SCHEMA_VERSION.js b/app/imports/constants/SCHEMA_VERSION.js index 4ac57384..829e4147 100644 --- a/app/imports/constants/SCHEMA_VERSION.js +++ b/app/imports/constants/SCHEMA_VERSION.js @@ -1,3 +1,3 @@ -const SCHEMA_VERSION = 1; +const SCHEMA_VERSION = 2; export default SCHEMA_VERSION; diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index cbc647e6..f9fd1f6c 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -30,7 +30,7 @@ const STORAGE_LIMITS = Object.freeze({ rollCount: 64, statsToTarget: 64, tagCount: 64, - writersCount: 20, + writersCount: 32, libraryCollectionCount: 32, pointBuyRowsCount: 32, }); diff --git a/app/imports/constants/VARIABLE_NAME_REGEX.js b/app/imports/constants/VARIABLE_NAME_REGEX.js index ac7c1804..8ea249ca 100644 --- a/app/imports/constants/VARIABLE_NAME_REGEX.js +++ b/app/imports/constants/VARIABLE_NAME_REGEX.js @@ -1,4 +1,4 @@ // Must contain a letter, and be made of word characters only -const VARIABLE_NAME_REGEX = /^[a-z][\w-]*$/i; +const VARIABLE_NAME_REGEX = /^[~#]?[a-zA-Z]*[a-ce-zA-Z][a-zA-Z0-9_]*$/i; export default VARIABLE_NAME_REGEX; diff --git a/app/imports/migrations/archive/cleanArchiveAt2.js b/app/imports/migrations/archive/cleanArchiveAt2.js new file mode 100644 index 00000000..224734ba --- /dev/null +++ b/app/imports/migrations/archive/cleanArchiveAt2.js @@ -0,0 +1,16 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; + +export default function cleanAt2(archive) { + archive.properties = archive.properties.map(prop => { + let cleanProp = prop; + try { + const schema = CreatureProperties.simpleSchema(prop); + // Clean according to schema + cleanProp = schema.clean(prop); + schema.validate(cleanProp); + } catch (e) { + console.warn('Failed to clean archive prop', { propId: prop._id, error: e.message || e.reason || e.toString() }); + } + return cleanProp; + }); +} diff --git a/app/imports/migrations/archive/migrateArchive.js b/app/imports/migrations/archive/migrateArchive.js new file mode 100644 index 00000000..029b146e --- /dev/null +++ b/app/imports/migrations/archive/migrateArchive.js @@ -0,0 +1,28 @@ +import migrateTo1 from './migrateArchiveTo1.js'; +import migrate1To2 from './migrateArchive1To2.js'; +import cleanAt2 from './cleanArchiveAt2.js'; + +/* eslint no-fallthrough: "off" -- Using switch fallthrough to run all +migration steps after the current version of the file. */ +export default function migrateArchive(archive) { + switch (archive.meta.schemaVersion) { + // V1 of DiceCloud + case 'version1': + migrateLegacyArchive(archive); + // V2 of DiceCloud, Schema version 1 + case 1: + migrateTo1(archive); + migrate1To2(archive); + // V2 of DiceCloud, Schema version 2 + case 2: + cleanAt2(archive); + break; + default: + throw 'Archive version not supported'; + } +} + +function migrateLegacyArchive() { + // TODO: + throw 'Not implemented'; +} diff --git a/app/imports/migrations/archive/migrateArchive1To2.js b/app/imports/migrations/archive/migrateArchive1To2.js new file mode 100644 index 00000000..b9a6c2a7 --- /dev/null +++ b/app/imports/migrations/archive/migrateArchive1To2.js @@ -0,0 +1,54 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; +import { get } from 'lodash'; + +const dollarSignRegex = /(\W|^)\$(\w+)/gi; + +export default function migrate1To2(archive) { + archive.properties = archive.properties.map(prop => { + try { + // Migrate slot fillers to folders + if (prop.type === 'slotFiller') { + prop.type = 'folder'; + // If the slot filler has a description, change it to a computed one + if (typeof prop.description == 'string') { + prop.description = { text: prop.description }; + } + } + // Migrate slot filler slot type to folders + if (prop.slotType === 'slotFiller') { + prop.slotType = 'folder'; + } + // Get the schema + const schema = CreatureProperties.simpleSchema(prop); + // Replace dollar signs in calculations with tildes + schema.inlineCalculationFields().forEach(key => { + applyFnToKey(prop, key, (prop, key) => { + const inlineCalcObj = get(prop, key); + const string = inlineCalcObj?.text; + if (!string) return; + const newString = string.replace(dollarSignRegex, '$1~$2'); + if (string !== newString) { + inlineCalcObj.text = newString; + inlineCalcObj.hash = null; + } + }); + }); + schema.computedFields().forEach(key => { + applyFnToKey(prop, key, (prop, key) => { + const inlineCalcObj = get(prop, key); + const string = inlineCalcObj?.calculation; + if (!string) return; + const newString = string.replace(dollarSignRegex, '$1~$2'); + if (string !== newString) { + inlineCalcObj.calculation = newString; + inlineCalcObj.hash = null; + } + }); + }); + } catch (e) { + console.warn('Property migration 1 -> 2 failed: ', { propId: prop._id, error: e.message || e.reason || e.toString() }); + } + return prop; + }); +} diff --git a/app/imports/migrations/server/dbv1/cleanAt1.js b/app/imports/migrations/archive/migrateArchiveTo1.js similarity index 62% rename from app/imports/migrations/server/dbv1/cleanAt1.js rename to app/imports/migrations/archive/migrateArchiveTo1.js index d58e3c74..6e8fd558 100644 --- a/app/imports/migrations/server/dbv1/cleanAt1.js +++ b/app/imports/migrations/archive/migrateArchiveTo1.js @@ -1,39 +1,45 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { get, set } from 'lodash'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { calculationUp } from '/imports/migrations/server/dbv1/dbv1.js'; -export default function cleanAt1(archive){ + +function calculationUp(val) { + if (typeof val !== 'string') return val; + if (!val.replace) console.log({ val, replace: val.replace }); + return val.replace(/#(\w+).(\w+)Result/g, '#$1.$2') + .replace(/\.value/g, '.total') + .replace(/\.currentValue/g, '.value'); +} + +export default function migrateTo1(archive) { archive.properties = archive.properties.map(prop => { - let cleanProp = prop; try { if (prop.type === 'attack') prop.type = 'action'; + if (prop.type === 'slotFiller') prop.type = 'folder'; // Get the schema const schema = CreatureProperties.simpleSchema(prop); // Clean all the text fields with inline calcs schema.inlineCalculationFields().forEach(key => { applyFnToKey(prop, key, (prop, key) => { let field = get(prop, key); - if (typeof field === 'string' || typeof field === 'number'){ + if (typeof field === 'string' || typeof field === 'number') { field = calculationUp(field); - set(prop, key, {text: `${field}`}); + set(prop, key, { text: `${field}` }); } }); }); schema.computedFields().forEach(key => { applyFnToKey(prop, key, (prop, key) => { let field = get(prop, key) || get(prop, key + 'Calculation'); - if (typeof field === 'string' || typeof field === 'number'){ + if (typeof field === 'string' || typeof field === 'number') { field = calculationUp(field); - set(prop, key, {calculation: `${field}`}); + set(prop, key, { calculation: `${field}` }); } }); }); - cleanProp = schema.clean(prop); - schema.validate(cleanProp); - } catch (e){ - console.warn({propId: prop._id, error: e.message || e.reason || e.toString()}); + } catch (e) { + console.warn('Property migration -> 1 failed: ', { propId: prop._id, error: e.message || e.reason || e.toString() }); } - return cleanProp; + return prop; }); } diff --git a/app/imports/migrations/server/dbv1/dbv1.js b/app/imports/migrations/server/dbv1/dbv1.js index 8ecfef81..afcce135 100644 --- a/app/imports/migrations/server/dbv1/dbv1.js +++ b/app/imports/migrations/server/dbv1/dbv1.js @@ -10,52 +10,52 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; Migrations.add({ version: 1, name: 'Unifies calculated field schema', - up(){ + up() { migrate(); }, - down(){ - migrate({reversed: true}); + down() { + migrate({ reversed: true }); }, }); -function migrate({reversed} = {}){ +function migrate({ reversed } = {}) { console.log('migrating creature properties'); - migrateCollection({collection: CreatureProperties, reversed}); + migrateCollection({ collection: CreatureProperties, reversed }); console.log('migrating library nodes') - migrateCollection({collection: LibraryNodes, reversed}); + migrateCollection({ collection: LibraryNodes, reversed }); } -function migrateCollection({collection, reversed}){ +function migrateCollection({ collection, reversed }) { const bulk = collection.rawCollection().initializeUnorderedBulkOp(); collection.find({}).forEach(prop => { - const newProp = migrateProperty({collection, reversed, prop}); + const newProp = migrateProperty({ collection, reversed, prop }); bulk.find({ _id: prop._id }).replaceOne(newProp); }); bulk.execute(); } -export function migrateProperty({collection, reversed, prop}){ +export function migrateProperty({ collection, reversed, prop }) { const transforms = [ ...(transformsByPropType[prop.type] || []), - {from: 'dependencies'} + { from: 'dependencies' } ]; let migratedProp = transformFields(prop, transforms, reversed); - const schema = collection.simpleSchema({type: migratedProp.type}); + const schema = collection.simpleSchema({ type: migratedProp.type }); // Only clean if the schema version matches our destination version - if(!reversed && SCHEMA_VERSION === 1){ + if (!reversed && SCHEMA_VERSION >= 1) { try { migratedProp = schema.clean(migratedProp); schema.validate(migratedProp); - } catch(e){ - if (e.details[0]?.type === 'maxString'){ + } catch (e) { + if (e.details[0]?.type === 'maxString') { console.log({ prop: prop, details: e.details, }); } else { - console.warn({prop, error: e}); + console.warn({ prop, error: e }); } } } @@ -74,31 +74,31 @@ const transformsByPropType = { 'action': actionTransforms, 'adjustment': [ ...getComputedPropertyTransforms('amount'), - {from: 'target', to: 'target', up: simplifyTarget}, + { from: 'target', to: 'target', up: simplifyTarget }, ], 'attack': [ ...actionTransforms, ...getComputedPropertyTransforms('rollBonus', 'attackRoll'), //change type to action - {from: 'type', to: 'type', up: () => 'action'}, + { from: 'type', to: 'type', up: () => 'action' }, ], 'attribute': [ // from: baseValue must be first or else it will delete the field we need - {from: 'baseValue', to: 'baseValue.value', up: nanToNull}, - {from: 'baseValueCalculation', to: 'baseValue.calculation', up: calculationUp, down: calculationDown}, - {from: 'baseValueErrors', to: 'baseValue.errors', up: trimErrors}, + { from: 'baseValue', to: 'baseValue.value', up: nanToNull }, + { from: 'baseValueCalculation', to: 'baseValue.calculation', up: calculationUp, down: calculationDown }, + { from: 'baseValueErrors', to: 'baseValue.errors', up: trimErrors }, ...getComputedPropertyTransforms('spellSlotLevel'), ...getInlineComputationTransforms('description'), - {from: 'value', to: 'total', up: nanToNull}, - {from: 'currentValue', to: 'value', up: nanToNull}, - {from: 'proficiency', to: 'proficiency', up: stripZero}, + { from: 'value', to: 'total', up: nanToNull }, + { from: 'currentValue', to: 'value', up: nanToNull }, + { from: 'proficiency', to: 'proficiency', up: stripZero }, ], 'buff': [ ...getComputedPropertyTransforms('duration'), ...getInlineComputationTransforms('description'), - {from: 'value', to: 'total', up: nanToNull}, - {from: 'target', to: 'target', up: simplifyTarget}, - {from: 'applied'}, + { from: 'value', to: 'total', up: nanToNull }, + { from: 'target', to: 'target', up: simplifyTarget }, + { from: 'applied' }, ], 'classLevel': [ ...getInlineComputationTransforms('description'), @@ -108,20 +108,22 @@ const transformsByPropType = { ], 'damage': [ ...getComputedPropertyTransforms('amount'), - {from: 'target', to: 'target', up: simplifyTarget}, + { from: 'target', to: 'target', up: simplifyTarget }, ], 'effect': [ - {from: 'calculation', to: 'amount.calculation'}, - {from: 'result', to: 'amount.value', up: nanToNull}, - {from: 'errors', to: 'amount.errors', up: trimErrors}, - {from: 'name', to: 'name', up(val, src, doc){ - if (src.operation === 'conditional'){ - doc.text = val; - return; - } else { - return val; + { from: 'calculation', to: 'amount.calculation' }, + { from: 'result', to: 'amount.value', up: nanToNull }, + { from: 'errors', to: 'amount.errors', up: trimErrors }, + { + from: 'name', to: 'name', up(val, src, doc) { + if (src.operation === 'conditional') { + doc.text = val; + return; + } else { + return val; + } } - }}, + }, ], 'feature': [ ...getInlineComputationTransforms('summary'), @@ -139,20 +141,20 @@ const transformsByPropType = { ], 'savingThrow': [ ...getComputedPropertyTransforms('dc'), - {from: 'target', to: 'target', up: simplifyTarget}, + { from: 'target', to: 'target', up: simplifyTarget }, ], 'skill': [ ...getComputedPropertyTransforms('baseValue'), ...getInlineComputationTransforms('description'), - {from: 'value', to: 'value', up: nanToNull}, - {from: 'passiveBonus', to: 'passiveBonus', up: nanToNull}, - {from: 'proficiency', to: 'proficiency', up: stripZero}, + { from: 'value', to: 'value', up: nanToNull }, + { from: 'passiveBonus', to: 'passiveBonus', up: nanToNull }, + { from: 'proficiency', to: 'proficiency', up: stripZero }, ], 'spell': [ ...actionTransforms, ], 'proficiency': [ - {from: 'value', to: 'value', up: stripZero}, + { from: 'value', to: 'value', up: stripZero }, ], 'propertySlot': [ ...getComputedPropertyTransforms('quantityExpected'), @@ -166,70 +168,70 @@ const transformsByPropType = { ...getInlineComputationTransforms('description'), ], 'toggle': [ - {from: 'condition', to: 'condition.calculation'}, - {from: 'toggleResult', to: 'condition.value', up: nanToNull}, - {from: 'errors', to: 'condition.errors', up: trimErrors}, + { from: 'condition', to: 'condition.calculation' }, + { from: 'toggleResult', to: 'condition.value', up: nanToNull }, + { from: 'errors', to: 'condition.errors', up: trimErrors }, ], }; -function getComputedPropertyTransforms(key, toKey){ +function getComputedPropertyTransforms(key, toKey) { if (!toKey) toKey = key; return [ - {from: key, to: `${toKey}.calculation`, up: calculationUp, down: calculationDown}, - {from: `${key}Result`, to: `${toKey}.value`, up: nanToNull}, - {from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors}, + { from: key, to: `${toKey}.calculation`, up: calculationUp, down: calculationDown }, + { from: `${key}Result`, to: `${toKey}.value`, up: nanToNull }, + { from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors }, ]; } -function getInlineComputationTransforms(key){ +function getInlineComputationTransforms(key) { return [ - {from: key, to: `${key}.text`, up: calculationUp, down: calculationDown}, - {from: `${key}Calculations`, to: `${key}.inlineCalculations`, up: calculationUp, down: calculationDown}, - {from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`}, + { from: key, to: `${key}.text`, up: calculationUp, down: calculationDown }, + { from: `${key}Calculations`, to: `${key}.inlineCalculations`, up: calculationUp, down: calculationDown }, + { from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value` }, ]; } -export function calculationUp(val){ +export function calculationUp(val) { if (typeof val !== 'string') return val; - if (!val.replace) console.log({val, replace: val.replace}); + if (!val.replace) console.log({ val, replace: val.replace }); return val.replace(/#(\w+).(\w+)Result/g, '#$1.$2') .replace(/\.value/g, '.total') .replace(/\.currentValue/g, '.value'); } -function calculationDown(val){ +function calculationDown(val) { if (typeof val !== 'string') return val; return val.replace(/\.value/g, '.currentValue').replace(/\.total/g, '.value'); } -function nanToNull(val){ - if (Number.isNaN(val)){ +function nanToNull(val) { + if (Number.isNaN(val)) { return null; } else { return val; } } -function stripZero(val){ - if (val === 0){ +function stripZero(val) { + if (val === 0) { return undefined; } else { return val; } } -function simplifyTarget(val){ - if (val === 'self'){ +function simplifyTarget(val) { + if (val === 'self') { return val; } else { return 'target'; } } -function trimErrors(arr){ - if(!arr) return arr; +function trimErrors(arr) { + if (!arr) return arr; arr.forEach(e => { - if (e.message.length > STORAGE_LIMITS.errorMessage){ + if (e.message.length > STORAGE_LIMITS.errorMessage) { e.message = e.message.slice(0, STORAGE_LIMITS.errorMessage); } }); diff --git a/app/imports/migrations/server/dbv1/dbv1.test.js b/app/imports/migrations/server/dbv1/dbv1.test.js index efbd4fdd..edc81d81 100644 --- a/app/imports/migrations/server/dbv1/dbv1.test.js +++ b/app/imports/migrations/server/dbv1/dbv1.test.js @@ -31,7 +31,7 @@ const exampleAction = { 'ancestors': [{ 'collection': 'creatures', 'id': 'X9rzFhsgFhodYfHmG' - }, ], + },], 'order': 315, 'summary': 'Curse a creature for 1 minute. The curse ends early if {warlock.level >14 ? "" : "the target dies, or"} you are incapacitated. \nGain the following benefits: \n- *Bonus to damage rolls against the cursed target of* **+{proficiencyBonus}**. \n- Any attack roll you make against the cursed target is a **critical hit on a roll of 19 or 20**. \n- If the cursed target dies, you **regain {warlock.level+charisma.modifier} hit points**. \n{warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."}', 'uses': '1', @@ -45,21 +45,21 @@ const exampleAction = { 'result': '- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.' }], 'summaryCalculations': [{ - 'calculation': 'warlock.level >14 ? "" : "the target dies, or"', - 'result': 'the target dies, or' - }, - { - 'calculation': 'proficiencyBonus', - 'result': '4' - }, - { - 'calculation': 'warlock.level+charisma.modifier', - 'result': '15' - }, - { - 'calculation': 'warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."', - 'result': '- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.' - } + 'calculation': 'warlock.level >14 ? "" : "the target dies, or"', + 'result': 'the target dies, or' + }, + { + 'calculation': 'proficiencyBonus', + 'result': '4' + }, + { + 'calculation': 'warlock.level+charisma.modifier', + 'result': '15' + }, + { + 'calculation': 'warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."', + 'result': '- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.' + } ] }; @@ -76,13 +76,13 @@ const exampleAttribute = { 'collection': 'creatureProperties' }, ancestors: [{ - 'collection': 'creatures', - 'id': 'm9sdCvs6iDf7qRaGv' - }, - { - 'id': '8jSWKxvgQyKbunFtD', - 'collection': 'creatureProperties' - } + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv' + }, + { + 'id': '8jSWKxvgQyKbunFtD', + 'collection': 'creatureProperties' + } ], order: 84, value: 20, @@ -110,13 +110,13 @@ const expectedMigratedAttribute = { 'collection': 'creatureProperties' }, ancestors: [{ - 'collection': 'creatures', - 'id': 'm9sdCvs6iDf7qRaGv' - }, - { - 'id': '8jSWKxvgQyKbunFtD', - 'collection': 'creatureProperties' - } + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv' + }, + { + 'id': '8jSWKxvgQyKbunFtD', + 'collection': 'creatureProperties' + } ], order: 84, total: 20, @@ -205,11 +205,10 @@ const expectedMigratedAttack = { }], 'order': 56, 'usesUsed': 2, - libraryTags: [], } -describe('migrateProperty', function() { - it('Migrates actions reversibly', function() { +describe('migrateProperty', function () { + it('Migrates actions reversibly', function () { const action = { ...exampleAction }; @@ -226,7 +225,7 @@ describe('migrateProperty', function() { assert.deepEqual(action, exampleAction, 'action should not be bashed'); assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible'); }); - it('Migrates attributes as expected', function() { + it('Migrates attributes as expected', function () { const attribute = { ...exampleAttribute }; @@ -237,7 +236,7 @@ describe('migrateProperty', function() { assert.deepEqual(newAttribute, expectedMigratedAttribute, 'Attribute should match the expected result'); }); - it('Migrates attacks as expected', function() { + it('Migrates attacks as expected', function () { const attribute = { ...exampleAttack }; diff --git a/app/imports/migrations/server/dbv2/dbv2.js b/app/imports/migrations/server/dbv2/dbv2.js new file mode 100644 index 00000000..1a78d4cd --- /dev/null +++ b/app/imports/migrations/server/dbv2/dbv2.js @@ -0,0 +1,174 @@ +import { Migrations } from 'meteor/percolate:migrations'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { union, get } from 'lodash'; +import Libraries from '/imports/api/library/Libraries.js'; +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; + +// Git version 2.0.52 +// Database version 2 +Migrations.add({ + version: 2, + name: 'Separates creature property tags from library tags', + + up() { + console.log('migrating up library nodes 1 -> 2'); + migrateCollection(LibraryNodes, migratePropUp); + console.log('migrating up creature props 1 -> 2'); + migrateCollection(CreatureProperties, migratePropUp); + console.log('Migrating up libraries and collections to count subscribers'); + countSubscribers(); + }, + + down() { + console.log('Migrating down library nodes 2 -> 1'); + migrateCollection(LibraryNodes, migratePropDown); + console.log('Migrating down creature props 2 -> 1'); + migrateCollection(CreatureProperties, migratePropDown); + }, + +}); + +function migrateCollection(collection, migrateDoc) { + collection.find({}).forEach((doc, index) => { + if (index % 1000 === 0) { + console.log(`Migrating document #${index}`); + } + migrateDoc(doc, collection) + }); +} + +export function migratePropUp(prop, collection) { + let update; + if (prop.type === 'slotFiller') { + update = update || { $set: {} }; + // Change the type to folder, slotFiller is deprecated + update.$set.type = 'folder' + // If the slot filler has an image set, move it + if (typeof prop.picture === 'string') { + update.$set.slotFillImage = prop.picture; + update.$unset = { picture: 1 }; + } + // If the slot filler has a description, change it to a computed one + if (typeof prop.description == 'string') { + prop.description = { text: prop.description }; + } + } + + // Don't look for slot fillers + if (prop.slotType === 'slotFiller') { + update = update || { $set: {} }; + update.$set.slotType = 'folder' + } + + // If there are tags, copy them to libraryTags and set findable flags + if (Array.isArray(prop.tags) && prop.tags.length && collection === LibraryNodes) { + update = update || { $set: {} }; + update.$set.libraryTags = prop.tags; + update.$set.fillSlots = true; + update.$set.searchable = true; + } + + // Replace dollar sign with tilde in calculated fields + update = dollarSignToTilde(prop, update); + + // update the document + if (update) { + try { + collection.update({ _id: prop._id }, update, { bypassCollection2: true }, e => { + if (e) console.warn('Doc Migration failed: ', prop._id, e); + }); + } catch (e) { + console.warn('Doc Migration failed: ', prop._id, e); + } + } +} + +export function migratePropDown(prop, collection) { + const update = { + $unset: { + slotFillImage: 1, + slotFillerCondition: 1, + libraryTags: 1, + fillSlots: 1, + searchable: 1, + } + }; + if (prop.libraryTags?.length) { + update.$set = { + tags: union(prop.libraryTags, prop.tags) + } + } + if (update) { + try { + collection.update({ _id: prop._id }, update, { bypassCollection2: true }, e => { + if (e) console.warn('Doc Migration failed: ', prop._id, e); + }); + } catch (e) { + console.warn('Doc Migration failed: ', prop._id, e); + } + } +} + +function countSubscribers() { + const bulkLib = Libraries.rawCollection().initializeUnorderedBulkOp(); + Libraries.find({}, { + fields: { _id: 1 } + }).forEach(lib => { + bulkLib.find({ _id: lib._id }).updateOne({ + $set: { + subscriberCount: Meteor.users.find({ subscribedLibraries: lib._id }).count(), + } + }); + }); + bulkLib.execute(); + + const bulkLibCols = LibraryCollections.rawCollection().initializeUnorderedBulkOp(); + LibraryCollections.find({}, { + fields: { _id: 1 } + }).forEach(col => { + bulkLibCols.find({ _id: col._id }).updateOne({ + $set: { + subscriberCount: Meteor.users.find({ subscribedLibraryCollections: col._id }).count(), + } + }); + }); + bulkLibCols.execute(); +} + +const dollarSignRegex = /(\W|^)\$(\w+)/gi; +function dollarSignToTilde(prop, update) { + computedSchemas[prop.type]?.inlineCalculationFields()?.forEach(calcKey => { + applyFnToKey(prop, calcKey, (prop, key) => { + const inlineCalcObj = get(prop, key); + const string = inlineCalcObj?.text; + if (!string) return; + const newString = string.replace(dollarSignRegex, '$1~$2'); + if (string !== newString) { + // If changed + update = update || { $set: {} }; + if (!update.$unset) update.$unset = {}; + update.$unset[key + '.hash'] = 1; // zero the hash so it re-parses the calculation + update.$set[key + '.text'] = newString + } + }); + }); + computedSchemas[prop.type]?.computedFields()?.forEach(calcKey => { + applyFnToKey(prop, calcKey, (prop, key) => { + const inlineCalcObj = get(prop, key); + const string = inlineCalcObj?.calculation; + if (!string) return; + const newString = string.replace(dollarSignRegex, '$1~$2'); + if (string !== newString) { + // If changed + update = update || { $set: {} }; + if (!update.$unset) update.$unset = {}; + update.$unset[key + '.hash'] = 1; // remove the hash so it re-parses the calculation + update.$set[key + '.calculation'] = newString + } + }); + }); + return update; +} diff --git a/app/imports/migrations/server/dbv2/dbv2.test.js b/app/imports/migrations/server/dbv2/dbv2.test.js new file mode 100644 index 00000000..642f4c2f --- /dev/null +++ b/app/imports/migrations/server/dbv2/dbv2.test.js @@ -0,0 +1,179 @@ +import { migratePropUp, migratePropDown } from './dbv2.js'; +import { assert } from 'chai'; + +const exampleAttack = { + '_id': 'vw23EnJwBRcXEJg7i', + 'actionType': 'attack', + 'target': 'singleTarget', + 'tags': ['attack', 'magical', 'very cool'], + 'resources': { + 'itemsConsumed': [], + 'attributesConsumed': [] + }, + 'attackRoll': { + calculation: 'dexterity.modifier + proficiency$Bonus + 2 - hp.total + hp.value + $dollarSign', + hash: 1234567, + }, + 'summary': { + text: 'What if we {$had} two {$dollarSigns?} ', + hash: 123456, + }, + 'type': 'action', + 'name': 'Claws', + 'parent': { + 'id': 'Jpx8q3WjM5SCoGBm8', + 'collection': 'creatureProperties' + }, + 'ancestors': [{ + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv' + }, { + 'id': '3WS2xsSPAqB4eF9YH', + 'collection': 'creatureProperties' + }, { + 'id': 'rhYLEycvtHjcioaQL', + 'collection': 'creatureProperties' + }, { + 'id': 'Jpx8q3WjM5SCoGBm8', + 'collection': 'creatureProperties' + }], + 'order': 56, + 'usesUsed': 2, +}; + +const expectedAttackUpdate = { + $set: { + 'attackRoll.calculation': 'dexterity.modifier + proficiency$Bonus + 2 - hp.total + hp.value + ~dollarSign', + 'libraryTags': ['attack', 'magical', 'very cool'], + 'fillSlots': true, + 'searchable': true, + 'summary.text': 'What if we {~had} two {~dollarSigns?} ', + }, + $unset: { + 'attackRoll.hash': 1, + 'summary.hash': 1, + }, +}; + +const emptyFolderExample = { + _id: 'DXPYsHKF6W8Hh3hZs', + type: 'folder', + name: 'Empty Folder', + 'parent': { + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv', + }, + 'ancestors': [{ + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv', + }], +}; + +const exampleSlotFiller = { + _id: 'DXPYsHKF6888h3hZs', + type: 'slotFiller', + name: 'Slot Filler Example', + 'picture': 'https://url.to.pic', + 'tags': ['slot', 'tags'], + 'parent': { + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv', + }, + 'ancestors': [{ + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv', + }], +}; +const expectedSlotFillerUpdate = { + $set: { + 'libraryTags': ['slot', 'tags'], + 'fillSlots': true, + 'searchable': true, + 'slotFillImage': 'https://url.to.pic', + }, + $unset: { + picture: 1, + }, +}; + +const DownMergeExample = { + _id: 'DXPYsHKF6W8Hh3hZs', + type: 'feature', + name: 'Feature With Tags and library Tags', + 'parent': { + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv', + }, + 'ancestors': [{ + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv', + }], + 'libraryTags': ['tags', 'from', 'library'], + 'tags': ['attack', 'magical', 'very cool'], +}; + +const expectedDownMergeUpdate = { + $unset: { + slotFillImage: 1, + slotFillerCondition: 1, + libraryTags: 1, + fillSlots: 1, + searchable: 1, + }, + $set: { + tags: ['tags', 'from', 'library', 'attack', 'magical', 'very cool'], + } +}; + +describe('dbv2 Migrate library nodes', function () { + it('Migrates attacks up', function () { + const bulk = stubBulk(); + migratePropUp(bulk, exampleAttack); + const { query, update } = bulk.result(); + assert.deepEqual(query, { _id: 'vw23EnJwBRcXEJg7i' }, 'The query should match the id of the given prop'); + assert.deepEqual(update, expectedAttackUpdate, 'The update should match the expected update'); + }); + it('Migrates props without tags up', function () { + const bulk = stubBulk(); + migratePropUp(bulk, emptyFolderExample); + const { query, update, timesFind, timesUpdate } = bulk.result(); + assert.isUndefined(query, 'There should be no query on a prop with no tags'); + assert.equal(timesFind, 0, 'Find should be called zero times on a prop with no tags'); + assert.isUndefined(update, 'There should be no update on a prop with no tags'); + assert.equal(timesUpdate, 0, 'Update should be called zero times on a prop with no tags'); + }); + it('Migrates slot fillers up', function () { + const bulk = stubBulk(); + migratePropUp(bulk, exampleSlotFiller); + const { query, update } = bulk.result(); + assert.deepEqual(query, { _id: 'DXPYsHKF6888h3hZs' }, 'The query should match the id of the given prop'); + assert.deepEqual(update, expectedSlotFillerUpdate, 'The update should match the expected update'); + }); + it('Merges tags when down migrating', function () { + const bulk = stubBulk(); + migratePropDown(bulk, DownMergeExample); + const { query, update } = bulk.result(); + assert.deepEqual(query, { _id: 'DXPYsHKF6W8Hh3hZs' }, 'The query should match the id of the given prop'); + assert.deepEqual(update, expectedDownMergeUpdate, 'The update should match the expected update'); + }); +}); + +// Create a stub for bulk udateOne operations that accepts a single op +function stubBulk() { + let query, update, timesFind = 0, timesUpdate = 0; + return { + find(inputQuery) { + query = inputQuery; + timesFind += 1; + return { + updateOne(inputUpdate) { + update = inputUpdate; + timesUpdate += 1; + } + } + }, + result() { + return { query, update, timesFind, timesUpdate } + } + } +} \ No newline at end of file diff --git a/app/imports/migrations/server/index.js b/app/imports/migrations/server/index.js index 54ca97d0..07b974f8 100644 --- a/app/imports/migrations/server/index.js +++ b/app/imports/migrations/server/index.js @@ -1 +1,2 @@ import './dbv1/dbv1.js'; +import './dbv2/dbv2.js'; diff --git a/app/imports/migrations/server/migrateArchive.js b/app/imports/migrations/server/migrateArchive.js deleted file mode 100644 index 0fb557ed..00000000 --- a/app/imports/migrations/server/migrateArchive.js +++ /dev/null @@ -1,19 +0,0 @@ -import cleanAt1 from '/imports/migrations/server/dbv1/cleanAt1.js'; - -/* eslint no-fallthrough: "off" -- Using switch fallthrough to run all -migration steps after the current version of the file. */ -export default function migrateArchive(archive){ - switch (archive.meta.schemaVersion){ - // V1 of DiceCloud - case 'version1': - migrateLegacyArchive(archive); - // V2 of DiceCloud, Schema version 1 - case 1: - cleanAt1(archive); - } -} - -function migrateLegacyArchive(archive){ - // TODO: - throw 'Not implemented'; -} diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index 90a7e8df..9482fd9b 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -1,11 +1,12 @@ import resolve from '/imports/parser/resolve.js' +import rollDice from '/imports/parser/rollDice.js'; export default { 'abs': { comment: 'Returns the absolute value of a number', examples: [ - {input: 'abs(9)', result: '9'}, - {input: 'abs(-3)', result: '3'}, + { input: 'abs(9)', result: '9' }, + { input: 'abs(-3)', result: '3' }, ], arguments: ['number'], resultType: 'number', @@ -14,8 +15,8 @@ export default { 'sqrt': { comment: 'Returns the square root of a number', examples: [ - {input: 'sqrt(16)', result: '4'}, - {input: 'sqrt(10)', result: '3.1622776601683795'}, + { input: 'sqrt(16)', result: '4' }, + { input: 'sqrt(10)', result: '3.1622776601683795' }, ], arguments: ['number'], resultType: 'number', @@ -23,14 +24,14 @@ export default { }, 'max': { comment: 'Returns the largest of the given numbers', - examples: [{input: 'max(12, 6, 3, 168)', result: '168'}], + examples: [{ input: 'max(12, 6, 3, 168)', result: '168' }], arguments: anyNumberOf('number'), resultType: 'number', fn: Math.max, }, 'min': { comment: 'Returns the smallest of the given numbers', - examples: [{input: 'min(12, 6, 3, 168)', result: '3'}], + examples: [{ input: 'min(12, 6, 3, 168)', result: '3' }], arguments: anyNumberOf('number'), resultType: 'number', fn: Math.min, @@ -38,9 +39,9 @@ export default { 'round': { comment: 'Returns the value of a number rounded to the nearest integer', examples: [ - {input: 'round(5.95)', result: '6'}, - {input: 'round(5.5)', result: '6'}, - {input: 'round(5.05)', result: '5'}, + { input: 'round(5.95)', result: '6' }, + { input: 'round(5.5)', result: '6' }, + { input: 'round(5.05)', result: '5' }, ], arguments: ['number'], resultType: 'number', @@ -49,10 +50,10 @@ export default { 'floor': { comment: 'Rounds a number down to the next smallest integer', examples: [ - {input: 'floor(5.95)', result: '5'}, - {input: 'floor(5.05)', result: '5'}, - {input: 'floor(5)', result: '5'}, - {input: 'floor(-5.5)', result: '-6'}, + { input: 'floor(5.95)', result: '5' }, + { input: 'floor(5.05)', result: '5' }, + { input: 'floor(5)', result: '5' }, + { input: 'floor(-5.5)', result: '-6' }, ], arguments: ['number'], resultType: 'number', @@ -61,10 +62,10 @@ export default { 'ceil': { comment: 'Rounds a number up to the next largest integer', examples: [ - {input: 'ceil(5.95)', result: '6'}, - {input: 'ceil(5.05)', result: '6'}, - {input: 'ceil(5)', result: '5'}, - {input: 'ceil(-5.5)', result: '-5'}, + { input: 'ceil(5.95)', result: '6' }, + { input: 'ceil(5.05)', result: '6' }, + { input: 'ceil(5)', result: '5' }, + { input: 'ceil(-5.5)', result: '-5' }, ], arguments: ['number'], resultType: 'number', @@ -73,21 +74,21 @@ export default { 'trunc': { comment: 'Returns the integer part of a number by removing any fractional digits', examples: [ - {input: 'trunc(5.95)', result: '5'}, - {input: 'trunc(5.05)', result: '5'}, - {input: 'trunc(5)', result: '5'}, - {input: 'trunc(-5.5)', result: '-5'}, + { input: 'trunc(5.95)', result: '5' }, + { input: 'trunc(5.05)', result: '5' }, + { input: 'trunc(5)', result: '5' }, + { input: 'trunc(-5.5)', result: '-5' }, ], - arguments:[ 'number'], + arguments: ['number'], resultType: 'number', fn: Math.trunc, }, 'sign': { comment: 'Returns either a positive or negative 1, indicating the sign of a number, or zero', examples: [ - {input: 'sign(-3)', result: '-1'}, - {input: 'sign(3)', result: '1'}, - {input: 'sign(0)', result: '0'}, + { input: 'sign(-3)', result: '-1' }, + { input: 'sign(3)', result: '1' }, + { input: 'sign(0)', result: '0' }, ], arguments: ['number'], resultType: 'number', @@ -96,15 +97,15 @@ export default { 'tableLookup': { comment: 'Returns the index of the last value in the array that is less than the specified amount', examples: [ - {input: 'tableLookup([100, 300, 900], 457)', result: '2'}, - {input: 'tableLookup([100, 300, 900], 23)', result: '0'}, - {input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'}, - {input: 'tableLookup([100, 300], 594)', result: '2'}, + { input: 'tableLookup([100, 300, 900], 457)', result: '2' }, + { input: 'tableLookup([100, 300, 900], 23)', result: '0' }, + { input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3' }, + { input: 'tableLookup([100, 300], 594)', result: '2' }, ], arguments: ['array', 'number'], resultType: 'number', - fn: function tableLookup(arrayNode, number){ - for(let i in arrayNode.values){ + fn: function tableLookup(arrayNode, number) { + for (let i in arrayNode.values) { let node = arrayNode.values[i]; if (node.value > number) return +i; } @@ -114,18 +115,146 @@ export default { 'resolve': { comment: 'Forces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is', examples: [ - {input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'}, - {input: 'resolve(1d6)', result: '4'}, + { input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7' }, + { input: 'resolve(1d6)', result: '4' }, ], arguments: ['parseNode'], - fn: function resolveFn(node){ - let {result} = resolve('reduce', node, this.scope, this.context); + fn: function resolveFn(node) { + let { result } = resolve('reduce', node, this.scope, this.context); return result; } - } + }, + 'dropLowest': { + comment: 'Removes one or more of the lowest values in a roll', + examples: [ + ], + arguments: ['rollArray', 'number'], + maxResolveLevels: ['roll', 'reduce'], + minArguments: 1, + maxArguments: 2, + resultType: 'rollArray', + fn: function dropLowestFn(rollArray, numberToDrop = 1) { + // Create a new array where the values are sorted in ascending order + const sortedArray = [...rollArray.values].sort(function (a, b) { + return a.value - b.value; + }); + + // mark the N smallest elements as dropped + for (let i = 0; i < numberToDrop; i += 1) { + console.log('dropped ' + sortedArray[i].value); + sortedArray[i].disabled = true; + sortedArray[i].disabledBy = 'dropLowest'; + } + return rollArray; + }, + }, + 'dropHighest': { + comment: 'Removes one or more of the highest values in a roll', + examples: [ + ], + arguments: ['rollArray', 'number'], + maxResolveLevels: ['roll', 'reduce'], + minArguments: 1, + maxArguments: 2, + resultType: 'rollArray', + fn: function dropHighestFn(rollArray, numberToDrop = 1) { + // Create a new array where the values are sorted in ascending order + const sortedArray = [...rollArray.values].sort(function (a, b) { + return b.value - a.value; + }); + + // mark the N smallest elements as dropped + for (let i = 0; i < numberToDrop; i += 1) { + sortedArray[i].disabled = true; + sortedArray[i].disabledBy = 'dropHighest'; + } + return rollArray; + }, + }, + 'reroll': { + comment: 'Rerolls if a number is less than or equal to the given value', + examples: [ + ], + arguments: ['rollArray', 'number', 'boolean'], + maxResolveLevels: ['roll', 'reduce'], + minArguments: 1, + maxArguments: 3, + resultType: 'rollArray', + fn: function rerollFn(rollArray, numberToReroll = 1, keepNewRoll = false) { + let rollValues = rollArray.values + // Iterate through the roll values + for (let i = 0; i < rollValues.length; i += 1) { + // If the number is less than the reroll limit + if (rollValues[i].value <= numberToReroll) { + // Disable it + rollValues[i].disabled = true; + rollValues[i].disabledBy = 'reroll'; + // Roll it again, insert the new roll into the list at the next index + rollValues.splice(i + 1, 0, { + value: rollDice(1, rollArray.diceSize)[0], + }); + // Skip iterating the inserted roll if we are forced to keep it + if (keepNewRoll) { + i += 1; + } + } + if (i >= 100) { + this.context.error('Can\'t roll more than 100 dice at once'); + return rollArray; + } + } + return rollArray; + }, + }, + 'explode': { + comment: 'Rerolls if a number is greater than or equal to the given value', + examples: [ + ], + arguments: ['rollArray', 'number', 'number'], + maxResolveLevels: ['roll', 'reduce', 'reduce'], + minArguments: 1, + maxArguments: 3, + resultType: 'rollArray', + fn: function explodeFn(rollArray, depth = 1, numberToReroll = rollArray.diceSize) { + let overflowErrored = false; + if (depth > 99) depth = 99; + let rollValues = rollArray.values + // Iterate through the roll values + for (let i = 0; i < rollValues.length; i += 1) { + // If the number is greater than or equal to the reroll limit + // And there is space to reroll it + if (rollValues[i].value >= numberToReroll) { + rollValues[i].bold = true; + let explodeDepth = 1; + let explodeRoll; + do { + // Before inserting this roll, make sure the total dice in the roll + // Doesn't exceed 100 + if (rollValues.length >= 100) { + if (!overflowErrored) { + this.context.error('Can\'t roll more than 100 dice at once'); + overflowErrored = true; + } + break; + } + explodeDepth += 1; + explodeRoll = rollDice(1, rollArray.diceSize)[0]; + const rollObj = { + value: explodeRoll, + italics: true, + }; + // Insert the roll + rollValues.splice(i + 1, 0, rollObj); + i += 1; + } while (explodeDepth <= depth && explodeRoll >= numberToReroll) + } + } + return rollArray; + }, + }, } -function anyNumberOf(type){ +function anyNumberOf(type) { let argumentArray = [type]; argumentArray.anyLength = true; return argumentArray; diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index 43f5b70f..8f513674 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -13,7 +13,7 @@ function id(x) { return x[0]; } value: s => s.slice(1, -1).replace('\\n', '\n'), }, name: { - match: /[a-zA-Z_#$]*[a-ce-zA-Z_#$][a-zA-Z0-9_#$]*/, + match: /[~#]?[a-zA-Z]*[a-ce-zA-Z][a-zA-Z0-9_]*/, type: moo.keywords({ 'keywords': ['true', 'false'], }), diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index 3ebdba75..e008ac54 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -11,7 +11,7 @@ value: s => s.slice(1, -1).replace('\\n', '\n'), }, name: { - match: /[a-zA-Z_#$]*[a-ce-zA-Z_#$][a-zA-Z0-9_#$]*/, + match: /[~#]?[a-zA-Z]*[a-ce-zA-Z][a-zA-Z0-9_]*/, type: moo.keywords({ 'keywords': ['true', 'false'], }), diff --git a/app/imports/parser/parseTree/call.js b/app/imports/parser/parseTree/call.js index 658c5f49..24b7d3c9 100644 --- a/app/imports/parser/parseTree/call.js +++ b/app/imports/parser/parseTree/call.js @@ -4,14 +4,14 @@ import functions from '/imports/parser/functions.js'; import resolve, { toString, traverse, map } from '../resolve.js'; const call = { - create({functionName, args}) { + create({ functionName, args }) { return { parseType: 'call', functionName, args, } }, - resolve(fn, node, scope, context){ + resolve(fn, node, scope, context) { let func = functions[node.functionName]; // Check that the function exists if (!func) { @@ -25,9 +25,22 @@ const call = { }; } + // Resolve a given node to a maximum depth of resolution + const resolveToLevel = (node, maxResolveFn = 'reduce') => { + // Determine the actual depth to resolve to + let resolveFn = 'reduce'; + if (fn === 'compile' || maxResolveFn === 'compile') { + resolveFn = 'compile'; + } else if (fn === 'roll' || maxResolveFn === 'roll') { + resolveFn = 'roll'; + } + // Resolve + return resolve(resolveFn, node, scope, context); + } + // Resolve the arguments - let resolvedArgs = node.args.map(arg => { - let { result } = resolve(fn, arg, scope, context); + let resolvedArgs = node.args.map((arg, i) => { + let { result } = resolveToLevel(arg, func.maxResolveLevels?.[i]); return result; }); @@ -36,12 +49,12 @@ const call = { node, fn, resolvedArgs, - argumentsExpected: func.arguments, + func, context, }); - if (checkFailed){ - if (fn === 'reduce'){ + if (checkFailed) { + if (fn === 'reduce') { context.error(`Invalid arguments to ${node.functionName} function`); return { result: error.create({ @@ -66,7 +79,7 @@ const call = { if ( arg.parseType === 'constant' && func.arguments[index] !== 'parseNode' - ){ + ) { return arg.value; } else { return arg; @@ -75,20 +88,21 @@ const call = { try { // Run the function - let value = func.fn.apply({scope, context}, mappedArgs); + let value = func.fn.apply({ + scope, + context, + }, mappedArgs); let valueType = typeof value; - if (valueType === 'number' || valueType === 'string' || valueType === 'boolean'){ + if (valueType === 'number' || valueType === 'string' || valueType === 'boolean') { // Convert constant results into constant nodes return { - result: constant.create({ value, valueType }), + result: constant.create({ value }), context, }; } else { - return { - result: value, - context, - }; + // Resolve the return value + return resolve(fn, value, scope, context); } } catch (error) { context.error(error.message || error); @@ -101,26 +115,28 @@ const call = { } } }, - toString(node){ + toString(node) { return `${node.functionName}(${node.args.map(arg => toString(arg)).join(', ')})`; }, - traverse(node, fn){ + traverse(node, fn) { fn(node); node.args.forEach(arg => traverse(arg, fn)); }, - map(node, fn){ + map(node, fn) { const resultingNode = fn(node); - if (resultingNode === node){ + if (resultingNode === node) { node.args = node.args.map(arg => map(arg, fn)); } return resultingNode; }, - checkArugments({node, fn, argumentsExpected, resolvedArgs, context}){ + checkArugments({ node, fn, func, resolvedArgs, context }) { + const argumentsExpected = func.arguments; // Check that the number of arguments matches the number expected if ( !argumentsExpected.anyLength && - argumentsExpected.length !== resolvedArgs.length - ){ + resolvedArgs.length > (func.maxArguments ?? argumentsExpected.length) || + resolvedArgs.length < (func.minArguments ?? argumentsExpected.length) + ) { context.error('Incorrect number of arguments ' + `to ${node.functionName} function, ` + `expected ${argumentsExpected.length} got ${resolvedArgs.length}`); @@ -131,14 +147,14 @@ const call = { // Check that each argument is of the correct type resolvedArgs.forEach((node, index) => { let type; - if (argumentsExpected.anyLength){ + if (argumentsExpected.anyLength) { type = argumentsExpected[0]; } else { type = argumentsExpected[index]; } if (type === 'parseNode') return; if (node.parseType !== type && node.valueType !== type) failed = true; - if (failed && fn === 'reduce'){ + if (failed && fn === 'reduce') { let typeName = typeof type === 'string' ? type : type.constructor.name; let nodeName = node.parseType; context.error(`Incorrect arguments to ${node.functionName} function` + diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js index 969b948d..30f37240 100644 --- a/app/imports/parser/parseTree/rollArray.js +++ b/app/imports/parser/parseTree/rollArray.js @@ -4,7 +4,7 @@ const rollArray = { create({ values, diceSize, diceNum }) { return { parseType: 'rollArray', - values, + values: values.map(v => ({ value: v })), diceSize, diceNum, }; @@ -16,10 +16,13 @@ const rollArray = { }; }, toString(node) { - return `${node.diceNum || ''}d${node.diceSize} [ ${node.values.join(', ')} ]`; + return `${node.diceNum || ''}d${node.diceSize} [${valuesToString(node.values)}]`; }, reduce(node, scope, context) { - const total = node.values.reduce((a, b) => a + b, 0); + const total = node.values.reduce((a, b) => { + if (b.disabled) return a; + return a + b.value; + }, 0); return { result: constant.create({ value: total, @@ -29,4 +32,15 @@ const rollArray = { }, } +function valuesToString(values) { + return values.map(v => { + let text = `${v.value}`; + if (v.disabled) text = `~~${text}~~`; + if (v.italics) text = `*${text}*`; + if (v.bold) text = `**${text}**`; + if (v.underline) text = `__${text}__`; + return text; + }).join(', '); +} + export default rollArray; diff --git a/app/imports/server/config/redisCaching.js b/app/imports/server/config/redisCaching.js new file mode 100644 index 00000000..7ed21c26 --- /dev/null +++ b/app/imports/server/config/redisCaching.js @@ -0,0 +1,3 @@ +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; + +LibraryNodes.startCaching?.(); diff --git a/app/imports/server/discord/sendWebhook.js b/app/imports/server/discord/sendWebhook.js index 044de8c3..a8a473df 100644 --- a/app/imports/server/discord/sendWebhook.js +++ b/app/imports/server/discord/sendWebhook.js @@ -1,5 +1,5 @@ import Discord from 'discord.js' -export default function sendWebhook({webhookURL, data = {}}){ +export default function sendWebhook({ webhookURL, data = {} }) { //webhookURL = https://discordapp.com/api/webhooks// let urlArray = webhookURL.split('/'); let token = urlArray.pop(); @@ -9,11 +9,16 @@ export default function sendWebhook({webhookURL, data = {}}){ data.disableMentions = 'all'; const hook = new Discord.WebhookClient(id, token); - // Send a message using the webhook - hook.send(data); + try { + // Send a message using the webhook + hook.send(data); + } catch (e) { + // Swallow the error, we don't really care + console.error(e); + } } -export function sendWebhookAsCreature({creature, data = {}}){ +export function sendWebhookAsCreature({ creature, data = {} }) { if (!creature || !creature.settings || !creature.settings.discordWebhook) return; data.username = creature.name; data.avatarURL = creature.avatarPicture; diff --git a/app/imports/server/publications/library.js b/app/imports/server/publications/library.js index 68c378b5..e35a5007 100644 --- a/app/imports/server/publications/library.js +++ b/app/imports/server/publications/library.js @@ -14,15 +14,19 @@ const LIBRARY_NODE_TREE_FIELDS = { order: 1, parent: 1, ancestors: 1, - tags: 1, - slotFillerCondition: 1, removed: 1, removedAt: 1, // Actions actionType: 1, // SlotFillers + libraryTags: 1, slotQuantityFilled: 1, slotFillerType: 1, + slotFillerConditionNote: 1, + slotFillerCondition: 1, + fillSlots: 1, + searchable: 1, + slotFillImage: 1, // Effect operation: 1, targetTags: 1, @@ -78,7 +82,14 @@ Meteor.publish('libraryCollection', function (libraryCollectionId) { }, { sort: { name: 1 } }); - return [libraryCollectionCursor, libraryCursor]; + return [ + libraryCollectionCursor, + libraryCursor, + Meteor.users.find( + libraryCollection.owner, + { fields: { username: 1 } } + ), + ]; }); }); }) @@ -137,6 +148,32 @@ Meteor.publish('libraries', function () { }); }); +Meteor.publish('browseLibraries', function () { + if (!this.userId) return []; + return [ + Libraries.find({ + showInMarket: true, + public: true, + }, { + sort: { + subscriberCount: 1, + name: 1, + }, + limit: 500, + }), + LibraryCollections.find({ + showInMarket: true, + public: true, + }, { + sort: { + subscriberCount: 1, + name: 1 + }, + limit: 500, + }), + ]; +}); + Meteor.publish('library', function (libraryId) { if (!libraryId) return []; libraryIdSchema.validate({ libraryId }); @@ -147,9 +184,15 @@ Meteor.publish('library', function (libraryId) { catch (e) { return this.error(e); } - return Libraries.find({ - _id: libraryId, - }); + return [ + Libraries.find({ + _id: libraryId, + }), + Meteor.users.find( + library.owner, + { fields: { username: 1 } } + ), + ]; }); }); @@ -160,22 +203,42 @@ let libraryIdSchema = new SimpleSchema({ }, }); -Meteor.publish('libraryNodes', function (libraryId) { +const extraFieldsSchema = new SimpleSchema({ + extraFields: { + type: Array, + optional: true, + }, + 'extraFields.$': { + type: String, + }, +}); + +Meteor.publish('libraryNodes', function (libraryId, extraFields) { if (!libraryId) return []; - libraryIdSchema.validate({ libraryId }); + try { + libraryIdSchema.validate({ libraryId }); + extraFieldsSchema.validate({ extraFields }); + } catch (e) { + return this.error(e); + } this.autorun(function () { let userId = this.userId; let library = Libraries.findOne(libraryId); - try { assertViewPermission(library, userId) } - catch (e) { + try { + assertViewPermission(library, userId) + } catch (e) { return this.error(e); } + const fields = { ...LIBRARY_NODE_TREE_FIELDS }; + extraFields?.forEach(field => { + fields[field] = 1; + }); return [ LibraryNodes.find({ 'ancestors.id': libraryId, }, { sort: { order: 1 }, - fields: LIBRARY_NODE_TREE_FIELDS, + fields, }), ]; }); diff --git a/app/imports/server/publications/searchLibraryNodes.js b/app/imports/server/publications/searchLibraryNodes.js index cedbc7a8..95e6a116 100644 --- a/app/imports/server/publications/searchLibraryNodes.js +++ b/app/imports/server/publications/searchLibraryNodes.js @@ -4,28 +4,31 @@ 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'; +import escapeRegex from '/imports/api/utility/escapeRegex.js'; -Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){ +Meteor.publish('selectedLibraryNodes', function (selectedNodeIds) { check(selectedNodeIds, Array); // Limit to 20 selected nodes - if (selectedNodeIds.length > 20){ + if (selectedNodeIds.length > 20) { selectedNodeIds = selectedNodeIds.slice(0, 20); } let libraryViewPermissions = {}; // Check view permissions of all libraries - for (let id of selectedNodeIds){ + for (let id of selectedNodeIds) { let node = LibraryNodes.findOne(id); if (!node) continue; let libraryId = node.ancestors[0].id; - if (libraryViewPermissions[id]){ + if (libraryViewPermissions[id]) { continue; } else { - let library = Libraries.findOne(libraryId, {fields: { - owner: 1, - readers: 1, - writers: 1, - public: 1, - }}); + let library = Libraries.findOne(libraryId, { + fields: { + owner: 1, + readers: 1, + writers: 1, + public: 1, + } + }); assertViewPermission(library, this.userId); libraryViewPermissions[id] = true; } @@ -33,15 +36,15 @@ Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){ // Return all nodes and their children return [LibraryNodes.find({ $or: [ - {_id: {$in: selectedNodeIds}}, - {'ancestors.id': {$in: selectedNodeIds}}, + { _id: { $in: selectedNodeIds } }, + { 'ancestors.id': { $in: selectedNodeIds } }, ], })]; }); -Meteor.publish('searchLibraryNodes', function(creatureId){ +Meteor.publish('searchLibraryNodes', function (creatureId) { let self = this; - this.autorun(function (){ + this.autorun(function () { let type = self.data('type'); if (!type) return []; @@ -60,20 +63,19 @@ Meteor.publish('searchLibraryNodes', function(creatureId){ // Build a filter for nodes in those libraries that match the type let filter = { - 'ancestors.id': {$in: libraryIds}, - removed: {$ne: true}, - tags: {$ne: []}, // Only tagged library nodes are considered + 'ancestors.id': { $in: libraryIds }, + removed: { $ne: true }, + searchable: true //library nodes must opt-in }; - if (type){ + if (type) { filter.$or = [{ - type, - },{ - type: 'slotFiller', - slotFillerType: type, + type, + }, { + slotFillerType: type, }]; } - this.autorun(function(){ + this.autorun(function () { // Get the limit of the documents the user can fetch var limit = self.data('limit') || 32; check(limit, Number); @@ -83,28 +85,40 @@ Meteor.publish('searchLibraryNodes', function(creatureId){ check(searchTerm, String); let options = undefined; - if (searchTerm){ - filter.$text = {$search: searchTerm}; + if (searchTerm) { + // Regex search instead of text index + filter.$and = [{ + $or: [ + { name: { $regex: escapeRegex(searchTerm), '$options': 'i' } }, + { libraryTags: searchTerm }, + ], + }]; + // filter.$text = {$search: searchTerm}; options = { + /* // relevant documents have a higher score. fields: { score: { $meta: 'textScore' } }, + */ sort: { // `score` property specified in the projection fields above. - score: { $meta: 'textScore' }, + // score: { $meta: 'textScore' }, 'ancestors.0.id': 1, name: 1, order: 1, } } } else { - delete filter.$text - options = {sort: { - 'ancestors.0.id': 1, - name: 1, - order: 1, - }}; + //delete filter.$text + delete filter.$and; + options = { + sort: { + 'ancestors.0.id': 1, + name: 1, + order: 1, + } + }; } options.limit = limit; @@ -118,17 +132,17 @@ Meteor.publish('searchLibraryNodes', function(creatureId){ Mongo.Collection._publishCursor(libraries, self, 'libraries'); let observeHandle = cursor.observeChanges({ - added: function (id, fields) { - fields._searchResult = true; - self.added('libraryNodes', id, fields); - }, - changed: function (id, fields) { - self.changed('libraryNodes', id, fields); - }, - removed: function (id) { - self.removed('libraryNodes', id); - } + added: function (id, fields) { + fields._searchResult = true; + self.added('libraryNodes', id, fields); }, + changed: function (id, fields) { + self.changed('libraryNodes', id, fields); + }, + removed: function (id) { + self.removed('libraryNodes', id); + } + }, // Publications don't mutate the documents { nonMutatingCallbacks: true } ); diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index 28d2da6f..b41c9dcd 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -19,10 +19,10 @@ Meteor.publish('singleCharacter', function (creatureId) { const self = this; try { schema.validate({ creatureId }); - } catch (e){ + } catch (e) { this.error(e); } - this.autorun(function (computation){ + this.autorun(function (computation) { let userId = this.userId; let permissionCreature = Creatures.findOne({ _id: creatureId, @@ -32,11 +32,11 @@ Meteor.publish('singleCharacter', function (creatureId) { try { assertViewPermission(permissionCreature, userId) } catch (e) { return [] } loadCreature(creatureId, self); - if (permissionCreature.computeVersion !== VERSION && computation.firstRun){ + if (permissionCreature.computeVersion !== VERSION && computation.firstRun) { try { computeCreature(creatureId) } - catch(e){ console.error(e) } + catch (e) { console.error(e) } } return [ Creatures.find({ @@ -52,7 +52,13 @@ Meteor.publish('singleCharacter', function (creatureId) { creatureId, }, { limit: 20, - sort: {date: -1}, + sort: { date: -1 }, + }), + // Also publish the owner's username + Meteor.users.find(permissionCreature.owner, { + fields: { + username: 1, + }, }), ]; }); diff --git a/app/imports/server/publications/slotFillers.js b/app/imports/server/publications/slotFillers.js index 6427af79..fba54ca6 100644 --- a/app/imports/server/publications/slotFillers.js +++ b/app/imports/server/publications/slotFillers.js @@ -5,22 +5,28 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur 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'; +import escapeRegex from '/imports/api/utility/escapeRegex.js'; -Meteor.publish('slotFillers', function(slotId, searchTerm){ +Meteor.publish('slotFillers', function (slotId, searchTerm, isDummySlot) { if (searchTerm) check(searchTerm, String); let self = this; - this.autorun(function (){ + this.autorun(function () { let userId = this.userId; if (!userId) { return []; } - // Get the slot - let slot = CreatureProperties.findOne(slotId); - if (!slot){ - return []; + + // Get the slot from the right collection + let slot; + if (isDummySlot) { + slot = LibraryNodes.findOne(slotId); + } else { + slot = CreatureProperties.findOne(slotId); } + if (!slot) return []; + // Get all the ids of libraries the user can access const creatureId = slot.ancestors[0].id; const libraryIds = getCreatureLibraryIds(creatureId, userId); @@ -36,31 +42,32 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ }); // Build a filter for nodes in those libraries that match the slot - let filter = getSlotFillFilter({slot, libraryIds}); - - this.autorun(function(){ + let filter = getSlotFillFilter({ slot, 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 = undefined; - if (searchTerm){ - filter.$text = {$search: searchTerm}; + if (searchTerm) { + filter.name = { $regex: escapeRegex(searchTerm), '$options': 'i' }; + //filter.$text = { $search: 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. - _score: { $meta: 'textScore' }, + //_score: { $meta: 'textScore' }, name: 1, order: 1, } } } else { - delete filter.$text + //delete filter.$text + delete filter.name options = { sort: { name: 1, @@ -73,6 +80,7 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ self.autorun(function () { self.setData('countAll', LibraryNodes.find(filter).count()); + self.setData('libraryNodeFilter', EJSON.stringify(filter)); }); self.autorun(function () { return [ @@ -84,18 +92,18 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ }); }); -Meteor.publish('classFillers', function(classId){ +Meteor.publish('classFillers', function (classId) { let self = this; if (!classId) return []; - this.autorun(function (){ + this.autorun(function () { let userId = this.userId; if (!userId) { return []; } // Get the class let classProp = CreatureProperties.findOne(classId); - if (!classProp){ + if (!classProp) { return []; } @@ -114,15 +122,16 @@ Meteor.publish('classFillers', function(classId){ }); // Build a filter for nodes in those libraries that match the slot - let filter = getSlotFillFilter({slot: classProp, libraryIds}); + let filter = getSlotFillFilter({ slot: classProp, libraryIds }); - this.autorun(function(){ + 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: { + level: 1, name: 1, order: 1, }, @@ -132,6 +141,7 @@ Meteor.publish('classFillers', function(classId){ self.autorun(function () { self.setData('countAll', LibraryNodes.find(filter).count()); + self.setData('libraryNodeFilter', EJSON.stringify(filter)); }); self.autorun(function () { return [LibraryNodes.find(filter, options), libraries]; diff --git a/app/imports/server/rest/apiPublications/healthCheck.js b/app/imports/server/rest/apiPublications/healthCheck.js new file mode 100644 index 00000000..c6aa00a8 --- /dev/null +++ b/app/imports/server/rest/apiPublications/healthCheck.js @@ -0,0 +1,37 @@ +// A simple endpoint that does a single round trip to the database to check everything is working + +const HealthCheckCollection = new Mongo.Collection('healthCheck'); + +// Don't use redis oplog optimization on this collection, we want to hit the database every time +HealthCheckCollection.disableRedis?.(); + +const healthCheckDoc = { + status: 'ok', +}; + +// Add the health check doc on startup if it's missing +// There should only be this single doc in the collection +// A capped collection would be marginally faster, but it's a pain to make one in Meteor +Meteor.startup(function () { + if (!HealthCheckCollection.findOne()) { + HealthCheckCollection.insert(healthCheckDoc); + } +}); + +Meteor.method('api-status', function () { + let dbHealthDoc; + try { + dbHealthDoc = HealthCheckCollection.findOne(); + } catch (e) { + this.setHttpStatusCode(503); + } + if (dbHealthDoc?.status === 'ok') { + this.setHttpStatusCode(200); + } else { + this.setHttpStatusCode(500); + } + return dbHealthDoc || {}; +}, { + httpMethod: 'GET', + url: 'api/status' +}); diff --git a/app/imports/server/rest/apiPublications/index.js b/app/imports/server/rest/apiPublications/index.js index 765d7988..e95d23db 100644 --- a/app/imports/server/rest/apiPublications/index.js +++ b/app/imports/server/rest/apiPublications/index.js @@ -1 +1,2 @@ import './creature.js'; +import './healthCheck.js'; diff --git a/app/jsconfig.json b/app/jsconfig.json index b97e629d..82b81ff2 100644 --- a/app/jsconfig.json +++ b/app/jsconfig.json @@ -10,14 +10,25 @@ "paths": { "/*": [ "./*" + ], + "meteor/aldeed:collection2": [ + "packages\\collection2\\collection2.js" ] - } + }, + "checkJs": false, + "allowJs": true }, "vueCompilerOptions": { - "target": 2, + "target": 2 // For Vue version <= 2.6.14 }, "exclude": [ "node_modules", - "**/node_modules/*" - ] + "**/node_modules/*", + ".meteor" + ], + "typeAcquisition": { + "include": [ + "meteor" + ] + } } \ No newline at end of file diff --git a/app/package-lock.json b/app/package-lock.json index f3020a8e..ae7c0414 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "dicecloud", - "version": "2.0.43", + "version": "2.0.51", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -50,17 +50,17 @@ } }, "@babel/parser": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz", - "integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.3.tgz", + "integrity": "sha512-vrukxyW/ep8UD1UDzOYpTKQ6abgjFoeG6L+4ar9+c5TN9QnlqiOi6QK7LSR5ewm/ERyGkT/Ai6VboNrxhbr9Uw==", "dev": true }, "@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", "requires": { - "regenerator-runtime": "^0.13.10" + "regenerator-runtime": "^0.13.11" } }, "@chenfengyuan/vue-countdown": { @@ -83,6 +83,29 @@ "mime-types": "^2.1.12" } }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true + } + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -178,6 +201,12 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/mocha": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", + "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", + "dev": true + }, "@types/semver": { "version": "7.3.13", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", @@ -185,18 +214,19 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.43.0.tgz", - "integrity": "sha512-wNPzG+eDR6+hhW4yobEmpR36jrqqQv1vxBq5LJO3fBAktjkvekfr4BRl+3Fn1CM/A+s8/EiGUbOMDoYqWdbtXA==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.2.tgz", + "integrity": "sha512-yVrXupeHjRxLDcPKL10sGQ/QlVrA8J5IYOEWVqk0lJaSZP7X5DfnP7Ns3cc74/blmbipQ1htFNVGsHX6wsYm0A==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.43.0", - "@typescript-eslint/type-utils": "5.43.0", - "@typescript-eslint/utils": "5.43.0", + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/type-utils": "5.59.2", + "@typescript-eslint/utils": "5.59.2", "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", - "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, @@ -211,9 +241,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -222,14 +252,14 @@ } }, "@typescript-eslint/parser": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.43.0.tgz", - "integrity": "sha512-2iHUK2Lh7PwNUlhFxxLI2haSDNyXvebBO9izhjhMoDC+S3XI9qt2DGFUsiJ89m2k7gGYch2aEpYqV5F/+nwZug==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.2.tgz", + "integrity": "sha512-uq0sKyw6ao1iFOZZGk9F8Nro/8+gfB5ezl1cA06SrqbgJAt0SRoFhb9pXaHvkrxUpZaoLxt8KlovHNk8Gp6/HQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.43.0", - "@typescript-eslint/types": "5.43.0", - "@typescript-eslint/typescript-estree": "5.43.0", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "debug": "^4.3.4" }, "dependencies": { @@ -245,23 +275,23 @@ } }, "@typescript-eslint/scope-manager": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.43.0.tgz", - "integrity": "sha512-XNWnGaqAtTJsUiZaoiGIrdJYHsUOd3BZ3Qj5zKp9w6km6HsrjPk/TGZv0qMTWyWj0+1QOqpHQ2gZOLXaGA9Ekw==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.2.tgz", + "integrity": "sha512-dB1v7ROySwQWKqQ8rEWcdbTsFjh2G0vn8KUyvTXdPoyzSL6lLGkiXEV5CvpJsEe9xIdKV+8Zqb7wif2issoOFA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.43.0", - "@typescript-eslint/visitor-keys": "5.43.0" + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2" } }, "@typescript-eslint/type-utils": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.43.0.tgz", - "integrity": "sha512-K21f+KY2/VvYggLf5Pk4tgBOPs2otTaIHy2zjclo7UZGLyFH86VfUOm5iq+OtDtxq/Zwu2I3ujDBykVW4Xtmtg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.2.tgz", + "integrity": "sha512-b1LS2phBOsEy/T381bxkkywfQXkV1dWda/z0PhnIy3bC5+rQWQDS7fk9CSpcXBccPY27Z6vBEuaPBCKCgYezyQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.43.0", - "@typescript-eslint/utils": "5.43.0", + "@typescript-eslint/typescript-estree": "5.59.2", + "@typescript-eslint/utils": "5.59.2", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -278,19 +308,19 @@ } }, "@typescript-eslint/types": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.43.0.tgz", - "integrity": "sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.2.tgz", + "integrity": "sha512-LbJ/HqoVs2XTGq5shkiKaNTuVv5tTejdHgfdjqRUGdYhjW1crm/M7og2jhVskMt8/4wS3T1+PfFvL1K3wqYj4w==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.43.0.tgz", - "integrity": "sha512-BZ1WVe+QQ+igWal2tDbNg1j2HWUkAa+CVqdU79L4HP9izQY6CNhXfkNwd1SS4+sSZAP/EthI1uiCSY/+H0pROg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.2.tgz", + "integrity": "sha512-+j4SmbwVmZsQ9jEyBMgpuBD0rKwi9RxRpjX71Brr73RsYnEr3Lt5QZ624Bxphp8HUkSKfqGnPJp1kA5nl0Sh7Q==", "dev": true, "requires": { - "@typescript-eslint/types": "5.43.0", - "@typescript-eslint/visitor-keys": "5.43.0", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/visitor-keys": "5.59.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -308,9 +338,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -319,34 +349,25 @@ } }, "@typescript-eslint/utils": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.43.0.tgz", - "integrity": "sha512-8nVpA6yX0sCjf7v/NDfeaOlyaIIqL7OaIGOWSPFqUKK59Gnumd3Wa+2l8oAaYO2lk0sO+SbWFWRSvhu8gLGv4A==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.2.tgz", + "integrity": "sha512-kSuF6/77TZzyGPhGO4uVp+f0SBoYxCDf+lW3GKhtKru/L8k/Hd7NFQxyWUeY7Z/KGB2C6Fe3yf2vVi4V9TsCSQ==", "dev": true, "requires": { + "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.43.0", - "@typescript-eslint/types": "5.43.0", - "@typescript-eslint/typescript-estree": "5.43.0", + "@typescript-eslint/scope-manager": "5.59.2", + "@typescript-eslint/types": "5.59.2", + "@typescript-eslint/typescript-estree": "5.59.2", "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", "semver": "^7.3.7" }, "dependencies": { - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -355,49 +376,79 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.43.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.43.0.tgz", - "integrity": "sha512-icl1jNH/d18OVHLfcwdL3bWUKsBeIiKYTGxMJCoGe7xFht+E4QgzOqoWYrU8XSLJWhVw8nTacbm03v23J/hFTg==", + "version": "5.59.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.2.tgz", + "integrity": "sha512-EEpsO8m3RASrKAHI9jpavNv9NlEUebV4qmF1OWxSTtKSFBpC1NCmWazDQHFivRf0O1DV11BA645yrLEVQ0/Lig==", "dev": true, "requires": { - "@typescript-eslint/types": "5.43.0", + "@typescript-eslint/types": "5.59.2", "eslint-visitor-keys": "^3.3.0" }, "dependencies": { "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", "dev": true } } }, "@vue/compiler-core": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.45.tgz", - "integrity": "sha512-rcMj7H+PYe5wBV3iYeUgbCglC+pbpN8hBLTJvRiK2eKQiWqu+fG9F+8sW99JdL4LQi7Re178UOxn09puSXvn4A==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", "dev": true, "requires": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.45", + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "@vue/compiler-dom": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.45.tgz", - "integrity": "sha512-tyYeUEuKqqZO137WrZkpwfPCdiiIeXYCcJ8L4gWz9vqaxzIQRccTSwSWZ/Axx5YR2z+LvpUbmPNXxuBU45lyRw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", "dev": true, "requires": { - "@vue/compiler-core": "3.2.45", - "@vue/shared": "3.2.45" + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "dev": true, + "requires": { + "@vue/shared": "3.3.4" + } + }, + "@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "dev": true, + "requires": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "dev": true, + "requires": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" } }, "@vue/shared": { - "version": "3.2.45", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz", - "integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "dev": true }, "abbrev": { @@ -425,6 +476,11 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true }, + "add-event-listener": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/add-event-listener/-/add-event-listener-0.0.1.tgz", + "integrity": "sha512-hjRmkeDqFUWEFcDHP/Lp0Pa4MhIJk/oQX8B7lFiNrjBKHjf0q+ivCJrucY8d8UI5d0QkZgV2jGdAGXxEZcm3nA==" + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -530,9 +586,9 @@ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, "aws-sdk": { - "version": "2.1258.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1258.0.tgz", - "integrity": "sha512-siqNFXlhJZVN1BizPZebJViFXtTUPgcA+yLfHKl2eC4Ied7kE7spOjZmAzpuiGUTzFagk1oWCaJ1Hit4llIoGg==", + "version": "2.1373.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1373.0.tgz", + "integrity": "sha512-3/P79VU2VVeiof25bn8TSepEhWCRhwuQGYoEWX/2pApQRJAY+w/3JFVKHjbAe3NYEEBNeiAE6PQ9DqWz5Pp+Lw==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -543,7 +599,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" }, "dependencies": { "uuid": { @@ -875,6 +931,46 @@ "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==" }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true + }, + "cytoscape": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.25.0.tgz", + "integrity": "sha512-7MW3Iz57mCUo6JQCho6CmPBCbTlJr7LzyEtIkutG255HLVd4XuBg2I9BkTZLI/e4HoaOB/BiAzXuQybQ95+r9Q==", + "requires": { + "heap": "^0.2.6", + "lodash": "^4.17.21" + } + }, + "cytoscape-dagre": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", + "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", + "requires": { + "dagre": "^0.8.5" + } + }, + "cytoscape-klay": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cytoscape-klay/-/cytoscape-klay-3.1.4.tgz", + "integrity": "sha512-VwPj0VR25GPfy6qXVQRi/MYlZM/zkdvRhHlgqbM//lSvstgM6fhp3ik/uM8Wr8nlhskfqz/M1fIDmR6UckbS2A==", + "requires": { + "klayjs": "^0.4.1" + } + }, + "dagre": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", + "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", + "requires": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -986,9 +1082,9 @@ } }, "dompurify": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz", - "integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA==" + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz", + "integrity": "sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==" }, "ecc-jsbn": { "version": "0.1.2", @@ -1299,9 +1395,9 @@ "dev": true }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -1416,9 +1512,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", + "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -1433,6 +1529,11 @@ "assert-plus": "^1.0.0" } }, + "gintersect": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/gintersect/-/gintersect-0.1.0.tgz", + "integrity": "sha512-jps8Ckj6u8yLxOYzBVJbPqvRdeHOINQgRtufaLHkunwNQcSEdZU0ejPBapSimXJEQ9mdQW4hsEUN7DfJEcTvQQ==" + }, "github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -1491,6 +1592,20 @@ "get-intrinsic": "^1.1.3" } }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "graphlib": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", + "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", + "requires": { + "lodash": "^4.17.15" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1550,6 +1665,11 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "heap": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/heap/-/heap-0.2.7.tgz", + "integrity": "sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==" + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1575,9 +1695,9 @@ "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==" + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, "ignore-styles": { "version": "5.0.1", @@ -1763,6 +1883,11 @@ "verror": "1.10.0" } }, + "klayjs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/klayjs/-/klayjs-0.4.1.tgz", + "integrity": "sha512-WUNxuO7O79TEkxCj6OIaK5TJBkaWaR/IKNTakgV9PwDn+mrr63MLHed34AcE2yTaDntgO6l0zGFIzhcoTeroTA==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1792,7 +1917,7 @@ "lodash.omit": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=" + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" }, "lodash.template": { "version": "4.5.0", @@ -1850,9 +1975,9 @@ } }, "marked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.2.tgz", - "integrity": "sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" }, "merge2": { "version": "1.4.1", @@ -2761,11 +2886,89 @@ "randexp": "0.4.6" } }, + "ngraph.centrality": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ngraph.centrality/-/ngraph.centrality-0.3.0.tgz", + "integrity": "sha512-Qmu9dDHJAx+GAW2AMqmhaub1rINS+fHZGZJ3zPI36ENAXmVNQ/Jkq79br1sg6NUHz/pRBT9MXMuwDyYKmMt8Mw==" + }, "ngraph.events": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.2.1.tgz", "integrity": "sha512-D4C+nXH/RFxioGXQdHu8ELDtC6EaCiNsZtih0IvyGN81OZSUby4jXoJ5+RNWasfsd0FnKxxpAROyUMzw64QNsw==" }, + "ngraph.expose": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/ngraph.expose/-/ngraph.expose-0.0.0.tgz", + "integrity": "sha512-Hr88MuhgoSLVGf2aaaXcKl22Rn95duWsjRcoeJMP9PtFmYHGFw/3ctDqBf5phnIyktm0P/Quxs5EGg6xgJcZAQ==" + }, + "ngraph.forcelayout": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ngraph.forcelayout/-/ngraph.forcelayout-0.5.0.tgz", + "integrity": "sha512-qOd1S9unFLw313+l0M/Dk1MePLDUSl4h9RyOtAbo0CyeefnN4PICiRz0LOewR5WuFmQD0/RmZLpjTKu0H7LTKQ==", + "requires": { + "ngraph.events": "0.0.4", + "ngraph.physics.simulator": "^0.3.0" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.4.tgz", + "integrity": "sha512-SY7MdNQoy5KyaVxg03PYCnGF6J7l4p8lEdmYm/5oIqFAmLhg0BmzZzlRqobJ0nEPT6xZlonUQbvCcXtarPZNrg==" + } + } + }, + "ngraph.fromjson": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/ngraph.fromjson/-/ngraph.fromjson-0.1.9.tgz", + "integrity": "sha512-f3GLjbUq239wx4s5A0fDptj9dcNeaEIJU3gm74hWvYK7onD7sFtedP7jVHZA7UJ2FwkKgEhzbPeltv92ycuKZQ==", + "requires": { + "ngraph.graph": "0.0.14" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + }, + "ngraph.graph": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-0.0.14.tgz", + "integrity": "sha512-ERTLng4KrsGbR7iLZFvg5H+zJ7V+SY8RDqZKYCnOZib5W8M5LCvcil9/8eiJcTRUIPPXW3j8hqPCdLnBvgsn/A==", + "requires": { + "ngraph.events": "0.0.3" + } + } + } + }, + "ngraph.generators": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/ngraph.generators/-/ngraph.generators-0.0.19.tgz", + "integrity": "sha512-P3XqB1sH4zrzM6bMGTtuT/6K76Rnhf1qE8Zu7PkAvhQVCQzdLYiL2/8DwhcPLsetRJHNJv0uwpW9TpntBAqKrw==", + "requires": { + "ngraph.graph": "0.0.14", + "ngraph.random": "0.1.0" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + }, + "ngraph.graph": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-0.0.14.tgz", + "integrity": "sha512-ERTLng4KrsGbR7iLZFvg5H+zJ7V+SY8RDqZKYCnOZib5W8M5LCvcil9/8eiJcTRUIPPXW3j8hqPCdLnBvgsn/A==", + "requires": { + "ngraph.events": "0.0.3" + } + }, + "ngraph.random": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-0.1.0.tgz", + "integrity": "sha512-KXCfzk/ZB79BxQSWMvYPGayx3Mb+7n5GPnc8SW0rwysqRV/3QxEKrLU/UVC8eGjc2SYGofqX+uhUE6IXfqR5VA==" + } + } + }, "ngraph.graph": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-19.1.0.tgz", @@ -2774,11 +2977,59 @@ "ngraph.events": "^1.2.1" } }, + "ngraph.merge": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ngraph.merge/-/ngraph.merge-0.0.1.tgz", + "integrity": "sha512-iXchI5xMjYzA96mee//O7I7gtd4cCakWaSTu11aMTxRDbvBK2qpDDytYg58jO3usAUkjFxBdy1gxYppKmBDuRQ==" + }, "ngraph.path": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ngraph.path/-/ngraph.path-1.4.0.tgz", "integrity": "sha512-yJZay4tP0wcjqkkf8zlMQ/T+JOgU+EWfdE4w4TG8OS94B12J/+Z44UOYxVJErE8E6/wFunX1hMZEB1/GHsBYHg==" }, + "ngraph.physics.primitives": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ngraph.physics.primitives/-/ngraph.physics.primitives-0.0.7.tgz", + "integrity": "sha512-7jPm14fYcuJ9kytOVNOKxFy6r/Uu9Dnj++uT3iR9XkBcsBahn2xcYJkV6vF1bIb1fQ5XrDCRjRIOcMwEum6jwQ==" + }, + "ngraph.physics.simulator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ngraph.physics.simulator/-/ngraph.physics.simulator-0.3.0.tgz", + "integrity": "sha512-ObW+HL+hQBIIdc6xG/+qrLe8qv+Sf0X3lq/l2hsjFrIwWtpRKLrSvUUoXiNIeFqRmY/C+PkGo3U+XY523lJ+Fw==", + "requires": { + "ngraph.events": "0.0.3", + "ngraph.expose": "0.0.0", + "ngraph.merge": "0.0.1", + "ngraph.physics.primitives": "0.0.7", + "ngraph.quadtreebh": "0.0.4", + "ngraph.random": "0.0.1" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + } + } + }, + "ngraph.quadtreebh": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ngraph.quadtreebh/-/ngraph.quadtreebh-0.0.4.tgz", + "integrity": "sha512-xTIkWGXt5Ajnoq9VOr0xDOI9ZL+q4sPhD0Z7vxvn4MCa+l0wf43rg0C7qv0t+RIOgbQBAp0xDpn568hpXAckJA==", + "requires": { + "ngraph.random": "0.0.1" + } + }, + "ngraph.random": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/ngraph.random/-/ngraph.random-0.0.1.tgz", + "integrity": "sha512-QPKU7ChXF/VrvMQxVo9aWcvXCXp98VfL4nKUteTW/olDqeUqQ61t7m+jvFb8Dj7kKvlKlnsbDA1aWLJGmm17XA==" + }, + "ngraph.tojson": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ngraph.tojson/-/ngraph.tojson-0.1.4.tgz", + "integrity": "sha512-Ii2BTqi8zBRMLH8vTc8pMUKQFJaqbgttG9DKUaazoPVpwC/ww4jyTOHe2ZKaGGZRepnGLqSZ27wZUm7n8MjIgA==" + }, "node-abi": { "version": "3.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.26.0.tgz", @@ -2920,9 +3171,9 @@ "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==" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.0.tgz", + "integrity": "sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==" }, "prism-media": { "version": "1.3.1", @@ -2960,9 +3211,9 @@ "integrity": "sha512-R7t6Y3fDDtcU7L4rtqwGUDP9xD64gJhIwpfjhRCTKmBoYF6SS49PIJHRJ048cse6OI7iwTwgyy2C46N9Ygoc6g==" }, "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, "querystring": { "version": "0.2.0", @@ -3018,9 +3269,9 @@ } }, "regenerator-runtime": { - "version": "0.13.10", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz", - "integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regexpp": { "version": "3.2.0", @@ -3218,6 +3469,14 @@ "is-arrayish": "^0.3.1" } }, + "simplesvg": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/simplesvg/-/simplesvg-0.0.10.tgz", + "integrity": "sha512-iCVx1A/kI4U3cGPRMRQaGLbIFNDXuB8rsaAsO2mM5wYFDs/MrfmHhrSCqNbOylgt9MhhZU3uMsSQnZM853kwXQ==", + "requires": { + "add-event-listener": "0.0.1" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3277,6 +3536,12 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, + "source-map-js": { + "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": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -3560,9 +3825,9 @@ "dev": true }, "typescript": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", - "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, "uri-js": { @@ -3627,10 +3892,43 @@ "extsprintf": "^1.2.0" } }, + "vivagraphjs": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/vivagraphjs/-/vivagraphjs-0.12.0.tgz", + "integrity": "sha512-Air+vUHXAWj8NTWUnbU800yKC7SiHpCVwpKIPfDtr5436YoMd7cpg8blt6Fn9xarx+sz1osRxGHBHTaHvcsR6Q==", + "requires": { + "gintersect": "0.1.0", + "ngraph.centrality": "0.3.0", + "ngraph.events": "0.0.3", + "ngraph.forcelayout": "0.5.0", + "ngraph.fromjson": "0.1.9", + "ngraph.generators": "0.0.19", + "ngraph.graph": "0.0.14", + "ngraph.merge": "0.0.1", + "ngraph.random": "0.0.1", + "ngraph.tojson": "0.1.4", + "simplesvg": "0.0.10" + }, + "dependencies": { + "ngraph.events": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-0.0.3.tgz", + "integrity": "sha512-UrewofHOFk/05otBm9GD4DA3PTEY/yaElhCclmGC4IcmAYaSDRrC3lENQxJ00AzeBnz1GY2xH7Ct7AfIdhsdWA==" + }, + "ngraph.graph": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-0.0.14.tgz", + "integrity": "sha512-ERTLng4KrsGbR7iLZFvg5H+zJ7V+SY8RDqZKYCnOZib5W8M5LCvcil9/8eiJcTRUIPPXW3j8hqPCdLnBvgsn/A==", + "requires": { + "ngraph.events": "0.0.3" + } + } + } + }, "vue": { - "version": "2.6.10", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", - "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.14.tgz", + "integrity": "sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==" }, "vue-eslint-parser": { "version": "7.11.0", @@ -3699,9 +3997,9 @@ } }, "vuetify": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.12.tgz", - "integrity": "sha512-qe3hcMpWmT1O15tp+p65lOS7UKZ/hQYQktCsw9iXx2u3RwVbX6GR82gY2iROrKsiAzYDvMgrYxWQwY/pUfkekw==" + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.6.15.tgz", + "integrity": "sha512-2a6sBSHzivXgi9pZMyHuzTgMyInCkj/BrVwTnoCa1Y/Dnfwj7lkWzgKQDScbGVK0q4vJ+YHoBBrLOmnhz1R0YA==" }, "vuetify-upload-button": { "version": "2.0.2", @@ -3774,18 +4072,18 @@ "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==" }, "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==" + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, "yallist": { "version": "4.0.0", diff --git a/app/package.json b/app/package.json index 6036d934..463be480 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "dicecloud", - "version": "2.0.44", + "version": "2.0.53", "description": "Unofficial Online Realtime D&D 5e App", "license": "GPL-3.0", "repository": { @@ -20,35 +20,40 @@ "npm": "6.13.x" }, "dependencies": { - "@babel/runtime": "^7.20.1", + "@babel/runtime": "^7.21.5", "@chenfengyuan/vue-countdown": "^1.1.5", "@tozd/vue-observer-utils": "^0.5.0", - "aws-sdk": "^2.1258.0", + "aws-sdk": "^2.1373.0", "bcrypt": "^5.1.0", "chroma-js": "^2.4.2", "css-box-shadow": "^1.0.0-3", + "cytoscape": "^3.25.0", + "cytoscape-dagre": "^2.5.0", + "cytoscape-klay": "^3.1.4", + "dagre": "^0.8.5", "date-fns": "^1.30.1", "ddp-rate-limiter-mixin": "^1.1.10", "discord.js": "^12.5.3", - "dompurify": "^2.4.1", - "ignore": "^5.2.0", + "dompurify": "^2.4.5", + "ignore": "^5.2.4", "ignore-styles": "^5.0.1", "lodash": "^4.17.20", - "marked": "^4.2.2", + "marked": "^4.3.0", "meteor-node-stubs": "^1.2.5", "minify-css-string": "^1.0.0", "moo": "^0.5.2", "nearley": "^2.19.1", "ngraph.graph": "^19.1.0", "ngraph.path": "^1.4.0", - "pretty-bytes": "^6.0.0", + "pretty-bytes": "^6.1.0", "qrcode.vue": "^1.7.0", "request": "^2.88.2", "sharp": "^0.30.7", "simpl-schema": "^1.13.1", "source-map-support": "^0.5.21", "speakingurl": "^14.0.1", - "vue": "2.6.10", + "vivagraphjs": "^0.12.0", + "vue": "2.6.14", "vue-meteor-tracker": "^2.0.0", "vue-reactive-provide": "^0.3.0", "vue-router": "^3.6.5", @@ -58,14 +63,16 @@ "vuex": "^3.1.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^5.43.0", - "@typescript-eslint/parser": "^5.43.0", - "@vue/compiler-dom": "^3.2.45", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.59.2", + "@typescript-eslint/parser": "^5.59.2", + "@vue/compiler-dom": "^3.3.4", + "@vue/runtime-dom": "^3.3.4", "chai": "^4.3.7", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.20.0", "eslint-plugin-vuetify": "^1.1.0", - "typescript": "^4.9.3" + "typescript": "^5.0" }, "eslintConfig": { "extends": [ diff --git a/app/packages/redis-oplog b/app/packages/redis-oplog new file mode 160000 index 00000000..83e302c1 --- /dev/null +++ b/app/packages/redis-oplog @@ -0,0 +1 @@ +Subproject commit 83e302c15456d6744047c50fc8d1add9e739b001 diff --git a/app/private/docs/computed-fields.md b/app/private/docs/computed-fields.md deleted file mode 100644 index 6ca1bef5..00000000 --- a/app/private/docs/computed-fields.md +++ /dev/null @@ -1,81 +0,0 @@ -# Computed fields - -Some fields in DiceCloud creature properties expect calculations. These fields are then computed by the DiceCloud engine. - -Some fields, like the value of an attirbute, resolve down to a single number, while others, like the damage to deal in an attack, only simplify their calculation as far as they can, and then resolve down to a number when applied. Avoid adding dice rolls to calculations that expect to resolve down to a number, because they will re-roll every time the creature is recalculated, causing instability in the creature's stats. - -## Parser - -The DiceCloud parser can understand the following syntax: - -| | | -| :- | :- | -| **Numbers** | `13`, `3.14` | -| **Dice rolls** | `3d6`, `(1 + 2)d4`| -| **Strings of text** | `'Some text'`, `"some other text"` | -| **Boolean values** | `true` or `false`. When DiceCloud expects a boolean, `0`, an empty string `''` and `false` are all considered false by DiceCloud's engine, every other value is considered true. | -| **Variable names** | `variableName` | -| **Addition and subtraction** | `1 + 2 + 3`, `12 - 6` | -| **Multiplication** | `6 * 4`, `12 * 2` = `24` | -| **Exponents** | `3 ^ 2` Raise 3 to the power of 2 | -| **Modulo** | Returns the remainder of a division operation `15 % 6` = `3` | -| **AND** | `&` or `&&`: Returns the value of the right hand side if the left side is true `true & 'cat'` = `'cat'` | -| **OR** | `|` or `||`: Returns the left hand side if it is true, otherwise returns the right hand side `'dog' || 'cat'` = `'dog'` | -| **NOT** | `!` returns false if the value after it is true, otherwise returns false | -| **Comparisons** | greater than: `>`, less than: `<`, greater than or equal to: `>=`, less than or equal to: `<=`, equal: `=` or `==` or `===`, not equal: `!=` or `!==` | -| **If-else** | `condition ? resultIfTrue : resultIfFalse`, `level > 10 ? 'high tier' : 'low tier'` | -| **Arrays** | lists of values `[3, 6, 9, 12]`. | -| **Array Indexes** | A value can be chosen from an array using another set of square brackets: `[3, 6, 9, 12][2]` = `[6]` because `[2]` fetches the 2nd value in the array. Arrays start at 1 in DiceCloud so that level tables can have 20 entries and be accessed by `array[level]`. | -| **Function calls** | `functionName(argument1, argument1)` See [Functions](/docs/functions) for a full list of available functions. | - -## Special variables - -### Built-in variables - -These variables are added to the creature automatically when relevant. They can be overriden if needed by creating a property with the same variable name. They can also be targetted by effects. - -- `xp` A total of all the experiences with xp added to the character sheet -- `milestoneLevels` A total of all the experiences with milestone levels added to the character sheet -- `itemsAttuned` Number of items the creature is attuned to -- `weightEquipment` Total weight of all equipment on the creature -- `valueEquipment` Total value of all equipment on the creature -- `weightTotal` Total weight of the creature's entire inventory -- `valueTotal` Total value of the creature's entire inventory -- `weightCarried` Total weight of all carried items and containers -- `valueCarried` Total value of all carried items and containers -- `level` The current level of the creature, including all class levels -- `criticalHitTarget` Defaults to 20, the natural roll needed to consider an attack roll as a critical hit - -### Action variables - -These variables are available during an action after the relevant property has been applied. - -For Advanced users, a [Roll](/docs/property/roll) can set these variables, overriding the default behavior. - -#### [Actions](/docs/property/action) - -- `$attackDiceRoll` The value of the d20 roll before any modifiers were applied. -- `$attackRoll` The total attack roll after modifiers. -- `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`. -- `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll. -- `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit. -- `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss. - -#### [Damage](/docs/property/damage) - -- `$lastDamageType` The type of damage dealt last, any damage that has the `extra` type will use this damage type instead - -#### [Saving throws](/docs/property/saving-throw) - -- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw -- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw -- `$saveDiceRoll` The unmodified d20 roll the target made to save -- `$saveRoll` The final value of the saving throw roll after modifiers - -## Ancestor references - -The ancestors of a property can be accessed directly using the `#ancestorType` syntax. - -For example, a spell might need to know the save DC of the spell list that it is inside of, it can use `#spellList.dc`. - -Triggers and their children work differently: They don't have access to their own ancestors, but rather inherit the ancestors of the property that caused them to fire. For example, a trigger at the root of the creature's tree might be fired by a spell being cast, you can still use references to ancestors like `#spellList.attackRollBonus` inside that trigger as if it were under the spell itself. diff --git a/app/private/docs/defaultDocs.json b/app/private/docs/defaultDocs.json index 7eaad790..7faa5a19 100644 --- a/app/private/docs/defaultDocs.json +++ b/app/private/docs/defaultDocs.json @@ -3,10 +3,9 @@ "_id": "ioei4uvDdGTAFqZrB", "name": "Properties", "order": 1, - "urlName": "properties", - "tags": [], + "urlName": "property", "ancestors": [], - "href": "/docs/properties", + "href": "/docs/property", "published": true, "description": "Properties are all the things you can add to a character, like ability scores, actions, spells, and items." }, @@ -21,16 +20,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 2, - "urlName": "actions", - "tags": [], - "description": "Actions are things your character can do. When an action is taken, all the properties under it are applied.\n\nAdd actions to your character sheet, then add children under the action to determine what happenes when the action is applied.\n\nWhen an action is applied it will create an entry in the character's log detailing all the properties that were applied and what their results were.\n\nThe following properties can all be applied by an action: \n\n - [Attribute Damage](/docs/property/attribute-damage)\n - [Branches](/docs/property/branch)\n - [Buffs](/docs/property/buff)\n - [Buff Removers](/docs/property/remove-buff)\n - [Damage](/docs/property/damage)\n - [Notes](/docs/property/note)\n - [Rolls](/docs/property/roll)\n - [Saving Throws](/docs/property/saving-throw)\n - Other actions\n\n---\n\n### Name\n\nThe name of the action.\n\n### Action type\n\nHow long the action takes to perform.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Attack roll\n\nA [computed field](/docs/computed-fields) which calculates the attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier.\n\nThe following variables may be added to the action scope when attack rolls are made:\n\n - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n - `$attackRoll` The total attack roll after modifiers.\n - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`.\n - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n### Summary\n\nA brief overview of what the action does. This will appear in the action card, and shows in the log when the action is applied.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Description\n\nA more detailed description of the action. The description does not show in the action card or the log when the action is applied.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Resource\n\nA resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the action can't be applied.\n\nIf you want to reduce an attribute when taking the action, but want the action to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the action instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number.\n\n#### Resource attribute\n\nThe variable name of the attribute that will be consumed when taking this action.\n\n#### Resource quantity\n\nA [computed field](/docs/computed-fields) which determines how much of the attribute is required to apply this action. This amount will be deducted from the attribute every time the action is taken.\n\n### Ammo\n\nAmmo represents items that are requied to take the action. If an item is not selected, or there is insufficient quantity of the selected item, the action can't be appled.\n\n#### Ammo item\n\nSpecify what tag an item must have to be considered valid ammo for this action. Any item with this tag can be selected as ammo for this action.\n\n#### Ammo quantity\n\nA [computed field](/docs/computed-fields) which determines how many of the selected items are required to take this action. The quantity is deducted from the total quantity of the item when this action is applied.\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Target\n\nWho this action should apply to. The properties under the action will be applied to the Targets.\n\n- **Self** The action will apply its properties to the creature taking the action\n- **Single Target** The action will apply its properties without a target (for now)\n- **Multiple Targets** The action will apply its properties without a target (for now)\n\n### Uses\n\nA [computed field](/docs/computed-fields) which determines how many times this action can be used before it needs to be reset.\n\n### Uses used\n\nHow many of this action's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the action has uses and its uses are reset.\n\n### Don't show in log\n\nWhen this is true, the action does not show up in the log. This does not stop the action's children from appearing in the log when they are applied.\n\n### Reset\n\nIf set, the uses used field is set to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed", + "urlName": "action", + "description": "Actions are things your character can do. When an action is taken, all the properties under it are applied.\n\nAdd actions to your character sheet, then add children under the action to determine what happenes when the action is applied.\n\nWhen an action is applied it will create an entry in the character's log detailing all the properties that were applied and what their results were.\n\nThe following properties can all be applied by an action: \n\n - [Attribute Damage](/docs/property/attribute-damage)\n - [Branches](/docs/property/branch)\n - [Buffs](/docs/property/buff)\n - [Buff Removers](/docs/property/remove-buff)\n - [Damage](/docs/property/damage)\n - [Notes](/docs/property/note)\n - [Rolls](/docs/property/roll)\n - [Saving Throws](/docs/property/saving-throw)\n - Other actions\n\n---\n\n### Name\n\nThe name of the action.\n\n### Action type\n\nHow long the action takes to perform.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Attack roll\n\nA [computed field](/docs/concepts/computed-fields) which calculates the attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier.\n\nThe following variables may be added to the action scope when attack rolls are made:\n\n - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n - `$attackRoll` The total attack roll after modifiers.\n - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`.\n - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n### Summary\n\nA brief overview of what the action does. This will appear in the action card, and shows in the log when the action is applied.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\n\nA more detailed description of the action. The description does not show in the action card or the log when the action is applied.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Resource\n\nA resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the action can't be applied.\n\nIf you want to reduce an attribute when taking the action, but want the action to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the action instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number.\n\n#### Resource attribute\n\nThe variable name of the attribute that will be consumed when taking this action.\n\n#### Resource quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how much of the attribute is required to apply this action. This amount will be deducted from the attribute every time the action is taken.\n\n### Ammo\n\nAmmo represents items that are requied to take the action. If an item is not selected, or there is insufficient quantity of the selected item, the action can't be appled.\n\n#### Ammo item\n\nSpecify what tag an item must have to be considered valid ammo for this action. Any item with this tag can be selected as ammo for this action.\n\n#### Ammo quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how many of the selected items are required to take this action. The quantity is deducted from the total quantity of the item when this action is applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Target\n\nWho this action should apply to. The properties under the action will be applied to the Targets.\n\n- **Self** The action will apply its properties to the creature taking the action\n- **Single Target** The action will apply its properties without a target (for now)\n- **Multiple Targets** The action will apply its properties without a target (for now)\n\n### Uses\n\nA [computed field](/docs/concepts/computed-fields) which determines how many times this action can be used before it needs to be reset.\n\n### Uses used\n\nHow many of this action's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the action has uses and its uses are reset.\n\n### Don't show in log\n\nWhen this is true, the action does not show up in the log. This does not stop the action's children from appearing in the log when they are applied.\n\n### Reset\n\nIf set, the uses used field is set to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed", "published": true, - "href": "/docs/properties/actions" + "href": "/docs/property/action" }, { "_id": "FwpkjToybWQKCDhSr", @@ -43,15 +41,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 3, "urlName": "attribute-damage", - "tags": [], - "description": "When applied, attribute damage reduces the value of the [Attribute](/docs/properties/attributes) by some amount or set the value of an attribute to some amount. Attribute damage can by applied by actions or triggers.\n\nUsing a negative value to damage an attribute will heal the attribute instead.\n\n---\n\n### Attribute\n\nThe variable name of the attribute to target.\n\n### Amount\n\nA [computed field](/docs/computed-fields) which determined the amount to damage the attribute or set the attribute's value to.\n\n### Operation\n\n- **Damage** Reduce the value of the attribute by the amount, negative values heal the attribute instead\n- **Set** Set the value of the attribute to the amount\n\n### Target\n\n- **Target** Apply the attribute damage to the same target as the action applying this property\n- **Self** Apply the attribute damage to the creature taking the action\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Don't show in log\n\nWhen this is set, the attribute damage is applied, but does not show in the log.", - "href": "/docs/properties/attribute-damage", + "description": "When applied, attribute damage reduces the value of the [Attribute](/docs/property/attribute) by some amount or set the value of an attribute to some amount. Attribute damage can by applied by actions or triggers.\n\nUsing a negative value to damage an attribute will heal the attribute instead.\n\n---\n\n### Attribute\n\nThe variable name of the attribute to target.\n\n### Amount\n\nA [computed field](/docs/concepts/computed-fields) which determined the amount to damage the attribute or set the attribute's value to.\n\n### Operation\n\n- **Damage** Reduce the value of the attribute by the amount, negative values heal the attribute instead\n- **Set** Set the value of the attribute to the amount\n\n### Target\n\n- **Target** Apply the attribute damage to the same target as the action applying this property\n- **Self** Apply the attribute damage to the creature taking the action\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Don't show in log\n\nWhen this is set, the attribute damage is applied, but does not show in the log.", + "href": "/docs/property/attribute-damage", "published": true }, { @@ -65,16 +62,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 4, - "urlName": "attributes", - "tags": [], - "description": "Attributes represent the numerical values of the creature.\n\nAttributes can be targeted by [effects](/docs/property/effect) which can change their total value in a non-destructive way. For example, if a class level gives you an ability score increase of +2 strength when it is taken, instead of directly editing the strength attribute, you add an effect to the class level that adds 2 to strength. The total value of strength will increase by 2 and it will show a record of that ability score increase and where it came from.\n\nAttributes, [skills](/docs/properties/skill), and [effects](/docs/property/effect) are the core properties of DiceCloud's creature engine.\n\nAttributes have the following fields that can be accessed in calculations with `variableName.field`:\n\n- `.total` The total of the attribute before being damaged\n- `.damage` the amount of damage the attribute has taken\n- `.value` The current value of the attribute including damage. `variableName` and `variableName.value` are equivalent.\n- `.modifier` If the attribute is an ability, this is its roll modifier, eg. `strength.modifier` is +2 when `strength.value` is 14\n\n---\n\n### Base value\n\nA [computed field](/docs/computed-fields) that determines the starting value of the attribute before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied.\n\n### Name\n\nThe name of the attribute\n\n### Variable name\n\nThe name used to refer to the attribute in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\nIf multiple attributes share a variable name, only the last attribute on the [character tree](/docs/tree) will count as the defining attribute and appear on the sheet, while other attributes with that variable name will be used as base value [effects](/docs/property/effect).\n\n### Attribute type\n\n- **Ability** Ablity scores like Strength, Dexterity, etc. Ability scores get a modifier which can be accessed in calculations as `variableName.modifier`,\n- **Stat** Any numerical value that appears on the sheet. Speed, armor class.\n- **Modifier** Any numical value that appears on the sheet with a `+` or `-` sign, eg. Proficiency bonus.\n- **Hit Dice** Hit dice let you select the appropriate hit dice size. Creatures regain half their total hit dice on long rest.\n- **Health Bar** Health bars can by made to take or ignore damage in a specified order\n- **Resource** Rages, sourcery points, things that are spent to use actions.\n- **Spell Slot** Spell slots have a specific level and are used to cast spells.\n- **Utility** Utility attributes don't show up anywhere on the sheet, but can still be used for calculations\n\n### Description\n\nA detailed description of the attribute.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Health bar settings\n\nHealth bars can take or ignore damage and healing from applied damage properties targeting a creature. A lower ordered health bar will take damage before a higher ordered one.\n\nHealth bars can also change color depending on their value. At 50%+ full they are their property color, between 50% and 0% they fade from their half-full color to their empty color.\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Allow decimal values\n\nIf this is set, the attribute will not round-down when its value has a decimal.\n\n### Can be damaged into negative values\n\nIf this is set the attribute can be damaged past zero into negative values.\n\n### Can be incremented above total\n\nIf this is set the attribute can have negative damage such that the value exceeds the total. This can be useful if you are using the attribute to count, it can start at zero and be healed upwards to keep count.\n\n### Reset\n\nIf set, the damage on this attribute is reset to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed", + "urlName": "attribute", + "description": "Attributes represent the numerical values of the creature.\n\nAttributes can be targeted by [effects](/docs/property/effect) which can change their total value in a non-destructive way. For example, if a class level gives you an ability score increase of +2 strength when it is taken, instead of directly editing the strength attribute, you add an effect to the class level that adds 2 to strength. The total value of strength will increase by 2 and it will show a record of that ability score increase and where it came from.\n\nAttributes, [skills](/docs/property/skill), and [effects](/docs/property/effect) are the core properties of DiceCloud's creature engine.\n\nAttributes have the following fields that can be accessed in calculations with `variableName.field`:\n\n- `.total` The total of the attribute before being damaged\n- `.damage` the amount of damage the attribute has taken\n- `.value` The current value of the attribute including damage. `variableName` and `variableName.value` are equivalent.\n- `.modifier` If the attribute is an ability, this is its roll modifier, eg. `strength.modifier` is +2 when `strength.value` is 14\n\n---\n\n### Base value\n\nA [computed field](/docs/concepts/computed-fields) that determines the starting value of the attribute before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied.\n\n### Name\n\nThe name of the attribute\n\n### Variable name\n\nThe name used to refer to the attribute in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\nIf multiple attributes share a variable name, only the last attribute on the [character tree](/docs/concepts/tree) will count as the defining attribute and appear on the sheet, while other attributes with that variable name will be used as base value [effects](/docs/property/effect).\n\n### Attribute type\n\n- **Ability** Ablity scores like Strength, Dexterity, etc. Ability scores get a modifier which can be accessed in calculations as `variableName.modifier`,\n- **Stat** Any numerical value that appears on the sheet. Speed, armor class.\n- **Modifier** Any numical value that appears on the sheet with a `+` or `-` sign, eg. Proficiency bonus.\n- **Hit Dice** Hit dice let you select the appropriate hit dice size. Creatures regain half their total hit dice on long rest.\n- **Health Bar** Health bars can by made to take or ignore damage in a specified order\n- **Resource** Rages, sourcery points, things that are spent to use actions.\n- **Spell Slot** Spell slots have a specific level and are used to cast spells.\n- **Utility** Utility attributes don't show up anywhere on the sheet, but can still be used for calculations\n\n### Description\n\nA detailed description of the attribute.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Health bar settings\n\nHealth bars can take or ignore damage and healing from applied damage properties targeting a creature. A lower ordered health bar will take damage before a higher ordered one.\n\nHealth bars can also change color depending on their value. At 50%+ full they are their property color, between 50% and 0% they fade from their half-full color to their empty color.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Allow decimal values\n\nIf this is set, the attribute will not round-down when its value has a decimal.\n\n### Can be damaged into negative values\n\nIf this is set the attribute can be damaged past zero into negative values.\n\n### Can be incremented above total\n\nIf this is set the attribute can have negative damage such that the value exceeds the total. This can be useful if you are using the attribute to count, it can start at zero and be healed upwards to keep count.\n\n### Reset\n\nIf set, the damage on this attribute is reset to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed", "published": true, - "href": "/docs/properties/attributes" + "href": "/docs/property/attribute" }, { "_id": "X5NKw8m6ruy9Srynd", @@ -87,16 +83,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 5, - "urlName": "branches", - "tags": [], + "urlName": "branch", "published": true, - "description": "Branches are applied by actions, when they are applied they can control which of their immediate children are applied.\n\n---\n\n### Branch type\n\n- **If condition is true** Apply children if the condition (a [computed field](/docs/computed-fields)) resolves to `true` or a non-zero number\n- **Attack hit** Apply children if the attack roll hit the target\n- **Attack hit** Apply children if the attack roll missed the target\n- **Save failed** Apply children if target failed its saving throw\n- **Save suceeded** Apply children if target made its saving throw\n- **Apply to each target** Apply children separately to each target\n- **Random** Apply one of the immediate children at random\n- **Calculated Index** Use the index (a [computed field](/docs/computed-fields)) to choose which child to apply, starting at 1 for the first child.\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Don't show in log\n\nWhen this is set, the branch is applied, but does not show in the log. This does not prevent its children from appearing in the log.", - "href": "/docs/properties/branches" + "description": "Branches are applied by actions, when they are applied they can control which of their immediate children are applied.\n\n---\n\n### Branch type\n\n- **If condition is true** Apply children if the condition (a [computed field](/docs/concepts/computed-fields)) resolves to `true` or a non-zero number\n- **Attack hit** Apply children if the attack roll hit the target\n- **Attack hit** Apply children if the attack roll missed the target\n- **Save failed** Apply children if target failed its saving throw\n- **Save suceeded** Apply children if target made its saving throw\n- **Apply to each target** Apply children separately to each target\n- **Random** Apply one of the immediate children at random\n- **Calculated Index** Use the index (a [computed field](/docs/concepts/computed-fields)) to choose which child to apply, starting at 1 for the first child.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Don't show in log\n\nWhen this is set, the branch is applied, but does not show in the log. This does not prevent its children from appearing in the log.", + "href": "/docs/property/branch" }, { "_id": "7KzMFHqo8DJtFtj7Q", @@ -109,14 +104,13 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 6, "urlName": "new-doc-6", - "href": "/docs/properties/new-doc-6", - "tags": [], + "href": "/docs/property/new-doc-6", "removed": true, "removedAt": "2022-11-21T15:45:45.975Z" }, @@ -131,14 +125,13 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 7, "urlName": "new-doc-7", - "href": "/docs/properties/new-doc-7", - "tags": [], + "href": "/docs/property/new-doc-7", "removed": true, "removedAt": "2022-11-21T15:49:16.862Z" }, @@ -153,15 +146,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 8, - "urlName": "buffs", - "href": "/docs/properties/buffs", - "tags": [], - "description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `$target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `$target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\n\nThe name of the buff.\n\n### Description\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Target\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't show in log\n\nIf set, the buff will not show its name and description in the log when applied.\n\n### Don't freeze variables\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "buff", + "href": "/docs/property/buff", + "description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `~target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `~target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\n\nThe name of the buff.\n\n### Description\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Target\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't show in log\n\nIf set, the buff will not show its name and description in the log when applied.\n\n### Don't freeze variables\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -170,7 +162,6 @@ "order": 9, "urlName": "new-doc-9", "href": "/docs/new-doc-9", - "tags": [], "ancestors": [], "removed": true, "removedAt": "2022-11-21T15:49:40.176Z" @@ -196,14 +187,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 11, - "urlName": "class-levels", - "href": "/docs/properties/class-levels", - "description": "A class level is a property that represents a single level in a class. It is generally used as a child of a [Class property](/docs/property/class).\n\nFeatures and bonuses that are given by a class level get added as children of the class level.\n\n---\n\n### Level\n\nWhich level this property represents.\n\n### Name\n\nThe name of the class or subclass this level is part of\n\n### Variable name\n\nThe same variable name of the class this level belongs to.\n\n### Description\n\nA description of the benefits gained with this level.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "class-level", + "href": "/docs/property/class-level", + "description": "A class level is a property that represents a single level in a class. It is generally used as a child of a [Class property](/docs/property/class).\n\nFeatures and bonuses that are given by a class level get added as children of the class level.\n\n---\n\n### Level\n\nWhich level this property represents.\n\n### Name\n\nThe name of the class or subclass this level is part of\n\n### Variable name\n\nThe same variable name of the class this level belongs to.\n\n### Description\n\nA description of the benefits gained with this level.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -217,15 +208,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 12, - "urlName": "classes", - "href": "/docs/properties/classes", + "urlName": "class", + "href": "/docs/property/class", "published": true, - "description": "A class is a property that expects [class levels](/docs/property/class-level) as its immediate children.\n\nLeveling up a class means choosing, or manually adding, class level properties to it. Class levels with the same variable name as the class, and that match all the required tags are found in libraries and added to the class.\n\nThe total level of the class can be accessed in calculations using `classVariableName.level`.\n\n## Making your own class\n\nSee [Create a Class](/docs/walkthroughs/create-a-class)\n\n---\n\n### Name\n\nThe name of the class\n\n### Variable name\n\nThe name used to refer to the class in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Description\n\nA description of the class.\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Tags required\n\nOnly class levels with the same variable name as the class, and with tags that match the tags required will be returned from libraries when leveling up this class.\n\n### Condition\n\nA [computed field](/docs/computed-fields) to determine if the class is allowed to level up. If this field results in `true` or a number that is not 0, the class can be levelled, otherwise leveling is disabled." + "description": "A class is a property that expects [class levels](/docs/property/class-level) as its immediate children.\n\nLeveling up a class means choosing, or manually adding, class level properties to it. Class levels with the same variable name as the class, and that match all the required tags are found in libraries and added to the class.\n\nThe total level of the class can be accessed in calculations using `classVariableName.level`.\n\n---\n\n### Name\n\nThe name of the class\n\n### Variable name\n\nThe name used to refer to the class in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\n### Description\n\nA description of the class.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Tags required\n\nOnly class levels with the same variable name as the class, and with tags that match the tags required will be returned from libraries when leveling up this class.\n\n### Condition\n\nA [computed field](/docs/concepts/computed-fields) to determine if the class is allowed to level up. If this field results in `true` or a number that is not 0, the class can be levelled, otherwise leveling is disabled." }, { "_id": "giHu6Ej7qvsZr4zrJ", @@ -238,14 +229,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 13, - "urlName": "constants", - "href": "/docs/properties/constants", - "description": "Constants are properties that store some primitive value in a variable name for use in other calculations.\n\nUnlike attributes, constants can store more than just numbers:\n\n- Arrays: `[1,2,3,4]`\n- Text strings: `'I am a cat'`\n- Numbers: `3.14`\n- Boolean values: `true`, `false`\n- Dice rolls: `1d20 + 2`\n\nConstants just can't use other variables in their calculations.\n\n### Overriding constants\n\nIf multiple constants have the same variable name, only the last active constant in the [character tree](/docs/tree) will be used as the definition for that variable name.\n\nThis can be used to re-write the value of some constant by ensuring there is a new active constant later in the sheet.\n\n---\n\n### Name\n\nThe name of the constants\n\n### Variable Name\n\nThe name used to refer to the constant in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Value\n\nA [calculation](/docs/computed-fields) of the final value of the constant.", + "urlName": "constant", + "href": "/docs/property/constant", + "description": "Constants are properties that store some primitive value in a variable name for use in other calculations.\n\nUnlike attributes, constants can store more than just numbers:\n\n- Arrays: `[1,2,3,4]`\n- Text strings: `'I am a cat'`\n- Numbers: `3.14`\n- Boolean values: `true`, `false`\n- Dice rolls: `1d20 + 2`\n\nConstants just can't use other variables in their calculations.\n\n### Overriding constants\n\nIf multiple constants have the same variable name, only the last active constant in the [character tree](/docs/concepts/tree) will be used as the definition for that variable name.\n\nThis can be used to re-write the value of some constant by ensuring there is a new active constant later in the sheet.\n\n---\n\n### Name\n\nThe name of the constants\n\n### Variable Name\n\nThe name used to refer to the constant in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\n### Value\n\nA [calculation](/docs/concepts/computed-fields) of the final value of the constant.", "published": true }, { @@ -259,14 +250,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 14, - "urlName": "containers", - "href": "/docs/properties/containers", - "description": "Containers are things that [items](/docs/property/item) can be put inside of.\n\n---\n\n### Name\n\nThe name of the container\n\n### Carried\n\nIf this is set the weight of the container and its contents will be added to the character's weight carried.\n\n### Value\n\nThe value of the container in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So a container that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp.\n\n### Weight\n\nThe weight of the container in lb.\n\n### Description\n\nA description of the container. \n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Contents are weightless\n\nIf this is set and the container is carried, only the container's own weight will be added to the weight carried by the creature.", + "urlName": "container", + "href": "/docs/property/container", + "description": "Containers are things that [items](/docs/property/item) can be put inside of.\n\n---\n\n### Name\n\nThe name of the container\n\n### Carried\n\nIf this is set the weight of the container and its contents will be added to the character's weight carried.\n\n### Value\n\nThe value of the container in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So a container that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp.\n\n### Weight\n\nThe weight of the container in lb.\n\n### Description\n\nA description of the container. \n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Contents are weightless\n\nIf this is set and the container is carried, only the container's own weight will be added to the weight carried by the creature.", "published": true }, { @@ -280,14 +271,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 15, - "urlName": "damage-multipliers", - "href": "/docs/properties/damage-multipliers", - "description": "Damage multipliers are used to define vulnerability, resistance, and immunity to damage types.\n\nA single multiplier can apply to multiple damage types, and choose whether or not to apply to an incoming source of damage based on the tags present on that damage.\n\n---\n\n### Name\n\nThe name of the feature that gives this damage multiplier\n\n### Value\n\n- **Immunity** The creature takes no damage from matching damage sources\n- **Resistance** Damage from matching sources is halved.\n- **Vulnerability** Damage from matching sources is doubled.\n\n### Damage types\n\nA list of damage types that this property applies to. Custom types can be used.\n\n### Damage tags required\n\nThis damage multiplier will only be applied if the incoming damage has all of these tags present.\n\n### Damage tags excluded\n\nThis damage multiplier will only apply if the incoming damage has none of these tags present.\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "damage-multiplier", + "href": "/docs/property/damage-multiplier", + "description": "Damage multipliers are used to define vulnerability, resistance, and immunity to damage types.\n\nA single multiplier can apply to multiple damage types, and choose whether or not to apply to an incoming source of damage based on the tags present on that damage.\n\n---\n\n### Name\n\nThe name of the feature that gives this damage multiplier\n\n### Value\n\n- **Immunity** The creature takes no damage from matching damage sources\n- **Resistance** Damage from matching sources is halved.\n- **Vulnerability** Damage from matching sources is doubled.\n\n### Damage types\n\nA list of damage types that this property applies to. Custom types can be used.\n\n### Damage tags required\n\nThis damage multiplier will only be applied if the incoming damage has all of these tags present.\n\n### Damage tags excluded\n\nThis damage multiplier will only apply if the incoming damage has none of these tags present.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -301,15 +292,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 16, "urlName": "damage", - "href": "/docs/properties/damage", + "href": "/docs/property/damage", "published": true, - "description": "Damage can be applied by an action to damage a target creature's [health bars](/docs/property/attribute). The damage will be modified by [damage multipliers](/docs/property/damage-multiplier), which apply vulnerability, resistance, and immunity before the damage is applied.\n\n---\n\n### Damage\n\nA [computed field](/docs/computed-fields) that determines how much damage to do to the target creature.\n\n### Damage type\n\nDamage type determines how the damage is treated by [damage multipliers](/docs/property/damage-multiplier). A custom type can be used, or one of the existing types can be selected.\n\nThere are two special damage types:\n\n**Extra damage** Damage with the `extra` type will take on the damage type of whatever damage was applied before it by an action. So if an action deals 12 `piercing` damage and `3` extra damage, it will instead deal 15 `piercing` damage.\n\n**Healing** Damage with the `healing` type will heal a creature instead of damaging them.\n\n### Target\n\n- **Target** Apply the damage to the target of the action\n- **Self** Apply the damage to the creature taking the action\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Don't show in log\n\nIf set, the damage will be applied but not show in the log." + "description": "Damage can be applied by an action to damage a target creature's [health bars](/docs/property/attribute). The damage will be modified by [damage multipliers](/docs/property/damage-multiplier), which apply vulnerability, resistance, and immunity before the damage is applied.\n\n---\n\n### Damage\n\nA [computed field](/docs/concepts/computed-fields) that determines how much damage to do to the target creature.\n\n### Damage type\n\nDamage type determines how the damage is treated by [damage multipliers](/docs/property/damage-multiplier). A custom type can be used, or one of the existing types can be selected.\n\nThere are two special damage types:\n\n**Extra damage** Damage with the `extra` type will take on the damage type of whatever damage was applied before it by an action. So if an action deals 12 `piercing` damage and `3` extra damage, it will instead deal 15 `piercing` damage.\n\n**Healing** Damage with the `healing` type will heal a creature instead of damaging them.\n\n### Target\n\n- **Target** Apply the damage to the target of the action\n- **Self** Apply the damage to the creature taking the action\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Don't show in log\n\nIf set, the damage will be applied but not show in the log." }, { "_id": "FHdAjYY2er9xfYsJs", @@ -322,14 +313,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 17, - "urlName": "effects", - "href": "/docs/properties/effects", - "description": "Effects are the core of the DiceCloud engine. Effect change the values of attributes, skills, and calculations in a way that is transparent and auditable, keeping character sheets organized and understandable, even when using intricate homebrew rules on high level characters.\n\n---\n\n### Name\n\nThe name of the feature that causes this effect.\n\n### Operation\n\nThe operation determines what the effect will do to the affected property or calcualtion.\n\n- **Base Value** Set the base value of the affected property. If a property has multiple base values, the highest is used\n- **Add** Add the value to the affected property or calculation\n- **Muliply** Multiply the affected property by the value\n- **Minimum** Prevent the affected property from having a value less than the effect value\n- **Maximum** Prevent the affected property from having a value greater than the effect value\n- **Maximum** Prevent the affected property from having a value greater than the effect value\n- **Set** Set the value affected property to the effect value\n- **Advantage** Give advantage to checks made using the affected property\n- **Disadvantage** Give disadvantage to checks made using the affected property\n- **Passive bonus** Add the effect value to the passive scores based on the affected property\n- **Fail** Checks made using the affected property automatically fail\n- **Conditional benefit** Add some text to the affected property describing the benefit recieved\n\n### Value\n\nA [computed field](/docs/computed-fields) that determines the value of the effect.\n\n### Text\n\nIf the operation is a conditional benefit, the note text that will show on affected properties.\n\n### Target stats by variable name\n\nIf selected the effect will apply to all properties that have the given variable names.\n\n### Variable names\n\nA list of variable names of properties to target with this effect.\n\n### Target properties by tags\n\nWhen targeting properties by tag, any property can be targeted with an effect. If the property is one that can usually be targeted by variable name, the effect will apply as ususal, however if the effect targets another property, it will apply to a [computed field](/docs/computed-fields) on the property instead.\n\nThese effects can be used for adding a bonus to a specific attack or damage roll, or manipulating any computed field on the creature.\n\n### Tags required\n\nOnly properties that match the required tags will be targeted by the effect.\n\n### Target field\n\nIf a property has multiple computed fields, which field should be targeted by this effect.\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "effect", + "href": "/docs/property/effect", + "description": "Effects are the core of the DiceCloud engine. Effect change the values of attributes, skills, and calculations in a way that is transparent and auditable, keeping character sheets organized and understandable, even when using intricate homebrew rules on high level characters.\n\n---\n\n### Name\n\nThe name of the feature that causes this effect.\n\n### Operation\n\nThe operation determines what the effect will do to the affected property or calcualtion.\n\n- **Base Value** Set the base value of the affected property. If a property has multiple base values, the highest is used\n- **Add** Add the value to the affected property or calculation\n- **Muliply** Multiply the affected property by the value\n- **Minimum** Prevent the affected property from having a value less than the effect value\n- **Maximum** Prevent the affected property from having a value greater than the effect value\n- **Maximum** Prevent the affected property from having a value greater than the effect value\n- **Set** Set the value affected property to the effect value\n- **Advantage** Give advantage to checks made using the affected property\n- **Disadvantage** Give disadvantage to checks made using the affected property\n- **Passive bonus** Add the effect value to the passive scores based on the affected property\n- **Fail** Checks made using the affected property automatically fail\n- **Conditional benefit** Add some text to the affected property describing the benefit recieved\n\n### Value\n\nA [computed field](/docs/concepts/computed-fields) that determines the value of the effect.\n\n### Text\n\nIf the operation is a conditional benefit, the note text that will show on affected properties.\n\n### Target stats by variable name\n\nIf selected the effect will apply to all properties that have the given variable names.\n\n### Variable names\n\nA list of variable names of properties to target with this effect.\n\n### Target properties by tags\n\nWhen targeting properties by tag, any property can be targeted with an effect. If the property is one that can usually be targeted by variable name, the effect will apply as usual, however if the effect targets another property, it will apply to a [computed field](/docs/concepts/computed-fields) on the property instead.\n\nThese effects can be used for adding a bonus to a specific attack or damage roll, or manipulating any computed field on the creature.\n\n### Tags required\n\nOnly properties that match the required tags will be targeted by the effect.\n\n### Target field\n\nIf a property has multiple computed fields, which field should be targeted by this effect.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -343,14 +334,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 18, - "urlName": "features", - "href": "/docs/properties/features", - "description": "Features appear on the features tab. Classes, backgrounds, and race can all give a creature features.\n\n---\n\n### Name\n\nThe name of the feature.\n\n### Summary\n\nA summary of the feature. This will appear on the feature card.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Description\n\nA detailed description of the feature.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "feature", + "href": "/docs/property/feature", + "description": "Features appear on the features tab. Classes, backgrounds, and race can all give a creature features.\n\n---\n\n### Name\n\nThe name of the feature.\n\n### Summary\n\nA summary of the feature. This will appear on the feature card.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\n\nA detailed description of the feature.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -364,15 +355,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 19, - "urlName": "folders", - "href": "/docs/properties/folders", + "urlName": "folder", + "href": "/docs/property/folder", "published": true, - "description": "Folders allow the [character tree](/docs/tree) to be organized.\n\n### Folders in actions\n\nWhen a folder is the child of an action, it and its children will not show on the action card, but will still appear in the detail view of the action and be applied when the action is taken.\n\n---\n\n### Name\n\nThe name of the folder.\n\n### Tags\n\nSee [Tags](/docs/tags)" + "description": "Folders allow the [character tree](/docs/concepts/tree) to be organized.\n\n### Folders in actions\n\nWhen a folder is the child of an action, it and its children will not show on the action card, but will still appear in the detail view of the action and be applied when the action is taken.\n\n### Grouping children\n\nWhen the folder is set to group stats on a card, the immediate children of the folder will be shown in a single card in a location on the sheet you can specify. For now, the following properties are supported:\n\n- Actions, including event actions\n- Attributes\n- Buffs\n- Containers\n- Features\n- Items\n- Notes\n- Skills\n- Toggles (with _Show on character sheet_ enabled)\n\n---\n\n### Name\n\nThe name of the folder.\n\n### Group children on a card\n\nImmediate children will be grouped on a card in the sheet.\n\n### Hide children from stats tab\n\nThe children of this card will not appear on the stats tab, but may still appear in other tabs.\n\n### Tab\n\nDetermines which tab the card will show up on.\n\n### Location\n\nWhere on the tab the card will be positioned.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)" }, { "_id": "5MsdJBbpALgMnYBwk", @@ -385,15 +376,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 20, - "urlName": "items", - "href": "/docs/properties/items", + "urlName": "item", + "href": "/docs/property/item", "published": true, - "description": "Items are shown on the Inventory tab. Items can be carried, put in containers, or equipped on a creature. The children of an item are not active unless the item is equipped.\n\n---\n\n### Icon\n\nAn icon representing the item.\n\n### Equipped\n\nIf set, the item appears in the equipment list on the inventory tab and its children become active on the creature.\n\n### Name\n\nThe name of the item.\n\n### Plural name\n\nThe name to use if the quantity of the item is higher than 1.\n\n### Value\n\nThe value of a single item in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So an item that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp.\n\n### Weight\n\nThe weight of a single item in lb.\n\n### Quantity\n\nNumber of items. The value and quantity will be multiplied by the quantity to get the total value and weight of this stack of items.\n\n### Description\n\nA description of the item. \n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Show increment button\n\nIf this is set, the item will show an increment button in the detail view and on the inventory tab. This button can be used to quickly adjust the quantity of the item.\n\n### Requires attunemnt\n\nIf set, the item requires attunemnt to use.\n\n### Attuned\n\nIf set, the item is attuned and counts towards the total number of attuned items for the creature.\n\nIf a child property needs to determine if its parent item is attuned it can use `#item.attuned` in calculations, see *Ancestor references* in [computed fields](/docs/computed-fields)." + "description": "Items are shown on the Inventory tab. Items can be carried, put in containers, or equipped on a creature. The children of an item are not active unless the item is equipped.\n\n---\n\n### Icon\n\nAn icon representing the item.\n\n### Equipped\n\nIf set, the item appears in the equipment list on the inventory tab and its children become active on the creature.\n\n### Name\n\nThe name of the item.\n\n### Plural name\n\nThe name to use if the quantity of the item is higher than 1.\n\n### Value\n\nThe value of a single item in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So an item that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp.\n\n### Weight\n\nThe weight of a single item in lb.\n\n### Quantity\n\nNumber of items. The value and quantity will be multiplied by the quantity to get the total value and weight of this stack of items.\n\n### Description\n\nA description of the item. \n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Show increment button\n\nIf this is set, the item will show an increment button in the detail view and on the inventory tab. This button can be used to quickly adjust the quantity of the item.\n\n### Requires attunement\n\nIf set, the item requires attunement to use.\n\n### Attuned\n\nIf set, the item is attuned and counts towards the total number of attuned items for the creature.\n\nIf a child property needs to determine if its parent item is attuned it can use `#item.attuned` in calculations, see *Ancestor references* in [computed fields](/docs/concepts/computed-fields)." }, { "_id": "74utQna6D4ayYyrLp", @@ -406,15 +397,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 21, - "urlName": "notes", - "href": "/docs/properties/notes", + "urlName": "note", + "href": "/docs/property/note", "published": true, - "description": "Notes are used to store text on the creature that does not have a direct mechanical impact. Notes appear on the journal tab when active on the character, or are shown in the log when applied by an [action](/docs/property/action).\n\n---\n\n### Name\n\nName of the note.\n\n### Summary\n\nA summary of the note. This will appear on the note card and in the log when applied by an [action](/docs/property/action).\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Description\n\nA detailed description of the feature.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)" + "description": "Notes are used to store text on the creature that does not have a direct mechanical impact. Notes appear on the journal tab when active on the character, or are shown in the log when applied by an [action](/docs/property/action).\n\n---\n\n### Name\n\nName of the note.\n\n### Summary\n\nA summary of the note. This will appear on the note card and in the log when applied by an [action](/docs/property/action).\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\n\nA detailed description of the feature.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)" }, { "_id": "cuhusZb8xYW8dj743", @@ -427,15 +418,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 22, "urlName": "point-buy", - "href": "/docs/properties/point-buy", + "href": "/docs/property/point-buy", "published": true, - "description": "A point buy is a set of rows that lets the user choose a set of stats based on a cost per stat.\n\n---\n\n### Table name\n\nThe name of the point buy table.\n\n### Min\n\nThe lowest value available for each row\n\n### Max\n\nThe highest value available for each row\n\n### Cost\n\nA function that uses `value` as the value of a row and determines the cost of that value. For standard D&D 5e 27 point buy, this function is `[0, 1, 2, 3, 4, 5, 7, 9][value - 7]`\n\n### Total available points\n\nA [computed field](/docs/computed-fields) that determines how many points are available to spend in total\n\n## Rows\n\nUp to 32 rows can be added to a point buy table\n\n### Row name\n\nThe name of the row that will appear in the table\n\n### Row variable name\n\nThe variable name of the row that can be used in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\nIf the variable name matches an attribute with the same variable name, the row's value will be used as a base value for that attribute." + "description": "A point buy is a set of rows that lets the user choose a set of stats based on a cost per stat.\n\n---\n\n### Table name\n\nThe name of the point buy table.\n\n### Min\n\nThe lowest value available for each row\n\n### Max\n\nThe highest value available for each row\n\n### Cost\n\nA function that uses `value` as the value of a row and determines the cost of that value. For standard D&D 5e 27 point buy, this function is `[0, 1, 2, 3, 4, 5, 7, 9][value - 7]`\n\n### Total available points\n\nA [computed field](/docs/concepts/computed-fields) that determines how many points are available to spend in total\n\n## Rows\n\nUp to 32 rows can be added to a point buy table\n\n### Row name\n\nThe name of the row that will appear in the table\n\n### Row variable name\n\nThe variable name of the row that can be used in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctuation.\n\nIf the variable name matches an attribute with the same variable name, the row's value will be used as a base value for that attribute." }, { "_id": "Jh92aYezHsEbSkriy", @@ -448,14 +439,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 23, - "urlName": "proficiencies", - "href": "/docs/properties/proficiencies", - "description": "Proficiencies add proficiency to an existing skill on the creature. If you need to add a tool or language proficiency to a creature, use a [Skill](/docs/property/skill) instead.\n\n---\n\n### Name\n\nName of the feature that is adding this proficiency\n\n### Skills\n\nA list of variable names of the skills to add proficiency to.\n\n### Proficiency\n\nHow much proficiency to add to the skill. If a skill has multiple proficiencies added to it, the highest one will be used.\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "proficiency", + "href": "/docs/property/proficiency", + "description": "Proficiencies add proficiency to an existing skill on the creature. If you need to add a tool or language proficiency to a creature, use a [Skill](/docs/property/skill) instead.\n\n---\n\n### Name\n\nName of the feature that is adding this proficiency\n\n### Skills\n\nA list of variable names of the skills to add proficiency to.\n\n### Proficiency\n\nHow much proficiency to add to the skill. If a skill has multiple proficiencies added to it, the highest one will be used.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -469,15 +460,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 24, "urlName": "remove-buff", - "href": "/docs/properties/remove-buff", + "href": "/docs/property/remove-buff", "published": true, - "description": "This property can remove a specific buff from a targeted creature.\n\n### Name\n\nThe name of the property. This shows in the log when the property is applied.\n\n### Remove parent buff\n\nWhen this is set and the property is applied, the property will remove the nearest parent buff. If this property is not the child of any buffs, it will log an error.\n\n### Remove all\n\nWhen this is set, all buffs that match the target tags will be removed from the targeted creature. If not set, only the first buff found with the matching tags will be removed.\n\n### Target\n\n- **Target** Matching buffs will be removed from the targeted creature\n- **Self** Matching buffs will be removed from the creature that applied the action\n\n### Tags required\n\nAny buff that has all of the required tags will be removed when the property is applied.\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Don't show in log\n\nWhen this is set, the property is applied, but does not show in the log." + "description": "This property can remove a specific buff from a targeted creature.\n\n### Name\n\nThe name of the property. This shows in the log when the property is applied.\n\n### Remove parent buff\n\nWhen this is set and the property is applied, the property will remove the nearest parent buff. If this property is not the child of any buffs, it will log an error.\n\n### Remove all\n\nWhen this is set, all buffs that match the target tags will be removed from the targeted creature. If not set, only the first buff found with the matching tags will be removed.\n\n### Target\n\n- **Target** Matching buffs will be removed from the targeted creature\n- **Self** Matching buffs will be removed from the creature that applied the action\n\n### Tags required\n\nAny buff that has all of the required tags will be removed when the property is applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Don't show in log\n\nWhen this is set, the property is applied, but does not show in the log." }, { "_id": "8e67Pmq7RvggHp4pX", @@ -490,15 +481,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 25, - "urlName": "rolls", - "href": "/docs/properties/rolls", + "urlName": "roll", + "href": "/docs/property/roll", "published": true, - "description": "Rolls are properties that store the result of a calculation to a variable name when applied by an [action](/docs/property/action). The variable name only exists for the duration of that particalar action.\n\nRolls can be useful if you need to deal the same damage to multiple targets, or if damage needs to be rolled then halved by succeeding on a saving throw.\n\n---\n\n### Name\n\nName of the roll. This will be shown in the log when the roll is applied.\n\n### Variable name\n\nThe variable name to store the result of the roll for the duration of the action. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Roll\n\nA [computed field](/docs/computed-fields) that is computed when the roll is applied by an action.\n\n### Don't show in log\n\nIf set, the roll will be applied and store its result in the variable name, but not be shown in the log.\n\n### Tags\n\nSee [Tags](/docs/tags)" + "description": "Rolls are properties that store the result of a calculation to a variable name when applied by an [action](/docs/property/action). The variable name only exists for the duration of that particalar action.\n\nRolls can be useful if you need to deal the same damage to multiple targets, or if damage needs to be rolled then halved by succeeding on a saving throw.\n\n---\n\n### Name\n\nName of the roll. This will be shown in the log when the roll is applied.\n\n### Variable name\n\nThe variable name to store the result of the roll for the duration of the action. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Roll\n\nA [computed field](/docs/concepts/computed-fields) that is computed when the roll is applied by an action.\n\n### Don't show in log\n\nIf set, the roll will be applied and store its result in the variable name, but not be shown in the log.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)" }, { "_id": "Ecc7oWEtoJgXaYLtS", @@ -511,14 +502,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 26, - "urlName": "saving-throws", - "href": "/docs/properties/saving-throws", - "description": "Saving throws are properties that cause the target to make a saving throw when applied. If you want to add a type of saving throw like Strength Save to a creature, use a [skill](/docs/property/skill) instead.\n\nWhen a saving throw is applied, the following variables are added to the scope of that action:\n\n- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw\n- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw\n- `$saveDiceRoll` The unmodified d20 roll the target made to save\n- `$saveRoll` The final value of the saving throw roll after modifiers\n\n### Name\n\nThe name of the saving throw. Usually the ability saving throw targeted: \"Strength Save\".\n\n### DC\n\nThe DC of the saving throw that the target needs to meet\n\n### Save\n\nThe variable name of the skill that will be used to make the saving throw.\n\n### Target\n\n- **Target** Apply the saving throw to the targets of the action. Each target will make the saving throw in turn. Child properties will be applied to each target separately with the results of their individual saving throw. If a value like damage needs to be shared between targets, it should be calculated in a [Roll](/docs/property/roll) before the saving throw.\n- **Self** Apply the saving throw to the creature taking the action. The creature taking the action will become the target for all child properties.\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Don't show in log\n\nIf set, the saving throw will not show in the log when applied, but will still be rolled and apply its children.", + "urlName": "saving-throw", + "href": "/docs/property/saving-throw", + "description": "Saving throws are properties that cause the target to make a saving throw when applied. If you want to add a type of saving throw like Strength Save to a creature, use a [skill](/docs/property/skill) instead.\n\nWhen a saving throw is applied, the following variables are added to the scope of that action:\n\n- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw\n- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw\n- `$saveDiceRoll` The unmodified d20 roll the target made to save\n- `$saveRoll` The final value of the saving throw roll after modifiers\n\n### Name\n\nThe name of the saving throw. Usually the ability saving throw targeted: \"Strength Save\".\n\n### DC\n\nThe DC of the saving throw that the target needs to meet\n\n### Save\n\nThe variable name of the skill that will be used to make the saving throw.\n\n### Target\n\n- **Target** Apply the saving throw to the targets of the action. Each target will make the saving throw in turn. Child properties will be applied to each target separately with the results of their individual saving throw. If a value like damage needs to be shared between targets, it should be calculated in a [Roll](/docs/property/roll) before the saving throw.\n- **Self** Apply the saving throw to the creature taking the action. The creature taking the action will become the target for all child properties.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Don't show in log\n\nIf set, the saving throw will not show in the log when applied, but will still be rolled and apply its children.", "published": true }, { @@ -532,14 +523,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 27, - "urlName": "skills", - "href": "/docs/properties/skills", - "description": "Skills represent things the creature can be proficient in. Skills can have their values or behavior modified by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).\n\n---\n\n### Name\n\nThe name of the skill.\n\n### Variable name\n\nThe name used to refer to the skill in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Ability\n\nThe ability score that is the basis for checks made with this skill\n\n### Type\n\n- **Skill** Regular skills like *Athletics*, *Sleight* of Hand\n- **Save** Saving throws like *Strength*, *Charisma*\n- **Check** Checks that aren't skill like *Initiative*\n- **Tool** Tool proficiencies\n- **Weapon** Weapon proficiencies\n- **Armor** Armor proficiencies\n- **Language** Language proficiencies\n- **Utility** Skills that don't show on the charcater sheet but can be used in calculations\n\n### Description\n\nA detailed description of the skill.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Base value\n\nA [computed field](/docs/computed-fields) that determines the starting value of the skill before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied.\n\n### Base proficiency\n\nThe starting proficiency of the skill.", + "urlName": "skill", + "href": "/docs/property/skill", + "description": "Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).\n\n---\n\n### Name\n\nThe name of the skill.\n\n### Variable name\n\nThe name used to refer to the skill in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Ability\n\nThe ability score that is the basis for checks made with this skill\n\n### Type\n\n- **Skill** Regular skills like *Athletics*, *Sleight* of Hand\n- **Save** Saving throws like *Strength*, *Charisma*\n- **Check** Checks that aren't skill like *Initiative*\n- **Tool** Tool proficiencies\n- **Weapon** Weapon proficiencies\n- **Armor** Armor proficiencies\n- **Language** Language proficiencies\n- **Utility** Skills that don't show on the charcater sheet but can be used in calculations\n\n### Description\n\nA detailed description of the skill.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Base value\n\nA [computed field](/docs/concepts/computed-fields) that determines the starting value of the skill before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied.\n\n### Base proficiency\n\nThe starting proficiency of the skill.", "published": true }, { @@ -553,15 +544,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 28, "urlName": "slot-filler", - "href": "/docs/properties/slot-filler", + "href": "/docs/property/slot-filler", "published": true, - "description": "A slot filler is a property that can be used to add more complex behavior to filling a [slot](/docs/property/slot) from a library.\n\n---\n\n### Name\n\nThe name of the slot filler that will show when choosing the filler from the library.\n\n### Icon\n\nIcon of the slot filler\n\n### Description\n\nA detailed description of the slot filler.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Picture URL\n\nA link to an image to use for this slot filler when being chosen from a library.\n\n### Type\n\nSlot fillers can pretend to be any type of property when a slot is being filled.\n\n### Quantity\n\nHow many spaces the slot filler will take up in a slot.\n\n### Condition\n\nA [computed field](/docs/computed-fields) that determines whether this slot filler can be added to a character.\n\n### Tags\n\nSee [Tags](/docs/tags)" + "description": "A slot filler is a property that can be used to add more complex behavior to filling a [slot](/docs/property/slot) from a library.\n\n---\n\n### Name\n\nThe name of the slot filler that will show when choosing the filler from the library.\n\n### Icon\n\nIcon of the slot filler\n\n### Description\n\nA detailed description of the slot filler.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Picture URL\n\nA link to an image to use for this slot filler when being chosen from a library.\n\n### Type\n\nSlot fillers can pretend to be any type of property when a slot is being filled.\n\n### Quantity\n\nHow many spaces the slot filler will take up in a slot.\n\n### Condition\n\nA [computed field](/docs/concepts/computed-fields) that determines whether this slot filler can be added to a character.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)" }, { "_id": "bj5Bh5gsmjkLpYqA4", @@ -574,14 +565,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 29, - "urlName": "slots", - "href": "/docs/properties/slots", - "description": "Slots are the main way creatures interact with libraries. A slot can be filled by choosing a property from a library that fits that particular slot.\n\nIn a complete library, a creature can be built entirely by choosing which properties to fill each slot with.\n\nSlots show up on the build tab, and are highlighted when they have space that can be filled.\n\nIf you are building a creature without a library, you should either ignore slots entirely, or fill them with your own custom properties.\n\n---\n\n### Name\n\nThe name of the slot.\n\n### Type \n\nWhat kind of property this slot expects to fill it.\n\n### Tags required\n\nProperties in a library must have the required tags to fill the slot.\n\n### Quantity\n\nHow many properties are expected to fill this slot. Use 0 for allowing an unlimited number of properties.\n\n### Condition\n\nA [computed field](/docs/computed-fields) that determines whether this slot can accept new properties.\n\n### Unique\n\nThe slot can control how it deals with the uniqueness of properties that fill it.\n\n### Description\n\nA detailed description of the attribute.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Hide when full\n\nWhen set the slot will hide itself when it is filled.\n\n### Ignored\n\nWhen set the slot will not show a prompt card on the build tab.\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "slot", + "href": "/docs/property/slot", + "description": "Slots are the main way creatures interact with libraries. A slot can be filled by choosing a property from a library that fits that particular slot.\n\nIn a complete library, a creature can be built entirely by choosing which properties to fill each slot with.\n\nSlots show up on the build tab, and are highlighted when they have space that can be filled.\n\nIf you are building a creature without a library, you should either ignore slots entirely, or fill them with your own custom properties.\n\n---\n\n### Name\n\nThe name of the slot.\n\n### Type \n\nWhat kind of property this slot expects to fill it.\n\n### Tags required\n\nProperties in a library must have the required tags to fill the slot.\n\n### Quantity\n\nHow many properties are expected to fill this slot. Use 0 for allowing an unlimited number of properties.\n\n### Condition\n\nA [computed field](/docs/concepts/computed-fields) that determines whether this slot can accept new properties.\n\n### Unique\n\nThe slot can control how it deals with the uniqueness of properties that fill it.\n\n### Description\n\nA detailed description of the attribute.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Hide when full\n\nWhen set the slot will hide itself when it is filled.\n\n### Ignored\n\nWhen set the slot will not show a prompt card on the build tab.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -595,15 +586,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 30, - "urlName": "spell-lists", - "href": "/docs/properties/spell-lists", + "urlName": "spell-list", + "href": "/docs/property/spell-list", "published": true, - "description": "Spell lists are collections of [spells](/docs/property/spell).\n\n---\n\n### Name\n\nThe name of the spell list.\n\n### Description\n\nA detailed description of the spell list.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Maximum prepared spells\n\nA [computed field](/docs/computed-fields) that determines how many spells can be considered ready to cast in this spell list.\n\n### Spell casting ability\n\nThe spellcasting ablity for this spell list. The variable name of the ability can be accessed using `#spellList.ability` and the ability modifier with `#spellList.abilityMod`. Setting this field will automatically update Spell save DC and Attack roll bonus if they aren't set manually.\n\n### Spell save DC\n\nA [computed field](/docs/computed-fields) that determines the DC of saving throws in this spell list. Spells can access the DC of their spell list using `#spellList.dc`\n\n### Attack roll bonus\n\nA [computed field](/docs/computed-fields) that determines the bonus to add to a d20 when making a spell attack with a spell in this spell list. Spells can access the attack roll bonus of their spell list using `#spellList.attackRollBonus`\n\n### Tags\n\nSee [Tags](/docs/tags)" + "description": "Spell lists are collections of [spells](/docs/property/spell).\n\n---\n\n### Name\n\nThe name of the spell list.\n\n### Description\n\nA detailed description of the spell list.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Maximum prepared spells\n\nA [computed field](/docs/concepts/computed-fields) that determines how many spells can be considered ready to cast in this spell list.\n\n### Spell casting ability\n\nThe spellcasting ability for this spell list. The variable name of the ability can be accessed using `#spellList.ability` and the ability modifier with `#spellList.abilityMod`. Setting this field will automatically update Spell save DC and Attack roll bonus if they aren't set manually.\n\n### Spell save DC\n\nA [computed field](/docs/concepts/computed-fields) that determines the DC of saving throws in this spell list. Spells can access the DC of their spell list using `#spellList.dc`\n\n### Attack roll bonus\n\nA [computed field](/docs/concepts/computed-fields) that determines the bonus to add to a d20 when making a spell attack with a spell in this spell list. Spells can access the attack roll bonus of their spell list using `#spellList.attackRollBonus`\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)" }, { "_id": "Mji9Cnp2TcFHmQebt", @@ -616,14 +607,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 31, - "urlName": "spells", - "href": "/docs/properties/spells", - "description": "Spells work similarly to [actions](/docs/property/action). They appear on the spells tab and can be cast with or without using up spell slots.\n\n---\n\n### Always prepared\n\nA spell that is always prepared does not count towards the spell list's maximum prepared spells and is always active and ready to cast.\n\n### Prepared\n\nA prepared spell is ready to cast and counts against a spell list's maximum prepared spells.\n\n### Cast without spell slots\n\nWhen set, this spell can be cast without consuming spell slots. It will however consume its own uses and resources.\n\n### School\n\nWhat school the spell belongs to.\n\n### Casting time\n\nHow long the spell takes to Cast\n\n### Range\n\nThe range of the spell\n\n### Duration\n\nHow long the spell lasts\n\n### Components\n\nWhether the spell requires verbal, somatic, or material components and whether the spell is a ritual or requires concentration.\n\n### Target\n\nWho this spell should apply to. The properties under the spell will be applied to the targets.\n\n- **Self** The spell will apply its properties to the creature casting the spell\n- **Single Target** The spell will apply its properties without a target (for now)\n- **Multiple Targets** The spell will apply its properties without a target (for now)\n\n### Attack roll\n\nA [computed field](/docs/computed-fields) which calculates the spell attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. To use the spell list's attack roll bonus use `#spellList.attackRollBonus`.\n\nThe following variables may be added to the action scope when attack rolls are made:\n\n - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n - `$attackRoll` The total attack roll after modifiers.\n - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`.\n - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n### Summary\n\nA brief overview of what the spell does. This will show in the log when the spell is cast.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Description\n\nA more detailed description of the spell. The description does not show in the log when the spell is cast.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Resource\n\nA resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the spell can't be cast.\n\nIf you want to reduce an attribute when casting the spell, but want the spell to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the spell instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number.\n\n#### Resource attribute\n\nThe variable name of the attribute that will be consumed when casting this spell.\n\n#### Resource quantity\n\nA [computed field](/docs/computed-fields) which determines how much of the attribute is required to apply this spell. This amount will be deducted from the attribute every time the spell is cast\n### Ammo\n\nAmmo represents items that are requied to cast the spell. If an item is not selected, or there is insufficient quantity of the selected item, the spell can't be appled.\n\n#### Ammo item\n\nSpecify what tag an item must have to be considered valid ammo for this spell. Any item with this tag can be selected as ammo for this spell.\n\n#### Ammo quantity\n\nA [computed field](/docs/computed-fields) which determines how many of the selected items are required to cast this spell. The quantity is deducted from the total quantity of the item when this spell is applied.\n\n### Uses\n\nA [computed field](/docs/computed-fields) which determines how many times this spell can be used before it needs to be reset.\n\n### Uses used\n\nHow many of this spell's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the spell has uses and its uses are reset.\n\n### Reset\n\nIf set, the uses used field is set to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed\n\n\n### Tags\n\nSee [Tags](/docs/tags)", + "urlName": "spell", + "href": "/docs/property/spell", + "description": "Spells work similarly to [actions](/docs/property/action). They appear on the spells tab and can be cast with or without using up spell slots.\n\n---\n\n### Always prepared\n\nA spell that is always prepared does not count towards the spell list's maximum prepared spells and is always active and ready to cast.\n\n### Prepared\n\nA prepared spell is ready to cast and counts against a spell list's maximum prepared spells.\n\n### Cast without spell slots\n\nWhen set, this spell can be cast without consuming spell slots. It will however consume its own uses and resources.\n\n### School\n\nWhat school the spell belongs to.\n\n### Casting time\n\nHow long the spell takes to Cast\n\n### Range\n\nThe range of the spell\n\n### Duration\n\nHow long the spell lasts\n\n### Components\n\nWhether the spell requires verbal, somatic, or material components and whether the spell is a ritual or requires concentration.\n\n### Target\n\nWho this spell should apply to. The properties under the spell will be applied to the targets.\n\n- **Self** The spell will apply its properties to the creature casting the spell\n- **Single Target** The spell will apply its properties without a target (for now)\n- **Multiple Targets** The spell will apply its properties without a target (for now)\n\n### Attack roll\n\nA [computed field](/docs/concepts/computed-fields) which calculates the spell attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. To use the spell list's attack roll bonus use `#spellList.attackRollBonus`.\n\nThe following variables may be added to the action scope when attack rolls are made:\n\n - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n - `$attackRoll` The total attack roll after modifiers.\n - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`.\n - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n### Summary\n\nA brief overview of what the spell does. This will show in the log when the spell is cast.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Description\n\nA more detailed description of the spell. The description does not show in the log when the spell is cast.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Resource\n\nA resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the spell can't be cast.\n\nIf you want to reduce an attribute when casting the spell, but want the spell to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the spell instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number.\n\n#### Resource attribute\n\nThe variable name of the attribute that will be consumed when casting this spell.\n\n#### Resource quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how much of the attribute is required to apply this spell. This amount will be deducted from the attribute every time the spell is cast\n### Ammo\n\nAmmo represents items that are requied to cast the spell. If an item is not selected, or there is insufficient quantity of the selected item, the spell can't be appled.\n\n#### Ammo item\n\nSpecify what tag an item must have to be considered valid ammo for this spell. Any item with this tag can be selected as ammo for this spell.\n\n#### Ammo quantity\n\nA [computed field](/docs/concepts/computed-fields) which determines how many of the selected items are required to cast this spell. The quantity is deducted from the total quantity of the item when this spell is applied.\n\n### Uses\n\nA [computed field](/docs/concepts/computed-fields) which determines how many times this spell can be used before it needs to be reset.\n\n### Uses used\n\nHow many of this spell's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the spell has uses and its uses are reset.\n\n### Reset\n\nIf set, the uses used field is set to 0 at the appropriate time.\n\n- **Long rest** Reset when the long rest button is pushed\n- **Short rest** Reset when either the long or short rest button is pushed\n\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "published": true }, { @@ -637,15 +628,15 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 32, - "urlName": "toggles", - "href": "/docs/properties/toggles", + "urlName": "toggle", + "href": "/docs/property/toggle", "published": true, - "description": "Toggles are a way to turn on and off parts of a creature. When a toggle is off, none of its children will be active.\n\nCalculated toggles should be avoided if possible, because while they offer a lot of power and flexibility to the creature engine, they often create [dependency loops](/docs/dependency-loops) that can be difficult to troubleshoot, causing parts of a creature to calculate incorrectly.\n\nCalculated toggles can be applied by [actions](/docs/property/action) and will apply their children if the condition is true, but they should be avoided in favor of [conditional branches](/docs/property/branch) which can do the same, but are more efficient.\n\n---\n\n### Name\n\nThe name of the toggle.\n\n### Variable name\n\nThe name used to refer to the value of the toggle in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Show on character sheet\n\nIf set, the toggle with show a checkbox on the character sheet. A calculated toggle will show a disabled checkbox, filled in if the toggle's calculation returned `true` or a value that isn't 0.\n\n### State\n\n- **Enabled** The toggle and its children are active\n- **Disabled** The toggle and its children are inactive\n- **Calculated** The active status of the toggle depends on the result of the condition. Use with caution.\n\n### Condition\n\nA [computed field](/docs/computed-fields) that determines if the toggle is active. Use with caution.\n\n### Tags\n\nSee [Tags](/docs/tags)" + "description": "Toggles are a way to turn on and off parts of a creature. When a toggle is off, none of its children will be active.\n\nCalculated toggles should be avoided if possible, because while they offer a lot of power and flexibility to the creature engine, they often create [dependency loops](/docs/concepts/dependency-loops) that can be difficult to troubleshoot, causing parts of a creature to calculate incorrectly.\n\nCalculated toggles can be applied by [actions](/docs/property/action) and will apply their children if the condition is true, but they should be avoided in favor of [conditional branches](/docs/property/branch) which can do the same, but are more efficient.\n\n---\n\n### Name\n\nThe name of the toggle.\n\n### Variable name\n\nThe name used to refer to the value of the toggle in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation.\n\n### Show on character sheet\n\nIf set, the toggle with show a checkbox on the character sheet. A calculated toggle will show a disabled checkbox, filled in if the toggle's calculation returned `true` or a value that isn't 0.\n\n### State\n\n- **Enabled** The toggle and its children are active\n- **Disabled** The toggle and its children are inactive\n- **Calculated** The active status of the toggle depends on the result of the condition. Use with caution.\n\n### Condition\n\nA [computed field](/docs/concepts/computed-fields) that determines if the toggle is active. Use with caution.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)" }, { "_id": "v7eRZRdMoDPah7ZtE", @@ -658,14 +649,14 @@ { "id": "ioei4uvDdGTAFqZrB", "collection": "docs", - "name": "Properties", - "urlName": "properties" + "urlName": "property", + "name": "Properties" } ], "order": 33, - "urlName": "triggers", - "href": "/docs/properties/triggers", - "description": "Triggers apply their children whenever their condition is met. They work like [actions](/docs/property/action) that are taken automatically.\n\n---\n\n### Name\n\nThe name of the trigger.\n\n### Timing\n\n- **Before** The trigger is applied before the triggering event takes place\n- **After** The trigger is fired after the triggering event\n\n### Event\n\n- **Do action** While the creature is doing an action, the action property specified in *Event type* is applied\n- **Roll check** The creature makes a check\n- **Attribute damaged or healed** One of the creature's attributes changed value through attribute damage or manual adjustment\n- **Short or long rest**\n- **Short rest**\n- **Long Rest**\n\n### Event type\n\nThe trigger will apply when this property type is applied by the action\n\n### Tags required\n\nIf this trigger is fired by a property, the property must match these tags for the trigger to fire.\n\n### Condition\n\nA [computed field](/docs/computed-fields) to determine if the trigger should fire. The trigger will fire if the condition field is empty or if it returns `true` or a value that isn't 0.\n\n### Description\n\nA detailed description of the trigger.\n\nAllows [inline calculations](/docs/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/tags)\n\n### Don't show in log\n\nWhen this is true, the trigger does not show up in the log. This does not stop the trigger's children from appearing in the log when they are applied.", + "urlName": "trigger", + "href": "/docs/property/trigger", + "description": "Triggers apply their children whenever their condition is met. They work like [actions](/docs/property/action) that are taken automatically.\n\n---\n\n### Name\n\nThe name of the trigger.\n\n### Timing\n\n- **Before** The trigger is applied before the triggering event takes place\n- **After** The trigger is fired after the triggering event\n\n### Event\n\n- **Do action** While the creature is doing an action, the action property specified in *Event type* is applied\n- **Roll check** The creature makes a check\n- **Attribute damaged or healed** One of the creature's attributes changed value through attribute damage or manual adjustment\n- **Short or long rest**\n- **Short rest**\n- **Long Rest**\n\n### Event type\n\nThe trigger will apply when this property type is applied by the action\n\n### Tags required\n\nIf this trigger is fired by a property, the property must match these tags for the trigger to fire.\n\n### Condition\n\nA [computed field](/docs/concepts/computed-fields) to determine if the trigger should fire. The trigger will fire if the condition field is empty or if it returns `true` or a value that isn't 0.\n\n### Description\n\nA detailed description of the trigger.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)\n\n### Don't show in log\n\nWhen this is true, the trigger does not show up in the log. This does not stop the trigger's children from appearing in the log when they are applied.", "published": true }, { @@ -687,7 +678,7 @@ "urlName": "computed-fields", "href": "/docs/concepts/computed-fields", "published": true, - "description": "Some fields in DiceCloud creature properties expect calculations. These fields are then computed by the DiceCloud engine.\n\nSome fields, like the value of an attirbute, resolve down to a single number, while others, like the damage to deal in an attack, only simplify their calculation as far as they can, and then resolve down to a number when applied. Avoid adding dice rolls to calculations that expect to resolve down to a number, because they will re-roll every time the creature is recalculated, causing instability in the creature's stats.\n\n## Parser\n\nThe DiceCloud parser can understand the following syntax:\n\n| | |\n| :- | :- |\n| **Numbers** | `13`, `3.14` |\n| **Dice rolls** | `3d6`, `(1 + 2)d4`|\n| **Strings of text** | `'Some text'`, `\"some other text\"` |\n| **Boolean values** | `true` or `false`. When DiceCloud expects a boolean, `0`, an empty string `''` and `false` are all considered false by DiceCloud's engine, every other value is considered true. |\n| **Variable names** | `variableName` |\n| **Addition and subtraction** | `1 + 2 + 3`, `12 - 6` |\n| **Multiplication** | `6 * 4`, `12 * 2` = `24` |\n| **Exponents** | `3 ^ 2` Raise 3 to the power of 2 |\n| **Modulo** | Returns the remainder of a division operation `15 % 6` = `3` |\n| **AND** | `&` or `&&`: Returns the value of the right hand side if the left side is true `true & 'cat'` = `'cat'` |\n| **OR** | `|` or `||`: Returns the left hand side if it is true, otherwise returns the right hand side `'dog' || 'cat'` = `'dog'` |\n| **NOT** | `!` returns false if the value after it is true, otherwise returns false |\n| **Comparisons** | greater than: `>`, less than: `<`, greater than or equal to: `>=`, less than or equal to: `<=`, equal: `=` or `==` or `===`, not equal: `!=` or `!==` |\n| **If-else** | `condition ? resultIfTrue : resultIfFalse`, `level > 10 ? 'high tier' : 'low tier'` |\n| **Arrays** | lists of values `[3, 6, 9, 12]`. |\n| **Array Indexes** | A value can be chosen from an array using another set of square brackets: `[3, 6, 9, 12][2]` = `[6]` because `[2]` fetches the 2nd value in the array. Arrays start at 1 in DiceCloud so that level tables can have 20 entries and be accessed by `array[level]`. |\n| **Function calls** | `functionName(argument1, argument1)` See [Functions](/docs/functions) for a full list of available functions. |\n\n## Special variables\n\n### Built-in variables\n\nThese variables are added to the creature automatically when relevant. They can be overriden if needed by creating a property with the same variable name. They can also be targetted by effects.\n\n- `xp` A total of all the experiences with xp added to the character sheet\n- `milestoneLevels` A total of all the experiences with milestone levels added to the character sheet\n- `itemsAttuned` Number of items the creature is attuned to\n- `weightEquipment` Total weight of all equipment on the creature\n- `valueEquipment` Total value of all equipment on the creature\n- `weightTotal` Total weight of the creature's entire inventory\n- `valueTotal` Total value of the creature's entire inventory\n- `weightCarried` Total weight of all carried items and containers\n- `valueCarried` Total value of all carried items and containers\n- `level` The current level of the creature, including all class levels\n- `criticalHitTarget` Defaults to 20, the natural roll needed to consider an attack roll as a critical hit\n\n### Action variables\n\nThese variables are available during an action after the relevant property has been applied.\n\nFor Advanced users, a [Roll](/docs/property/roll) can set these variables, overriding the default behavior.\n\n#### [Actions](/docs/property/action)\n\n- `$attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n- `$attackRoll` The total attack roll after modifiers.\n- `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`.\n- `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n- `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n- `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n#### [Damage](/docs/property/damage)\n\n- `$lastDamageType` The type of damage dealt last, any damage that has the `extra` type will use this damage type instead\n\n#### [Saving throws](/docs/property/saving-throw)\n\n- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw\n- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw\n- `$saveDiceRoll` The unmodified d20 roll the target made to save\n- `$saveRoll` The final value of the saving throw roll after modifiers\n\n## Ancestor references\n\nThe ancestors of a property can be accessed directly using the `#ancestorType` syntax.\n\nFor example, a spell might need to know the save DC of the spell list that it is inside of, it can use `#spellList.dc`.\n\nTriggers and their children work differently: They don't have access to their own ancestors, but rather inherit the ancestors of the property that caused them to fire. For example, a trigger at the root of the creature's tree might be fired by a spell being cast, you can still use references to ancestors like `#spellList.attackRollBonus` inside that trigger as if it were under the spell itself." + "description": "Some fields in DiceCloud creature properties expect calculations. These fields are then computed by the DiceCloud engine.\n\nSome fields, like the value of an attirbute, resolve down to a single number, while others, like the damage to deal in an attack, only simplify their calculation as far as they can, and then resolve down to a number when applied. Avoid adding dice rolls to calculations that expect to resolve down to a number, because they will re-roll every time the creature is recalculated, causing instability in the creature's stats.\n\n## Parser\n\nThe DiceCloud parser can understand the following syntax:\n\n| | |\n| :- | :- |\n| **Numbers** | `13`, `3.14` |\n| **Dice rolls** | `3d6`, `(1 + 2)d4`|\n| **Strings of text** | `'Some text'`, `\"some other text\"` |\n| **Boolean values** | `true` or `false`. When DiceCloud expects a boolean, `0`, an empty string `''` and `false` are all considered false by DiceCloud's engine, every other value is considered true. |\n| **Variable names** | `variableName` |\n| **Addition and subtraction** | `1 + 2 + 3`, `12 - 6` |\n| **Multiplication** | `6 * 4`, `12 * 2` = `24` |\n| **Exponents** | `3 ^ 2` Raise 3 to the power of 2 |\n| **Modulo** | Returns the remainder of a division operation `15 % 6` = `3` |\n| **AND** | `&` or `&&`: Returns the value of the right hand side if the left side is true `true & 'cat'` = `'cat'` |\n| **OR** | `|` or `||`: Returns the left hand side if it is true, otherwise returns the right hand side `'dog' || 'cat'` = `'dog'` |\n| **NOT** | `!` returns false if the value after it is true, otherwise returns false |\n| **Comparisons** | greater than: `>`, less than: `<`, greater than or equal to: `>=`, less than or equal to: `<=`, equal: `=` or `==` or `===`, not equal: `!=` or `!==` |\n| **If-else** | `condition ? resultIfTrue : resultIfFalse`, `level > 10 ? 'high tier' : 'low tier'` |\n| **Arrays** | lists of values `[3, 6, 9, 12]`. |\n| **Array Indexes** | A value can be chosen from an array using another set of square brackets: `[3, 6, 9, 12][2]` = `[6]` because `[2]` fetches the 2nd value in the array. Arrays start at 1 in DiceCloud so that level tables can have 20 entries and be accessed by `array[level]`. |\n| **Function calls** | `functionName(argument1, argument1)` See [Functions](/docs/concepts/functions) for a full list of available functions. |\n\n## Special variables\n\n### Built-in variables\n\nThese variables are added to the creature automatically when relevant. They can be overriden if needed by creating a property with the same variable name. They can also be targetted by effects.\n\n- `xp` A total of all the experiences with xp added to the character sheet\n- `milestoneLevels` A total of all the experiences with milestone levels added to the character sheet\n- `itemsAttuned` Number of items the creature is attuned to\n- `weightEquipment` Total weight of all equipment on the creature\n- `valueEquipment` Total value of all equipment on the creature\n- `weightTotal` Total weight of the creature's entire inventory\n- `valueTotal` Total value of the creature's entire inventory\n- `weightCarried` Total weight of all carried items and containers\n- `valueCarried` Total value of all carried items and containers\n- `level` The current level of the creature, including all class levels\n- `criticalHitTarget` Defaults to 20, the natural roll needed to consider an attack roll as a critical hit\n\n### Action variables\n\nThese variables are available during an action after the relevant property has been applied.\n\nFor Advanced users, a [Roll](/docs/property/roll) can set these variables, overriding the default behavior.\n\n#### [Actions](/docs/property/action)\n\n- `$attackDiceRoll` The value of the d20 roll before any modifiers were applied.\n- `$attackRoll` The total attack roll after modifiers.\n- `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`.\n- `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll.\n- `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit.\n- `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss.\n\n#### [Damage](/docs/property/damage)\n\n- `$lastDamageType` The type of damage dealt last, any damage that has the `extra` type will use this damage type instead\n\n#### [Saving throws](/docs/property/saving-throw)\n\n- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw\n- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw\n- `$saveDiceRoll` The unmodified d20 roll the target made to save\n- `$saveRoll` The final value of the saving throw roll after modifiers\n\n## Ancestor references\n\nThe ancestors of a property can be accessed directly using the `#ancestorType` syntax.\n\nFor example, a spell might need to know the save DC of the spell list that it is inside of, it can use `#spellList.dc`.\n\nTriggers and their children work differently: They don't have access to their own ancestors, but rather inherit the ancestors of the property that caused them to fire. For example, a trigger at the root of the creature's tree might be fired by a spell being cast, you can still use references to ancestors like `#spellList.attackRollBonus` inside that trigger as if it were under the spell itself." }, { "_id": "o8u2Z5gZW54ZXNeZB", @@ -708,7 +699,7 @@ "urlName": "dependency-loops", "href": "/docs/concepts/dependency-loops", "published": true, - "description": "When a variable is referenced in a calculation, that calculation can be said to depend on that variable. In order for the calculation to compute, the value of the variable needs to be known.\n\nBut consider the following property values that could be added to a creature\n\n- The creature's Strength base value is set to `dexterity + 1` so that it will always have 1 more strength than dexterity\n- The creature's Dexterity base value is set to `constitution + 1` so that it will always have 1 more dexterity than constitution\n- The creature's Constitution base value is set to `strength` so that its constitution is always equal to its strength\n\nIt is not possible to resolve these calculations, not just because no values exist which satisfy the constraints, but because strength depends on dexterity which depends on constitution which depends on strength. None can be computed before the others have finalized their values. This is a dependency loop.\n\nMost dependency loops that appear in actual DiceCloud creatures are less trivial than this example, but they cause the same result: a sheet that can't be accurately computed. In these cases, DiceCloud does its best, chooses an order to resolve the calculations arbitrarily, and continues calculating. An error will show on the Build tab to let you know that something went wrong.\n\n![dependency loop example](/images/docs/dependency-loop.png)\n\n## Toggles\n\nCalculated [Toggles](/docs/property/toggle) are the main source of dependency loops on creatures, because they create a dependency that isn't as obvious as a calculation might be. When a toggle is in calculated mode, its children do not know whether they are active or not until the calulation is resolved. Because of this, every calculation under the toggle depends on the toggles calaculation, making the chance for a loop to be formed more likely the more children are under a toggle.\n\nConsider this example\n\n- A calculated toggle that is active if `strength < 10`\n- An effect under that toggle that adds 2 to `strength`\n\nThe effect can't compute, because it does not know if it is active yet, so the toggle must compute its calculation first. The toggle needs to know if `strength` is greater than 10. Strength depends on all of the effects targeting it, it must know if the +2 effect is active or not. This creates a dependency loop, because there is no valid order in which everything can be calculated.\n\n## Troubleshooting a dependency loop\n\n- First, identify all the properties that make up the dependency loop. These are linked in the depdency loop error message. The field names in square brackets after the property name indicates which calculations on the property are directly involved.\n- Move any properties in the loop out from being children of calculated Toggles\n- Use static values in place of variables where they are not stricly needed\n- Ask for [help](/feedback)" + "description": "When a variable is referenced in a calculation, that calculation can be said to depend on that variable. In order for the calculation to compute, the value of the variable needs to be known.\n\nBut consider the following property values that could be added to a creature\n\n- The creature's Strength base value is set to `dexterity + 1` so that it will always have 1 more strength than dexterity\n- The creature's Dexterity base value is set to `constitution + 1` so that it will always have 1 more dexterity than constitution\n- The creature's Constitution base value is set to `strength` so that its constitution is always equal to its strength\n\nIt is not possible to resolve these calculations, not just because no values exist which satisfy the constraints, but because strength depends on dexterity which depends on constitution which depends on strength. None can be computed before the others have finalized their values. This is a dependency loop.\n\nMost dependency loops that appear in actual DiceCloud creatures are less trivial than this example, but they cause the same result: a sheet that can't be accurately computed. In these cases, DiceCloud does its best, chooses an order to resolve the calculations arbitrarily, and continues calculating. An error will show on the Build tab to let you know that something went wrong.\n\n![dependency loop example](/images/docs/dependency-loop.png)\n\n## Toggles\n\nCalculated [Toggles](/docs/property/toggle) are the main source of dependency loops on creatures, because they create a dependency that isn't as obvious as a calculation might be. When a toggle is in calculated mode, its children do not know whether they are active or not until the calulation is resolved. Because of this, every calculation under the toggle depends on the toggles calaculation, making the chance for a loop to be formed more likely the more children are under a toggle.\n\nConsider this example\n\n- A calculated toggle that is active if `strength < 10`\n- An effect under that toggle that adds 2 to `strength`\n\nThe effect can't compute, because it does not know if it is active yet, so the toggle must compute its calculation first. The toggle needs to know if `strength` is greater than 10. Strength depends on all of the effects targeting it, it must know if the +2 effect is active or not. This creates a dependency loop, because there is no valid order in which everything can be calculated.\n\n## Troubleshooting a dependency loop\n\n- First, identify all the properties that make up the dependency loop. These are linked in the dependency loop error message. The field names in square brackets after the property name indicates which calculations on the property are directly involved.\n- Move any properties in the loop out from being children of calculated Toggles\n- Use static values in place of variables where they are not strictly needed\n- Ask for [help](/feedback)" }, { "_id": "KFkmXFLQrdPQNpJ7X", @@ -728,7 +719,7 @@ "order": 36, "urlName": "inline-calculations", "href": "/docs/concepts/inline-calculations", - "description": "Most long-format fields allow inline [calculations](/docs/computed-fields) to be included. Calculations inside of curly bracers will be computed down to numbers using the characters stats.\n\nFor example a creature's strength attribute may have the following in its description: `Your carrying capacity is {strength * 15} lbs.`\n\nWhen the creature is calculated, if it has 8 strength, the action description will become: \"Your carrying capacity is 120 lbs.\"\n\nIf a description includes a dice roll, only the part that can be calculated to a single number should be included in the calulation bracers: `The attack does an extra {paladin.level}d8 damage`, which becomes `The attack does an extra 4d8 damage`.\n\nDo not inlclude the dice roll in the calaculation: `The attack does an extra {(paladin.level)d8} damage`, because it will become `The attack does an extra 16 damage` but the number 16 will change every time the creature recalculates.", + "description": "Most long-format fields allow inline [calculations](/docs/concepts/computed-fields) to be included. Calculations inside of curly bracers will be computed down to numbers using the characters stats.\n\nFor example a creature's strength attribute may have the following in its description: `Your carrying capacity is {strength * 15} lbs.`\n\nWhen the creature is calculated, if it has 8 strength, the action description will become: \"Your carrying capacity is 120 lbs.\"\n\nIf a description includes a dice roll, only the part that can be calculated to a single number should be included in the calulation bracers: `The attack does an extra {paladin.level}d8 damage`, which becomes `The attack does an extra 4d8 damage`.\n\nDo not inlclude the dice roll in the calaculation: `The attack does an extra {(paladin.level)d8} damage`, because it will become `The attack does an extra 16 damage` but the number 16 will change every time the creature recalculates.", "published": true }, { @@ -771,5 +762,26 @@ "href": "/docs/concepts/functions", "description": "## min\nReturns the smallest of the given numbers\n`min(12, 6, 3, 168)` = `3`\n\n## round\nReturns the value of a number rounded to the nearest integer\n`round(5.95)` = `6`\n`round(5.5)` = `6`\n`round(5.05)` = `5`\n\n## floor\nRounds a number down to the next smallest integer\n`floor(5.95)` = `5`\n`floor(5.05)` = `5`\n`floor(5)` = `5`\n`floor(-5.5)` = `-6`\n\n## ceil\nRounds a number up to the next largest integer\n`ceil(5.95)` = `6`\n`ceil(5.05)` = `6`\n`ceil(5)` = `5`\n`ceil(-5.5)` = `-5`\n\n## trunc\nReturns the integer part of a number by removing any fractional digits\n`trunc(5.95)` = `5`\n`trunc(5.05)` = `5`\n`trunc(5)` = `5`\n`trunc(-5.5)` = `-5`\n\n## sign\nReturns either a positive or negative 1, indicating the sign of a number, or zero\n`sign(-3)` = `-1`\n`sign(3)` = `1`\n`sign(0)` = `0`\n\n## tableLookup\nReturns the index of the last value in the array that is less than the specified amount\n`tableLookup([100, 300, 900], 457)` = `2`\n`tableLookup([100, 300, 900], 23)` = `0`\n`tableLookup([100, 300, 900, 1200], 900)` = `3`\n`tableLookup([100, 300], 594)` = `2`\n\n## resolve\nForces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is\n`resolve(someUndefinedVariable + 3 + 4)` = `7`\n`resolve(1d6)` = `4`", "published": true + }, + { + "_id": "AQGjqq6grmKXZN6dB", + "name": "Character Tree", + "parent": { + "id": "E2DFwsCoiKy2Rc9Mz", + "collection": "docs" + }, + "ancestors": [ + { + "id": "E2DFwsCoiKy2Rc9Mz", + "collection": "docs", + "name": "Concepts", + "urlName": "concepts" + } + ], + "order": 39, + "urlName": "tree", + "href": "/docs/concepts/tree", + "published": false, + "description": "TODO" } ] \ No newline at end of file diff --git a/app/private/docs/dependency-loops.md b/app/private/docs/dependency-loops.md deleted file mode 100644 index 20f8d82f..00000000 --- a/app/private/docs/dependency-loops.md +++ /dev/null @@ -1,33 +0,0 @@ -# Dependency loops - -When a variable is referenced in a calculation, that calculation can be said to depend on that variable. In order for the calculation to compute, the value of the variable needs to be known. - -But consider the following property values that could be added to a creature - -- The creature's Strength base value is set to `dexterity + 1` so that it will always have 1 more strength than dexterity -- The creature's Dexterity base value is set to `constitution + 1` so that it will always have 1 more dexterity than constitution -- The creature's Constitution base value is set to `strength` so that its constitution is always equal to its strength - -It is not possible to resolve these calculations, not just because no values exist which satisfy the constraints, but because strength depends on dexterity which depends on constitution which depends on strength. None can be computed before the others have finalized their values. This is a dependency loop. - -Most dependency loops that appear in actual DiceCloud creatures are less trivial than this example, but they cause the same result: a sheet that can't be accurately computed. In these cases, DiceCloud does its best, chooses an order to resolve the calculations arbitrarily, and continues calculating. An error will show on the Build tab to let you know that something went wrong. - -![dependency loop example](/images/docs/dependency-loop.png) - -## Toggles - -Calculated [Toggles](/docs/property/toggle) are the main source of dependency loops on creatures, because they create a dependency that isn't as obvious as a calculation might be. When a toggle is in calculated mode, its children do not know whether they are active or not until the calulation is resolved. Because of this, every calculation under the toggle depends on the toggles calaculation, making the chance for a loop to be formed more likely the more children are under a toggle. - -Consider this example - -- A calculated toggle that is active if `strength < 10` -- An effect under that toggle that adds 2 to `strength` - -The effect can't compute, because it does not know if it is active yet, so the toggle must compute its calculation first. The toggle needs to know if `strength` is greater than 10. Strength depends on all of the effects targeting it, it must know if the +2 effect is active or not. This creates a dependency loop, because there is no valid order in which everything can be calculated. - -## Troubleshooting a dependency loop - -- First, identify all the properties that make up the dependency loop. These are linked in the depdency loop error message. The field names in square brackets after the property name indicates which calculations on the property are directly involved. -- Move any properties in the loop out from being children of calculated Toggles -- Use static values in place of variables where they are not stricly needed -- Ask for [help](/feedback) diff --git a/app/private/docs/docs.md b/app/private/docs/docs.md deleted file mode 100644 index 424a157e..00000000 --- a/app/private/docs/docs.md +++ /dev/null @@ -1,39 +0,0 @@ -# DiceCloud Docs - -## Properties - -- ### [Action](/docs/property/action) -- ### [Attribute](/docs/property/attribute) -- ### [Attribute Damage](/docs/property/attribute-damage) -- ### [Buff](/docs/property/buff) -- ### [Remove Buff](/docs/property/remove-buff) -- ### [Branch](/docs/property/branch) -- ### [Class](/docs/property/class) -- ### [Class Level](/docs/property/class-level) -- ### [Constant](/docs/property/constant) -- ### [Container](/docs/property/container) -- ### [Damage](/docs/property/damage) -- ### [Damage Multiplier](/docs/property/damage-multiplier) -- ### [Effect](/docs/property/effect) -- ### [Feature](/docs/property/feature) -- ### [Item](/docs/property/item) -- ### [Note](/docs/property/note) -- ### [Point Buy](/docs/property/point-buy) -- ### [Proficiency](/docs/property/proficiency) -- ### [Roll](/docs/property/roll) -- ### [Saving Throw](/docs/property/saving-throw) -- ### [Skill](/docs/property/skill) -- ### [Slot](/docs/property/slot) -- ### [Slot Filler](/docs/property/slot-filler) -- ### [Spell List](/docs/property/spell-list) -- ### [Spell](/docs/property/spell) -- ### [Toggle](/docs/property/toggle) -- ### [Trigger](/docs/property/trigger) - -## Topics - -- ### [Computed fields](/docs/computed-fields) -- ### [Inline Calculations](/docs/inline-calculations) -- ### [Dependency Loops](/docs/dependency-loops) -- ### [Functions](/docs/functions) -- ### [Tags](/docs/tags) diff --git a/app/private/docs/inline-calculations.md b/app/private/docs/inline-calculations.md deleted file mode 100644 index 3d7c0f4d..00000000 --- a/app/private/docs/inline-calculations.md +++ /dev/null @@ -1,11 +0,0 @@ -# Inline Calculations - -Most long-format fields allow inline [calculations](/docs/computed-fields) to be included. Calculations inside of curly bracers will be computed down to numbers using the characters stats. - -For example a creature's strength attribute may have the following in its description: `Your carrying capacity is {strength * 15} lbs.` - -When the creature is calculated, if it has 8 strength, the action description will become: "Your carrying capacity is 120 lbs." - -If a description includes a dice roll, only the part that can be calculated to a single number should be included in the calulation bracers: `The attack does an extra {paladin.level}d8 damage`, which becomes `The attack does an extra 4d8 damage`. - -Do not inlclude the dice roll in the calaculation: `The attack does an extra {(paladin.level)d8} damage`, because it will become `The attack does an extra 16 damage` but the number 16 will change every time the creature recalculates. diff --git a/app/private/docs/property/action.md b/app/private/docs/property/action.md deleted file mode 100644 index ca1ae0ab..00000000 --- a/app/private/docs/property/action.md +++ /dev/null @@ -1,113 +0,0 @@ -# Actions - -Actions are things your character can do. When an action is taken, all the properties under it are applied. - -Add actions to your character sheet, then add children under the action to determine what happenes when the action is applied. - -When an action is applied it will create an entry in the character's log detailing all the properties that were applied and what their results were. - -The following properties can all be applied by an action: - - - [Attribute Damage](/docs/property/attribute-damage) - - [Branches](/docs/property/branch) - - [Buffs](/docs/property/buff) - - [Buff Removers](/docs/property/remove-buff) - - [Damage](/docs/property/damage) - - [Notes](/docs/property/note) - - [Rolls](/docs/property/roll) - - [Saving Throws](/docs/property/saving-throw) - - Other actions - ---- - -### Name - -The name of the action. - -### Action type - -How long the action takes to perform. - -Allows [inline calculations](/docs/inline-calculations). - -### Attack roll - -A [computed field](/docs/computed-fields) which calculates the attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. - -The following variables may be added to the action scope when attack rolls are made: - - - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied. - - `$attackRoll` The total attack roll after modifiers. - - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`. - - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll. - - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit. - - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss. - -### Summary - -A brief overview of what the action does. This will appear in the action card, and shows in the log when the action is applied. - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A more detailed description of the action. The description does not show in the action card or the log when the action is applied. - -Allows [inline calculations](/docs/inline-calculations). - -### Resource - -A resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the action can't be applied. - -If you want to reduce an attribute when taking the action, but want the action to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the action instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number. - -#### Resource attribute - -The variable name of the attribute that will be consumed when taking this action. - -#### Resource quantity - -A [computed field](/docs/computed-fields) which determines how much of the attribute is required to apply this action. This amount will be deducted from the attribute every time the action is taken. - -### Ammo - -Ammo represents items that are requied to take the action. If an item is not selected, or there is insufficient quantity of the selected item, the action can't be appled. - -#### Ammo item - -Specify what tag an item must have to be considered valid ammo for this action. Any item with this tag can be selected as ammo for this action. - -#### Ammo quantity - -A [computed field](/docs/computed-fields) which determines how many of the selected items are required to take this action. The quantity is deducted from the total quantity of the item when this action is applied. - -### Tags - -See [Tags](/docs/tags) - -### Target - -Who this action should apply to. The properties under the action will be applied to the Targets. - -- **Self** The action will apply its properties to the creature taking the action -- **Single Target** The action will apply its properties without a target (for now) -- **Multiple Targets** The action will apply its properties without a target (for now) - -### Uses - -A [computed field](/docs/computed-fields) which determines how many times this action can be used before it needs to be reset. - -### Uses used - -How many of this action's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the action has uses and its uses are reset. - -### Don't show in log - -When this is true, the action does not show up in the log. This does not stop the action's children from appearing in the log when they are applied. - -### Reset - -If set, the uses used field is set to 0 at the appropriate time. - -- **Long rest** Reset when the long rest button is pushed -- **Short rest** Reset when either the long or short rest button is pushed diff --git a/app/private/docs/property/attribute-damage.md b/app/private/docs/property/attribute-damage.md deleted file mode 100644 index 166f864c..00000000 --- a/app/private/docs/property/attribute-damage.md +++ /dev/null @@ -1,33 +0,0 @@ -# Attribute Damage - -When applied, attribute damage reduces the value of the attribute by some amount or set the value of an attribute to some amount. Attribute damage can by applied by actions or triggers. - -Using a negative value to damage an attribute will heal the attribute instead. - ---- - -### Attribute - -The variable name of the attribute to target. - -### Amount - -A [computed field](/docs/computed-fields) which determined the amount to damage the attribute or set the attribute's value to. - -### Operation - -- **Damage** Reduce the value of the attribute by the amount, negative values heal the attribute instead -- **Set** Set the value of the attribute to the amount - -### Target - -- **Target** Apply the attribute damage to the same target as the action applying this property -- **Self** Apply the attribute damage to the creature taking the action - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is set, the attribute damage is applied, but does not show in the log. diff --git a/app/private/docs/property/attribute.md b/app/private/docs/property/attribute.md deleted file mode 100644 index 156289f5..00000000 --- a/app/private/docs/property/attribute.md +++ /dev/null @@ -1,76 +0,0 @@ -# Attribute - -Attributes represent the numerical values of the creature. - -Attributes can be targeted by [effects](/docs/property/effect) which can change their total value in a non-destructive way. For example, if a class level gives you an ability score increase of +2 strength when it is taken, instead of directly editing the strength attribute, you add an effect to the class level that adds 2 to strength. The total value of strength will increase by 2 and it will show a record of that ability score increase and where it came from. - -Attributes, [skills](/docs/properties/skill), and [effects](/docs/property/effect) are the core properties of DiceCloud's creature engine. - -Attributes have the following fields that can be accessed in calculations with `variableName.field`: - -- `.total` The total of the attribute before being damaged -- `.damage` the amount of damage the attribute has taken -- `.value` The current value of the attribute including damage. `variableName` and `variableName.value` are equivalent. -- `.modifier` If the attribute is an ability, this is its roll modifier, eg. `strength.modifier` is +2 when `strength.value` is 14 - ---- - -### Base value - -A [computed field](/docs/computed-fields) that determines the starting value of the attribute before it is modified by effects and other properties. Multiple properties can set the base value for a given variable name, when this happens the highest base value is chosen, and then all other effects are applied. - -### Name - -The name of the attribute - -### Variable name - -The name used to refer to the attribute in calculations and by effects. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -If multiple attributes share a variable name, only the last attribute on the [character tree](/docs/tree) will count as the defining attribute and appear on the sheet, while other attributes with that variable name will be used as base value [effects](/docs/property/effect). - -### Attribute type - -- **Ability** Ablity scores like Strength, Dexterity, etc. Ability scores get a modifier which can be accessed in calculations as `variableName.modifier`, -- **Stat** Any numerical value that appears on the sheet. Speed, armor class. -- **Modifier** Any numical value that appears on the sheet with a `+` or `-` sign, eg. Proficiency bonus. -- **Hit Dice** Hit dice let you select the appropriate hit dice size. Creatures regain half their total hit dice on long rest. -- **Health Bar** Health bars can by made to take or ignore damage in a specified order -- **Resource** Rages, sourcery points, things that are spent to use actions. -- **Spell Slot** Spell slots have a specific level and are used to cast spells. -- **Utility** Utility attributes don't show up anywhere on the sheet, but can still be used for calculations - -### Description - -A detailed description of the attribute. - -Allows [inline calculations](/docs/inline-calculations). - -### Health bar settings - -Health bars can take or ignore damage and healing from applied damage properties targeting a creature. A lower ordered health bar will take damage before a higher ordered one. - -Health bars can also change color depending on their value. At 50%+ full they are their property color, between 50% and 0% they fade from their half-full color to their empty color. - -### Tags - -See [Tags](/docs/tags) - -### Allow decimal values - -If this is set, the attribute will not round-down when its value has a decimal. - -### Can be damaged into negative values - -If this is set the attribute can be damaged past zero into negative values. - -### Can be incremented above total - -If this is set the attribute can have negative damage such that the value exceeds the total. This can be useful if you are using the attribute to count, it can start at zero and be healed upwards to keep count. - -### Reset - -If set, the damage on this attribute is reset to 0 at the appropriate time. - -- **Long rest** Reset when the long rest button is pushed -- **Short rest** Reset when either the long or short rest button is pushed diff --git a/app/private/docs/property/branch.md b/app/private/docs/property/branch.md deleted file mode 100644 index cdd579d9..00000000 --- a/app/private/docs/property/branch.md +++ /dev/null @@ -1,24 +0,0 @@ -# Branches - -Branches are applied by actions, when they are applied they can control which of their immediate children are applied. - ---- - -### Branch type - -- **If condition is true** Apply children if the condition (a [computed field](/docs/computed-fields)) resolves to `true` or a non-zero number -- **Attack hit** Apply children if the attack roll hit the target -- **Attack hit** Apply children if the attack roll missed the target -- **Save failed** Apply children if target failed its saving throw -- **Save suceeded** Apply children if target made its saving throw -- **Apply to each target** Apply children separately to each target -- **Random** Apply one of the immediate children at random -- **Calculated Index** Use the index (a [computed field](/docs/computed-fields)) to choose which child to apply, starting at 1 for the first child. - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is set, the branch is applied, but does not show in the log. This does not prevent its children from appearing in the log. diff --git a/app/private/docs/property/buff.md b/app/private/docs/property/buff.md deleted file mode 100644 index 3196f0ab..00000000 --- a/app/private/docs/property/buff.md +++ /dev/null @@ -1,44 +0,0 @@ -# Buffs - -Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. - -Buffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property. - -### Variable freezing - -When a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `$target.`. - -For example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `$target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character. - ---- - -### Name - -The name of the buff. - -### Description - -Description of the applied buff. - -Allows [inline calculations](/docs/inline-calculations). - -### Target - -- **Target** Apply the buff to the target of the action -- **Self** Apply the buff to the creature taking the action - -### Hide remove button - -If this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action. - -### Don't show in log - -If set, the buff will not show its name and description in the log when applied. - -### Don't freeze variables - -Prevent the buff from freezing variables in child property calculations to their value at the time the buff was applied. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/class-level.md b/app/private/docs/property/class-level.md deleted file mode 100644 index c4b1a3f3..00000000 --- a/app/private/docs/property/class-level.md +++ /dev/null @@ -1,29 +0,0 @@ -# Class level - -A class level is a property that represents a single level in a class. It is generally used as a child of a [Class property](/docs/property/class). - -Features and bonuses that are given by a class level get added as children of the class level. - ---- - -### Level - -Which level this property represents. - -### Name - -The name of the class or subclass this level is part of - -### Variable name - -The same variable name of the class this level belongs to. - -### Description - -A description of the benefits gained with this level. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/class.md b/app/private/docs/property/class.md deleted file mode 100644 index b8630da3..00000000 --- a/app/private/docs/property/class.md +++ /dev/null @@ -1,37 +0,0 @@ -# Classes - -A class is a property that expects [class levels](/docs/property/class-level) as its immediate children. - -Leveling up a class means choosing, or manually adding, class level properties to it. Class levels with the same variable name as the class, and that match all the required tags are found in libraries and added to the class. - -The total level of the class can be accessed in calculations using `classVariableName.level`. - -## Making your own class - -See [Create a Class](/docs/walkthroughs/create-a-class) - ---- - -### Name - -The name of the class - -### Variable name - -The name used to refer to the class in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Description - -A description of the class. - -### Tags - -See [Tags](/docs/tags) - -### Tags required - -Only class levels with the same variable name as the class, and with tags that match the tags required will be returned from libraries when leveling up this class. - -### Condition - -A [computed field](/docs/computed-fields) to determine if the class is allowed to level up. If this field results in `true` or a number that is not 0, the class can be levelled, otherwise leveling is disabled. diff --git a/app/private/docs/property/constant.md b/app/private/docs/property/constant.md deleted file mode 100644 index a37af5a5..00000000 --- a/app/private/docs/property/constant.md +++ /dev/null @@ -1,33 +0,0 @@ -# Constants - -Constants are properties that store some primitive value in a variable name for use in other calculations. - -Unlike attributes, constants can store more than just numbers: - -- Arrays: `[1,2,3,4]` -- Text strings: `'I am a cat'` -- Numbers: `3.14` -- Boolean values: `true`, `false` -- Dice rolls: `1d20 + 2` - -Constants just can't use other variables in their calculations. - -### Overriding constants - -If multiple constants have the same variable name, only the last active constant in the [character tree](/docs/tree) will be used as the definition for that variable name. - -This can be used to re-write the value of some constant by ensuring there is a new active constant later in the sheet. - ---- - -### Name - -The name of the constants - -### Variable Name - -The name used to refer to the constant in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Value - -A [calculation](/docs/computed-fields) of the final value of the constant. diff --git a/app/private/docs/property/container.md b/app/private/docs/property/container.md deleted file mode 100644 index 30e302ce..00000000 --- a/app/private/docs/property/container.md +++ /dev/null @@ -1,35 +0,0 @@ -# Containers - -Containers are things that [items](/docs/property/item) can be put inside of. - ---- - -### Name - -The name of the container - -### Carried - -If this is set the weight of the container and its contents will be added to the character's weight carried. - -### Value - -The value of the container in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So a container that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp. - -### Weight - -The weight of the container in lb. - -### Description - -A description of the container. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) - -### Contents are weightless - -If this is set and the container is carried, only the container's own weight will be added to the weight carried by the creature. diff --git a/app/private/docs/property/damage-multiplier.md b/app/private/docs/property/damage-multiplier.md deleted file mode 100644 index c9e97869..00000000 --- a/app/private/docs/property/damage-multiplier.md +++ /dev/null @@ -1,33 +0,0 @@ -# Damage multipliers - -Damage multipliers are used to define vulnerability, resistance, and immunity to damage types. - -A single multiplier can apply to multiple damage types, and choose whether or not to apply to an incoming source of damage based on the tags present on that damage. - ---- - -### Name - -The name of the feature that gives this damage multiplier - -### Value - -- **Immunity** The creature takes no damage from matching damage sources -- **Resistance** Damage from matching sources is halved. -- **Vulnerability** Damage from matching sources is doubled. - -### Damage types - -A list of damage types that this property applies to. Custom types can be used. - -### Damage tags required - -This damage multiplier will only be applied if the incoming damage has all of these tags present. - -### Damage tags excluded - -This damage multiplier will only apply if the incoming damage has none of these tags present. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/damage.md b/app/private/docs/property/damage.md deleted file mode 100644 index 4c004ffd..00000000 --- a/app/private/docs/property/damage.md +++ /dev/null @@ -1,32 +0,0 @@ -# Damage - -Damage can be applied by an action to damage a target creature's [health bars](/docs/property/attribute). The damage will be modified by [damage multipliers](/docs/property/damage-multiplier), which apply vulnerability, resistance, and immunity before the damage is applied. - ---- - -### Damage - -A [computed field](/docs/computed-fields) that determines how much damage to do to the target creature. - -### Damage type - -Damage type determines how the damage is treated by [damage multipliers](/docs/property/damage-multiplier). A custom type can be used, or one of the existing types can be selected. - -There are two special damage types: - -**Extra damage** Damage with the `extra` type will take on the damage type of whatever damage was applied before it by an action. So if an action deals 12 `piercing` damage and `3` extra damage, it will instead deal 15 `piercing` damage. - -**Healing** Damage with the `healing` type will heal a creature instead of damaging them. - -### Target - -- **Target** Apply the damage to the target of the action -- **Self** Apply the damage to the creature taking the action - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -If set, the damage will be applied but not show in the log. diff --git a/app/private/docs/property/effect.md b/app/private/docs/property/effect.md deleted file mode 100644 index b453e94f..00000000 --- a/app/private/docs/property/effect.md +++ /dev/null @@ -1,60 +0,0 @@ -# Effects - -Effects are the core of the DiceCloud engine. Effect change the values of attributes, skills, and calculations in a way that is transparent and auditable, keeping character sheets organized and understandable, even when using intricate homebrew rules on high level characters. - ---- - -### Name - -The name of the feature that causes this effect. - -### Operation - -The operation determines what the effect will do to the affected property or calcualtion. - -- **Base Value** Set the base value of the affected property. If a property has multiple base values, the highest is used -- **Add** Add the value to the affected property or calculation -- **Muliply** Multiply the affected property by the value -- **Minimum** Prevent the affected property from having a value less than the effect value -- **Maximum** Prevent the affected property from having a value greater than the effect value -- **Maximum** Prevent the affected property from having a value greater than the effect value -- **Set** Set the value affected property to the effect value -- **Advantage** Give advantage to checks made using the affected property -- **Disadvantage** Give disadvantage to checks made using the affected property -- **Passive bonus** Add the effect value to the passive scores based on the affected property -- **Fail** Checks made using the affected property automatically fail -- **Conditional benefit** Add some text to the affected property describing the benefit recieved - -### Value - -A [computed field](/docs/computed-fields) that determines the value of the effect. - -### Text - -If the operation is a conditional benefit, the note text that will show on affected properties. - -### Target stats by variable name - -If selected the effect will apply to all properties that have the given variable names. - -### Variable names - -A list of variable names of properties to target with this effect. - -### Target properties by tags - -When targeting properties by tag, any property can be targeted with an effect. If the property is one that can usually be targeted by variable name, the effect will apply as ususal, however if the effect targets another property, it will apply to a [computed field](/docs/computed-fields) on the property instead. - -These effects can be used for adding a bonus to a specific attack or damage roll, or manipulating any computed field on the creature. - -### Tags required - -Only properties that match the required tags will be targeted by the effect. - -### Target field - -If a property has multiple computed fields, which field should be targeted by this effect. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/feature.md b/app/private/docs/property/feature.md deleted file mode 100644 index 98bc0c28..00000000 --- a/app/private/docs/property/feature.md +++ /dev/null @@ -1,25 +0,0 @@ -# Features - -Features appear on the features tab. Classes, backgrounds, and race can all give a creature features. - ---- - -### Name - -The name of the feature. - -### Summary - -A summary of the feature. This will appear on the feature card. - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A detailed description of the feature. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/folder.md b/app/private/docs/property/folder.md deleted file mode 100644 index 61d04523..00000000 --- a/app/private/docs/property/folder.md +++ /dev/null @@ -1,17 +0,0 @@ -# Folders - -Folders allow the [character tree](/docs/tree) to be organized. - -### Folders in actions - -When a folder is the child of an action, it and its children will not show on the action card, but will still appear in the detail view of the action and be applied when the action is taken. - ---- - -### Name - -The name of the folder. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/item.md b/app/private/docs/property/item.md deleted file mode 100644 index 1855f36c..00000000 --- a/app/private/docs/property/item.md +++ /dev/null @@ -1,57 +0,0 @@ -# Items - -Items are shown on the Inventory tab. Items can be carried, put in containers, or equipped on a creature. The children of an item are not active unless the item is equipped. - ---- - -### Icon - -An icon representing the item. - -### Equipped - -If set, the item appears in the equipment list on the inventory tab and its children become active on the creature. - -### Name - -The name of the item. - -### Plural name - -The name to use if the quantity of the item is higher than 1. - -### Value - -The value of a single item in gold pieces. Silver pieces are worth 0.1 gp and copper pieces are worth 0.01 gp. So an item that is worth 2 gp 4 sp 7 cp will have a value of 2.47 gp. - -### Weight - -The weight of a single item in lb. - -### Quantity - -Number of items. The value and quantity will be multiplied by the quantity to get the total value and weight of this stack of items. - -### Description - -A description of the item. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) - -### Show increment button - -If this is set, the item will show an increment button in the detail view and on the inventory tab. This button can be used to quickly adjust the quantity of the item. - -### Requires attunemnt - -If set, the item requires attunemnt to use. - -### Attuned - -If set, the item is attuned and counts towards the total number of attuned items for the creature. - -If a child property needs to determine if its parent item is attuned it can use `#item.attuned` in calculations, see *Ancestor references* in [computed fields](/docs/computed-fields). diff --git a/app/private/docs/property/note.md b/app/private/docs/property/note.md deleted file mode 100644 index 928a0f2c..00000000 --- a/app/private/docs/property/note.md +++ /dev/null @@ -1,25 +0,0 @@ -# Notes - -Notes are used to store text on the creature that does not have a direct mechanical impact. Notes appear on the journal tab when active on the character, or are shown in the log when applied by an [action](/docs/property/action). - ---- - -### Name - -Name of the note. - -### Summary - -A summary of the note. This will appear on the note card and in the log when applied by an [action](/docs/property/action). - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A detailed description of the feature. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/point-buy.md b/app/private/docs/property/point-buy.md deleted file mode 100644 index 419a8cb3..00000000 --- a/app/private/docs/property/point-buy.md +++ /dev/null @@ -1,39 +0,0 @@ -# Point buy - -A point buy is a set of rows that lets the user choose a set of stats based on a cost per stat. - ---- - -### Table name - -The name of the point buy table. - -### Min - -The lowest value available for each row - -### Max - -The highest value available for each row - -### Cost - -A function that uses `value` as the value of a row and determines the cost of that value. For standard D&D 5e 27 point buy, this function is `[0, 1, 2, 3, 4, 5, 7, 9][value - 7]` - -### Total available points - -A [computed field](/docs/computed-fields) that determines how many points are available to spend in total - -## Rows - -Up to 32 rows can be added to a point buy table - -### Row name - -The name of the row that will appear in the table - -### Row variable name - -The variable name of the row that can be used in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -If the variable name matches an attribute with the same variable name, the row's value will be used as a base value for that attribute. diff --git a/app/private/docs/property/proficiency.md b/app/private/docs/property/proficiency.md deleted file mode 100644 index 2a1c7c41..00000000 --- a/app/private/docs/property/proficiency.md +++ /dev/null @@ -1,21 +0,0 @@ -# Proficiencies - -Proficiencies add proficiency to an existing skill on the creature. If you need to add a tool or language proficiency to a creature, use a [Skill](/docs/property/skill) instead. - ---- - -### Name - -Name of the feature that is adding this proficiency - -### Skills - -A list of variable names of the skills to add proficiency to. - -### Proficiency - -How much proficiency to add to the skill. If a skill has multiple proficiencies added to it, the highest one will be used. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/remove-buff.md b/app/private/docs/property/remove-buff.md deleted file mode 100644 index 77888a98..00000000 --- a/app/private/docs/property/remove-buff.md +++ /dev/null @@ -1,32 +0,0 @@ -# Remove Buff - -This property can remove a specific buff from a targeted creature. - -### Name - -The name of the property. This shows in the log when the property is applied. - -### Remove parent buff - -When this is set and the property is applied, the property will remove the nearest parent buff. If this property is not the child of any buffs, it will log an error. - -### Remove all - -When this is set, all buffs that match the target tags will be removed from the targeted creature. If not set, only the first buff found with the matching tags will be removed. - -### Target - -- **Target** Matching buffs will be removed from the targeted creature -- **Self** Matching buffs will be removed from the creature that applied the action - -### Tags required - -Any buff that has all of the required tags will be removed when the property is applied. - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is set, the property is applied, but does not show in the log. \ No newline at end of file diff --git a/app/private/docs/property/roll.md b/app/private/docs/property/roll.md deleted file mode 100644 index 35898bdf..00000000 --- a/app/private/docs/property/roll.md +++ /dev/null @@ -1,27 +0,0 @@ -# Rolls - -Rolls are properties that store the result of a calculation to a variable name when applied by an [action](/docs/property/action). The variable name only exists for the duration of that particalar action. - -Rolls can be useful if you need to deal the same damage to multiple targets, or if damage needs to be rolled then halved by succeeding on a saving throw. - ---- - -### Name - -Name of the roll. This will be shown in the log when the roll is applied. - -### Variable name - -The variable name to store the result of the roll for the duration of the action. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Roll - -A [computed field](/docs/computed-fields) that is computed when the roll is applied by an action. - -### Don't show in log - -If set, the roll will be applied and store its result in the variable name, but not be shown in the log. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/saving-throw.md b/app/private/docs/property/saving-throw.md deleted file mode 100644 index 907b8fe4..00000000 --- a/app/private/docs/property/saving-throw.md +++ /dev/null @@ -1,35 +0,0 @@ -# Saving throws - -Saving throws are properties that cause the target to make a saving throw when applied. If you want to add a type of saving throw like Strength Save to a creature, use a [skill](/docs/property/skill) instead. - -When a saving throw is applied, the following variables are added to the scope of that action: - -- `$saveFailed` Set to `true` if the target failed its saving throw or there are no targets for the saving throw -- `$saveSucceeded` Set to `true` if the target made its saving throw or there are no targets for the saving throw -- `$saveDiceRoll` The unmodified d20 roll the target made to save -- `$saveRoll` The final value of the saving throw roll after modifiers - -### Name - -The name of the saving throw. Usually the ability saving throw targeted: "Strength Save". - -### DC - -The DC of the saving throw that the target needs to meet - -### Save - -The variable name of the skill that will be used to make the saving throw. - -### Target - -- **Target** Apply the saving throw to the targets of the action. Each target will make the saving throw in turn. Child properties will be applied to each target separately with the results of their individual saving throw. If a value like damage needs to be shared between targets, it should be calculated in a [Roll](/docs/property/roll) before the saving throw. -- **Self** Apply the saving throw to the creature taking the action. The creature taking the action will become the target for all child properties. - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -If set, the saving throw will not show in the log when applied, but will still be rolled and apply its children. diff --git a/app/private/docs/property/slot-filler.md b/app/private/docs/property/slot-filler.md deleted file mode 100644 index 9e465a18..00000000 --- a/app/private/docs/property/slot-filler.md +++ /dev/null @@ -1,39 +0,0 @@ -# Slot filler - -A slot filler is a property that can be used to add more complex behavior to filling a [slot](/docs/property/slot) from a library. - ---- - -### Name - -The name of the slot filler that will show when choosing the filler from the library. - -### Icon - -Icon of the slot filler - -### Description - -A detailed description of the slot filler. - -Allows [inline calculations](/docs/inline-calculations). - -### Picture URL - -A link to an image to use for this slot filler when being chosen from a library. - -### Type - -Slot fillers can pretend to be any type of property when a slot is being filled. - -### Quantity - -How many spaces the slot filler will take up in a slot. - -### Condition - -A [computed field](/docs/computed-fields) that determines whether this slot filler can be added to a character. - -### Tags - -See [Tags](/docs/tags) diff --git a/app/private/docs/property/slot.md b/app/private/docs/property/slot.md deleted file mode 100644 index 61793f81..00000000 --- a/app/private/docs/property/slot.md +++ /dev/null @@ -1,53 +0,0 @@ -# Slots - -Slots are the main way creatures interact with libraries. A slot can be filled by choosing a property from a library that fits that particular slot. - -In a complete library, a creature can be built entirely by choosing which properties to fill each slot with. - -Slots show up on the build tab, and are highlighted when they have space that can be filled. - -If you are building a creature without a library, you should either ignore slots entirely, or fill them with your own custom properties. - ---- - -### Name - -The name of the slot. - -### Type - -What kind of property this slot expects to fill it. - -### Tags required - -Properties in a library must have the required tags to fill the slot. - -### Quantity - -How many properties are expected to fill this slot. Use 0 for allowing an unlimited number of properties. - -### Condition - -A [computed field](/docs/computed-fields) that determines whether this slot can accept new properties. - -### Unique - -The slot can control how it deals with the uniqueness of properties that fill it. - -### Description - -A detailed description of the attribute. - -Allows [inline calculations](/docs/inline-calculations). - -### Hide when full - -When set the slot will hide itself when it is filled. - -### Ignored - -When set the slot will not show a prompt card on the build tab. - -### Tags - -See [Tags](/docs/tags) \ No newline at end of file diff --git a/app/private/docs/property/spell.md b/app/private/docs/property/spell.md deleted file mode 100644 index ca878747..00000000 --- a/app/private/docs/property/spell.md +++ /dev/null @@ -1,115 +0,0 @@ -# Spells - -Spells work similarly to [actions](/docs/property/action). They appear on the spells tab and can be cast with or without using up spell slots. - ---- - -### Always prepared - -A spell that is always prepared does not count towards the spell list's maximum prepared spells and is always active and ready to cast. - -### Prepared - -A prepared spell is ready to cast and counts against a spell list's maximum prepared spells. - -### Cast without spell slots - -When set, this spell can be cast without consuming spell slots. It will however consume its own uses and resources. - -### School - -What school the spell belongs to. - -### Casting time - -How long the spell takes to Cast - -### Range - -The range of the spell - -### Duration - -How long the spell lasts - -### Components - -Whether the spell requires verbal, somatic, or material components and whether the spell is a ritual or requires concentration. - -### Target - -Who this spell should apply to. The properties under the spell will be applied to the targets. - -- **Self** The spell will apply its properties to the creature casting the spell -- **Single Target** The spell will apply its properties without a target (for now) -- **Multiple Targets** The spell will apply its properties without a target (for now) - -### Attack roll - -A [computed field](/docs/computed-fields) which calculates the spell attack roll modifier. If this field is empty, no attack roll will be made. Use 0 to make an attack roll without a modifier. To use the spell list's attack roll bonus use `#spellList.attackRollBonus`. - -The following variables may be added to the action scope when attack rolls are made: - - - `$attackDiceRoll` The value of the d20 roll before any modifiers were applied. - - `$attackRoll` The total attack roll after modifiers. - - `$criticalHit` Set to `true` if the attack roll's d20 is a natural 20. If `criticalHitTarget` is set, the attack roll's d20 must instead be equal to or greater than `criticalHitTarget` for this to be set to `true`. - - `$criticalMiss` Set to `true` if the attack roll was not a critical hit and rolled a natural 1 on the d20 roll. - - `$attackHit` If the attack roll is higher than or equal to the target's AC or a critical hit this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical hit. - - `$attackMiss` If the attack roll is lower than the target's AC or a critical miss, this is set to `true`. Remains unset if there is no target for the attack unless the attack is a critical miss. - -### Summary - -A brief overview of what the spell does. This will show in the log when the spell is cast. - -Allows [inline calculations](/docs/inline-calculations). - -### Description - -A more detailed description of the spell. The description does not show in the log when the spell is cast. - -Allows [inline calculations](/docs/inline-calculations). - -### Resource - -A resource can be any attribute that has a variable name. If the resource attribute is less than the amount required, the spell can't be cast. - -If you want to reduce an attribute when casting the spell, but want the spell to be applied regardless of the value of that attribute, consider using an [Attribute Damage](/docs/property/attribute-damage) property as a child of the spell instead. Also use Attribute Damage when the amount to reduce the attribute is determined by a dice roll rather than a stable computed number. - -#### Resource attribute - -The variable name of the attribute that will be consumed when casting this spell. - -#### Resource quantity - -A [computed field](/docs/computed-fields) which determines how much of the attribute is required to apply this spell. This amount will be deducted from the attribute every time the spell is cast -### Ammo - -Ammo represents items that are requied to cast the spell. If an item is not selected, or there is insufficient quantity of the selected item, the spell can't be appled. - -#### Ammo item - -Specify what tag an item must have to be considered valid ammo for this spell. Any item with this tag can be selected as ammo for this spell. - -#### Ammo quantity - -A [computed field](/docs/computed-fields) which determines how many of the selected items are required to cast this spell. The quantity is deducted from the total quantity of the item when this spell is applied. - -### Uses - -A [computed field](/docs/computed-fields) which determines how many times this spell can be used before it needs to be reset. - -### Uses used - -How many of this spell's uses have already been used. Should ideally be between 0 and the total uses available. This number is set to 0 when the spell has uses and its uses are reset. - -### Reset - -If set, the uses used field is set to 0 at the appropriate time. - -- **Long rest** Reset when the long rest button is pushed -- **Short rest** Reset when either the long or short rest button is pushed - - -### Tags - -See [Tags](/docs/tags) \ No newline at end of file diff --git a/app/private/docs/property/toggle.md b/app/private/docs/property/toggle.md deleted file mode 100644 index 1dcaf102..00000000 --- a/app/private/docs/property/toggle.md +++ /dev/null @@ -1,36 +0,0 @@ -# Toggles - -Toggles are a way to turn on and off parts of a creature. When a toggle is off, none of its children will be active. - -Calculated toggles should be avoided if possible, because while they offer a lot of power and flexibility to the creature engine, they often create [dependency loops](/docs/dependency-loops) that can be difficult to troubleshoot, causing parts of a creature to calculate incorrectly. - -Calculated toggles can be applied by [actions](/docs/property/action) and will apply their children if the condition is true, but they should be avoided in favor of [conditional branches](/docs/property/branch) which can do the same, but are more efficient. - ---- - -### Name - -The name of the toggle. - -### Variable name - -The name used to refer to the value of the toggle in calculations. Must start with a letter and be made up of only letters and numbers without spaces, symbols, or punctiation. - -### Show on character sheet - -If set, the toggle with show a checkbox on the character sheet. A calculated toggle will show a disabled checkbox, filled in if the toggle's calculation returned `true` or a value that isn't 0. - -### State - -- **Enabled** The toggle and its children are active -- **Disabled** The toggle and its children are inactive -- **Calculated** The active status of the toggle depends on the result of the condition. Use with caution. - -### Condition - -A [computed field](/docs/computed-fields) that determines if the toggle is active. Use with caution. - -### Tags - -See [Tags](/docs/tags) - diff --git a/app/private/docs/property/trigger.md b/app/private/docs/property/trigger.md deleted file mode 100644 index 9e996838..00000000 --- a/app/private/docs/property/trigger.md +++ /dev/null @@ -1,49 +0,0 @@ -# Triggers - -Triggers apply their children whenever their condition is met. They work like [actions](/docs/property/action) that are taken automatically. - ---- - -### Name - -The name of the trigger. - -### Timing - -- **Before** The trigger is applied before the triggering event takes place -- **After** The trigger is fired after the triggering event - -### Event - -- **Do action** While the creature is doing an action, the action property specified in *Event type* is applied -- **Roll check** The creature makes a check -- **Attribute damaged or healed** One of the creature's attributes changed value through attribute damage or manual adjustment -- **Short or long rest** -- **Short rest** -- **Long Rest** - -### Event type - -The trigger will apply when this property type is applied by the action - -### Tags required - -If this trigger is fired by a property, the property must match these tags for the trigger to fire. - -### Condition - -A [computed field](/docs/computed-fields) to determine if the trigger should fire. The trigger will fire if the condition field is empty or if it returns `true` or a value that isn't 0. - -### Description - -A detailed description of the trigger. - -Allows [inline calculations](/docs/inline-calculations). - -### Tags - -See [Tags](/docs/tags) - -### Don't show in log - -When this is true, the trigger does not show up in the log. This does not stop the trigger's children from appearing in the log when they are applied. diff --git a/app/private/docs/tags.md b/app/private/docs/tags.md deleted file mode 100644 index bc84771d..00000000 --- a/app/private/docs/tags.md +++ /dev/null @@ -1,14 +0,0 @@ -# Tags - -Tags are used to reference multiple similar properties at once. A slot can require only properties from your library that has matching tags, effects and some other properties can also target properties to apply to by tags. - -## Built in tags - -Properties have specific tags automatically for use with tag-targeting. These aren't relevant for slots and finding properties in a library with specific tags. - -- `#type` Actions will have the `#action` tag, etc. Damage will either have the tag `#damage` or the tag `#healing` if the damage type is healing -- `variableName` if a property has a variable name it will be included as a tag -- The type of damage done by a [damage](/docs/property/damage) property: `bludgeoning`, `slashing`, `...` -- The skill type of a [skill](/docs/property/skill) property: `skill`, `save`, `check`, `tool`, `weapon`, `armor`, `language`, `utility` -- The attribute type of an [attribute](/docs/property/attribute) property: `ability`, `stat`, `modifier`, `hitDice`, `healthBar`, `resource`, `spellSlot`, `utility` -- When the property resets: `longRest`, `shortRest` diff --git a/app/private/docs/walkthroughs/create-a-class.md b/app/private/docs/walkthroughs/create-a-class.md deleted file mode 100644 index 6987b391..00000000 --- a/app/private/docs/walkthroughs/create-a-class.md +++ /dev/null @@ -1,47 +0,0 @@ -# Create a Class - -This is a guide on creating a custom class in a character sheet. If possible, it is always faster to use an existing library that contains the class you want to use. Before continuing, check the #libraries channel of the [official discord](https://discord.gg/qEvdfeB) to see if a library exists with the class you are creating. - -This guide assumes you are using the ruleset provided in the [5e System Reference Document library](/library/qkv8aptJH2fCXARcJ). If you are using a different ruleset for your character, there may be some discrepancies. - -## Adding the class property - -On the build tab of your character, in the card labeled **Slots**, expand the rulset, then click the slot where you would like to place the custom class, if it is your starting class in an SRD character, this would be the Class slot. Be sure to click the name of the slot, not the **+** button. - -![Screenshot of Build Tab > Slots > Ruleset > Class](/images/docs/walkthroughs/create-a-class-1.png) - -This opens the slot in detail view, showing you how the slot expected to be filled from a library, instead of filling the slot, we will be manually adding a class to the slot that we create ourselves. - -Click the **Edit** button in the top right of the slot detail dialog. - -![Screenshot of slot detail dialog](/images/docs/walkthroughs/create-a-class-2.png) - -Expand the children of the class slot, and click the plus button to add a child property. - -![Screenshot of adding a child property](/images/docs/walkthroughs/create-a-class-3.png) - -This brings up the create a property dialog, we are creating a class, so select the class property type. - -![Screenshot of choosing a class property](/images/docs/walkthroughs/create-a-class-4.png) - -Now that we have selected the class property type, the create tab is selected where we can enter the details of our class, fill in the form and click **Create**. - -![Screenshot of the class form](/images/docs/walkthroughs/create-a-class-5.png) - -Now that our custom class is created, we can close the class slot dialog. - -On the Build tab, in the card with the title **Level**, you will see your new class, with a button to **Level Up**, clicking the level up button would usually search your libraries for class levels that match the variable name of the class, however, since it's a custom class, it will probably not find any levels. - -Instead, as we did with the class slot, click on the class name to bring up the class detail dialog, click **Edit**, expand children and click the **+** button to add a child to the class. Here we will add all of the things our class gives the character. - -Add an [Effect](/docs/property/effect) which targets `hitPoints` to add the starting hitpoints of the class. Add a [proficiencies](/docs/property/proficiency) for all the skill and saving throw proficiencies the class gives. Add [skills](/docs/property/skill) for all the tool and weapon proficiencies of the class, making sure to set the base proficiency of those skills to proficient. Add any text [features](/docs/property/feature) the class gives you, along with [actions](/docs/property/action) which may be children of those features, or direct children of the class. - -Once you have added Everything the class gives you, it's time to add class levels. As a child of the class, add a [class level](/docs/property/class-level) property. Set the level to 1 and the name and variable name to match the variable name of the class. - -Once the class level is created, open the class level and edit it. Use the **+** button in the children of the class level to add all the properties the class level gives your character. - -Repeat this for every level of the class until your character is at the correct level. - -You can use a separate character with levels in a class that is available in your libraries as an example of what properties you may want to add to your class and class levels. - -![Example wizard class](/images/docs/walkthroughs/create-a-class-6.png) diff --git a/app/public/images/crown-dice-on-ipad.webp b/app/public/images/crown-dice-on-ipad.webp new file mode 100644 index 00000000..4f5583af Binary files /dev/null and b/app/public/images/crown-dice-on-ipad.webp differ diff --git a/app/public/images/paper-dice-crown-with-candy.png b/app/public/images/paper-dice-crown-with-candy.png deleted file mode 100644 index da31db3a..00000000 Binary files a/app/public/images/paper-dice-crown-with-candy.png and /dev/null differ diff --git a/app/public/images/paper-dice-crown.png b/app/public/images/paper-dice-crown.png deleted file mode 100644 index 5191fff4..00000000 Binary files a/app/public/images/paper-dice-crown.png and /dev/null differ diff --git a/app/public/images/paper-dice-crown.webp b/app/public/images/paper-dice-crown.webp new file mode 100644 index 00000000..0a08c994 Binary files /dev/null and b/app/public/images/paper-dice-crown.webp differ diff --git a/app/public/images/paragons/vibes.png b/app/public/images/paragons/vibes.png new file mode 100644 index 00000000..026cacac Binary files /dev/null and b/app/public/images/paragons/vibes.png differ diff --git a/app/public/images/screenshots/actions.webp b/app/public/images/screenshots/actions.webp new file mode 100644 index 00000000..6420ba0f Binary files /dev/null and b/app/public/images/screenshots/actions.webp differ diff --git a/app/public/images/screenshots/auditable.webp b/app/public/images/screenshots/auditable.webp new file mode 100644 index 00000000..d802ff45 Binary files /dev/null and b/app/public/images/screenshots/auditable.webp differ diff --git a/app/public/images/screenshots/automated-dice-rolls.webp b/app/public/images/screenshots/automated-dice-rolls.webp new file mode 100644 index 00000000..73e8f22f Binary files /dev/null and b/app/public/images/screenshots/automated-dice-rolls.webp differ diff --git a/app/public/images/screenshots/build-system.webp b/app/public/images/screenshots/build-system.webp new file mode 100644 index 00000000..fcc48dbc Binary files /dev/null and b/app/public/images/screenshots/build-system.webp differ diff --git a/app/public/images/screenshots/inventory.webp b/app/public/images/screenshots/inventory.webp new file mode 100644 index 00000000..69212efd Binary files /dev/null and b/app/public/images/screenshots/inventory.webp differ diff --git a/app/public/images/screenshots/libraries-of-content.webp b/app/public/images/screenshots/libraries-of-content.webp new file mode 100644 index 00000000..7e32cf72 Binary files /dev/null and b/app/public/images/screenshots/libraries-of-content.webp differ diff --git a/app/public/images/screenshots/printing.webp b/app/public/images/screenshots/printing.webp new file mode 100644 index 00000000..15f2bdca Binary files /dev/null and b/app/public/images/screenshots/printing.webp differ diff --git a/app/public/images/screenshots/send-to-discord.webp b/app/public/images/screenshots/send-to-discord.webp new file mode 100644 index 00000000..8bcfd58e Binary files /dev/null and b/app/public/images/screenshots/send-to-discord.webp differ diff --git a/app/redis-settings.json b/app/redis-settings.json new file mode 100644 index 00000000..45fc18d7 --- /dev/null +++ b/app/redis-settings.json @@ -0,0 +1,19 @@ +{ + "redisOplog": { + "redis": { + "port": 6379, + "host": "127.0.0.1" + }, + "retryIntervalMs": 1000, + "mutationDefaults": { + "optimistic": true, + "pushToRedis": true + }, + "cacheTimeout": 1800000, + "cacheTimer": 300000, + "secondaryReads": null, + "raceDetectionDelay": 1000, + "raceDetection": true, + "debug": false + } +} \ No newline at end of file diff --git a/app/server/main.js b/app/server/main.js index 642113e0..eddb6d67 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -5,6 +5,7 @@ import '/imports/server/rest/index.js'; import '/imports/server/config/accountsEmailConfig.js'; import '/imports/server/config/simpleSchemaDebug.js'; import '/imports/server/config/SyncedCronConfig.js'; +import '/imports/server/config/redisCaching.js'; import '/imports/server/publications/index.js'; import '/imports/server/cron/deleteSoftRemovedDocuments.js'; import '/imports/api/parenting/organizeMethods.js';