From 7f95680559ae96fcc2ea248b560af584dcec4b14 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 30 Aug 2021 16:32:31 +0200 Subject: [PATCH 01/85] Updated thanks page to reflect that Sam is no longer my fiancee, since we're married now <3 --- app/imports/ui/pages/About.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/imports/ui/pages/About.vue b/app/imports/ui/pages/About.vue index a75e094d..0f3c922b 100644 --- a/app/imports/ui/pages/About.vue +++ b/app/imports/ui/pages/About.vue @@ -29,10 +29,10 @@ Special Thanks

- Sam My fiancée, without whom DiceCloud could not hope to exist + Sam; without her love, DiceCloud could not hope to exist

The "Heroes" of Asaea The D&D party whose joy was the fuel - with which DiceCloud was powered + with which the early versions of DiceCloud was powered

Paragon tier Patrons From c9d4d874aa4fb43bceeddb95f9379f0718f13859 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 18:45:06 +0000 Subject: [PATCH 02/85] Bump tar from 6.1.6 to 6.1.11 in /app Bumps [tar](https://github.com/npm/node-tar) from 6.1.6 to 6.1.11. - [Release notes](https://github.com/npm/node-tar/releases) - [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/npm/node-tar/compare/v6.1.6...v6.1.11) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- app/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/package-lock.json b/app/package-lock.json index 055c293f..bfc46de8 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2966,9 +2966,9 @@ } }, "tar": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz", - "integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", From fc0cc6e689a594c6ac4d3daf82c7877979c545f1 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sun, 5 Sep 2021 18:51:29 +0200 Subject: [PATCH 03/85] References now merge children, fixed infinite reference loops --- .../methods/insertPropertyFromLibraryNode.js | 45 +++++++++---------- app/imports/api/parenting/parenting.js | 10 ++--- .../server/publications/searchLibraryNodes.js | 1 - 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 39d410c0..1cb44914 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -116,7 +116,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){ nodes = [node, ...nodes]; // set libraryNodeIds - storeLibraryNodeReferences(nodes, nodeId); + storeLibraryNodeReferences(nodes); // re-map all the ancestors setLineageOfDocs({ @@ -149,6 +149,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){ function storeLibraryNodeReferences(nodes){ nodes.forEach(node => { + if (node.libraryNodeId) return; node.libraryNodeId = node._id; }); } @@ -162,20 +163,11 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ // Filter out the reference nodes we replace let resultingNodes = nodes.filter(node => { - - // We have already visited this ref and replaced it - if (visitedRefs.has(node._id)) return false; - - // Already replaced an ancestor node - for (let i; i < node.ancestors.length; i++){ - if (visitedRefs.has(node.ancestors[i].id)) return false; - } - // This isn't a reference node, continue as normal if (node.type !== 'reference') return true; // We have gone too deep, keep the reference node as an error - if (depth > 10){ + if (depth >= 10){ if (Meteor.isClient) console.warn('Reference depth limit exceeded'); node.cache = {error: 'Reference depth limit exceeded'}; return true; @@ -211,26 +203,31 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ oldParent: referencedNode.parent, }); - // Remove all the looped references and descendents from the new nodes - // We can't rely on the reify recursion to do this, since the IDs are - // getting renewed before it is called - addedNodes = addedNodes.filter(node => { - // Exclude removed referenced - if (visitedRefs.has(node._id)) return false; - - // Exclude descendants of removed references - for (let i; i < node.ancestors.length; i++){ - if (visitedRefs.has(node.ancestors[i].id)) return false; + // Filter all the looped references + addedNodes = addedNodes.filter(addedNode => { + // Add all non-reference nodes + if (addedNode.type !== 'reference'){ + return true; + } + // If this exact reference has already been resolved before, filter it out + if (visitedRefs.has(addedNode._id)){ + return false; + } else { + // Otherwise mark it as visited, and keep it + visitedRefs.add(addedNode._id); + return true; } - return true; }); - // TODO: Force the referencedNode to take the old id of the reference - // such that the reference's children can be kept + // Before renewing Ids make sure the library node reference is stored + storeLibraryNodeReferences(addedNodes); // Give the new referenced sub-tree new ids + // The referenced node must get the id of the ref node so that the + // descendants of the ref node keep their ancestry intact renewDocIds({ docArray: addedNodes, + idMap: { [referencedNode._id]: node._id }, }); // Reify the subtree as well with recursion diff --git a/app/imports/api/parenting/parenting.js b/app/imports/api/parenting/parenting.js index 70e7747d..e5a8cc62 100644 --- a/app/imports/api/parenting/parenting.js +++ b/app/imports/api/parenting/parenting.js @@ -101,7 +101,6 @@ export function getAncestry({parentRef, inheritedFields = {}}){ } export function setLineageOfDocs({docArray, oldParent, newAncestry}){ - //const oldParent = oldAncestry[oldAncestry.length - 1]; const newParent = newAncestry[newAncestry.length - 1]; docArray.forEach(doc => { if(doc.parent.id === oldParent.id){ @@ -109,6 +108,7 @@ export function setLineageOfDocs({docArray, oldParent, newAncestry}){ } let oldAncestors = doc.ancestors; let oldParentIndex = oldAncestors.findIndex(a => a.id === oldParent.id); + if (oldParentIndex === -1) return; doc.ancestors = [...newAncestry, ...oldAncestors.slice(oldParentIndex + 1)]; }); } @@ -117,17 +117,15 @@ export function setLineageOfDocs({docArray, oldParent, newAncestry}){ * Give documents new random ids and transform their references. * Transform collections of re-IDed docs according to the collection map */ -export function renewDocIds({docArray, collectionMap}){ - // map of {oldId: newId} - let idMap = {}; - +export function renewDocIds({docArray, collectionMap, idMap = {}}){ + // idMap is a map of {oldId: newId} // Get a random generator that's consistent on client and server let randomSrc = DDP.randomStream('renewDocIds'); // Give new ids and map the changes as {oldId: newId} docArray.forEach(doc => { let oldId = doc._id; - let newId = randomSrc.id(); + let newId = idMap[oldId] || randomSrc.id(); doc._id = newId; idMap[oldId] = newId; }); diff --git a/app/imports/server/publications/searchLibraryNodes.js b/app/imports/server/publications/searchLibraryNodes.js index 88d7e678..ddbbf4ba 100644 --- a/app/imports/server/publications/searchLibraryNodes.js +++ b/app/imports/server/publications/searchLibraryNodes.js @@ -4,7 +4,6 @@ import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js'; Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){ - console.log('attempting selectedLibraryNodes') check(selectedNodeIds, Array); // Limit to 20 selected nodes if (selectedNodeIds.length > 20){ From 235560eb44a0409f8538b56f9c3418a6df8a2240 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 6 Sep 2021 11:36:42 +0200 Subject: [PATCH 04/85] Started on DBv1 migration --- app/.meteor/packages | 1 + app/.meteor/versions | 1 + .../creatureProperties/CreatureProperties.js | 4 + app/imports/api/properties/Actions.js | 245 ++++++------------ .../subSchemas/AttributeConsumedSchema.js | 48 +++- .../subSchemas/ComputedFieldSchema.js | 35 +++ .../InlineCalculationFieldSchema.js | 37 +++ .../subSchemas/InlineComputationSchema.js | 4 +- .../subSchemas/ItemConsumedSchema.js | 61 ++++- .../properties/subSchemas/ResourcesSchema.js | 52 +++- app/imports/constants/STORAGE_LIMITS.js | 1 + app/imports/migrations/2.0-beta.33-dbv1.js | 186 +++++++++++++ app/imports/migrations/index.js | 2 + app/imports/migrations/methods/getVersion.js | 30 +++ app/imports/migrations/methods/index.js | 2 + app/imports/migrations/methods/migrateTo.js | 29 +++ app/imports/ui/pages/Admin.vue | 87 +++++++ app/imports/ui/router.js | 6 + app/server/main.js | 1 + 19 files changed, 658 insertions(+), 174 deletions(-) create mode 100644 app/imports/api/properties/subSchemas/ComputedFieldSchema.js create mode 100644 app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js create mode 100644 app/imports/migrations/2.0-beta.33-dbv1.js create mode 100644 app/imports/migrations/index.js create mode 100644 app/imports/migrations/methods/getVersion.js create mode 100644 app/imports/migrations/methods/index.js create mode 100644 app/imports/migrations/methods/migrateTo.js create mode 100644 app/imports/ui/pages/Admin.vue diff --git a/app/.meteor/packages b/app/.meteor/packages index d0f672c2..f627a7cd 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -51,3 +51,4 @@ peerlibrary:subscription-data seba:minifiers-autoprefixer akryum:vue-component akryum:vue-sass +percolate:migrations diff --git a/app/.meteor/versions b/app/.meteor/versions index 586384a6..73bc1e23 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -93,6 +93,7 @@ peerlibrary:reactive-mongo@0.4.0 peerlibrary:reactive-publish@0.10.0 peerlibrary:server-autorun@0.8.0 peerlibrary:subscription-data@0.8.0 +percolate:migrations@1.0.3 percolate:synced-cron@1.3.2 promise@0.11.2 raix:eventemitter@1.0.0 diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index d690b620..4b7d170f 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -10,6 +10,10 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; let CreatureProperties = new Mongo.Collection('creatureProperties'); let CreaturePropertySchema = new SimpleSchema({ + _migrationError: { + type: String, + optional: true, + }, type: { type: String, allowedValues: Object.keys(propertySchemasIndex), diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 11782ffb..c1e058ce 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -1,7 +1,19 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; -import { storedIconsSchema } from '/imports/api/icons/Icons.js'; +import { + InlineCalculationFieldToComputeSchema, + ComputedOnlyInlineCalculationFieldSchema, + InlineCalculationFieldSchema, +} from '/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js'; +import { + FieldToComputeSchema, + ComputedOnlyFieldSchema, + ComputedFieldSchema, +} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; +import { + ResourcesSchema, + ResourcesComputedOnlySchema, + ResourcesComputedSchema, +} from '/imports/api/properties/subSchemas/ResourcesSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; /* @@ -11,186 +23,75 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; * to this action */ let ActionSchema = new SimpleSchema({ - name: { - type: String, - optional: true, + name: { + type: String, + optional: true, max: STORAGE_LIMITS.name, - }, - summary: { - type: String, - optional: true, - max: STORAGE_LIMITS.summary, - }, - description: { - type: String, - optional: true, - max: STORAGE_LIMITS.description, - }, - // What time-resource is used to take the action in combat - // long actions take longer than 1 round to cast - actionType: { - type: String, - allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'], - defaultValue: 'action', - }, - // Who is the action directed at - target: { - type: String, - defaultValue: 'singleTarget', - allowedValues: [ + }, + summary: { + type: InlineCalculationFieldToComputeSchema, + optional: true, + }, + description: { + type: InlineCalculationFieldToComputeSchema, + optional: true, + }, + // What time-resource is used to take the action in combat + // long actions take longer than 1 round to cast + actionType: { + type: String, + allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'], + defaultValue: 'action', + }, + // Who is the action directed at + target: { + type: String, + defaultValue: 'singleTarget', + allowedValues: [ 'self', 'singleTarget', - 'multipleTargets', + 'multipleTargets', ], - }, - // Duplicate the ResourceSchema here so we can extend it elegantly. + }, + // Resources schema changes for between standard, computed, and computedOnly resources: { - type: Object, + type: ResourcesSchema, defaultValue: {}, }, - 'resources.itemsConsumed': { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.resourcesCount, - }, - 'resources.itemsConsumed.$': { - type: Object, - }, - 'resources.itemsConsumed.$._id': { - type: String, - regEx: SimpleSchema.RegEx.Id, - autoValue(){ - if (!this.isSet) return Random.id(); - } - }, - 'resources.itemsConsumed.$.tag': { - type: String, + // Calculation of how many times this action can be used + uses: { + type: FieldToComputeSchema, optional: true, - max: STORAGE_LIMITS.tagLength, }, - 'resources.itemsConsumed.$.quantity': { - type: Number, - defaultValue: 1, - }, - 'resources.itemsConsumed.$.itemId': { - type: String, - optional: true, - max: STORAGE_LIMITS.name, - }, - 'resources.attributesConsumed': { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.resourcesCount, - }, - 'resources.attributesConsumed.$': { - type: Object, - }, - 'resources.attributesConsumed.$._id': { - type: String, - regEx: SimpleSchema.RegEx.Id, - autoValue(){ - if (!this.isSet) return Random.id(); - } - }, - 'resources.attributesConsumed.$.variableName': { - type: String, - optional: true, - max: STORAGE_LIMITS.variableName, - }, - 'resources.attributesConsumed.$.quantity': { - type: Number, - defaultValue: 1, - }, - // Calculation of how many times this action can be used - uses: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, - // Integer of how many times it has already been used - usesUsed: { - type: SimpleSchema.Integer, - optional: true, - }, - // How this action's uses are reset automatically - reset: { - type: String, - allowedValues: ['longRest', 'shortRest'], - optional: true, - }, -}); - -const ComputedOnlyActionSchema = new SimpleSchema({ - summaryCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, - }, - 'summaryCalculations.$': InlineComputationSchema, - - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, - }, - 'descriptionCalculations.$': InlineComputationSchema, - - usesResult: { + // Integer of how many times it has already been used + usesUsed: { type: SimpleSchema.Integer, optional: true, }, - usesErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'usesErrors.$':{ - type: ErrorSchema, - }, - resources: Object, - 'resources.itemsConsumed': Array, - 'resources.itemsConsumed.$': Object, - 'resources.itemsConsumed.$.available': { - type: Number, - optional: true, - }, - // This appears both in the computed and uncomputed schema because it can be - // set by both a computation or a form - 'resources.itemsConsumed.$.itemId': { + // How this action's uses are reset automatically + reset: { type: String, - regEx: SimpleSchema.RegEx.Id, + allowedValues: ['longRest', 'shortRest'], optional: true, }, - 'resources.itemsConsumed.$.itemName': { - type: String, - max: STORAGE_LIMITS.name, +}); + +const ComputedOnlyActionSchema = new SimpleSchema({ + summary: { + type: ComputedOnlyInlineCalculationFieldSchema, optional: true, }, - 'resources.itemsConsumed.$.itemIcon': { - type: storedIconsSchema, - optional: true, - max: STORAGE_LIMITS.icon, - }, - 'resources.itemsConsumed.$.itemColor': { - type: String, - optional: true, - max: STORAGE_LIMITS.color, - }, - 'resources.attributesConsumed': Array, - 'resources.attributesConsumed.$': Object, - 'resources.attributesConsumed.$.available': { - type: Number, + description: { + type: ComputedOnlyInlineCalculationFieldSchema, optional: true, }, - 'resources.attributesConsumed.$.statId': { - type: String, - regEx: SimpleSchema.RegEx.Id, + uses: { + type: ComputedOnlyFieldSchema, optional: true, }, - 'resources.attributesConsumed.$.statName': { - type: String, - optional: true, - max: STORAGE_LIMITS.name, + resources: { + type: ResourcesComputedOnlySchema, + defaultValue: {}, }, // True if the uses left is zero, or any item or attribute consumed is // insufficient @@ -202,6 +103,24 @@ const ComputedOnlyActionSchema = new SimpleSchema({ const ComputedActionSchema = new SimpleSchema() .extend(ActionSchema) - .extend(ComputedOnlyActionSchema); + .extend(ComputedOnlyActionSchema) + .extend({ + uses: { + type: ComputedFieldSchema, + optional: true, + }, + summary: { + type: InlineCalculationFieldSchema, + optional: true, + }, + description: { + type: InlineCalculationFieldSchema, + optional: true, + }, + resources: { + type: ResourcesComputedSchema, + defaultValue: {}, + }, + }); export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema}; diff --git a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js index 8c85d13f..622e87a3 100644 --- a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js @@ -1,5 +1,11 @@ import SimpleSchema from 'simpl-schema'; import { Random } from 'meteor/random'; +import { + FieldToComputeSchema, + ComputedOnlyFieldSchema, + ComputedFieldSchema, +} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; const AttributeConsumedSchema = new SimpleSchema({ _id: { @@ -12,11 +18,47 @@ const AttributeConsumedSchema = new SimpleSchema({ variableName: { type: String, optional: true, + max: STORAGE_LIMITS.variableName, }, quantity: { - type: Number, - defaultValue: 1, + type: FieldToComputeSchema, + optional: true, }, }); -export default AttributeConsumedSchema; +const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({ + available: { + type: Number, + optional: true, + }, + statId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + statName: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + quantity: { + type: ComputedOnlyFieldSchema, + optional: true, + }, +}); + +const ComputedAttributeConsumedSchema = new SimpleSchema() + .extend(AttributeConsumedSchema) + .extend(ComputedOnlyAttributeConsumedSchema) + .extend({ + quantity: { + type: ComputedFieldSchema, + optional: true, + }, + }); + +export { + AttributeConsumedSchema, + ComputedOnlyAttributeConsumedSchema, + ComputedAttributeConsumedSchema +}; diff --git a/app/imports/api/properties/subSchemas/ComputedFieldSchema.js b/app/imports/api/properties/subSchemas/ComputedFieldSchema.js new file mode 100644 index 00000000..70307db4 --- /dev/null +++ b/app/imports/api/properties/subSchemas/ComputedFieldSchema.js @@ -0,0 +1,35 @@ +import SimpleSchema from 'simpl-schema'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const FieldToComputeSchema = new SimpleSchema({ + // This is required, if we don't have a calculation delete the whole object + calculation: { + type: String, + }, +}); + +const ComputedOnlyFieldSchema = new SimpleSchema({ + value: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + }, + errors: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.errorCount, + }, + 'errors.$':{ + type: ErrorSchema, + }, +}); + +const ComputedFieldSchema = new SimpleSchema() + .extend(FieldToComputeSchema) + .extend(ComputedOnlyFieldSchema) + +export { + FieldToComputeSchema, + ComputedOnlyFieldSchema, + ComputedFieldSchema +}; diff --git a/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js b/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js new file mode 100644 index 00000000..085347b1 --- /dev/null +++ b/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js @@ -0,0 +1,37 @@ +import SimpleSchema from 'simpl-schema'; +import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const InlineCalculationFieldToComputeSchema = new SimpleSchema({ + text: { + type: String, + optional: true, + max: STORAGE_LIMITS.inlineCalculationField, + }, +}); + +const ComputedOnlyInlineCalculationFieldSchema = new SimpleSchema({ + 'inlineCalculations': { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.inlineCalculationCount, + }, + 'inlineCalculations.$': { + type: InlineComputationSchema, + }, + value: { + type: String, + optional: true, + max: STORAGE_LIMITS.inlineCalculationField, + }, +}); + +const InlineCalculationFieldSchema = new SimpleSchema() + .extend(InlineCalculationFieldToComputeSchema) + .extend(ComputedOnlyInlineCalculationFieldSchema) + +export { + InlineCalculationFieldToComputeSchema, + ComputedOnlyInlineCalculationFieldSchema, + InlineCalculationFieldSchema, +}; diff --git a/app/imports/api/properties/subSchemas/InlineComputationSchema.js b/app/imports/api/properties/subSchemas/InlineComputationSchema.js index ea17e4e5..e220a3aa 100644 --- a/app/imports/api/properties/subSchemas/InlineComputationSchema.js +++ b/app/imports/api/properties/subSchemas/InlineComputationSchema.js @@ -8,8 +8,8 @@ const InlineComputationSchema = new SimpleSchema({ type: String, max: STORAGE_LIMITS.calculation, }, - result: { - type: String, + value: { + type: SimpleSchema.oneOf(String, Number), optional: true, max: STORAGE_LIMITS.calculation, }, diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index 8545b6d1..b17d8b0f 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -1,5 +1,12 @@ import SimpleSchema from 'simpl-schema'; import { Random } from 'meteor/random'; +import { + FieldToComputeSchema, + ComputedOnlyFieldSchema, + ComputedFieldSchema, +} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; +import { storedIconsSchema } from '/imports/api/icons/Icons.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; const ItemConsumedSchema = new SimpleSchema({ _id: { @@ -14,13 +21,61 @@ const ItemConsumedSchema = new SimpleSchema({ optional: true, }, quantity: { - type: Number, - defaultValue: 1, + type: FieldToComputeSchema, + optional: true, }, itemId: { type: String, + regEx: SimpleSchema.RegEx.Id, optional: true, }, }); -export default ItemConsumedSchema; +const ComputedOnlyItemConsumedSchema = new SimpleSchema({ + available: { + type: Number, + optional: true, + }, + quantity: { + type: ComputedOnlyFieldSchema, + optional: true, + }, + // This appears both in the computed and uncomputed schema because it can be + // set by both a computation or a form + itemId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + itemName: { + type: String, + max: STORAGE_LIMITS.name, + optional: true, + }, + itemIcon: { + type: storedIconsSchema, + optional: true, + max: STORAGE_LIMITS.icon, + }, + itemColor: { + type: String, + optional: true, + max: STORAGE_LIMITS.color, + }, +}) + +const ComputedItemConsumedSchema = new SimpleSchema() + .extend(ItemConsumedSchema) + .extend(ComputedOnlyItemConsumedSchema) + .extend({ + quantity: { + type: ComputedFieldSchema, + optional: true, + }, + }); + +export { + ItemConsumedSchema, + ComputedOnlyItemConsumedSchema, + ComputedItemConsumedSchema +}; diff --git a/app/imports/api/properties/subSchemas/ResourcesSchema.js b/app/imports/api/properties/subSchemas/ResourcesSchema.js index bc7ae9da..4c36bbb2 100644 --- a/app/imports/api/properties/subSchemas/ResourcesSchema.js +++ b/app/imports/api/properties/subSchemas/ResourcesSchema.js @@ -1,6 +1,14 @@ import SimpleSchema from 'simpl-schema'; -import ItemConsumedSchema from '/imports/api/properties/subSchemas/ItemConsumedSchema.js'; -import AttributeConsumedSchema from '/imports/api/properties/subSchemas/AttributeConsumedSchema.js'; +import { + ItemConsumedSchema, + ComputedOnlyItemConsumedSchema, + ComputedItemConsumedSchema +} from '/imports/api/properties/subSchemas/ItemConsumedSchema.js'; +import { + AttributeConsumedSchema, + ComputedOnlyAttributeConsumedSchema, + ComputedAttributeConsumedSchema +} from '/imports/api/properties/subSchemas/AttributeConsumedSchema.js'; const ResourcesSchema = new SimpleSchema({ itemsConsumed: { @@ -19,4 +27,42 @@ const ResourcesSchema = new SimpleSchema({ }, }); -export default ResourcesSchema; +const ResourcesComputedOnlySchema = new SimpleSchema({ + itemsConsumed: { + type: Array, + defaultValue: [], + }, + 'itemsConsumed.$': { + type: ComputedOnlyItemConsumedSchema, + }, + attributesConsumed: { + type: Array, + defaultValue: [], + }, + 'attributesConsumed.$': { + type: ComputedOnlyAttributeConsumedSchema, + }, +}); + +const ResourcesComputedSchema = new SimpleSchema({ + itemsConsumed: { + type: Array, + defaultValue: [], + }, + 'itemsConsumed.$': { + type: ComputedItemConsumedSchema, + }, + attributesConsumed: { + type: Array, + defaultValue: [], + }, + 'attributesConsumed.$': { + type: ComputedAttributeConsumedSchema, + }, +}); + +export { + ResourcesSchema, + ResourcesComputedOnlySchema, + ResourcesComputedSchema, +}; diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index 23f07fbe..065089ca 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -4,6 +4,7 @@ const STORAGE_LIMITS = Object.freeze({ collectionName: 64, color: 10000, description: 49473, //the length of the Bee Movie script + inlineCalculationField: 49473, errorMessage: 256, icon: 10000, name: 128, diff --git a/app/imports/migrations/2.0-beta.33-dbv1.js b/app/imports/migrations/2.0-beta.33-dbv1.js new file mode 100644 index 00000000..f8f107f8 --- /dev/null +++ b/app/imports/migrations/2.0-beta.33-dbv1.js @@ -0,0 +1,186 @@ +import { Migrations } from 'meteor/percolate:migrations'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import { get, merge } from 'lodash'; + +// Git version 2.0-beta.33 +// Database version 1 +Migrations.add({ + version: 1, + name: 'Unifies calculated field schema', + up(){ + CreatureProperties.find({}).forEach(prop => { + const modifier = getUpPropModifier(prop); + if (!modifier) return; + updateOrStoreError(CreatureProperties, prop, modifier); + }); + }, + down(){ + CreatureProperties.find({}).forEach(prop => { + const modifier = getDownPropModifier(prop); + if (!modifier) return; + updateOrStoreError(CreatureProperties, prop, modifier); + }); + }, +}); + +function updateOrStoreError(collection, prop, modifier){ + try { + collection.update(prop._id, modifier, { + bypassCollection2: true, + //selector: {type: prop.type}, + }); + } catch(e){ + let errorString = e.toString(); + if (errorString){ + console.warn(errorString, prop._id); + collection.update(prop._id, { + $set: {_migrationError: e.toString()} + }, { + bypassCollection2: true, + }); + } + } +} + +function getUpPropModifier(prop){ + const modifiers = typeUpModifiers[prop.type]?.(prop); + if (!modifiers) return; + return cleanModifier(merge(...modifiers)); +} + +function getDownPropModifier(prop){ + const modifiers = typeDownModifiers[prop.type]?.(prop); + if (!modifiers) return; + return cleanModifier(merge(...modifiers)); +} + +function cleanModifier(modifier){ + if (modifier.$set && !Object.keys(modifier.$set).length){ + delete modifier.$set; + } + if (modifier.$unset && !Object.keys(modifier.$unset).length){ + delete modifier.$unset; + } + if (!modifier.$set && !modifier.$unset) return; + return modifier; +} + +const typeUpModifiers = { + action(prop){ + return [ + convertComputedField(prop, 'uses'), + // TODO: This doesn't work on itemsConsumed because it is an array field + // Need to iterate over every item consumed + convertComputedField(prop, 'resources.itemsConsumed.quantity'), + convertComputedField(prop, 'resources.attributesConsumed.quantity'), + convertInlineComputationField(prop, 'summary'), + convertInlineComputationField(prop, 'description'), + ]; + }, +}; + +const typeDownModifiers = { + action(prop){ + const modifiers = [ + unConvertComputedField(prop, 'uses'), + unConvertComputedField(prop, 'resources.itemsConsumed.quantity'), + unConvertComputedField(prop, 'resources.attributesConsumed.quantity'), + unConvertInlineComputationField(prop, 'summary'), + unConvertInlineComputationField(prop, 'description'), + ]; + return modifiers; + }, +}; + +function convertComputedField(object, field){ + const calculation = get(object, field); + if (!calculation) return { + $unset: { + [field]: 1, + [field + 'Errors']: 1, + [field + 'Result']: 1, + } + }; + const errors = get(object, field + 'Errors'); + let value = get(object, field + 'Result'); + // If the calculation can be cast to number, use that for value + if (value === undefined && Number.isFinite(+calculation)){ + value = +calculation; + } + const modifier = { + $unset:{ + [field + 'Errors']: 1, + [field + 'Result']: 1, + }, + $set: { + [field]: { + value, + calculation, + errors, + } + } + }; + return modifier; +} + +function unConvertComputedField(object, field){ + const calculation = get(object, field)?.calculation; + if (!calculation) return { + $unset: { + [field]: 1, + } + }; + const errors = get(object, field).errors; + let value = get(object, field).value; + // If the calculation can be cast to number, use that for value + if (value === undefined && Number.isFinite(+calculation)){ + value = +calculation; + } + const modifier = { + $set:{ + [field]: calculation, + [field + 'Errors']: errors, + [field + 'Result']: value, + }, + }; + return modifier; +} + +function convertInlineComputationField(object, field){ + const text = get(object, field); + const inlineCalculations = get(object, field + 'Calculations'); + if (inlineCalculations){ + inlineCalculations.forEach(calc => { + calc.value = calc.result; + delete calc.result; + }); + } + return { + $unset: { + [field + 'Calculations']: 1, + }, + $set: { + [field]: { + text, + inlineCalculations, + } + }, + }; +} + +function unConvertInlineComputationField(object, field){ + const text = get(object, field)?.text; + const inlineCalculations = get(object, field)?.inlineCalculations; + if (inlineCalculations) { + inlineCalculations.forEach(calc => { + calc.result = calc.value; + delete calc.value; + }); + } + return { + $set: { + [field]: text, + [field + 'Calculations']: inlineCalculations, + }, + }; +} diff --git a/app/imports/migrations/index.js b/app/imports/migrations/index.js new file mode 100644 index 00000000..97fcde14 --- /dev/null +++ b/app/imports/migrations/index.js @@ -0,0 +1,2 @@ +import './2.0-beta.33-dbv1.js'; +import './methods/index.js'; diff --git a/app/imports/migrations/methods/getVersion.js b/app/imports/migrations/methods/getVersion.js new file mode 100644 index 00000000..8b67d5fe --- /dev/null +++ b/app/imports/migrations/methods/getVersion.js @@ -0,0 +1,30 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; +import { Migrations } from 'meteor/percolate:migrations'; + +const dbVersionToGitVersion = { + 0: '2.0-beta.32 and lower', + 1: '2.0-beta.33', +} + +const getVersion = new ValidatedMethod({ + name: 'admin.getVersion', + validate: null, + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run() { + if (Meteor.isClient) return; + assertAdmin(this.userId); + const dbVersion = Migrations.getVersion(); + return { + dbVersion, + gitVersion: dbVersionToGitVersion[dbVersion], + } + }, +}); + +export default getVersion; diff --git a/app/imports/migrations/methods/index.js b/app/imports/migrations/methods/index.js new file mode 100644 index 00000000..6d5dc8c5 --- /dev/null +++ b/app/imports/migrations/methods/index.js @@ -0,0 +1,2 @@ +import './migrateTo.js'; +import './getVersion.js'; diff --git a/app/imports/migrations/methods/migrateTo.js b/app/imports/migrations/methods/migrateTo.js new file mode 100644 index 00000000..c5248c9d --- /dev/null +++ b/app/imports/migrations/methods/migrateTo.js @@ -0,0 +1,29 @@ +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import SimpleSchema from 'simpl-schema'; +import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js'; +import { Migrations } from 'meteor/percolate:migrations'; + +const migrateTo = new ValidatedMethod({ + name: 'admin.migrateTo', + validate: new SimpleSchema({ + version: { + type: SimpleSchema.oneOf( + SimpleSchema.Integer, + String + ), + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 5, + timeInterval: 5000, + }, + run({version}) { + if (Meteor.isClient) return; + assertAdmin(this.userId); + Migrations.migrateTo(version); + }, +}); + +export default migrateTo; diff --git a/app/imports/ui/pages/Admin.vue b/app/imports/ui/pages/Admin.vue new file mode 100644 index 00000000..e905052a --- /dev/null +++ b/app/imports/ui/pages/Admin.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js index 0bda3df6..d4ed9fb6 100644 --- a/app/imports/ui/router.js +++ b/app/imports/ui/router.js @@ -24,6 +24,7 @@ import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue'; import Tabletops from '/imports/ui/pages/Tabletops.vue'; import Tabletop from '/imports/ui/pages/Tabletop.vue'; import TabletopToolbar from '/imports/ui/tabletop/TabletopToolbar.vue'; +import Admin from '/imports/ui/pages/Admin.vue'; let userSubscription = Meteor.subscribe('user'); @@ -242,6 +243,11 @@ RouterFactory.configure(factory => { name: 'iconAdmin', component: IconAdmin, beforeEnter: ensureAdmin, + },{ + path: '/admin', + name: 'admin', + component: Admin, + beforeEnter: ensureAdmin, }, ]); }); diff --git a/app/server/main.js b/app/server/main.js index b8988bef..f7ac36c6 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -6,3 +6,4 @@ import '/imports/server/publications/index.js'; import '/imports/server/cron/deleteSoftRemovedDocuments.js'; import '/imports/api/parenting/organizeMethods.js'; import '/imports/api/users/patreon/updatePatreonOnLogin.js'; +import '/imports/migrations/index.js'; From e79b8fda3b0bb559dfb76542b844e335f930a4d7 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 6 Sep 2021 17:40:57 +0200 Subject: [PATCH 05/85] Improved migration code substantially, wrote migrations for more properties --- app/.meteor/packages | 1 + app/.meteor/versions | 3 + ...js => creatureComputation.disabledTest.js} | 0 .../creatureProperties/CreatureProperties.js | 4 + app/imports/api/properties/Actions.js | 19 +- app/imports/api/properties/Adjustments.js | 30 ++- app/imports/api/properties/Attacks.js | 28 +-- .../subSchemas/AttributeConsumedSchema.js | 23 +-- .../subSchemas/ComputedFieldSchema.js | 71 ++++--- .../subSchemas/ItemConsumedSchema.js | 23 +-- app/imports/constants/SCHEMA_VERSION.js | 3 + app/imports/migrations/2.0-beta.33-dbv1.js | 186 ------------------ .../migrations/server/2.0-beta.33-dbv1.js | 88 +++++++++ .../server/2.0-beta.33-dbv1.test.js | 85 ++++++++ app/imports/migrations/{ => server}/index.js | 1 - .../migrations/server/transformFields.js | 112 +++++++++++ .../migrations/server/transformFields.test.js | 85 ++++++++ app/package-lock.json | 59 +++++- app/package.json | 4 +- app/server/main.js | 3 +- 20 files changed, 525 insertions(+), 303 deletions(-) rename app/imports/api/creature/computation/{creatureComputation.test.js => creatureComputation.disabledTest.js} (100%) create mode 100644 app/imports/constants/SCHEMA_VERSION.js delete mode 100644 app/imports/migrations/2.0-beta.33-dbv1.js create mode 100644 app/imports/migrations/server/2.0-beta.33-dbv1.js create mode 100644 app/imports/migrations/server/2.0-beta.33-dbv1.test.js rename app/imports/migrations/{ => server}/index.js (52%) create mode 100644 app/imports/migrations/server/transformFields.js create mode 100644 app/imports/migrations/server/transformFields.test.js diff --git a/app/.meteor/packages b/app/.meteor/packages index f627a7cd..258a617b 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -52,3 +52,4 @@ seba:minifiers-autoprefixer akryum:vue-component akryum:vue-sass percolate:migrations +meteortesting:mocha diff --git a/app/.meteor/versions b/app/.meteor/versions index 73bc1e23..bcc20581 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -62,6 +62,9 @@ mdg:validated-method@1.2.0 meteor@1.9.3 meteor-base@1.4.0 meteorhacks:subs-manager@1.6.4 +meteortesting:browser-tests@1.3.4 +meteortesting:mocha@2.0.2 +meteortesting:mocha-core@8.0.1 mikowals:batch-insert@1.2.0 minifier-css@1.5.4 minifier-js@2.6.1 diff --git a/app/imports/api/creature/computation/creatureComputation.test.js b/app/imports/api/creature/computation/creatureComputation.disabledTest.js similarity index 100% rename from app/imports/api/creature/computation/creatureComputation.test.js rename to app/imports/api/creature/computation/creatureComputation.disabledTest.js diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index 4b7d170f..fe1f51a6 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -10,6 +10,10 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; let CreatureProperties = new Mongo.Collection('creatureProperties'); let CreaturePropertySchema = new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, _migrationError: { type: String, optional: true, diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index c1e058ce..d17c3d66 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -5,9 +5,8 @@ import { InlineCalculationFieldSchema, } from '/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js'; import { - FieldToComputeSchema, - ComputedOnlyFieldSchema, - ComputedFieldSchema, + fieldToCompute, + computedOnlyField, } from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import { ResourcesSchema, @@ -60,7 +59,7 @@ let ActionSchema = new SimpleSchema({ }, // Calculation of how many times this action can be used uses: { - type: FieldToComputeSchema, + type: Object, optional: true, }, // Integer of how many times it has already been used @@ -74,7 +73,7 @@ let ActionSchema = new SimpleSchema({ allowedValues: ['longRest', 'shortRest'], optional: true, }, -}); +}).extend(fieldToCompute('uses')); const ComputedOnlyActionSchema = new SimpleSchema({ summary: { @@ -85,10 +84,6 @@ const ComputedOnlyActionSchema = new SimpleSchema({ type: ComputedOnlyInlineCalculationFieldSchema, optional: true, }, - uses: { - type: ComputedOnlyFieldSchema, - optional: true, - }, resources: { type: ResourcesComputedOnlySchema, defaultValue: {}, @@ -99,16 +94,12 @@ const ComputedOnlyActionSchema = new SimpleSchema({ type: Boolean, optional: true, }, -}); +}).extend(computedOnlyField('uses')); const ComputedActionSchema = new SimpleSchema() .extend(ActionSchema) .extend(ComputedOnlyActionSchema) .extend({ - uses: { - type: ComputedFieldSchema, - optional: true, - }, summary: { type: InlineCalculationFieldSchema, optional: true, diff --git a/app/imports/api/properties/Adjustments.js b/app/imports/api/properties/Adjustments.js index 9b3c12fb..8ab97d4f 100644 --- a/app/imports/api/properties/Adjustments.js +++ b/app/imports/api/properties/Adjustments.js @@ -1,15 +1,20 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import { + fieldToCompute, + computedOnlyField, +} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; const AdjustmentSchema = new SimpleSchema({ // The roll that determines how much to change the attribute // This can be simplified, but should only compute when activated amount: { - type: String, + type: Object, optional: true, - defaultValue: '1', - max: STORAGE_LIMITS.calculation, + }, + 'amount.calculation': { + type: String, + defaultValue: 1, }, // Who this adjustment applies to target: { @@ -32,22 +37,9 @@ const AdjustmentSchema = new SimpleSchema({ allowedValues: ['set', 'increment'], defaultValue: 'increment', }, -}); +}).extend(fieldToCompute('amount')); -const ComputedOnlyAdjustmentSchema = new SimpleSchema({ - amountResult: { - type: SimpleSchema.oneOf(String, Number), - optional: true, - }, - amountErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'amountErrors.$':{ - type: ErrorSchema, - }, -}); +const ComputedOnlyAdjustmentSchema = computedOnlyField('amount'); const ComputedAdjustmentSchema = new SimpleSchema() .extend(AdjustmentSchema) diff --git a/app/imports/api/properties/Attacks.js b/app/imports/api/properties/Attacks.js index e2363e59..e10677b8 100644 --- a/app/imports/api/properties/Attacks.js +++ b/app/imports/api/properties/Attacks.js @@ -1,6 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; +import { + fieldToCompute, + computedOnlyField, +} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Attacks are special instances of actions @@ -9,10 +12,12 @@ let AttackSchema = new SimpleSchema() .extend({ // What gets added to the d20 roll rollBonus: { + type: Object, + optional: true, + }, + 'rollBonus.calculation': { type: String, defaultValue: 'strength.modifier + proficiencyBonus', - optional: true, - max: STORAGE_LIMITS.calculation, }, // Set better defaults for the action actionType: { @@ -29,24 +34,11 @@ let AttackSchema = new SimpleSchema() type: String, max: STORAGE_LIMITS.tagLength, }, - }); + }).extend(fieldToCompute('rollBonus')); const ComputedOnlyAttackSchema = new SimpleSchema() .extend(ComputedOnlyActionSchema) - .extend({ - rollBonusResult: { - type: Number, - optional: true, - }, - rollBonusErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'rollBonusErrors.$':{ - type: ErrorSchema, - }, - }); + .extend(computedOnlyField('rollBonus')); const ComputedAttackSchema = new SimpleSchema() .extend(AttackSchema) diff --git a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js index 622e87a3..0c00ade4 100644 --- a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js @@ -1,9 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { Random } from 'meteor/random'; import { - FieldToComputeSchema, - ComputedOnlyFieldSchema, - ComputedFieldSchema, + fieldToCompute, + computedOnlyField, } from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; @@ -21,10 +20,10 @@ const AttributeConsumedSchema = new SimpleSchema({ max: STORAGE_LIMITS.variableName, }, quantity: { - type: FieldToComputeSchema, + type: Object, optional: true, }, -}); +}).extend(fieldToCompute('quantity')); const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({ available: { @@ -41,21 +40,11 @@ const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.name, }, - quantity: { - type: ComputedOnlyFieldSchema, - optional: true, - }, -}); +}).extend(computedOnlyField('quantity')); const ComputedAttributeConsumedSchema = new SimpleSchema() .extend(AttributeConsumedSchema) - .extend(ComputedOnlyAttributeConsumedSchema) - .extend({ - quantity: { - type: ComputedFieldSchema, - optional: true, - }, - }); + .extend(ComputedOnlyAttributeConsumedSchema); export { AttributeConsumedSchema, diff --git a/app/imports/api/properties/subSchemas/ComputedFieldSchema.js b/app/imports/api/properties/subSchemas/ComputedFieldSchema.js index 70307db4..2455c237 100644 --- a/app/imports/api/properties/subSchemas/ComputedFieldSchema.js +++ b/app/imports/api/properties/subSchemas/ComputedFieldSchema.js @@ -2,34 +2,53 @@ import SimpleSchema from 'simpl-schema'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -const FieldToComputeSchema = new SimpleSchema({ - // This is required, if we don't have a calculation delete the whole object - calculation: { - type: String, - }, -}); +// Get schemas that apply fields directly so they can be gracefully extended +// because {type: Schema} fields can't be extended +function fieldToCompute(field){ + return new SimpleSchema({ + // The object should already be set, but set again just in case + [field]: { + type: Object, + optional: true, + }, + // This is required, if we don't have a calculation delete the whole object + [`${field}.calculation`]: { + type: String, + max: STORAGE_LIMITS.calculation, + }, + }); +} -const ComputedOnlyFieldSchema = new SimpleSchema({ - value: { - type: SimpleSchema.oneOf(String, Number), - optional: true, - }, - errors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'errors.$':{ - type: ErrorSchema, - }, -}); +function computedOnlyField(field){ + return new SimpleSchema({ + // The object should already be set, but set again just in case + [field]: { + type: Object, + optional: true, + }, + [`${field}.value`]: { + type: SimpleSchema.oneOf(String, Number), + optional: true, + }, + [`${field}.errors`]: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.errorCount, + }, + [`${field}.errors.$`]:{ + type: ErrorSchema, + }, + }); +} -const ComputedFieldSchema = new SimpleSchema() - .extend(FieldToComputeSchema) - .extend(ComputedOnlyFieldSchema) +// This should rarely be used, since the other two will merge correctly when +// uncomputed and computedOnly schemas are merged +function computedField(field){ + return computedField(field).extend(computedOnlyField(field)); +} export { - FieldToComputeSchema, - ComputedOnlyFieldSchema, - ComputedFieldSchema + fieldToCompute, + computedOnlyField, + computedField, }; diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index b17d8b0f..762ed985 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -1,9 +1,8 @@ import SimpleSchema from 'simpl-schema'; import { Random } from 'meteor/random'; import { - FieldToComputeSchema, - ComputedOnlyFieldSchema, - ComputedFieldSchema, + fieldToCompute, + computedOnlyField, } from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; @@ -21,7 +20,7 @@ const ItemConsumedSchema = new SimpleSchema({ optional: true, }, quantity: { - type: FieldToComputeSchema, + type: Object, optional: true, }, itemId: { @@ -29,17 +28,13 @@ const ItemConsumedSchema = new SimpleSchema({ regEx: SimpleSchema.RegEx.Id, optional: true, }, -}); +}).extend(fieldToCompute('quantity')); const ComputedOnlyItemConsumedSchema = new SimpleSchema({ available: { type: Number, optional: true, }, - quantity: { - type: ComputedOnlyFieldSchema, - optional: true, - }, // This appears both in the computed and uncomputed schema because it can be // set by both a computation or a form itemId: { @@ -62,17 +57,11 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.color, }, -}) +}).extend(computedOnlyField('quantity')); const ComputedItemConsumedSchema = new SimpleSchema() .extend(ItemConsumedSchema) - .extend(ComputedOnlyItemConsumedSchema) - .extend({ - quantity: { - type: ComputedFieldSchema, - optional: true, - }, - }); + .extend(ComputedOnlyItemConsumedSchema); export { ItemConsumedSchema, diff --git a/app/imports/constants/SCHEMA_VERSION.js b/app/imports/constants/SCHEMA_VERSION.js new file mode 100644 index 00000000..4ac57384 --- /dev/null +++ b/app/imports/constants/SCHEMA_VERSION.js @@ -0,0 +1,3 @@ +const SCHEMA_VERSION = 1; + +export default SCHEMA_VERSION; diff --git a/app/imports/migrations/2.0-beta.33-dbv1.js b/app/imports/migrations/2.0-beta.33-dbv1.js deleted file mode 100644 index f8f107f8..00000000 --- a/app/imports/migrations/2.0-beta.33-dbv1.js +++ /dev/null @@ -1,186 +0,0 @@ -import { Migrations } from 'meteor/percolate:migrations'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { get, merge } from 'lodash'; - -// Git version 2.0-beta.33 -// Database version 1 -Migrations.add({ - version: 1, - name: 'Unifies calculated field schema', - up(){ - CreatureProperties.find({}).forEach(prop => { - const modifier = getUpPropModifier(prop); - if (!modifier) return; - updateOrStoreError(CreatureProperties, prop, modifier); - }); - }, - down(){ - CreatureProperties.find({}).forEach(prop => { - const modifier = getDownPropModifier(prop); - if (!modifier) return; - updateOrStoreError(CreatureProperties, prop, modifier); - }); - }, -}); - -function updateOrStoreError(collection, prop, modifier){ - try { - collection.update(prop._id, modifier, { - bypassCollection2: true, - //selector: {type: prop.type}, - }); - } catch(e){ - let errorString = e.toString(); - if (errorString){ - console.warn(errorString, prop._id); - collection.update(prop._id, { - $set: {_migrationError: e.toString()} - }, { - bypassCollection2: true, - }); - } - } -} - -function getUpPropModifier(prop){ - const modifiers = typeUpModifiers[prop.type]?.(prop); - if (!modifiers) return; - return cleanModifier(merge(...modifiers)); -} - -function getDownPropModifier(prop){ - const modifiers = typeDownModifiers[prop.type]?.(prop); - if (!modifiers) return; - return cleanModifier(merge(...modifiers)); -} - -function cleanModifier(modifier){ - if (modifier.$set && !Object.keys(modifier.$set).length){ - delete modifier.$set; - } - if (modifier.$unset && !Object.keys(modifier.$unset).length){ - delete modifier.$unset; - } - if (!modifier.$set && !modifier.$unset) return; - return modifier; -} - -const typeUpModifiers = { - action(prop){ - return [ - convertComputedField(prop, 'uses'), - // TODO: This doesn't work on itemsConsumed because it is an array field - // Need to iterate over every item consumed - convertComputedField(prop, 'resources.itemsConsumed.quantity'), - convertComputedField(prop, 'resources.attributesConsumed.quantity'), - convertInlineComputationField(prop, 'summary'), - convertInlineComputationField(prop, 'description'), - ]; - }, -}; - -const typeDownModifiers = { - action(prop){ - const modifiers = [ - unConvertComputedField(prop, 'uses'), - unConvertComputedField(prop, 'resources.itemsConsumed.quantity'), - unConvertComputedField(prop, 'resources.attributesConsumed.quantity'), - unConvertInlineComputationField(prop, 'summary'), - unConvertInlineComputationField(prop, 'description'), - ]; - return modifiers; - }, -}; - -function convertComputedField(object, field){ - const calculation = get(object, field); - if (!calculation) return { - $unset: { - [field]: 1, - [field + 'Errors']: 1, - [field + 'Result']: 1, - } - }; - const errors = get(object, field + 'Errors'); - let value = get(object, field + 'Result'); - // If the calculation can be cast to number, use that for value - if (value === undefined && Number.isFinite(+calculation)){ - value = +calculation; - } - const modifier = { - $unset:{ - [field + 'Errors']: 1, - [field + 'Result']: 1, - }, - $set: { - [field]: { - value, - calculation, - errors, - } - } - }; - return modifier; -} - -function unConvertComputedField(object, field){ - const calculation = get(object, field)?.calculation; - if (!calculation) return { - $unset: { - [field]: 1, - } - }; - const errors = get(object, field).errors; - let value = get(object, field).value; - // If the calculation can be cast to number, use that for value - if (value === undefined && Number.isFinite(+calculation)){ - value = +calculation; - } - const modifier = { - $set:{ - [field]: calculation, - [field + 'Errors']: errors, - [field + 'Result']: value, - }, - }; - return modifier; -} - -function convertInlineComputationField(object, field){ - const text = get(object, field); - const inlineCalculations = get(object, field + 'Calculations'); - if (inlineCalculations){ - inlineCalculations.forEach(calc => { - calc.value = calc.result; - delete calc.result; - }); - } - return { - $unset: { - [field + 'Calculations']: 1, - }, - $set: { - [field]: { - text, - inlineCalculations, - } - }, - }; -} - -function unConvertInlineComputationField(object, field){ - const text = get(object, field)?.text; - const inlineCalculations = get(object, field)?.inlineCalculations; - if (inlineCalculations) { - inlineCalculations.forEach(calc => { - calc.result = calc.value; - delete calc.value; - }); - } - return { - $set: { - [field]: text, - [field + 'Calculations']: inlineCalculations, - }, - }; -} diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js new file mode 100644 index 00000000..da930803 --- /dev/null +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -0,0 +1,88 @@ +import { Migrations } from 'meteor/percolate:migrations'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { get } from 'lodash'; +import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js'; +import transformFields from '/imports/migrations/server/transformFields.js'; +import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; + +// Git version 2.0-beta.33 +// Database version 1 +Migrations.add({ + version: 1, + name: 'Unifies calculated field schema', + up(){ + migrate(); + }, + down(){ + migrate({reversed: true}); + }, +}); + +function migrate({reversed} = {}){ + migrateCollection({collection: CreatureProperties, reversed}); + migrateCollection({collection: LibraryNodes, reversed}); +} + +function migrateCollection({collection, reversed}){ + const bulk = collection.rawCollection().initializeUnorderedBulkOp(); + collection.find({}).forEach(prop => { + const newProp = migrateProperty({collection, reversed, prop}); + bulk.find({ _id: prop._id }).replaceOne(newProp); + }); + bulk.execute(); +} + +export default function migrateProperty({collection, reversed, prop}){ + const transforms = transformsByPropType[prop.type]; + let migratedProp = transformFields(prop, transforms, reversed); + const schema = collection.simpleSchema({type: prop.type}); + // Only clean if the schema version matches our destination version + if(!reversed && SCHEMA_VERSION === 1){ + try { + migratedProp = schema.clean(migratedProp); + schema.validate(migratedProp); + } catch(e){ + console.warn(e); + } + } + return migratedProp; +} + +const actionTransforms = [ + ...getComputedPropertyTransforms('uses'), + ...getComputedPropertyTransforms('resources.attributesConsumed.$.quantity'), + ...getComputedPropertyTransforms('resources.itemsConsumed.$.quantity'), + ...getInlineComputationTransforms('summary'), + ...getInlineComputationTransforms('description'), +]; + +const transformsByPropType = { + 'action': actionTransforms, + 'adjustment': [ + ...getComputedPropertyTransforms('amount'), + ], + 'attack': [ + ...actionTransforms, + ...getComputedPropertyTransforms('rollBonus'), + ], +}; + +function getComputedPropertyTransforms(key){ + return [ + {from: key, to: `${key}.calculation`}, + {from: `${key}Result`, to: `${key}.value`}, + {from: `${key}Errors`, to: `${key}.errors`}, + ]; +} + +function getInlineComputationTransforms(key){ + return [ + {from: key, to: `${key}.text`}, + {from: `${key}Calculations`, to: `${key}.inlineCalculations`}, + {to: `${key}.value`, up: (val, doc) => + embedInlineCalculations(get(doc, key), get(doc, `${key}Calculations`)) + }, + {from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`}, + ]; +} diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.test.js b/app/imports/migrations/server/2.0-beta.33-dbv1.test.js new file mode 100644 index 00000000..fd79387c --- /dev/null +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.test.js @@ -0,0 +1,85 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import migrateProperty from './2.0-beta.33-dbv1.js'; +import { assert } from 'chai'; + +const exampleAction = { + '_id':'hY5MKZ4ivaoTRpNWy', + 'actionType':'bonus', + 'target':'singleTarget', + 'tags':[], + 'resources':{ + 'itemsConsumed':[], + 'attributesConsumed':[{ + '_id':'FaK6jXEj3pSe7mNuu', + 'quantity':1, + 'variableName':'HunterTech', + 'statId':'qccf9j5tfNJjZ3GGn', + 'statName':'Hunter\'s Technique', + 'available':5 + }], + }, + 'type':'action', + 'name':'Hexblade\\\'s Curse', + 'parent':{ + 'id':'JqtDmqa5Zd3xpts5G', + 'collection':'creatureProperties' + }, + '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', + 'usesResult':1, + 'reset':'shortRest', + 'usesUsed':0, + 'description':'Starting at 1st level, you gain the ability to place a baleful curse on someone. As a bonus action, choose one creature you can see within 30 feet of you. The target is cursed for 1 minute. The curse ends early if the target dies, you die, or you are incapacitated. Until the curse ends, you gain the following benefits:\n\n- You gain a bonus to damage rolls against the cursed target. The bonus equals your proficiency bonus.\n- Any attack roll you make against the cursed target is a critical hit on a roll of 19 or 20 on the d20.\n- If the cursed target dies, you regain hit points equal to your warlock level + your Charisma modifier (minimum of 1 hit point). \n{warlock.level <10 ? "" :"- 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."} \nYou can\\\'t use this feature again until you finish a short or long rest.', + 'color':'#8e24aa', + 'dependencies':[ + '4eM4YkgAaoCJfCfQ8', + ], + 'descriptionCalculations':[ + { + 'calculation':'warlock.level <10 ? "" :"- 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.' + } + ], + '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.' + } + ] +}; + +describe('migrateProperty', function () { + it('Migrates actions reversibly', function () { + const action = {...exampleAction}; + const newAction = migrateProperty({ + collection: CreatureProperties, + prop: action + }); + const reversedAction = migrateProperty({ + collection: CreatureProperties, + prop: newAction, + reversed: true, + }); + assert.deepEqual(action, exampleAction, 'action should not be bashed'); + assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible'); + }); +}); diff --git a/app/imports/migrations/index.js b/app/imports/migrations/server/index.js similarity index 52% rename from app/imports/migrations/index.js rename to app/imports/migrations/server/index.js index 97fcde14..615035e1 100644 --- a/app/imports/migrations/index.js +++ b/app/imports/migrations/server/index.js @@ -1,2 +1 @@ import './2.0-beta.33-dbv1.js'; -import './methods/index.js'; diff --git a/app/imports/migrations/server/transformFields.js b/app/imports/migrations/server/transformFields.js new file mode 100644 index 00000000..e99d17af --- /dev/null +++ b/app/imports/migrations/server/transformFields.js @@ -0,0 +1,112 @@ +import { get, set, unset, forEachRight, cloneDeep } from 'lodash'; + +export default function transformFields(src, transformList, reversed = false){ + // don't bash the old document during the transforms + let doc = cloneDeep(src); + for(let originalTransform of transformList){ + let transform; + // Swap to and from when reversing + if (reversed){ + transform = { + to: originalTransform.from, + from: originalTransform.to, + up: originalTransform.down, + } + } else { + transform = {...originalTransform}; + } + if (transform.from?.includes('$.')){ + transformArrayField(src, doc, transform, reversed); + } else { + transformSingleField(src, doc, transform); + } + } + return doc; +} + +function transformSingleField(src, doc, {from, to, up}){ + // Get the value in the `from` path and delete it + let value = undefined; + if (from){ + value = get(src, from); + unset(doc, from); + } + + // apply the transform function + if (up){ + value = up(value, src, doc); + } + + // Store the value in the `to` path or unset it if undefined + if (to){ + if (value === undefined){ + unset(doc, to); + } else { + set(doc, to, value); + } + } +} + +/** + * from: 'from.$.here', to: 'to.$.here' + * where from and to are an [array, of, objects] that each need to be modified + * documents at 'from.x.here' will map to 'to.x.here' + * Attempts to support 'from.$.here.$.nested' + * by mapping 'from.x.here.y.nest.z.deep' to 'to.y.nest.z.lessDeep' + * from depth must be >= to depth + */ +function transformArrayField(src, doc, {from, to, up}, reversed){ + const fromSplit = from.split('.$'); + const toSplit = to.split('.$'); + + if (toSplit.length > fromSplit.length){ + throw 'Can\'t transform array fields where "to" is deeper than "from"' + } + + // Stack based depth first traversal of arrays + const stack = [{ + array: get(src, fromSplit[0]), + paths: fromSplit.slice(1), + currentPath: fromSplit[0], + indices: [], + }]; + while(stack.length){ + const state = stack.pop(); + // Iterate forwads or backwads defpending on our migration direction + if (reversed){ + forEachRight(state.array, iterate(stack, state, src, doc, toSplit, up)); + } else { + state.array.forEach(iterate(stack, state, src, doc, toSplit, up)); + } + } +} + +function iterate(stack, state, src, doc, toSplit, up){return function(key, index){ + const currentPath = `${state.currentPath}[${index}]${state.paths[0]}` + if (state.paths.length == 1){ + transformSingleField(src, doc, { + from: currentPath, + to: buildToPath(toSplit, [...state.indices, index]), + up + }); + } else { + stack.push({ + array: get(src, currentPath), + paths: state.paths.slice(1), + currentPath, + indices: [...state.indices, index], + }); + } +}} + +function buildToPath(toSplit, indices){ + let toPath = ''; + let offset = indices.length - toSplit.length + 1; + toSplit.forEach((path, i) => { + toPath += `${path}`; + if (i < toSplit.length - 1){ + toPath += `[${indices[i + offset]}]` + } + }); + return toPath; +} diff --git a/app/imports/migrations/server/transformFields.test.js b/app/imports/migrations/server/transformFields.test.js new file mode 100644 index 00000000..b34d09e0 --- /dev/null +++ b/app/imports/migrations/server/transformFields.test.js @@ -0,0 +1,85 @@ +import transformFields from './transformFields.js'; +import { assert } from 'chai'; + +const originalDoc = { + name: 'doc name', + description: 'a document to test transforms on', + nest: { + deeper: { + field: 'some nested field' + }, + }, + array: [{num: 1}, {num: 3}, {num: 5}], + nestArray: [ + {array: [{item: 2},{item: 4},{item: 6}]}, + {array: [{item: 8},{item: 10},{item: 12}]}, + {array: [{item: 14},{item: 16},{item: 18}]}, + ], +}; + +describe('transformFields', function () { + + it('Takes a doc and transforms it according to single field rules', function () { + let doc = {...originalDoc}; + const transformList = [ + {from: 'name', to: 'title'}, + ]; + + assert.equal(doc.name, 'doc name', '.name is set'); + assert.doesNotHaveAnyKeys(doc, ['title'], '.title doesn\'t exist'); + + doc = transformFields(doc, transformList); + + assert.equal(doc.title, 'doc name', '.name -> .title'); + assert.doesNotHaveAnyKeys(doc, ['name'], '.name deleted'); + }); + + it('Takes a doc and transforms it with functions', function () { + let doc = {...originalDoc}; + const transformList = [ + {from: 'name', to: 'name', up: name => name.toUpperCase()}, + ]; + assert.equal(doc.name, 'doc name', 'name in lowercase'); + doc = transformFields(doc, transformList); + assert.equal(doc.name, 'DOC NAME', 'name in uppercase'); + }); + + it('Handles empty to and from fields', function () { + let doc = {...originalDoc}; + const transformList = [ + {to: 'created', up: () => 'from thin air'}, + {from: 'description'}, + ]; + doc = transformFields(doc, transformList); + assert.equal(doc.created, 'from thin air', 'created field success'); + assert.doesNotHaveAnyKeys(doc, ['description'], '.description deleted'); + }); + + it('Takes a nested field and transforms it into a different nested field', function () { + let doc = {...originalDoc}; + const transformList = [ + {from: 'nest.deeper', to: 'different.deep'}, + ]; + doc = transformFields(doc, transformList); + assert.equal(doc.different.deep.field, 'some nested field', 'field moved correctly'); + assert.doesNotHaveAnyKeys(doc.nest, ['deeper'], 'doc.nest.deeper deleted'); + }); + + it('Transforms arrays', function () { + let doc = {...originalDoc}; + const transformList = [ + {from: 'array.$.num', to: 'list.$.number'}, + ]; + doc = transformFields(doc, transformList); + assert.equal(doc.list[1].number, 3, 'array field moved correctly'); + }); + + it('Transforms deep arrays', function () { + let doc = {...originalDoc}; + const transformList = [ + {from: 'nestArray.$.array.$.item', to: 'nestList.$.list.$.ting'}, + ]; + doc = transformFields(doc, transformList); + assert.equal(doc.nestList[2].list[1].ting, 16, 'nested array field moved correctly'); + }); +}); diff --git a/app/package-lock.json b/app/package-lock.json index 055c293f..d38d5600 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -289,6 +289,12 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -433,6 +439,20 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, + "chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", @@ -484,6 +504,12 @@ } } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "chokidar": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", @@ -651,6 +677,15 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1106,6 +1141,12 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -2491,6 +2532,12 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -2966,9 +3013,9 @@ } }, "tar": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.6.tgz", - "integrity": "sha512-oaWyu5dQbHaYcyZCTfyPpC+VmI62/OM2RTUYavTk1MDr1cwW5Boi3baeYQKiZbY2uSQJGr+iMOzb/JFxLrft+g==", + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -3024,6 +3071,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", diff --git a/app/package.json b/app/package.json index 36bfacf1..3e39c519 100644 --- a/app/package.json +++ b/app/package.json @@ -52,6 +52,7 @@ "vuex": "^3.1.3" }, "devDependencies": { + "chai": "^4.3.4", "eslint": "^7.31.0", "eslint-plugin-vue": "^7.14.0", "eslint-plugin-vuetify": "^1.0.1", @@ -96,7 +97,8 @@ "es2020": true, "browser": true, "node": true, - "meteor": true + "meteor": true, + "mocha": true }, "rules": { "quotes": [ diff --git a/app/server/main.js b/app/server/main.js index f7ac36c6..1a70dfbe 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -6,4 +6,5 @@ import '/imports/server/publications/index.js'; import '/imports/server/cron/deleteSoftRemovedDocuments.js'; import '/imports/api/parenting/organizeMethods.js'; import '/imports/api/users/patreon/updatePatreonOnLogin.js'; -import '/imports/migrations/index.js'; +import '/imports/migrations/server/index.js'; +import '/imports/migrations/methods/index.js' From b0980d26ac6952842f5f54b2271a1e22b7ee957b Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 6 Sep 2021 23:59:52 +0200 Subject: [PATCH 06/85] Iterated on migration :( --- app/imports/api/properties/Actions.js | 36 ++++-------- app/imports/api/properties/Adjustments.js | 18 +++--- .../subSchemas/AttributeConsumedSchema.js | 19 ++++--- .../InlineCalculationFieldSchema.js | 37 ------------- .../subSchemas/ItemConsumedSchema.js | 17 +++--- ...omputedFieldSchema.js => computedField.js} | 0 .../subSchemas/createPropertySchema.js | 43 +++++++++++++++ .../subSchemas/inlineCalculationField.js | 55 +++++++++++++++++++ 8 files changed, 137 insertions(+), 88 deletions(-) delete mode 100644 app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js rename app/imports/api/properties/subSchemas/{ComputedFieldSchema.js => computedField.js} (100%) create mode 100644 app/imports/api/properties/subSchemas/createPropertySchema.js create mode 100644 app/imports/api/properties/subSchemas/inlineCalculationField.js diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index d17c3d66..9a23959f 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -1,18 +1,10 @@ import SimpleSchema from 'simpl-schema'; -import { - InlineCalculationFieldToComputeSchema, - ComputedOnlyInlineCalculationFieldSchema, - InlineCalculationFieldSchema, -} from '/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js'; -import { - fieldToCompute, - computedOnlyField, -} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import { ResourcesSchema, ResourcesComputedOnlySchema, ResourcesComputedSchema, } from '/imports/api/properties/subSchemas/ResourcesSchema.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; /* @@ -21,18 +13,18 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; * Any actions that are children of this action will be considered alternatives * to this action */ -let ActionSchema = new SimpleSchema({ +let ActionSchema = createPropertySchema({ name: { type: String, optional: true, max: STORAGE_LIMITS.name, }, summary: { - type: InlineCalculationFieldToComputeSchema, + type: 'inlineCalculationFieldToCompute', optional: true, }, description: { - type: InlineCalculationFieldToComputeSchema, + type: 'inlineCalculationFieldToCompute', optional: true, }, // What time-resource is used to take the action in combat @@ -59,7 +51,7 @@ let ActionSchema = new SimpleSchema({ }, // Calculation of how many times this action can be used uses: { - type: Object, + type: 'fieldToCompute', optional: true, }, // Integer of how many times it has already been used @@ -73,15 +65,15 @@ let ActionSchema = new SimpleSchema({ allowedValues: ['longRest', 'shortRest'], optional: true, }, -}).extend(fieldToCompute('uses')); +}); -const ComputedOnlyActionSchema = new SimpleSchema({ +const ComputedOnlyActionSchema = createPropertySchema({ summary: { - type: ComputedOnlyInlineCalculationFieldSchema, + type: 'computedOnlyInlineCalculationField', optional: true, }, description: { - type: ComputedOnlyInlineCalculationFieldSchema, + type: 'computedOnlyInlineCalculationField', optional: true, }, resources: { @@ -94,20 +86,12 @@ const ComputedOnlyActionSchema = new SimpleSchema({ type: Boolean, optional: true, }, -}).extend(computedOnlyField('uses')); +}); const ComputedActionSchema = new SimpleSchema() .extend(ActionSchema) .extend(ComputedOnlyActionSchema) .extend({ - summary: { - type: InlineCalculationFieldSchema, - optional: true, - }, - description: { - type: InlineCalculationFieldSchema, - optional: true, - }, resources: { type: ResourcesComputedSchema, defaultValue: {}, diff --git a/app/imports/api/properties/Adjustments.js b/app/imports/api/properties/Adjustments.js index 8ab97d4f..b3d69d25 100644 --- a/app/imports/api/properties/Adjustments.js +++ b/app/imports/api/properties/Adjustments.js @@ -1,15 +1,12 @@ import SimpleSchema from 'simpl-schema'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -import { - fieldToCompute, - computedOnlyField, -} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -const AdjustmentSchema = new SimpleSchema({ +const AdjustmentSchema = createPropertySchema({ // The roll that determines how much to change the attribute // This can be simplified, but should only compute when activated amount: { - type: Object, + type: 'fieldToCompute', optional: true, }, 'amount.calculation': { @@ -37,9 +34,14 @@ const AdjustmentSchema = new SimpleSchema({ allowedValues: ['set', 'increment'], defaultValue: 'increment', }, -}).extend(fieldToCompute('amount')); +}); -const ComputedOnlyAdjustmentSchema = computedOnlyField('amount'); +const ComputedOnlyAdjustmentSchema = createPropertySchema({ + amount: { + type: 'computedOnlyField', + optional: true, + }, +}); const ComputedAdjustmentSchema = new SimpleSchema() .extend(AdjustmentSchema) diff --git a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js index 0c00ade4..63f16635 100644 --- a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js @@ -1,12 +1,9 @@ import SimpleSchema from 'simpl-schema'; import { Random } from 'meteor/random'; -import { - fieldToCompute, - computedOnlyField, -} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -const AttributeConsumedSchema = new SimpleSchema({ +const AttributeConsumedSchema = createPropertySchema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -20,12 +17,12 @@ const AttributeConsumedSchema = new SimpleSchema({ max: STORAGE_LIMITS.variableName, }, quantity: { - type: Object, + type: 'fieldToCompute', optional: true, }, -}).extend(fieldToCompute('quantity')); +}); -const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({ +const ComputedOnlyAttributeConsumedSchema = createPropertySchema({ available: { type: Number, optional: true, @@ -40,7 +37,11 @@ const ComputedOnlyAttributeConsumedSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.name, }, -}).extend(computedOnlyField('quantity')); + quantity: { + type: 'computedOnlyField', + optional: true, + }, +}); const ComputedAttributeConsumedSchema = new SimpleSchema() .extend(AttributeConsumedSchema) diff --git a/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js b/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js deleted file mode 100644 index 085347b1..00000000 --- a/app/imports/api/properties/subSchemas/InlineCalculationFieldSchema.js +++ /dev/null @@ -1,37 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -const InlineCalculationFieldToComputeSchema = new SimpleSchema({ - text: { - type: String, - optional: true, - max: STORAGE_LIMITS.inlineCalculationField, - }, -}); - -const ComputedOnlyInlineCalculationFieldSchema = new SimpleSchema({ - 'inlineCalculations': { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, - }, - 'inlineCalculations.$': { - type: InlineComputationSchema, - }, - value: { - type: String, - optional: true, - max: STORAGE_LIMITS.inlineCalculationField, - }, -}); - -const InlineCalculationFieldSchema = new SimpleSchema() - .extend(InlineCalculationFieldToComputeSchema) - .extend(ComputedOnlyInlineCalculationFieldSchema) - -export { - InlineCalculationFieldToComputeSchema, - ComputedOnlyInlineCalculationFieldSchema, - InlineCalculationFieldSchema, -}; diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index 762ed985..42eeb402 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -1,13 +1,10 @@ import SimpleSchema from 'simpl-schema'; import { Random } from 'meteor/random'; -import { - fieldToCompute, - computedOnlyField, -} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -const ItemConsumedSchema = new SimpleSchema({ +const ItemConsumedSchema = createPropertySchema({ _id: { type: String, regEx: SimpleSchema.RegEx.Id, @@ -20,7 +17,7 @@ const ItemConsumedSchema = new SimpleSchema({ optional: true, }, quantity: { - type: Object, + type: 'fieldToCompute', optional: true, }, itemId: { @@ -28,13 +25,17 @@ const ItemConsumedSchema = new SimpleSchema({ regEx: SimpleSchema.RegEx.Id, optional: true, }, -}).extend(fieldToCompute('quantity')); +}); const ComputedOnlyItemConsumedSchema = new SimpleSchema({ available: { type: Number, optional: true, }, + quantity: { + type: 'computedOnlyField', + optional: true, + }, // This appears both in the computed and uncomputed schema because it can be // set by both a computation or a form itemId: { @@ -57,7 +58,7 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.color, }, -}).extend(computedOnlyField('quantity')); +}); const ComputedItemConsumedSchema = new SimpleSchema() .extend(ItemConsumedSchema) diff --git a/app/imports/api/properties/subSchemas/ComputedFieldSchema.js b/app/imports/api/properties/subSchemas/computedField.js similarity index 100% rename from app/imports/api/properties/subSchemas/ComputedFieldSchema.js rename to app/imports/api/properties/subSchemas/computedField.js diff --git a/app/imports/api/properties/subSchemas/createPropertySchema.js b/app/imports/api/properties/subSchemas/createPropertySchema.js new file mode 100644 index 00000000..36c5d5e4 --- /dev/null +++ b/app/imports/api/properties/subSchemas/createPropertySchema.js @@ -0,0 +1,43 @@ +import { + inlineCalculationFieldToCompute, + computedOnlyInlineCalculationField, +} from '/imports/api/properties/subSchemas/inlineCalculationField.js'; +import { + fieldToCompute, + computedOnlyField, +} from '/imports/api/properties/subSchemas/computedField.js'; +import SimpleSchema from 'simpl-schema'; + +export default function createPropertySchema(definition){ + const computationFields = { + inlineCalculationFieldToCompute: [], + computedOnlyInlineCalculationField: [], + fieldToCompute: [], + computedOnlyField: [], + }; + const computedKeys = Object.keys(computationFields); + + for (let key in definition){ + const def = definition[key]; + if (computedKeys.includes(def.type)){ + computationFields[def.type].push(key); + def.type = Object; + } + } + + const schema = new SimpleSchema(definition); + + computationFields.inlineCalculationFieldToCompute.forEach(key => { + schema.extend(inlineCalculationFieldToCompute(key)) + }); + computationFields.computedOnlyInlineCalculationField.forEach(key => { + schema.extend(computedOnlyInlineCalculationField(key)) + }); + computationFields.fieldToCompute.forEach(key => { + schema.extend(fieldToCompute(key)) + }); + computationFields.computedOnlyField.forEach(key => { + schema.extend(computedOnlyField(key)) + }); + return schema +} diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.js new file mode 100644 index 00000000..93bfbf23 --- /dev/null +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.js @@ -0,0 +1,55 @@ +import SimpleSchema from 'simpl-schema'; +import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +// Get schemas that apply fields directly so they can be gracefully extended +// because {type: Schema} fields can't be extended +function inlineCalculationFieldToCompute(field){ + return new SimpleSchema({ + // The object should already be set, but set again just in case + [field]: { + type: Object, + optional: true, + }, + [`${field}.text`]: { + type: String, + optional: true, + max: STORAGE_LIMITS.inlineCalculationField, + }, + }); +} + +function computedOnlyInlineCalculationField(field){ + return new SimpleSchema({ + // The object should already be set, but set again just in case + [field]: { + type: Object, + optional: true, + }, + [`${field}.inlineCalculations`]: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.inlineCalculationCount, + }, + [`${field}.inlineCalculations.$`]: { + type: InlineComputationSchema, + }, + [`${field}.value`]: { + type: String, + optional: true, + max: STORAGE_LIMITS.inlineCalculationField, + }, + }); +} + +function computedInlineCalculationField(field){ + return inlineCalculationFieldToCompute(field).extend( + computedOnlyInlineCalculationField(field) + ) +} + +export { + inlineCalculationFieldToCompute, + computedOnlyInlineCalculationField, + computedInlineCalculationField, +}; From 43f056ae95632689125df1d46ceace414b696949 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 7 Sep 2021 15:48:51 +0200 Subject: [PATCH 07/85] more small progress on migration --- app/imports/api/properties/Actions.js | 4 ++ app/imports/api/properties/Attacks.js | 18 +++--- app/imports/api/properties/Attributes.js | 59 ++++++------------- .../server/2.0-beta.33-dbv1.test.js | 2 +- app/imports/ui/pages/Home.vue | 2 +- 5 files changed, 34 insertions(+), 51 deletions(-) diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 9a23959f..897d2139 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -86,6 +86,10 @@ const ComputedOnlyActionSchema = createPropertySchema({ type: Boolean, optional: true, }, + uses: { + type: 'computedOnlyField', + optional: true, + }, }); const ComputedActionSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Attacks.js b/app/imports/api/properties/Attacks.js index e10677b8..b500bb6c 100644 --- a/app/imports/api/properties/Attacks.js +++ b/app/imports/api/properties/Attacks.js @@ -1,18 +1,15 @@ import SimpleSchema from 'simpl-schema'; import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; -import { - fieldToCompute, - computedOnlyField, -} from '/imports/api/properties/subSchemas/ComputedFieldSchema.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Attacks are special instances of actions let AttackSchema = new SimpleSchema() .extend(ActionSchema) - .extend({ + .extend(createPropertySchema({ // What gets added to the d20 roll rollBonus: { - type: Object, + type: 'fieldToCompute', optional: true, }, 'rollBonus.calculation': { @@ -34,11 +31,16 @@ let AttackSchema = new SimpleSchema() type: String, max: STORAGE_LIMITS.tagLength, }, - }).extend(fieldToCompute('rollBonus')); + })); const ComputedOnlyAttackSchema = new SimpleSchema() .extend(ComputedOnlyActionSchema) - .extend(computedOnlyField('rollBonus')); + .extend(createPropertySchema({ + rollBonus: { + type: 'computedOnlyField', + optional: true, + }, + })); const ComputedAttackSchema = new SimpleSchema() .extend(AttackSchema) diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index b5c1c605..ddaa975e 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -1,13 +1,12 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; /* * Attributes are numbered stats of a character */ -let AttributeSchema = new SimpleSchema({ +let AttributeSchema = createPropertySchema({ name: { type: String, defaultValue: 'New Attribute', @@ -41,24 +40,22 @@ let AttributeSchema = new SimpleSchema({ // For type hitDice, the size needs to be stored separately hitDiceSize: { type: String, - allowedValues: ['d4', 'd6', 'd8', 'd10', 'd12', 'd20'], + allowedValues: ['d1', 'd2', 'd4', 'd6', 'd8', 'd10', 'd12', 'd20', 'd100'], optional: true, }, // For type spellSlot, the level needs to be stored separately - spellSlotLevelCalculation: { - type: String, + spellSlotLevel: { + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.calculation, }, // The starting value, before effects - baseValueCalculation: { - type: String, + baseValue: { + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.calculation, }, // Description of what the attribute is used for description: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, max: STORAGE_LIMITS.description, }, @@ -81,47 +78,27 @@ let AttributeSchema = new SimpleSchema({ }, }); -let ComputedOnlyAttributeSchema = new SimpleSchema({ - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, +let ComputedOnlyAttributeSchema = createPropertySchema({ + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, }, - 'descriptionCalculations.$': InlineComputationSchema, - // The result of baseValueCalculation baseValue: { - type: SimpleSchema.oneOf(Number, String, Boolean), + type: 'computedOnlyField', optional: true, }, - baseValueErrors: { - type: Array, - optional: true, - }, - 'baseValueErrors.$': { - type: ErrorSchema, - maxCount: STORAGE_LIMITS.errorCount, - }, - // The result of spellSlotLevelCalculation - spellSlotLevelValue: { - type: SimpleSchema.oneOf(Number, String, Boolean), - optional: true, - }, - spellSlotLevelErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'spellSlotLevelErrors.$': { - type: ErrorSchema, + spellSlotLevel: { + type: 'computedOnlyField', + optional: true, }, // The computed value of the attribute - value: { + total: { type: SimpleSchema.oneOf(Number, String, Boolean), defaultValue: 0, optional: true, }, // The computed value of the attribute minus the damage - currentValue: { + value: { type: SimpleSchema.oneOf(Number, String, Boolean), defaultValue: 0, optional: true, diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.test.js b/app/imports/migrations/server/2.0-beta.33-dbv1.test.js index fd79387c..d74c1896 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.test.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.test.js @@ -11,7 +11,7 @@ const exampleAction = { 'itemsConsumed':[], 'attributesConsumed':[{ '_id':'FaK6jXEj3pSe7mNuu', - 'quantity':1, + 'quantity': '1', 'variableName':'HunterTech', 'statId':'qccf9j5tfNJjZ3GGn', 'statName':'Hunter\'s Technique', diff --git a/app/imports/ui/pages/Home.vue b/app/imports/ui/pages/Home.vue index 1b638bc8..171ade01 100644 --- a/app/imports/ui/pages/Home.vue +++ b/app/imports/ui/pages/Home.vue @@ -94,7 +94,7 @@ > From 06da15c44a545135f4f6af72c4b8d1fcd466e03a Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 8 Sep 2021 17:23:00 +0200 Subject: [PATCH 08/85] Began rebuilding computation engine to be dependency graph centric --- app/client/main.js | 1 + .../computation/newEngine/applyFnToKey.js | 46 +++++ .../computation/newEngine/computeCreature.js | 174 ++++++++++++++++++ .../newEngine/findAncestorByType.js | 10 + .../creatureProperties/CreatureProperties.js | 6 + app/imports/api/properties/Adjustments.js | 5 +- app/imports/api/properties/Attacks.js | 5 +- app/imports/api/properties/Attributes.js | 3 +- app/imports/api/properties/Buffs.js | 30 +-- app/imports/api/properties/ClassLevels.js | 19 +- app/imports/api/properties/Containers.js | 20 +- app/imports/api/properties/Damages.js | 22 +-- app/imports/api/properties/Effects.js | 25 +-- app/imports/api/properties/Features.js | 31 ++-- app/imports/api/properties/Items.js | 19 +- app/imports/api/properties/Notes.js | 35 ++-- app/imports/api/properties/Rolls.js | 23 +-- app/imports/api/properties/SavingThrows.js | 21 +-- app/imports/api/properties/Skills.js | 34 ++-- app/imports/api/properties/Slots.js | 43 ++--- app/imports/api/properties/SpellLists.js | 79 +++----- app/imports/api/properties/Toggles.js | 23 +-- .../subSchemas/createPropertySchema.js | 41 +++++ app/imports/api/simpleSchemaConfig.js | 3 + .../migrations/server/transformFields.js | 14 +- app/package-lock.json | 13 ++ app/package.json | 1 + app/server/main.js | 1 + 28 files changed, 451 insertions(+), 296 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/applyFnToKey.js create mode 100644 app/imports/api/creature/computation/newEngine/computeCreature.js create mode 100644 app/imports/api/creature/computation/newEngine/findAncestorByType.js create mode 100644 app/imports/api/simpleSchemaConfig.js diff --git a/app/client/main.js b/app/client/main.js index 4836e4d2..b036a39f 100644 --- a/app/client/main.js +++ b/app/client/main.js @@ -1,3 +1,4 @@ +import '/imports/api/simpleSchemaConfig.js'; import '/imports/ui/vueSetup.js'; import '/imports/ui/styles/stylesIndex.js'; import '/imports/client/config.js'; diff --git a/app/imports/api/creature/computation/newEngine/applyFnToKey.js b/app/imports/api/creature/computation/newEngine/applyFnToKey.js new file mode 100644 index 00000000..3e3780b4 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/applyFnToKey.js @@ -0,0 +1,46 @@ +import { get } from 'lodash'; + +export function applyFnToKey(doc, key, fn){ + if (key.includes('$.')){ + applyToArrayKey(doc, key, fn); + } else { + applyToSingleKey(doc, key, fn); + } +} + +function applyToSingleKey(doc, key, fn){ + // call the function with the current value and document for context + fn(doc, key); +} + +/** + * Applies the given function to all instances in a document key + * key.$.with.$.subdocs will apply to all key[i...n].with[j...m].subdocs + */ +function applyToArrayKey(doc, key, fn){ + const keySplit = key.split('.$'); + + // Stack based depth first traversal of arrays + const stack = [{ + array: get(doc, keySplit[0]), + paths: keySplit.slice(1), + currentPath: keySplit[0], + indices: [], + }]; + while(stack.length){ + const state = stack.pop(); + for (let index in state.array.length){ + const currentPath = `${state.currentPath}[${index}]${state.paths[0]}` + if (state.paths.length == 1){ + applyToSingleKey(doc, currentPath, fn); + } else { + stack.push({ + array: get(doc, currentPath), + paths: state.paths.slice(1), + currentPath, + indices: [...state.indices, index], + }); + } + } + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeCreature.js b/app/imports/api/creature/computation/newEngine/computeCreature.js new file mode 100644 index 00000000..8d4567a3 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeCreature.js @@ -0,0 +1,174 @@ +import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import CreatureProperties, + { DenormalisedOnlyCreaturePropertySchema as denormSchema } + from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; +import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; +import applyFnToKey from '/imports/api/creature/computation/newEngine/applyFnToKey.js'; +import { cloneDeep, unset } from 'lodash'; +import { prettifyParseError, parse } from '/imports/parser/parser.js'; +import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; +import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; +import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; +import createGraph from 'ngraph.graph'; +import findAncestorByType from 'imports/api/creature/computation/newEngine/findAncestorByType.js'; + +/** + * Store index of properties + * recompute static tree-based enabled/disabled status + * Build a dependency graph + * id -> id dependencies for docs that rely on other docs directly + * id -> variable deps for docs that rely on a variable's value + * TODO: + * variable -> id deps for variables that are impacted by docs + * Depth first traversal or dependency graph to: + * Find loops in the dependency graph + * resolve variables in dependency order + */ + +export default function computeCreature(creatureId){ + let properties = CreatureProperties.find({ + 'ancestors.id': creatureId, + 'removed': {$ne: true}, + }, { + sort: {order: 1} + }); + + const originalPropsById = {}; + const propsById = {}; + const propsByType = {}; + + // Process the properties one by one + properties.forEach(prop => { + // Store the prop by Id and Type + originalPropsById[prop._id] = cloneDeep(prop); + propsById[prop._id] = prop; + if (!propsByType[prop.type]) propsByType[prop.type] = []; + propsByType[prop.type].push(prop); + + // Store the prop in the dependency graph + dependencyGraph.addNode(prop._id, prop); + + // Remove all computed only fields + computedOnlySchemas[prop.type]._schemaKeys.forEach(key => + applyFnToKey(prop, key, unset) + ); + + // Remove all denormalised fields + denormSchema._schemaKeys.forEach(key => + applyFnToKey(prop, key, unset) + ); + + // Add a place to store all the computation details + prop._computationDetails = { + calculations: [], + toggleAncestors: [], + }; + + // parse every calculation field + computedSchemas[prop.type]._schemaKeys.forEach( key => { + if (key.slice(-11) !== 'calculation') return; + const calcKey = key.sclice(0, -11); + applyFnToKey(prop, calcKey, calcObj => { + // Store a reference to all the calculations + prop._computationDetails.calculations.push(calcObj); + // Parse the calculation + parseCalculation(calcObj); + return calcObj; + }); + }); + }); + + // Dependency graph where edge(a, b) means a depends on b + const dependencyGraph = createGraph(); + // Build graph now that all props are stored + properties.forEach(prop => { + linkDependencies(dependencyGraph, prop, propsById); + }); + + // Process the properties in tree format + let creatureTree = nodeArrayToTree(properties); + walkDown(creatureTree, node => { + denormaliseInactiveStatus(node); + inheritToggleDependencies(node); + }); +} + +function walkDown(tree, callback){ + let stack = [...tree]; + while(stack.length){ + let node = stack.pop(); + callback(node); + stack.push(...node.children); + } +} + +function denormaliseInactiveStatus(node){ + const prop = node.node; + if (isActive(prop)) return; + prop.inactive = true; + prop.deactivatedBySelf = true; + // Mark children as inactive due to ancestor + walkDown(node.children, child => { + child.node.inactive = true; + child.node.deactivatedByAncestor = true; + }); +} + +function isActive(prop){ + if (prop.disabled) return false; + switch (prop.type){ + case 'buff': return !!prop.applied; + case 'item': return !!prop.equipped; + case 'spell': return !!prop.prepared || !!prop.alwaysPrepared; + default: return true; + } +} + +function inheritToggleDependencies(node, dependencyGraph){ + 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; + walkDown(node.children, child => { + child.node._computationDetails.toggleAncestors.push(prop._id); + dependencyGraph.addLink(child.node._id, prop._id, prop.condition); + }); +} + +function parseCalculation(calcObj){ + let calculation = calcObj.calculation || ''; + try { + calcObj._parsedCalculation = parse(calculation); + } catch (e) { + let error = prettifyParseError(e); + calcObj.errors ? + calcObj.errors.push(error) : + calcObj.errors = [error]; + calcObj._parsedCalculation = new ErrorNode({error}); + } +} + +function linkDependencies(dependencyGraph, prop, propsById){ + let variableNames = []; + prop._computationDetails.calculations.forEach(calcObj => { + calcObj._parsedCalculation.travese(node => { + if (node instanceof SymbolNode || node instanceof AccessorNode){ + if (node.name[0] !== '#'){ + dependencyGraph.addLink(prop._id, node.name, calcObj); + } else { + let ancestorProp = findAncestorByType( + prop, node.name.slice(1), propsById + ); + if (!ancestorProp) return; + dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); + } + } + }); + }); + return variableNames; +} diff --git a/app/imports/api/creature/computation/newEngine/findAncestorByType.js b/app/imports/api/creature/computation/newEngine/findAncestorByType.js new file mode 100644 index 00000000..492cc6d0 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/findAncestorByType.js @@ -0,0 +1,10 @@ +export default function findAncestorByType(prop, type, propsById){ + if (!prop || !prop.ancestors) return; + let ancestor; + for (let i = prop.ancestors.length - 1; i >= 0; i--){ + ancestor = propsById[prop.ancestors[i].id]; + if (ancestor && ancestor.type === type){ + return ancestor; + } + } +} diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index fe1f51a6..aff521f4 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -46,6 +46,9 @@ let CreaturePropertySchema = new SimpleSchema({ regEx: SimpleSchema.RegEx.Id, optional: true, }, +}); + +const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ // Denormalised flag if this property is inactive on the sheet for any reason // Including being disabled, or a decendent of a disabled property inactive: { @@ -86,6 +89,8 @@ let CreaturePropertySchema = new SimpleSchema({ }, }); +CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); + for (let key in propertySchemasIndex){ let schema = new SimpleSchema({}); schema.extend(propertySchemasIndex[key]); @@ -104,5 +109,6 @@ import '/imports/api/creature/actions/castSpellWithSlot.js'; export default CreatureProperties; export { + DenormalisedOnlyCreaturePropertySchema, CreaturePropertySchema, }; diff --git a/app/imports/api/properties/Adjustments.js b/app/imports/api/properties/Adjustments.js index b3d69d25..b4825de0 100644 --- a/app/imports/api/properties/Adjustments.js +++ b/app/imports/api/properties/Adjustments.js @@ -7,10 +7,8 @@ const AdjustmentSchema = createPropertySchema({ // This can be simplified, but should only compute when activated amount: { type: 'fieldToCompute', + parseLevel: 'compile', optional: true, - }, - 'amount.calculation': { - type: String, defaultValue: 1, }, // Who this adjustment applies to @@ -39,6 +37,7 @@ const AdjustmentSchema = createPropertySchema({ const ComputedOnlyAdjustmentSchema = createPropertySchema({ amount: { type: 'computedOnlyField', + parseLevel: 'compile', optional: true, }, }); diff --git a/app/imports/api/properties/Attacks.js b/app/imports/api/properties/Attacks.js index b500bb6c..c0e947d5 100644 --- a/app/imports/api/properties/Attacks.js +++ b/app/imports/api/properties/Attacks.js @@ -10,10 +10,8 @@ let AttackSchema = new SimpleSchema() // What gets added to the d20 roll rollBonus: { type: 'fieldToCompute', + parseLevel: 'compile', optional: true, - }, - 'rollBonus.calculation': { - type: String, defaultValue: 'strength.modifier + proficiencyBonus', }, // Set better defaults for the action @@ -38,6 +36,7 @@ const ComputedOnlyAttackSchema = new SimpleSchema() .extend(createPropertySchema({ rollBonus: { type: 'computedOnlyField', + parseLevel: 'compile', optional: true, }, })); diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index ddaa975e..1913b8ee 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -57,7 +57,6 @@ let AttributeSchema = createPropertySchema({ description: { type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, // The damage done to the attribute, always positive damage: { @@ -108,7 +107,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ type: SimpleSchema.Integer, optional: true, }, - // The computed creature constitution modifier + // The computed creature constitution modifier for hit dice constitutionMod: { type: Number, optional: true, diff --git a/app/imports/api/properties/Buffs.js b/app/imports/api/properties/Buffs.js index 5ed2913b..50b334cd 100644 --- a/app/imports/api/properties/Buffs.js +++ b/app/imports/api/properties/Buffs.js @@ -1,22 +1,21 @@ import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -let BuffSchema = new SimpleSchema({ - name: { +let BuffSchema = createPropertySchema({ + name: { type: String, optional: true, max: STORAGE_LIMITS.name, }, description: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, + // How many rounds this buff lasts duration: { - type: String, + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.name, }, applied: { type: Boolean, @@ -34,13 +33,16 @@ let BuffSchema = new SimpleSchema({ }, }); -let ComputedOnlyBuffSchema = new SimpleSchema({ - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, - }, - 'descriptionCalculations.$': InlineComputationSchema, +let ComputedOnlyBuffSchema = createPropertySchema({ + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + max: STORAGE_LIMITS.description, + }, + duration: { + type: 'computedOnlyField', + optional: true, + }, durationSpent: { type: Number, optional: true, diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index c83eef24..37224f12 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -1,19 +1,19 @@ 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'; -let ClassLevelSchema = new SimpleSchema({ +let ClassLevelSchema = createPropertySchema({ name: { type: String, optional: true, max: STORAGE_LIMITS.name, }, // Only used by slot filling dialog, not computed - description: { - type: String, - optional: true, - max: STORAGE_LIMITS.description, - }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, // The name of this class level's variable variableName: { type: String, @@ -25,13 +25,6 @@ let ClassLevelSchema = new SimpleSchema({ type: SimpleSchema.Integer, defaultValue: 1, }, - nextLevelTags: { - type: Array, - defaultValue: [], - }, - 'nextLevelTags.$': { - type: String, - }, // Same as in SlotFillers.js slotFillerCondition: { type: String, diff --git a/app/imports/api/properties/Containers.js b/app/imports/api/properties/Containers.js index d7911237..c48b521d 100644 --- a/app/imports/api/properties/Containers.js +++ b/app/imports/api/properties/Containers.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -let ContainerSchema = new SimpleSchema({ +let ContainerSchema = createPropertySchema({ name: { type: String, optional: true, @@ -29,20 +29,16 @@ let ContainerSchema = new SimpleSchema({ optional: true, }, description: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, - trim: false, - max: STORAGE_LIMITS.description, }, }); -const ComputedOnlyContainerSchema = new SimpleSchema({ - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, - }, - 'descriptionCalculations.$': InlineComputationSchema, +const ComputedOnlyContainerSchema = createPropertySchema({ + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, // Weight of all the contents, zero if `contentsWeightless` is true contentsWeight:{ type: Number, diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js index 25802861..007d97f5 100644 --- a/app/imports/api/properties/Damages.js +++ b/app/imports/api/properties/Damages.js @@ -1,16 +1,14 @@ import SimpleSchema from 'simpl-schema'; import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -const DamageSchema = new SimpleSchema({ +const DamageSchema = createPropertySchema({ // The roll that determines how much to damage the attribute // This can be simplified, but only computed when applied amount: { - type: String, + type: 'fieldToCompute', optional: true, defaultValue: '1d8 + strength.modifier', - max: STORAGE_LIMITS.calculation, }, // Who this damage applies to target: { @@ -29,19 +27,11 @@ const DamageSchema = new SimpleSchema({ }, }); -const ComputedOnlyDamageSchema = new SimpleSchema({ - amountResult: { - type: SimpleSchema.oneOf(String, Number), +const ComputedOnlyDamageSchema = createPropertySchema({ + amount: { + type: 'computedOnlyField', optional: true, }, - amountErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'amountErrors.$':{ - type: ErrorSchema, - }, }); const ComputedDamageSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index 2f782533..8731a591 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -1,12 +1,12 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; /* * Effects are reason-value attached to skills and abilities * that modify their final value or presentation in some way */ -let EffectSchema = new SimpleSchema({ +let EffectSchema = createPropertySchema({ name: { type: String, optional: true, @@ -30,10 +30,9 @@ let EffectSchema = new SimpleSchema({ 'rollBonus', ], }, - calculation: { - type: String, + amount: { + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.calculation, }, //which stats the effect is applied to stats: { @@ -47,20 +46,10 @@ let EffectSchema = new SimpleSchema({ }, }); -const ComputedOnlyEffectSchema = new SimpleSchema({ - // The computed result of the effect - result: { - type: SimpleSchema.oneOf(Number, String, Boolean), - optional: true, - }, - // The errors encountered while computing the result - errors: { - type: Array, +const ComputedOnlyEffectSchema = createPropertySchema({ + amount: { + type: 'computedOnlyField', optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'errors.$':{ - type: ErrorSchema, }, }); diff --git a/app/imports/api/properties/Features.js b/app/imports/api/properties/Features.js index c6b57d2f..9421c965 100644 --- a/app/imports/api/properties/Features.js +++ b/app/imports/api/properties/Features.js @@ -1,40 +1,31 @@ import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -let FeatureSchema = new SimpleSchema({ +let FeatureSchema = createPropertySchema({ name: { type: String, max: STORAGE_LIMITS.name, }, summary: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.summary, }, description: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, }); -let ComputedOnlyFeatureSchema = new SimpleSchema({ - - summaryCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, +let ComputedOnlyFeatureSchema = createPropertySchema({ + summary: { + type: 'computedOnlyInlineCalculationField', + optional: true, }, - 'summaryCalculations.$': InlineComputationSchema, - - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, }, - 'descriptionCalculations.$': InlineComputationSchema, - }); const ComputedFeatureSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Items.js b/app/imports/api/properties/Items.js index ba1ac8fa..373a6054 100644 --- a/app/imports/api/properties/Items.js +++ b/app/imports/api/properties/Items.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -const ItemSchema = new SimpleSchema({ +const ItemSchema = createPropertySchema({ name: { type: String, optional: true, @@ -14,10 +14,9 @@ const ItemSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.name, }, - description: { - type: String, + description: { + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, // Number currently held quantity: { @@ -58,13 +57,11 @@ const ItemSchema = new SimpleSchema({ }, }); -let ComputedOnlyItemSchema = new SimpleSchema({ - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, +let ComputedOnlyItemSchema = createPropertySchema({ + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, }, - 'descriptionCalculations.$': InlineComputationSchema, }); const ComputedItemSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Notes.js b/app/imports/api/properties/Notes.js index 3017f930..dd76d906 100644 --- a/app/imports/api/properties/Notes.js +++ b/app/imports/api/properties/Notes.js @@ -1,41 +1,32 @@ import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -let NoteSchema = new SimpleSchema({ +let NoteSchema = createPropertySchema({ name: { type: String, optional: true, max: STORAGE_LIMITS.name, }, - summary: { - type: String, + summary: { + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.summary, }, - description: { - type: String, + description: { + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, }); -let ComputedOnlyNoteSchema = new SimpleSchema({ - - summaryCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, +let ComputedOnlyNoteSchema = createPropertySchema({ + summary: { + type: 'computedOnlyInlineCalculationField', + optional: true, }, - 'summaryCalculations.$': InlineComputationSchema, - - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, }, - 'descriptionCalculations.$': InlineComputationSchema, - }); const ComputedNoteSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Rolls.js b/app/imports/api/properties/Rolls.js index bcf9799e..5acc0ce7 100644 --- a/app/imports/api/properties/Rolls.js +++ b/app/imports/api/properties/Rolls.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; 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'; /** * Rolls are children to actions or other rolls, they are triggered with 0 or @@ -21,7 +21,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; * If the roll fails to meet or exceed the target number, the adjustments and * child rolls are applied */ -let RollSchema = new SimpleSchema({ +let RollSchema = createPropertySchema({ name: { type: String, defaultValue: 'New Roll', @@ -37,25 +37,18 @@ let RollSchema = new SimpleSchema({ }, // The roll, can be simplified, but only computed in context roll: { - type: String, + type: 'fieldToCompute', + parseLevel: 'compile', optional: true, - max: STORAGE_LIMITS.calculation, }, }); -let ComputedOnlyRollSchema = new SimpleSchema({ - rollResult: { - type: SimpleSchema.Integer, +let ComputedOnlyRollSchema = createPropertySchema({ + roll: { + type: 'computedOnlyField', + parseLevel: 'compile', optional: true, }, - rollErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'rollErrors.$':{ - type: ErrorSchema, - }, }); const ComputedRollSchema = new SimpleSchema() diff --git a/app/imports/api/properties/SavingThrows.js b/app/imports/api/properties/SavingThrows.js index bc640bde..23a7bbf2 100644 --- a/app/imports/api/properties/SavingThrows.js +++ b/app/imports/api/properties/SavingThrows.js @@ -1,10 +1,10 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; // These are the rolls made when saves are called for // For the saving throw bonus or proficiency, see ./Skills.js -let SavingThrowSchema = new SimpleSchema ({ +let SavingThrowSchema = createPropertySchema({ name: { type: String, optional: true, @@ -12,9 +12,8 @@ let SavingThrowSchema = new SimpleSchema ({ }, // The computed DC dc: { - type: String, + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.calculation, }, // Who this saving throw applies to target: { @@ -34,19 +33,11 @@ let SavingThrowSchema = new SimpleSchema ({ }, }); -const ComputedOnlySavingThrowSchema = new SimpleSchema({ - dcResult: { - type: Number, +const ComputedOnlySavingThrowSchema = createPropertySchema({ + dc: { + type: 'computedOnlyField', optional: true, }, - dcErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'dcErrors.$':{ - type: ErrorSchema, - }, }); const ComputedSavingThrowSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index bdd407eb..ef9bc852 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -1,13 +1,13 @@ import SimpleSchema from 'simpl-schema'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; /* * Skills are anything that results in a modifier to be added to a D20 * Skills have an ability score modifier that they use as their basis */ -let SkillSchema = new SimpleSchema({ +let SkillSchema = createPropertySchema({ name: { type: String, optional: true, @@ -42,26 +42,24 @@ let SkillSchema = new SimpleSchema({ ], defaultValue: 'skill', }, - // The starting value, before effects - baseValueCalculation: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, // The base proficiency of this skill baseProficiency: { type: Number, optional: true, }, + // The starting value, before effects + baseValue: { + type: 'fieldToCompute', + optional: true, + }, // Description of what the skill is used for description: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, }); -let ComputedOnlySkillSchema = new SimpleSchema({ +let ComputedOnlySkillSchema = createPropertySchema({ // Computed value of skill to be added to skill rolls value: { type: Number, @@ -69,17 +67,13 @@ let ComputedOnlySkillSchema = new SimpleSchema({ }, // The result of baseValueCalculation baseValue: { - type: SimpleSchema.oneOf(Number, String, Boolean), + type: 'computedOnlyField', optional: true, }, - baseValueErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'baseValueErrors.$': { - type: ErrorSchema, - }, + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, // Computed value added by the ability abilityMod: { type: SimpleSchema.Integer, diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 7804ab75..1d388969 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -1,17 +1,16 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -let SlotSchema = new SimpleSchema({ +let SlotSchema = createPropertySchema({ name: { type: String, optional: true, max: STORAGE_LIMITS.name, }, description: { - type: String, + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, slotType: { type: String, @@ -57,19 +56,17 @@ let SlotSchema = new SimpleSchema({ max: STORAGE_LIMITS.tagLength, }, quantityExpected: { - type: String, + type: 'fieldToCompute', optional: true, defaultValue: '1', - max: STORAGE_LIMITS.calculation, }, ignored: { type: Boolean, optional: true, }, slotCondition: { - type: String, + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.calculation, }, hideWhenFull: { type: Boolean, @@ -89,33 +86,15 @@ let SlotSchema = new SimpleSchema({ }, }); -const ComputedOnlySlotSchema = new SimpleSchema({ - // Condition calculation results - slotConditionResult: { - type: SimpleSchema.oneOf(Number, String, Boolean), - optional: true, - }, - slotConditionErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'slotConditionErrors.$':{ - type: ErrorSchema, - }, - - // Quantity Expected calculation results - quantityExpectedResult: { - type: SimpleSchema.Integer, +const ComputedOnlySlotSchema = createPropertySchema({ + // Computed fields + quantityExpected: { + type: 'computedOnlyField', optional: true, }, - quantityExpectedErrors: { - type: Array, + slotCondition: { + type: 'computedOnlyField', optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'quantityExpectedErrors.$':{ - type: ErrorSchema, }, // Denormalised fields diff --git a/app/imports/api/properties/SpellLists.js b/app/imports/api/properties/SpellLists.js index bcc8a608..a0fe4e59 100644 --- a/app/imports/api/properties/SpellLists.js +++ b/app/imports/api/properties/SpellLists.js @@ -1,87 +1,50 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -let SpellListSchema = new SimpleSchema({ +let SpellListSchema = createPropertySchema({ name: { type: String, optional: true, max: STORAGE_LIMITS.name, }, - description: { - type: String, + description: { + type: 'inlineCalculationFieldToCompute', optional: true, - max: STORAGE_LIMITS.description, }, // Calculation of how many spells in this list can be prepared maxPrepared: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, + type: 'fieldToCompute', + optional: true, + }, // Calculation of The attack roll bonus used by spell attacks in this list attackRollBonus: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, + type: 'fieldToCompute', + optional: true, + }, // Calculation of the save dc used by spells in this list dc: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, - }, + type: 'fieldToCompute', + optional: true, + }, }); const ComputedOnlySpellListSchema = new SimpleSchema({ - descriptionCalculations: { - type: Array, - defaultValue: [], - maxCount: STORAGE_LIMITS.inlineCalculationCount, - }, - 'descriptionCalculations.$': InlineComputationSchema, - - // maxPrepared - maxPreparedResult: { - type: Number, + description: { + type: 'computedOnlyInlineCalculationField', optional: true, }, - maxPreparedErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'maxPreparedErrors.$':{ - type: ErrorSchema, - }, - - // attackRollBonus - attackRollBonusResult: { - type: Number, + maxPrepared: { + type: 'fieldToCompute', optional: true, }, - attackRollBonusErrors: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'attackRollBonusErrors.$':{ - type: ErrorSchema, - }, - - // dc - dcResult: { - type: Number, + attackRollBonus: { + type: 'fieldToCompute', optional: true, }, - dcErrors: { - type: Array, + dc: { + type: 'fieldToCompute', optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'dcErrors.$':{ - type: ErrorSchema, }, }); diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index b63a6ff9..ea6d509e 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -1,8 +1,8 @@ import SimpleSchema from 'simpl-schema'; -import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -const ToggleSchema = new SimpleSchema({ +const ToggleSchema = createPropertySchema({ name: { type: String, optional: true, @@ -19,26 +19,15 @@ const ToggleSchema = new SimpleSchema({ // if neither disabled or enabled, the condition will be run to determine // if the children of the toggle should be active condition: { - type: String, + type: 'fieldToCompute', optional: true, - max: STORAGE_LIMITS.calculation, }, }); -const ComputedOnlyToggleSchema = new SimpleSchema({ - // The computed result of the effect - toggleResult: { - type: Boolean, - optional: true, - }, - // The errors encountered while computing the result - errors: { - type: Array, +const ComputedOnlyToggleSchema = createPropertySchema({ + condition: { + type: 'computedOnlyField', optional: true, - maxCount: STORAGE_LIMITS.errorCount, - }, - 'errors.$': { - type: ErrorSchema, }, }); diff --git a/app/imports/api/properties/subSchemas/createPropertySchema.js b/app/imports/api/properties/subSchemas/createPropertySchema.js index 36c5d5e4..7e7b1c36 100644 --- a/app/imports/api/properties/subSchemas/createPropertySchema.js +++ b/app/imports/api/properties/subSchemas/createPropertySchema.js @@ -8,6 +8,9 @@ import { } from '/imports/api/properties/subSchemas/computedField.js'; import SimpleSchema from 'simpl-schema'; +// Search through the schema for keys whose type is 'fieldToCompute' etc. +// replace the type with Object and attach extend the schema with +// the required fields to make the computation work export default function createPropertySchema(definition){ const computationFields = { inlineCalculationFieldToCompute: [], @@ -21,12 +24,20 @@ export default function createPropertySchema(definition){ const def = definition[key]; if (computedKeys.includes(def.type)){ computationFields[def.type].push(key); + applyDefaultCalculationValue(definition, key); def.type = Object; + if (!def.optional){ + console.warn( + `computed field: '${key}' of '${def.type}' is expected to be optional` + ); + } } } + // Create a schema with the edited definition const schema = new SimpleSchema(definition); + // Extend the schema with all the computation fields computationFields.inlineCalculationFieldToCompute.forEach(key => { schema.extend(inlineCalculationFieldToCompute(key)) }); @@ -41,3 +52,33 @@ export default function createPropertySchema(definition){ }); return schema } + +function applyDefaultCalculationValue(definition, key){ + const def = definition[key]; + if ( + def.type === 'computedOnlyField' || + def.type === 'computedOnlyInlineCalculationField' + ){ + // don't apply defaults to computed only fields + // because it would add the calculation field which should only appear + // on the fields to compute + return; + } + let defaultValue = def.defaultValue; + if (!defaultValue) return; + let calcField; + if (def.type === 'fieldToCompute'){ + calcField = key + '.calculation' + } else { + calcField = key + '.text' + } + if (definition[calcField]){ + definition[calcField].defaultValue = defaultValue; + } else { + definition[calcField] = { + type: String, + defaultValue, + }; + } + delete def.defaultValue; +} diff --git a/app/imports/api/simpleSchemaConfig.js b/app/imports/api/simpleSchemaConfig.js new file mode 100644 index 00000000..d3c4e059 --- /dev/null +++ b/app/imports/api/simpleSchemaConfig.js @@ -0,0 +1,3 @@ +import SimpleSchema from 'simpl-schema'; + +SimpleSchema.extendOptions(['parseLevel']); diff --git a/app/imports/migrations/server/transformFields.js b/app/imports/migrations/server/transformFields.js index e99d17af..5fc68b51 100644 --- a/app/imports/migrations/server/transformFields.js +++ b/app/imports/migrations/server/transformFields.js @@ -15,15 +15,19 @@ export default function transformFields(src, transformList, reversed = false){ } else { transform = {...originalTransform}; } - if (transform.from?.includes('$.')){ - transformArrayField(src, doc, transform, reversed); - } else { - transformSingleField(src, doc, transform); - } + transformField(src, doc, transform, reversed); } return doc; } +export function transformField(src, doc, transform, reversed){ + if (transform.from?.includes('$.')){ + transformArrayField(src, doc, transform, reversed); + } else { + transformSingleField(src, doc, transform); + } +} + function transformSingleField(src, doc, {from, to, up}){ // Get the value in the `from` path and delete it let value = undefined; diff --git a/app/package-lock.json b/app/package-lock.json index d38d5600..8c6f1e4d 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2408,6 +2408,19 @@ "randexp": "0.4.6" } }, + "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.graph": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/ngraph.graph/-/ngraph.graph-19.1.0.tgz", + "integrity": "sha512-9cws84qfPkrYa7BaBtT+KgZfLXrd6pNL9Gl5Do+MBO/0Hm6rOM7qK78MZaO1uEoIK6p2pgUs6lu29zn/6tP59w==", + "requires": { + "ngraph.events": "^1.2.1" + } + }, "node-addon-api": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz", diff --git a/app/package.json b/app/package.json index 3e39c519..568f1174 100644 --- a/app/package.json +++ b/app/package.json @@ -35,6 +35,7 @@ "meteor-node-stubs": "^1.1.0", "moo": "^0.5.1", "nearley": "^2.19.1", + "ngraph.graph": "^19.1.0", "qrcode": "^1.4.4", "request": "^2.88.2", "simpl-schema": "^1.12.0", diff --git a/app/server/main.js b/app/server/main.js index 1a70dfbe..1a52b852 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -1,3 +1,4 @@ +import '/imports/api/simpleSchemaConfig.js'; import '/imports/server/config/accountsEmailConfig.js'; import '/imports/server/config/SimpleRestConfig.js'; import '/imports/server/config/simpleSchemaDebug.js'; From 23e99565dc1940ea6733fba86a04695c435798e5 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 9 Sep 2021 01:31:20 +0200 Subject: [PATCH 09/85] Started inventory calculation --- .../computation/newEngine/computeCreature.js | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/app/imports/api/creature/computation/newEngine/computeCreature.js b/app/imports/api/creature/computation/newEngine/computeCreature.js index 8d4567a3..ffd2d80a 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreature.js +++ b/app/imports/api/creature/computation/newEngine/computeCreature.js @@ -79,18 +79,22 @@ export default function computeCreature(creatureId){ }); }); - // Dependency graph where edge(a, b) means a depends on b - const dependencyGraph = createGraph(); - // Build graph now that all props are stored - properties.forEach(prop => { - linkDependencies(dependencyGraph, prop, propsById); - }); // Process the properties in tree format let creatureTree = nodeArrayToTree(properties); walkDown(creatureTree, node => { denormaliseInactiveStatus(node); inheritToggleDependencies(node); + computeInventory(node); + }); + + // Dependency graph where edge(a, b) means a depends on b + const dependencyGraph = createGraph(); + // Build graph now that all props are stored + properties.forEach(prop => { + linkTypeDependencies(dependencyGraph, prop, propsById); + if (prop.inactive) return; + linkCalculationDependencies(dependencyGraph, prop, propsById); }); } @@ -140,6 +144,33 @@ function inheritToggleDependencies(node, dependencyGraph){ }); } +function computeInventory(forest){ + const data = { + weightTotal: 0, + weightEquipment: 0, + weightCarried: 0, + valueTotal: 0, + valueEquipment: 0, + valueCarried: 0, + itemsAttuned: 0, + } + // The stack of properties to still navigate + const stack = [...forest]; + // The current containers we are inside of + const containerStack = []; + const visitedNodes = new Set(); + + while(stack.length){ + const top = stack[stack.length - 1]; + // Leaf node + if (top.children.length === 0){ + + } else { + + } + } +} + function parseCalculation(calcObj){ let calculation = calcObj.calculation || ''; try { @@ -153,7 +184,7 @@ function parseCalculation(calcObj){ } } -function linkDependencies(dependencyGraph, prop, propsById){ +function linkCalculationDependencies(dependencyGraph, prop, propsById){ let variableNames = []; prop._computationDetails.calculations.forEach(calcObj => { calcObj._parsedCalculation.travese(node => { @@ -172,3 +203,37 @@ function linkDependencies(dependencyGraph, prop, propsById){ }); return variableNames; } + +const inventoryVariables = [ + 'weightTotal', + 'weightEquipment', + 'weightCarried', + 'valueTotal', + 'valueEquipment', + 'valueCarried', + 'itemsAttuned', +]; + +const linkDependenciesByType = { + attribute: linkVariableName, + classLevel: linkVariableName, + constant: linkVariableName, + container: linkInventoryVariables, +} + +function linkVariableName(dependencyGraph, prop){ + if (prop.inactive) return; + if (prop.variableName){ + dependencyGraph.addLink(prop.variableName, prop._id); + } +} + +function linkInventoryVariables(dependencyGraph, prop){ + inventoryVariables.forEach(variableName => { + dependencyGraph.addLink(variableName, prop._id); + }); +} + +function linkTypeDependencies(dependencyGraph, prop){ + linkDependenciesByType[prop.type]?.(prop); +} From 55bca633fc575050e7b54758c02bd5b9c7253298 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 9 Sep 2021 13:47:41 +0200 Subject: [PATCH 10/85] Substantial progress on rebuilding computation engine --- .../buildComputation/computeInactiveStatus.js | 24 ++ .../buildComputation/computeInventory.js | 98 +++++++ .../computeSlotQuantityFilled.js | 17 ++ .../computeToggleDependencies.js | 16 ++ .../linkCalculationDependencies.js | 24 ++ .../buildComputation/linkTypeDependencies.js | 40 +++ .../parseCalculationFields.js | 35 +++ .../newEngine/buildCreatureComputation.js | 126 +++++++++ .../computation/newEngine/computeCreature.js | 239 ------------------ .../newEngine/{ => utility}/applyFnToKey.js | 0 .../{ => utility}/findAncestorByType.js | 0 .../computation/newEngine/utility/walkdown.js | 8 + app/imports/api/properties/Containers.js | 10 + .../api/properties/DamageMultipliers.js | 3 +- app/imports/api/properties/Damages.js | 4 +- app/imports/api/properties/SlotFillers.js | 1 - 16 files changed, 401 insertions(+), 244 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js create mode 100644 app/imports/api/creature/computation/newEngine/buildCreatureComputation.js delete mode 100644 app/imports/api/creature/computation/newEngine/computeCreature.js rename app/imports/api/creature/computation/newEngine/{ => utility}/applyFnToKey.js (100%) rename app/imports/api/creature/computation/newEngine/{ => utility}/findAncestorByType.js (100%) create mode 100644 app/imports/api/creature/computation/newEngine/utility/walkdown.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js new file mode 100644 index 00000000..fdf651d8 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js @@ -0,0 +1,24 @@ +import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; + +export default function computeInactiveStatus(node){ + const prop = node.node; + if (isActive(prop)) return; + prop.inactive = true; + prop.deactivatedBySelf = true; + // Mark children as inactive due to ancestor + walkDown(node.children, child => { + child.node.inactive = true; + child.node.deactivatedByAncestor = true; + }); +} + +function isActive(prop){ + if (prop.disabled) return false; + switch (prop.type){ + case 'buff': return !!prop.applied; + case 'item': return !!prop.equipped; + case 'spell': return !!prop.prepared || !!prop.alwaysPrepared; + case 'note': return false; + default: return true; + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js new file mode 100644 index 00000000..9e96c70e --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js @@ -0,0 +1,98 @@ +/** + * Performs a depth first traversal of the character tree, summing the container + * and inventory contents on the way up the tree + */ +export default function computeInventory(forest, dependencyGraph){ + const data = { + weightTotal: 0, + weightEquipment: 0, + weightCarried: 0, + valueTotal: 0, + valueEquipment: 0, + valueCarried: 0, + itemsAttuned: 0, + }; + // The stack of properties to still navigate + const stack = [...forest]; + // The current containers we are inside of + const containerStack = []; + + while(stack.length){ + const top = stack[stack.length - 1]; + const prop = top.node; + if (prop._computationDetails.inventoryChildrenVisited){ + stack.pop(); + handleProp(prop, containerStack, data, dependencyGraph); + } else { + // Add all containers to the stack when we first visit them + if (prop.type === 'container'){ + containerStack.push(top.node); + setDefaultContainerData(prop); + } + // Push children onto the stack and mark this as children are visited + stack.push(...top.children); + prop._computationDetails.inventoryChildrenVisited = true; + } + } + // Store all the computed values on the dependency graph variables + for (let key in data){ + dependencyGraph.addNode(key, {engineValue: data[key]}); + } +} + +function setDefaultContainerData(container){ + container.contentsWeight = 0; + container.carriedWeight = 0; + container.contentsValue = 0; + container.carriedValue = 0; +} + +function handleProp(prop, containerStack, data, dependencyGraph){ + // Determine if this property is carried, items are carried by default + let carried = prop.type === 'container' ? prop.carried : true; + + // Weight and value for this property + const weight = (prop.weight || 0) + (prop.contentsWeight || 0); + const carriedWeight = (prop.weight || 0) + (prop.carriedWeight || 0); + const value = (prop.value || 0) + (prop.value || 0); + const carriedValue = (prop.value || 0) + (prop.carriedValue || 0); + + // Sum the item-specific data + if (prop.type === 'item'){ + dependencyGraph.addLink('itemsAttuned', prop._id); + if (prop.attuned) data.itemsAttuned += 1; + if (prop.equipped){ + dependencyGraph.addLink('weightEquipment', prop._id); + data.weightEquipment += weight; + dependencyGraph.addLink('valueEquipment', prop._id); + data.valueEquipment += value; + } + } + + // Get the parent container + const container = containerStack[containerStack.length - 1]; + + if (container){ + // The container depends on this prop for its contents data + dependencyGraph.addLink(container._id, prop._id); + // Add this property's weights and values to the container + if (!container.weightless){ + container.contentsWeight += weight; + if (carried) container.carriedWeight += carriedWeight; + } + container.contentsValue += value; + if (carried) container.carriedValue += carriedValue; + } else { + // There is no parent container, add weights/value to the character data + dependencyGraph.addLink('weightTotal', prop._id); + data.weightTotal += weight; + dependencyGraph.addLink('valueTotal', prop._id); + data.valueTotal += value; + if (carried){ + dependencyGraph.addLink('weightCarried', prop._id); + data.weightCarried += carriedWeight; + dependencyGraph.addLink('valueCarried', prop._id); + data.valueCarried += carriedValue; + } + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js new file mode 100644 index 00000000..dbaa9fda --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js @@ -0,0 +1,17 @@ +/** + * Only computes `totalFilled`, need to compute `quantityExpected.value` + * before `spacesLeft` can be computed + */ +export default function computeSlotQuantityFilled(node, dependencyGraph){ + let slot = node.node; + slot.totalFilled = 0; + node.children.forEach(child => { + let childProp = child.node; + dependencyGraph.addLink(slot._id, childProp._id) + if (childProp.type === 'slotFiller'){ + slot.totalFilled += child.slotQuantityFilled; + } else { + slot.totalFilled++; + } + }); +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js new file mode 100644 index 00000000..882b28c7 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js @@ -0,0 +1,16 @@ +import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; + +export default function computeToggleDependencies(node, dependencyGraph){ + 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; + walkDown(node.children, child => { + child.node._computationDetails.toggleAncestors.push(prop._id); + dependencyGraph.addLink(child.node._id, prop._id, prop.condition); + }); +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js new file mode 100644 index 00000000..075fdb2e --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -0,0 +1,24 @@ +import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; +import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; +import findAncestorByType from 'imports/api/creature/computation/newEngine/utility/findAncestorByType.js'; + +export default function linkCalculationDependencies(dependencyGraph, prop, propsById){ + prop._computationDetails.calculations.forEach(calcObj => { + // Traverse the parsed calculation looking for variable names + calcObj._parsedCalculation.travese(node => { + if (node instanceof SymbolNode || node instanceof AccessorNode){ + // Link ancestor references as direct property dependencies + if (node.name[0] === '#'){ + let ancestorProp = findAncestorByType( + prop, node.name.slice(1), propsById + ); + if (!ancestorProp) return; + dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); + } else { + // Link variable name references as variable dependencies + dependencyGraph.addLink(prop._id, node.name, calcObj); + } + } + }); + }); +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js new file mode 100644 index 00000000..38e640ea --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -0,0 +1,40 @@ +const linkDependenciesByType = { + attribute: linkVariableName, + classLevel: linkVariableName, + constant: linkVariableName, + damageMultiplier: linkDamageMultiplier, + proficiency: linkStats, + effect: linkStats, + skill: linkSkill, +} + +export default function linkTypeDependencies(dependencyGraph, prop){ + linkDependenciesByType[prop.type]?.(prop); +} + +function linkVariableName(dependencyGraph, prop){ + // The variableName of the prop depends on the prop + if (prop.variableName){ + dependencyGraph.addLink(prop.variableName, prop._id); + } +} + +function linkDamageMultiplier(dependencyGraph, prop){ + prop.damageTypes.forEach(damageType => { + dependencyGraph.addLink(`${damageType}Multiplier`, prop._id); + }); +} + +function linkStats(dependencyGraph, prop){ + // The stats a prop references depend on that prop + prop.stats.forEach(variableName => { + if (!variableName) return; + dependencyGraph.addLink(variableName, prop._id); + }); +} + +function linkSkill(dependencyGraph, prop){ + linkVariableName(dependencyGraph, prop); + // The prop depends on the variable references as the ability + if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability); +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js new file mode 100644 index 00000000..e9584d3d --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -0,0 +1,35 @@ +import { prettifyParseError, parse } from '/imports/parser/parser.js'; +import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; +import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; + +export default function parseCalculationFields(prop, schemas){ + // For each key in the schema + schemas[prop.type]._schemaKeys.forEach( key => { + // that ends in '.calculation' + if (key.slice(-12) !== '.calculation') return; + const calcKey = key.sclice(0, -12); + + // For all fields matching they keys + // supports `keys.$.with.$.arrays` + applyFnToKey(prop, calcKey, calcObj => { + // Store a reference to all the calculations + prop._computationDetails.calculations.push(calcObj); + // Parse the calculation + parseCalculation(calcObj); + }); + + }); +} + +function parseCalculation(calcObj){ + let calculation = calcObj.calculation || ''; + try { + calcObj._parsedCalculation = parse(calculation); + } catch (e) { + let error = prettifyParseError(e); + calcObj.errors ? + calcObj.errors.push(error) : + calcObj.errors = [error]; + calcObj._parsedCalculation = new ErrorNode({error}); + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js new file mode 100644 index 00000000..e99df529 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -0,0 +1,126 @@ +import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; +import CreatureProperties, + { DenormalisedOnlyCreaturePropertySchema as denormSchema } + from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; +import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; +import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; +import { cloneDeep, unset } from 'lodash'; +import createGraph from 'ngraph.graph'; +import computeInventory from '/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js'; +import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; +import parseCalculationFields from '/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js'; +import computeInactiveStatus from '/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js'; +import computeToggleDependencies from '/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js'; +import linkCalculationDependencies from '/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js'; +import linkTypeDependencies from '/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js'; +import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js'; + +/** + * Store index of properties + * recompute static tree-based enabled/disabled status + * Build a dependency graph + * id -> id dependencies for docs that rely on other docs directly + * id -> variable deps for docs that rely on a variable's value + * variable -> id deps for variables that are impacted by docs + * TODO: + * Depth first traversal or dependency graph to: + * Find loops in the dependency graph + * resolve variables in dependency order + */ + +/** + * TODO + * compute slots spaces left (after computed field of quantityExpected) + * compute damage multipliers + * compute dependencyGraph variables and properties + */ + +export default function buildCreatureComputation(creatureId){ + let properties = CreatureProperties.find({ + 'ancestors.id': creatureId, + 'removed': {$ne: true}, + }, { + sort: {order: 1} + }); + + // Dependency graph where edge(a, b) means a depends on b + // The graph includes all dependencies even of inactive properties + // such that any properties changing without changing their dependencies + // can limit the recompute to connected parts of the graph + const dependencyGraph = createGraph(); + + const computation = { + originalPropsById: {}, + propsById: {}, + propsByType: {}, + propsByVariableName: {}, + props: properties, + dependencyGraph, + }; + + // Process the properties one by one + properties.forEach(prop => { + + // Store the prop in the memo by type, variableName and id + storePropInMemo(prop, computation) + + // Store the prop in the dependency graph + dependencyGraph.addNode(prop._id, prop); + + // Remove old computed only fields + computedOnlySchemas[prop.type]._schemaKeys.forEach(key => + applyFnToKey(prop, key, unset) + ); + + // Remove old denormalised fields + denormSchema._schemaKeys.forEach(key => + applyFnToKey(prop, key, unset) + ); + + // Add a place to store all the computation details + prop._computationDetails = { + calculations: [], + toggleAncestors: [], + }; + + // Parse all the calculations + parseCalculationFields(prop, computedSchemas) + }); + + // Get all the properties as trees based on their ancestors + let forest = nodeArrayToTree(properties); + // Walk the property trees computing things that need to be inherited + walkDown(forest, node => { + computeInactiveStatus(node); + computeToggleDependencies(node); + computeSlotQuantityFilled(node); + }); + + // Compute the inventory + computeInventory(forest, dependencyGraph); + + // Graph functions that rely on the props being stored first + properties.forEach(prop => { + linkTypeDependencies(dependencyGraph, prop, computation.propsById); + linkCalculationDependencies(dependencyGraph, prop, computation.propsById); + }); + + return computation; +} + +function storePropInMemo(prop, memo){ + // Store dicts for easy access later + // Store a copy of the unmodified prop + memo.originalPropsById[prop._id] = cloneDeep(prop); + // Store by id + memo.propsById[prop._id] = prop; + // Store by type + memo.propsByType[prop.type] ? + memo.propsByType[prop.type].push(prop) : + memo.propsByType[prop.type] = [prop]; + // Store by variableName + memo.propsByVariableName[prop.variableName] ? + memo.propsByVariableName[prop.variableName].push(prop) : + memo.propsByVariableName[prop.variableName]= [prop]; +} diff --git a/app/imports/api/creature/computation/newEngine/computeCreature.js b/app/imports/api/creature/computation/newEngine/computeCreature.js deleted file mode 100644 index ffd2d80a..00000000 --- a/app/imports/api/creature/computation/newEngine/computeCreature.js +++ /dev/null @@ -1,239 +0,0 @@ -import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; -import CreatureProperties, - { DenormalisedOnlyCreaturePropertySchema as denormSchema } - from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; -import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; -import applyFnToKey from '/imports/api/creature/computation/newEngine/applyFnToKey.js'; -import { cloneDeep, unset } from 'lodash'; -import { prettifyParseError, parse } from '/imports/parser/parser.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; -import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; -import createGraph from 'ngraph.graph'; -import findAncestorByType from 'imports/api/creature/computation/newEngine/findAncestorByType.js'; - -/** - * Store index of properties - * recompute static tree-based enabled/disabled status - * Build a dependency graph - * id -> id dependencies for docs that rely on other docs directly - * id -> variable deps for docs that rely on a variable's value - * TODO: - * variable -> id deps for variables that are impacted by docs - * Depth first traversal or dependency graph to: - * Find loops in the dependency graph - * resolve variables in dependency order - */ - -export default function computeCreature(creatureId){ - let properties = CreatureProperties.find({ - 'ancestors.id': creatureId, - 'removed': {$ne: true}, - }, { - sort: {order: 1} - }); - - const originalPropsById = {}; - const propsById = {}; - const propsByType = {}; - - // Process the properties one by one - properties.forEach(prop => { - // Store the prop by Id and Type - originalPropsById[prop._id] = cloneDeep(prop); - propsById[prop._id] = prop; - if (!propsByType[prop.type]) propsByType[prop.type] = []; - propsByType[prop.type].push(prop); - - // Store the prop in the dependency graph - dependencyGraph.addNode(prop._id, prop); - - // Remove all computed only fields - computedOnlySchemas[prop.type]._schemaKeys.forEach(key => - applyFnToKey(prop, key, unset) - ); - - // Remove all denormalised fields - denormSchema._schemaKeys.forEach(key => - applyFnToKey(prop, key, unset) - ); - - // Add a place to store all the computation details - prop._computationDetails = { - calculations: [], - toggleAncestors: [], - }; - - // parse every calculation field - computedSchemas[prop.type]._schemaKeys.forEach( key => { - if (key.slice(-11) !== 'calculation') return; - const calcKey = key.sclice(0, -11); - applyFnToKey(prop, calcKey, calcObj => { - // Store a reference to all the calculations - prop._computationDetails.calculations.push(calcObj); - // Parse the calculation - parseCalculation(calcObj); - return calcObj; - }); - }); - }); - - - // Process the properties in tree format - let creatureTree = nodeArrayToTree(properties); - walkDown(creatureTree, node => { - denormaliseInactiveStatus(node); - inheritToggleDependencies(node); - computeInventory(node); - }); - - // Dependency graph where edge(a, b) means a depends on b - const dependencyGraph = createGraph(); - // Build graph now that all props are stored - properties.forEach(prop => { - linkTypeDependencies(dependencyGraph, prop, propsById); - if (prop.inactive) return; - linkCalculationDependencies(dependencyGraph, prop, propsById); - }); -} - -function walkDown(tree, callback){ - let stack = [...tree]; - while(stack.length){ - let node = stack.pop(); - callback(node); - stack.push(...node.children); - } -} - -function denormaliseInactiveStatus(node){ - const prop = node.node; - if (isActive(prop)) return; - prop.inactive = true; - prop.deactivatedBySelf = true; - // Mark children as inactive due to ancestor - walkDown(node.children, child => { - child.node.inactive = true; - child.node.deactivatedByAncestor = true; - }); -} - -function isActive(prop){ - if (prop.disabled) return false; - switch (prop.type){ - case 'buff': return !!prop.applied; - case 'item': return !!prop.equipped; - case 'spell': return !!prop.prepared || !!prop.alwaysPrepared; - default: return true; - } -} - -function inheritToggleDependencies(node, dependencyGraph){ - 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; - walkDown(node.children, child => { - child.node._computationDetails.toggleAncestors.push(prop._id); - dependencyGraph.addLink(child.node._id, prop._id, prop.condition); - }); -} - -function computeInventory(forest){ - const data = { - weightTotal: 0, - weightEquipment: 0, - weightCarried: 0, - valueTotal: 0, - valueEquipment: 0, - valueCarried: 0, - itemsAttuned: 0, - } - // The stack of properties to still navigate - const stack = [...forest]; - // The current containers we are inside of - const containerStack = []; - const visitedNodes = new Set(); - - while(stack.length){ - const top = stack[stack.length - 1]; - // Leaf node - if (top.children.length === 0){ - - } else { - - } - } -} - -function parseCalculation(calcObj){ - let calculation = calcObj.calculation || ''; - try { - calcObj._parsedCalculation = parse(calculation); - } catch (e) { - let error = prettifyParseError(e); - calcObj.errors ? - calcObj.errors.push(error) : - calcObj.errors = [error]; - calcObj._parsedCalculation = new ErrorNode({error}); - } -} - -function linkCalculationDependencies(dependencyGraph, prop, propsById){ - let variableNames = []; - prop._computationDetails.calculations.forEach(calcObj => { - calcObj._parsedCalculation.travese(node => { - if (node instanceof SymbolNode || node instanceof AccessorNode){ - if (node.name[0] !== '#'){ - dependencyGraph.addLink(prop._id, node.name, calcObj); - } else { - let ancestorProp = findAncestorByType( - prop, node.name.slice(1), propsById - ); - if (!ancestorProp) return; - dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); - } - } - }); - }); - return variableNames; -} - -const inventoryVariables = [ - 'weightTotal', - 'weightEquipment', - 'weightCarried', - 'valueTotal', - 'valueEquipment', - 'valueCarried', - 'itemsAttuned', -]; - -const linkDependenciesByType = { - attribute: linkVariableName, - classLevel: linkVariableName, - constant: linkVariableName, - container: linkInventoryVariables, -} - -function linkVariableName(dependencyGraph, prop){ - if (prop.inactive) return; - if (prop.variableName){ - dependencyGraph.addLink(prop.variableName, prop._id); - } -} - -function linkInventoryVariables(dependencyGraph, prop){ - inventoryVariables.forEach(variableName => { - dependencyGraph.addLink(variableName, prop._id); - }); -} - -function linkTypeDependencies(dependencyGraph, prop){ - linkDependenciesByType[prop.type]?.(prop); -} diff --git a/app/imports/api/creature/computation/newEngine/applyFnToKey.js b/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/applyFnToKey.js rename to app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js diff --git a/app/imports/api/creature/computation/newEngine/findAncestorByType.js b/app/imports/api/creature/computation/newEngine/utility/findAncestorByType.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/findAncestorByType.js rename to app/imports/api/creature/computation/newEngine/utility/findAncestorByType.js diff --git a/app/imports/api/creature/computation/newEngine/utility/walkdown.js b/app/imports/api/creature/computation/newEngine/utility/walkdown.js new file mode 100644 index 00000000..e0c4728a --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/utility/walkdown.js @@ -0,0 +1,8 @@ +export default function walkDown(tree, callback){ + let stack = [...tree]; + while(stack.length){ + let node = stack.pop(); + callback(node); + stack.push(...node.children); + } +} diff --git a/app/imports/api/properties/Containers.js b/app/imports/api/properties/Containers.js index c48b521d..ca911e67 100644 --- a/app/imports/api/properties/Containers.js +++ b/app/imports/api/properties/Containers.js @@ -44,10 +44,20 @@ const ComputedOnlyContainerSchema = createPropertySchema({ type: Number, optional: true, }, + // Weight of all the carried contents (some sub-containers might not be carried) + // zero if `contentsWeightless` is true + carriedWeight:{ + type: Number, + optional: true, + }, contentsValue:{ type: Number, optional: true, }, + carriedValue:{ + type: Number, + optional: true, + }, }); const ComputedContainerSchema = new SimpleSchema() diff --git a/app/imports/api/properties/DamageMultipliers.js b/app/imports/api/properties/DamageMultipliers.js index 34c3d55f..4ed9c52f 100644 --- a/app/imports/api/properties/DamageMultipliers.js +++ b/app/imports/api/properties/DamageMultipliers.js @@ -1,5 +1,4 @@ import SimpleSchema from 'simpl-schema'; -import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; /* @@ -20,7 +19,7 @@ let DamageMultiplierSchema = new SimpleSchema({ // The technical, lowercase, single-word name used in formulae 'damageTypes.$': { type: String, - allowedValues: DAMAGE_TYPES, + max: STORAGE_LIMITS.calculation, }, // The value of the damage multiplier value: { diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js index 007d97f5..8b4881f9 100644 --- a/app/imports/api/properties/Damages.js +++ b/app/imports/api/properties/Damages.js @@ -1,6 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; const DamageSchema = createPropertySchema({ // The roll that determines how much to damage the attribute @@ -22,7 +22,7 @@ const DamageSchema = createPropertySchema({ }, damageType: { type: String, - allowedValues: DAMAGE_TYPES, + max: STORAGE_LIMITS.calculation, defaultValue: 'slashing', }, }); diff --git a/app/imports/api/properties/SlotFillers.js b/app/imports/api/properties/SlotFillers.js index b485a5c6..c3ac0d3b 100644 --- a/app/imports/api/properties/SlotFillers.js +++ b/app/imports/api/properties/SlotFillers.js @@ -30,7 +30,6 @@ let SlotFillerSchema = new SimpleSchema({ slotQuantityFilled: { type: SimpleSchema.Integer, defaultValue: 1, - min: 0, }, // Filters out of UI if condition isn't met, but isn't otherwise enforced slotFillerCondition: { From 28ec7082ee93fbc16020b2636463c7c8baf1c2ed Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 10 Sep 2021 17:14:39 +0200 Subject: [PATCH 11/85] Fixed typo in slot form --- app/imports/ui/properties/forms/SlotForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/imports/ui/properties/forms/SlotForm.vue b/app/imports/ui/properties/forms/SlotForm.vue index 38d09dba..a29a5a6a 100644 --- a/app/imports/ui/properties/forms/SlotForm.vue +++ b/app/imports/ui/properties/forms/SlotForm.vue @@ -187,7 +187,7 @@ text: 'Each property inside this slot should be unique', value: 'uniqueInSlot', }, { - text: 'Properties in this slot should be unique accross the whole character', + text: 'Properties in this slot should be unique across the whole character', value: 'uniqueInCreature', }], }; From b877a8b45f4939e6583f249a983dcca136d60b43 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 10 Sep 2021 19:51:03 +0200 Subject: [PATCH 12/85] Computation engine rewrite continues --- app/client/main.js | 2 + .../buildComputation/computeInventory.js | 16 ++-- .../computeSlotQuantityFilled.js | 2 +- .../computeToggleDependencies.js | 2 +- .../linkCalculationDependencies.js | 43 ++++++++--- .../buildComputation/linkTypeDependencies.js | 8 +- .../parseCalculationFields.js | 7 +- .../aggregateProps/aggregateBaseValue.js | 13 ++++ .../aggregateProps/aggregateDefinitions.js | 24 ++++++ .../aggregateProps/index.js | 17 +++++ .../computeComputation/computeVariable.js | 33 +++++++++ .../computeComputation/evaluateCalculation.js | 11 +++ .../newEngine/computeCreatureComputation.js | 74 +++++++++++++++++++ app/imports/api/properties/Toggles.js | 9 +++ app/package-lock.json | 2 +- 15 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js create mode 100644 app/imports/api/creature/computation/newEngine/computeCreatureComputation.js diff --git a/app/client/main.js b/app/client/main.js index b036a39f..d9d770b7 100644 --- a/app/client/main.js +++ b/app/client/main.js @@ -3,3 +3,5 @@ import '/imports/ui/vueSetup.js'; import '/imports/ui/styles/stylesIndex.js'; import '/imports/client/config.js'; import '/imports/client/serviceWorker.js'; + +import 'ngraph.graph'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js index 9e96c70e..c97f92d4 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js @@ -59,12 +59,12 @@ function handleProp(prop, containerStack, data, dependencyGraph){ // Sum the item-specific data if (prop.type === 'item'){ - dependencyGraph.addLink('itemsAttuned', prop._id); + dependencyGraph.addLink('itemsAttuned', prop._id, 'inventory'); if (prop.attuned) data.itemsAttuned += 1; if (prop.equipped){ - dependencyGraph.addLink('weightEquipment', prop._id); + dependencyGraph.addLink('weightEquipment', prop._id, 'inventory'); data.weightEquipment += weight; - dependencyGraph.addLink('valueEquipment', prop._id); + dependencyGraph.addLink('valueEquipment', prop._id, 'inventory'); data.valueEquipment += value; } } @@ -74,7 +74,7 @@ function handleProp(prop, containerStack, data, dependencyGraph){ if (container){ // The container depends on this prop for its contents data - dependencyGraph.addLink(container._id, prop._id); + dependencyGraph.addLink(container._id, prop._id, 'inventory'); // Add this property's weights and values to the container if (!container.weightless){ container.contentsWeight += weight; @@ -84,14 +84,14 @@ function handleProp(prop, containerStack, data, dependencyGraph){ if (carried) container.carriedValue += carriedValue; } else { // There is no parent container, add weights/value to the character data - dependencyGraph.addLink('weightTotal', prop._id); + dependencyGraph.addLink('weightTotal', prop._id, 'inventory'); data.weightTotal += weight; - dependencyGraph.addLink('valueTotal', prop._id); + dependencyGraph.addLink('valueTotal', prop._id, 'inventory'); data.valueTotal += value; if (carried){ - dependencyGraph.addLink('weightCarried', prop._id); + dependencyGraph.addLink('weightCarried', prop._id, 'inventory'); data.weightCarried += carriedWeight; - dependencyGraph.addLink('valueCarried', prop._id); + dependencyGraph.addLink('valueCarried', prop._id, 'inventory'); data.valueCarried += carriedValue; } } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js index dbaa9fda..7dd1694d 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js @@ -7,7 +7,7 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){ slot.totalFilled = 0; node.children.forEach(child => { let childProp = child.node; - dependencyGraph.addLink(slot._id, childProp._id) + dependencyGraph.addLink(slot._id, childProp._id, 'slotFill') if (childProp.type === 'slotFiller'){ slot.totalFilled += child.slotQuantityFilled; } else { diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js index 882b28c7..665ea71e 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js @@ -11,6 +11,6 @@ export default function computeToggleDependencies(node, dependencyGraph){ ) return; walkDown(node.children, child => { child.node._computationDetails.toggleAncestors.push(prop._id); - dependencyGraph.addLink(child.node._id, prop._id, prop.condition); + dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js index 075fdb2e..06cf1a3d 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -4,21 +4,40 @@ import findAncestorByType from 'imports/api/creature/computation/newEngine/utili export default function linkCalculationDependencies(dependencyGraph, prop, propsById){ prop._computationDetails.calculations.forEach(calcObj => { + // Store resolved ancestors + let memo = { + // ancestors: {} //this gets added if there are resolved ancestors + }; // Traverse the parsed calculation looking for variable names calcObj._parsedCalculation.travese(node => { - if (node instanceof SymbolNode || node instanceof AccessorNode){ - // Link ancestor references as direct property dependencies - if (node.name[0] === '#'){ - let ancestorProp = findAncestorByType( - prop, node.name.slice(1), propsById - ); - if (!ancestorProp) return; - dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); - } else { - // Link variable name references as variable dependencies - dependencyGraph.addLink(prop._id, node.name, calcObj); - } + // Skip nodes that aren't symbols or accessors + if (!(node instanceof SymbolNode || node instanceof AccessorNode)) return; + // Link ancestor references as direct property dependencies + if (node.name[0] === '#'){ + let ancestorProp = getAncestorProp( + node.name.slice(1), memo, prop, propsById + ); + if (!ancestorProp) return; + dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); + } else { + // Link variable name references as variable dependencies + dependencyGraph.addLink(prop._id, node.name, calcObj); } }); + // Store the resolved ancestors in this calculation's local scope + if (memo.ancestors) { + calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors}; + } }); } + +function getAncestorProp(type, memo, prop, propsById){ + if (memo.ancestors && memo.ancestors['#' + type]){ + return memo.ancestors['#' + type]; + } else { + var ancestorProp = findAncestorByType( prop, type, propsById ); + if (!memo.ancestors) memo.ancestors = {}; + memo.ancestors['#' + type] = ancestorProp; + return ancestorProp; + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index 38e640ea..f2bea38b 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -15,13 +15,13 @@ export default function linkTypeDependencies(dependencyGraph, prop){ function linkVariableName(dependencyGraph, prop){ // The variableName of the prop depends on the prop if (prop.variableName){ - dependencyGraph.addLink(prop.variableName, prop._id); + dependencyGraph.addLink(prop.variableName, prop._id, 'definition'); } } function linkDamageMultiplier(dependencyGraph, prop){ prop.damageTypes.forEach(damageType => { - dependencyGraph.addLink(`${damageType}Multiplier`, prop._id); + dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, 'damageMultiplier'); }); } @@ -29,12 +29,12 @@ function linkStats(dependencyGraph, prop){ // The stats a prop references depend on that prop prop.stats.forEach(variableName => { if (!variableName) return; - dependencyGraph.addLink(variableName, prop._id); + dependencyGraph.addLink(variableName, prop._id, 'statChange'); }); } function linkSkill(dependencyGraph, prop){ linkVariableName(dependencyGraph, prop); // The prop depends on the variable references as the ability - if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability); + if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js index e9584d3d..9952f92b 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -9,15 +9,20 @@ export default function parseCalculationFields(prop, schemas){ if (key.slice(-12) !== '.calculation') return; const calcKey = key.sclice(0, -12); + // Determine the level the calculation should compute down to + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, calcObj => { // Store a reference to all the calculations prop._computationDetails.calculations.push(calcObj); + // Store the level to compute down to later + calcObj._parseLevel = parseLevel; // Parse the calculation parseCalculation(calcObj); }); - + }); } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js new file mode 100644 index 00000000..96446353 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js @@ -0,0 +1,13 @@ + +/** + * Iterate through all the defining properties and choose the highest + * `baseValue.value` + */ +export default function aggregateBaseValue({node, linkedNode, link}){ + if (link.data !== 'definition') return; + const propBaseValue = linkedNode.data.baseValue?.value; + if (propBaseValue === undefined) return; + if (node.baseValue === undefined || propBaseValue > node.baseValue){ + node.baseValue = propBaseValue; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js new file mode 100644 index 00000000..e21a4d14 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js @@ -0,0 +1,24 @@ + +export default function aggregateDefinitions({node, linkedNode, link}){ + // Look at all definition links + if (link.data !== 'definition') return; + const prop = linkedNode.data; + // get current defining prop + const definingProp = node.data.definingProp; + // Find the last defining prop + if (!definingProp || prop.order > definingProp.order){ + // override the current defining prop + overrideProp(definingProp, node); + // set this prop as the new defining prop + node.data.definingProp = prop; + } else { + overrideProp(prop, node); + } +} + +function overrideProp(prop, node){ + if (!prop) return; + prop.overriden = true; + if (!node.data.overridenProps) node.data.overridenProps = []; + node.data.overridenProp.push(prop); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js new file mode 100644 index 00000000..e792c5b7 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js @@ -0,0 +1,17 @@ +import definitions from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js'; +import baseValue from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js'; +import damageMultipliers from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDamageMultipliers.js'; +import effects from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateEffects.js'; +import proficiencies from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; +import skills from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; +import toggles from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateToggles.js'; + +export default Object.freeze({ + definitions, + baseValue, + damageMultipliers, + effects, + proficiencies, + skills, + toggles, +}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js new file mode 100644 index 00000000..486fc817 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js @@ -0,0 +1,33 @@ +import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js'; + +export default function computeVariable(graph, node){ + if (!node.data) node.data = {}; + aggregateLinks(graph, node); +} + +function aggregateLinks(graph, node){ + let definingProp; + let overridenProps = []; + graph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + if (!linkedNode.data) linkedNode.data = {}; + // Ignore inactive props + if (linkedNode.data.inactive) return; + // Apply all the aggregations + let arg = {node, linkedNode, link}; + aggregate.definitions(arg); + aggregate.baseValue(arg); + aggregate.damageMultipliers(arg); + aggregate.effects(arg); + aggregate.proficiencies(arg); + aggregate.skills(arg); + aggregate.toggles(arg); + }, + true // enumerate only outbound links + ); + // store the defining and overriden props on the node + if (!node.data) node.data = {}; + node.data.definingProp = definingProp; + node.data.overridenProps = overridenProps; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js b/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js new file mode 100644 index 00000000..81217078 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js @@ -0,0 +1,11 @@ +import { CompilationContext } from '/imports/parser/parser.js'; + +export default function evaluateCalculation(calculation, scope){ + const context = new CompilationContext(); + const parseNode = calculation._parsedCalculation; + const fn = calculation._parseLevel || 'reduce'; + const calculationScope = {...calculation._localScope, ...scope}; + const result = parseNode[fn](calculationScope, context); + calculation.value = result; + calculation.errors = context.errors; +} diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js new file mode 100644 index 00000000..e79620bb --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -0,0 +1,74 @@ +import evaluateCalculation from '/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js'; +import computeVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js'; + +export default function computeCreatureComputation(computation){ + const stack = []; + // dict of computed nodes by id + const scope = {}; + const graph = computation.dependencyGraph; + // Add all nodes to the stack + graph.forEachNode(node => stack.push({ + node, + visited: false, + visitedChildren: false, + })); + // Depth first traversal of nodes + while (stack.length){ + let top = stack[stack.length - 1]; + if (top.visited){ + // The object has already + stack.pop(); + } else if (top.visitedChildren){ + // Compute the top object of the stack + compute(graph, top.node, scope); + // If the node holds a variable, store it in the scope + if (!top.node.data?.type){ + scope[top.node.id] = top.node.data; + } + // Mark the object as visited and remove from stack + top.visited = true; + stack.pop(); + } else { + // Push children to graph + pushDependenciesToStack(top.node.id, graph, stack); + top.visitedChildren = true; + } + } +} + +function compute(graph, node, scope){ + // Get the property + let prop = node.data; + + // evaluate all the calculations + if (prop?._computationDetails?.calculations){ + prop._computationDetails.calculations.forEach(calcObj => { + evaluateCalculation(calcObj, scope) + }); + } + + // Compute the property by type + let typeCompute = propTypeComputations[prop?.type || '_variable']; + typeCompute?.(graph, node); +} + +var propTypeComputations = { + '_variable': computeVariable, +}; + +function pushDependenciesToStack(nodeId, graph, stack){ + graph.forEachLinkedNode( + nodeId, + (linkedNode, link) => { + // Ignore inventory links, they can't cause dependency loops + // and are already fully computed when they are created + if (link.data === 'inventory') return; + stack.push({ + node: linkedNode, + visited: false, + visitedChildren: false, + }); + }, + true // enumerate only outbound links + ); +} diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index ea6d509e..7a76d061 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -8,6 +8,15 @@ const ToggleSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, + variableName: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + showUI: { + type: Boolean, + optional: true, + }, disabled: { type: Boolean, optional: true, diff --git a/app/package-lock.json b/app/package-lock.json index 8c6f1e4d..19da6540 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2795,7 +2795,7 @@ }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simpl-schema": { From 5c84836238b4386917267a00ec207f9240d31fb8 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 13 Sep 2021 16:12:04 +0200 Subject: [PATCH 13/85] More engine rewrite progress, starting to get messy again --- .../creatureComputation.disabledTest.js | 97 ------------------- .../computeToggleDependencies.js | 2 +- .../linkCalculationDependencies.js | 2 +- .../buildComputation/linkTypeDependencies.js | 45 ++++++++- .../parseCalculationFields.js | 38 +++++--- .../newEngine/buildCreatureComputation.js | 19 ++-- .../aggregateProps/aggregateBaseValue.js | 13 --- .../aggregateProps/index.js | 17 ---- .../computeComputation/computeByType.js | 10 ++ .../computeByType/computeAction.js | 21 ++++ .../computeAction/computeResources.js | 10 ++ .../computeByType/computeSlot.js | 6 ++ .../computeByType/computeVariable.js | 79 +++++++++++++++ .../aggregate/aggregateDamageMultiplier.js | 26 +++++ .../aggregate/aggregateDefinition.js} | 13 ++- .../aggregate/aggregateEffect.js | 78 +++++++++++++++ .../aggregate/aggregateProficiency.js | 21 ++++ .../computeVariable/aggregate/index.js | 11 +++ .../computeImplicitVariable.js | 35 +++++++ .../computeVariableAsAttribute.js | 25 +++++ .../computeVariableAsConstant.js | 13 +++ .../computeVariable/computeVariableAsSkill.js | 63 ++++++++++++ .../computeVariable/getAggregatorResult.js | 42 ++++++++ .../computeComputation/computeCalculations.js | 33 +++++++ .../computeComputation/computeToggles.js | 12 +++ .../computeComputation/computeVariable.js | 33 ------- .../computeComputation/evaluateCalculation.js | 11 --- .../newEngine/computeCreatureComputation.js | 39 +++----- .../utility/stripFloatingPointOddities.js | 0 .../creatureProperties/CreatureProperties.js | 9 -- app/imports/api/properties/Actions.js | 5 + app/imports/api/properties/Attributes.js | 6 ++ app/imports/api/properties/Effects.js | 6 ++ app/imports/api/properties/SavingThrows.js | 1 + app/imports/api/properties/Skills.js | 12 +-- .../subSchemas/AttributeConsumedSchema.js | 8 +- .../subSchemas/ItemConsumedSchema.js | 7 -- app/imports/parser/parseTree/SymbolNode.js | 8 +- 38 files changed, 614 insertions(+), 262 deletions(-) delete mode 100644 app/imports/api/creature/computation/creatureComputation.disabledTest.js delete mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js delete mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js rename app/imports/api/creature/computation/newEngine/computeComputation/{aggregateProps/aggregateDefinitions.js => computeByType/computeVariable/aggregate/aggregateDefinition.js} (57%) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js delete mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js delete mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js rename app/imports/{ui => api/creature/computation/newEngine}/utility/stripFloatingPointOddities.js (100%) diff --git a/app/imports/api/creature/computation/creatureComputation.disabledTest.js b/app/imports/api/creature/computation/creatureComputation.disabledTest.js deleted file mode 100644 index 783d8cae..00000000 --- a/app/imports/api/creature/computation/creatureComputation.disabledTest.js +++ /dev/null @@ -1,97 +0,0 @@ -import {computeCreature} from "./recomputeCreature.js"; -import assert from "assert"; - -const makeEffect = function(operation, value){ - let effect = {computed: false, result: 0, operation} - if (_.isFinite(value)){ - effect.value = +value; - } else { - effect.calculation = value; - } - return effect; -} - -describe('computeCreature', function () { - it('computes an aritrary creature', function () { - let char = { - atts: { - attribute1: { - computed: false, - busyComputing: false, - type: "attribute", - attributeType: "ability", - result: 0, - mod: 0, // The resulting modifier if this is an ability - base: 0, - add: 0, - mul: 1, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - effects: [ - makeEffect("base", 10), - makeEffect("add", 5), - makeEffect("mul", 2), - ], - }, - attribute2: { - computed: false, - busyComputing: false, - type: "attribute", - result: 0, - mod: 0, // The resulting modifier if this is an ability - base: 0, - add: 0, - mul: 1, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - effects: [ - makeEffect("base", "attribute1"), - makeEffect("max", 2), - ], - }, - }, - skills: { - skill1: { - computed: false, - busyComputing: false, - type: "skill", - ability: "attribute1", - result: 0, - proficiency: 0, - add: 0, - mul: 1, - min: Number.NEGATIVE_INFINITY, - max: Number.POSITIVE_INFINITY, - advantage: 0, - disadvantage: 0, - passiveAdd: 0, - fail: 0, - conditional: 0, - effects: [], - proficiencies: [], - }, - }, - dms: { - dm1: { - computed: false, - busyComputing: false, - type: "damageMultiplier", - result: 0, - immunityCount: 0, - ressistanceCount: 0, - vulnerabilityCount: 0, - effects: [], - } - }, - classes: { - Barbarian: { - level: 5, - }, - }, - level: 5, - }; - char = computeCreature(char); - console.log(char); - assert(true); - }); -}); diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js index 665ea71e..40d77ea5 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js @@ -10,7 +10,7 @@ export default function computeToggleDependencies(node, dependencyGraph){ prop.enabled ) return; walkDown(node.children, child => { - child.node._computationDetails.toggleAncestors.push(prop._id); + child.node._computationDetails.toggleAncestors.push(prop); dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js index 06cf1a3d..d08fa7c2 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -2,7 +2,7 @@ import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; import findAncestorByType from 'imports/api/creature/computation/newEngine/utility/findAncestorByType.js'; -export default function linkCalculationDependencies(dependencyGraph, prop, propsById){ +export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { // Store resolved ancestors let memo = { diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index f2bea38b..ad3b231a 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -1,11 +1,14 @@ const linkDependenciesByType = { - attribute: linkVariableName, + action: linkResources, + attack: linkResources, + attribute: linkAttribute, classLevel: linkVariableName, constant: linkVariableName, damageMultiplier: linkDamageMultiplier, proficiency: linkStats, effect: linkStats, skill: linkSkill, + spell: linkResources, } export default function linkTypeDependencies(dependencyGraph, prop){ @@ -19,9 +22,39 @@ function linkVariableName(dependencyGraph, prop){ } } +function linkResources(dependencyGraph, prop, {propsById}){ + prop.resources.itemsConsumed.forEach(itemConsumed => { + if (!itemConsumed.itemId) return; + const item = propsById[itemConsumed.itemId]; + if (!item.equipped) { + itemConsumed.itemId = undefined; + return; + } + if (!item) return; + // none of these dependencies are computed, we can use them immediately + prop.available = item.quantity; + prop.itemName = item.name; + prop.itemIcon = item.icon; + prop.itemColor = item.color; + dependencyGraph.addLink(prop._id, item._id, 'inventory'); + }); + prop.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + dependencyGraph.addLink(prop._id, attConsumed.variableName, 'resource'); + }); +} + +function linkAttribute(dependencyGraph, prop){ + linkVariableName(dependencyGraph, prop); + // hit dice depend on constitution + if (prop.attributeType === 'hitDice'){ + dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod'); + } +} + function linkDamageMultiplier(dependencyGraph, prop){ prop.damageTypes.forEach(damageType => { - dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, 'damageMultiplier'); + dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, prop.type); }); } @@ -29,12 +62,16 @@ function linkStats(dependencyGraph, prop){ // The stats a prop references depend on that prop prop.stats.forEach(variableName => { if (!variableName) return; - dependencyGraph.addLink(variableName, prop._id, 'statChange'); + dependencyGraph.addLink(variableName, prop._id, prop.type); }); } function linkSkill(dependencyGraph, prop){ linkVariableName(dependencyGraph, prop); // The prop depends on the variable references as the ability - if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); + if (prop.ability){ + dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); + } + // Skills depend on the creature's proficiencyBonus + dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js index 9952f92b..df451a3b 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -6,23 +6,31 @@ export default function parseCalculationFields(prop, schemas){ // For each key in the schema schemas[prop.type]._schemaKeys.forEach( key => { // that ends in '.calculation' - if (key.slice(-12) !== '.calculation') return; - const calcKey = key.sclice(0, -12); + if (key.slice(-12) === '.calculation'){ + const calcKey = key.sclice(0, -12); - // Determine the level the calculation should compute down to - let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + // Determine the level the calculation should compute down to + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + + // For all fields matching they keys + // supports `keys.$.with.$.arrays` + applyFnToKey(prop, calcKey, calcObj => { + // Store a reference to all the calculations + prop._computationDetails.calculations.push(calcObj); + // Store the level to compute down to later + calcObj._parseLevel = parseLevel; + // Parse the calculation + parseCalculation(calcObj); + }); + // Or that ends in .inlineCalculations + } else if (key.slice(-19) === '.inlineCalculations'){ + const inlineCalcKey = key.sclice(0, -19); + applyFnToKey(prop, inlineCalcKey, inlineCalcObj => { + // Store a reference to all the inline calculations + prop._computationDetails.inlineCalculations.push(inlineCalcObj); + }); + } - // For all fields matching they keys - // supports `keys.$.with.$.arrays` - applyFnToKey(prop, calcKey, calcObj => { - // Store a reference to all the calculations - prop._computationDetails.calculations.push(calcObj); - // Store the level to compute down to later - calcObj._parseLevel = parseLevel; - // Parse the calculation - parseCalculation(calcObj); - }); - }); } diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js index e99df529..c1a3886e 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -23,17 +23,16 @@ import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngi * id -> id dependencies for docs that rely on other docs directly * id -> variable deps for docs that rely on a variable's value * variable -> id deps for variables that are impacted by docs - * TODO: - * Depth first traversal or dependency graph to: - * Find loops in the dependency graph - * resolve variables in dependency order + */ + +/** + * Forseen issues: Anything that computes during the build step will not obey + * computed toggles */ /** * TODO - * compute slots spaces left (after computed field of quantityExpected) - * compute damage multipliers - * compute dependencyGraph variables and properties + * compute class levels */ export default function buildCreatureComputation(creatureId){ @@ -48,6 +47,8 @@ export default function buildCreatureComputation(creatureId){ // The graph includes all dependencies even of inactive properties // such that any properties changing without changing their dependencies // can limit the recompute to connected parts of the graph + // Each node's data represents a prop or a virtual prop like a variable + // Each link's data: {type: String, data: Object, requiresComputation: Boolean} const dependencyGraph = createGraph(); const computation = { @@ -102,8 +103,8 @@ export default function buildCreatureComputation(creatureId){ // Graph functions that rely on the props being stored first properties.forEach(prop => { - linkTypeDependencies(dependencyGraph, prop, computation.propsById); - linkCalculationDependencies(dependencyGraph, prop, computation.propsById); + linkTypeDependencies(dependencyGraph, prop, computation); + linkCalculationDependencies(dependencyGraph, prop, computation); }); return computation; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js deleted file mode 100644 index 96446353..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js +++ /dev/null @@ -1,13 +0,0 @@ - -/** - * Iterate through all the defining properties and choose the highest - * `baseValue.value` - */ -export default function aggregateBaseValue({node, linkedNode, link}){ - if (link.data !== 'definition') return; - const propBaseValue = linkedNode.data.baseValue?.value; - if (propBaseValue === undefined) return; - if (node.baseValue === undefined || propBaseValue > node.baseValue){ - node.baseValue = propBaseValue; - } -} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js deleted file mode 100644 index e792c5b7..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import definitions from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js'; -import baseValue from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js'; -import damageMultipliers from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDamageMultipliers.js'; -import effects from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateEffects.js'; -import proficiencies from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; -import skills from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; -import toggles from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateToggles.js'; - -export default Object.freeze({ - definitions, - baseValue, - damageMultipliers, - effects, - proficiencies, - skills, - toggles, -}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js new file mode 100644 index 00000000..4e3b4b3f --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js @@ -0,0 +1,10 @@ +import _variable from './computeByType/computeVariable.js'; +import action from './computeByType/computeAction.js'; +import slot from './computeByType/computeSlot.js'; + +export default Object.freeze({ + _variable, + action, + attack: action, + slot, +}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js new file mode 100644 index 00000000..f8d201b4 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js @@ -0,0 +1,21 @@ +import computeResources from './computeAction/computeResources.js'; + +export default function computeAction(graph, node, scope){ + const prop = node.data; + if (prop.uses){ + prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); + } + computeResources(graph, node, scope); + prop.resources.itemsConsumed.forEach(itemConsumed => { + if (!itemConsumed.itemId) return; + if (itemConsumed.available < itemConsumed.quantity.value){ + prop.insufficientResources = true; + } + }); + prop.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + if (attConsumed.available < attConsumed.quantity.value){ + prop.insufficientResources = true; + } + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js new file mode 100644 index 00000000..ccda0235 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js @@ -0,0 +1,10 @@ +export default function computeResources(graph, node, scope){ + const prop = node.data; + prop.resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + const att = scope[attConsumed.variableName]; + attConsumed.available = att.value; + attConsumed.statId = att._id; + attConsumed.statName = att.name; + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js new file mode 100644 index 00000000..66c9e790 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js @@ -0,0 +1,6 @@ +export default function computSlot(graph, node){ + const prop = node.data; + if (prop.quantityExpected){ + prop.spaceLeft = prop.quantityExpected - prop.totalFilled; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js new file mode 100644 index 00000000..35352de6 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -0,0 +1,79 @@ +import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/aggregate/index.js'; +import computeVariableAsAttribute from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsAttribute.js'; +import computeVariableAsSkill from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsSkill.js'; +import computeVariableAsConstant from '/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js'; +import computeImplicitVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeImplicitVariable.js'; + +export default function computeVariable(graph, node, scope){ + if (!node.data) node.data = {}; + aggregateLinks(graph, node); + combineAggregations(node, scope); + if (node.definingProp){ + // Add the defining variable to the scope + scope[node.id] = node.definingProp + } else { + // Otherwise add an implicit variable to the scope + scope[node.id] = computeImplicitVariable(node, scope); + } +} + +function aggregateLinks(graph, node){ + graph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + if (!linkedNode.data) linkedNode.data = {}; + // Ignore inactive props + if (linkedNode.data.inactive) return; + // Apply all the aggregations + let arg = {node, linkedNode, link}; + aggregate.definition(arg); + aggregate.damageMultiplier(arg); + aggregate.effect(arg); + aggregate.proficiency(arg); + }, + true // enumerate only outbound links + ); +} + +function combineAggregations(node, scope){ + combineMultiplierAggregator(node); + node.overridenProps.forEach(prop => { + computeVariableProp(node, prop, scope); + }); + computeVariableProp(node, node.definingProp, scope); +} + +function computeVariableProp(node, prop, scope){ + if (prop.type === 'attribute'){ + computeVariableAsAttribute(node, prop, scope) + } else if (prop.type === 'skill'){ + computeVariableAsSkill(node, prop, scope) + } else if (prop.type === 'constant'){ + computeVariableAsConstant(node, prop, scope) + } +} + +function combineMultiplierAggregator(node){ + // get a reference to the aggregator + const aggregator = node.data.multiplierAggregator; + + // Combine + let value; + if (aggregator.immunityCount){ + value = 0; + } else if ( + aggregator.ressistanceCount && + !aggregator.vulnerabilityCount + ){ + value = 0.5; + } else if ( + !aggregator.ressistanceCount && + aggregator.vulnerabilityCount + ){ + value = 2; + } else { + value = 1; + } + + node.data.damageMultiplyValue = value; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js new file mode 100644 index 00000000..256a5c48 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js @@ -0,0 +1,26 @@ + +/** + * + */ +export default function aggregateDamageMultipliers({node, linkedNode, link}){ + if (link.data !== 'damageMultiplier') return; + const multiplierValue = linkedNode.data.value; + if (multiplierValue === undefined) return; + // Store an aggregator, its presence indicates damage multipliers target this + // variable + if (!node.data.multiplierAggregator) node.data.multiplierAggregator = { + immunityCount: 0, + resistanceCount: 0, + vulnerabilityCount: 0, + } + // Store a short reference to the aggregator + const aggregator = node.data.multiplierAggregator; + // Sum the counts of each type of multiplier + if (multiplierValue === 0){ + aggregator.immunityCount += 1; + } else if (multiplierValue === 0.5){ + aggregator.resistanceCount += 1; + } else if (multiplierValue === 2){ + aggregator.vulnerabilityCount += 1; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js similarity index 57% rename from app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js rename to app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index e21a4d14..b4fabd7f 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -1,7 +1,9 @@ -export default function aggregateDefinitions({node, linkedNode, link}){ +export default function aggregateDefinition({node, linkedNode, link}){ // Look at all definition links if (link.data !== 'definition') return; + + // Store which property is THE defining property and which are overriden const prop = linkedNode.data; // get current defining prop const definingProp = node.data.definingProp; @@ -14,11 +16,18 @@ export default function aggregateDefinitions({node, linkedNode, link}){ } else { overrideProp(prop, node); } + + // Aggregate the base value due to the defining properties + const propBaseValue = linkedNode.data.baseValue?.value; + if (propBaseValue === undefined) return; + if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ + node.data.baseValue = propBaseValue; + } } function overrideProp(prop, node){ if (!prop) return; prop.overriden = true; if (!node.data.overridenProps) node.data.overridenProps = []; - node.data.overridenProp.push(prop); + node.data.overridenProps.push(prop); } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js new file mode 100644 index 00000000..5d454957 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -0,0 +1,78 @@ +export default function aggregateEffect({node, linkedNode, link}){ + if (link.data !== 'effect') return; + // store the effect aggregator, its presence indicates that the variable is + // targeted by effects + if (!node.data.effectAggregator) node.data.effectAggregator = { + base: undefined, + add: 0, + mul: 1, + min: Number.NEGATIVE_INFINITY, + max: Number.POSITIVE_INFINITY, + advantage: 0, + disadvantage: 0, + passiveAdd: undefined, + fail: 0, + set: undefined, + conditional: [], + rollBonus: [], + }; + // get a shorter reference to the aggregator document + const aggregator = node.data.effectAggregator; + // Get the result of the effect + const result = linkedNode.data.amount?.value; + // Aggregate the effect based on its operation + switch(linkedNode.data.operation){ + case 'base': + // Take the largest base value + if (Number.isFinite(result)){ + if(Number.isFinite(aggregator.base)){ + aggregator.base = Math.max(aggregator.base, result); + } else { + aggregator.base = result; + } + } + break; + case 'add': + // Add all adds together + aggregator.add += result || 0; + break; + case 'mul': + // Multiply the muls together + aggregator.mul *= result || 1; + break; + case 'min': + // Take the largest min value + aggregator.min = result > aggregator.min ? result : aggregator.min; + break; + case 'max': + // Take the smallest max value + aggregator.max = result < aggregator.max ? result : aggregator.max; + break; + case 'set': + // Take the highest set value + aggregator.set = aggregator.set === undefined || (result > aggregator.set) ? + result : + aggregator.set; + break; + case 'advantage': + // Sum number of advantages + aggregator.advantage++; + break; + case 'disadvantage': + // Sum number of disadvantages + aggregator.disadvantage++; + break; + case 'passiveAdd': + // Add all passive adds together + aggregator.passiveAdd = (aggregator.passiveAdd || 0) + result; + break; + case 'fail': + // Sum number of fails + aggregator.fail++; + break; + case 'conditional': + // Store array of conditionals + aggregator.conditional.push(linkedNode.data.text); + break; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js new file mode 100644 index 00000000..ea4383e6 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js @@ -0,0 +1,21 @@ +export default function aggregateProficiency({node, linkedNode, link}){ + if ( + link.data !== 'proficiency' && + !(link.data === 'definition' && linkedNode.data.type === 'skill') + ) return; + let proficiency; + if (link.data === 'proficiency'){ + proficiency = linkedNode.data.value || 0; + } else if (link.data === 'definition' && linkedNode.data.type === 'skill'){ + proficiency = linkedNode.data.baseProficiency || 0; + } else { + return; + } + // Store the highest proficiency + if ( + node.data.proficiency === undefined || + proficiency > node.data.proficiency + ){ + node.data.proficiency = proficiency; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js new file mode 100644 index 00000000..5cb17b1c --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js @@ -0,0 +1,11 @@ +import definition from './aggregateDefinition.js'; +import damageMultiplier from './aggregateDamageMultiplier.js'; +import effect from './aggregateEffect.js'; +import proficiency from './aggregateProficiency.js'; + +export default Object.freeze({ + definition, + damageMultiplier, + effect, + proficiency, +}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js new file mode 100644 index 00000000..5d1d365c --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -0,0 +1,35 @@ +import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js'; + +/* + * Variables with effects, proficiencies, or damage multipliers but no defining + * properties are added to the scope as implicit variables + */ + export default function computeImplicitVariable(node){ + const prop = {}; + const result = getAggregatorResult(node); + prop.total = result; + prop.value = result; + prop.proficiency = node.data.proficiency; + + // denormalise the aggregator fields + const aggregator = node.data.effectAggregator; + if (aggregator.advantage && !aggregator.disadvantage){ + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage){ + prop.advantage = -1; + } else { + prop.advantage = 0; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; + + return prop; + } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js new file mode 100644 index 00000000..93b1aefe --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -0,0 +1,25 @@ +import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js'; + +export default function computeVariableAsAttribute(node, prop, scope){ + let result = getAggregatorResult(node); + prop.total = result; + prop.value = prop.total - (prop.damage || 0); + + // Proficiency + prop.proficiency = node.data.proficiency; + + // Ability scores get modifiers + if (prop.attributeType === 'ability'){ + prop.modifier = Math.floor((prop.currentValue - 10) / 2); + } + + // Hit dice denormalise constitution modifier + if (prop.attributeType === 'hitDice') { + prop.constitutionMod = scope['constitution']?.modifier || 0; + } + + // Stats that have no effects or base value can be hidden + prop.hide = !node.data.effectAggregator && + prop.baseValue === undefined || + undefined +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js new file mode 100644 index 00000000..c9070937 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js @@ -0,0 +1,13 @@ +import { parse } from '/imports/parser/parser.js'; + +export default function computeVariableAsConstant(node, prop){ + let string = prop.calculation; + if (!string) return; + let parseNode; + try { + parseNode = parse(string); + } catch (e) { + return; + } + prop.value = parseNode; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js new file mode 100644 index 00000000..e9ae5ae1 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -0,0 +1,63 @@ +export default function computeVariableAsSkill(node, prop, scope){ + // Skills are based on some ability Modifier + let ability = scope[prop.ability]; + prop.abilityMod = ability?.modifier || 0; + // TODO: Use this ability's skill effects/profs iff this skill is not a save + + // Proficiency + prop.proficiency = node.data.proficiency; + + // Get the character's proficiency bonus to apply + let profBonus = scope['proficiencyBonus']?.value || 0; + + // Multiply the proficiency bonus by the actual proficiency + if(prop.proficiency === 0.49){ + // Round down proficiency bonus in the special case + profBonus = Math.floor(profBonus * 0.5); + } else { + profBonus = Math.ceil(profBonus * prop.proficiency); + } + + // Combine everything to get the final result + const statBase = node.data.baseValue; + const aggregator = node.data.effectAggregator; + + // If there is no aggregator, determine if the prop can hide, then exit + if (!aggregator){ + prop.hide = statBase === undefined && + prop.proficiency == 0 || + undefined; + prop.value = statBase; + return; + } + // Combine aggregator + const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0; + let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul; + if (result < aggregator.min) result = aggregator.min; + if (result > aggregator.max) result = aggregator.max; + if (aggregator.set !== undefined) { + result = aggregator.set; + } + if (Number.isFinite(result)){ + result = Math.floor(result); + } + prop.value = result; + // Advantage/disadvantage + if (aggregator.advantage && !aggregator.disadvantage){ + prop.advantage = 1; + } else if (aggregator.disadvantage && !aggregator.advantage){ + prop.advantage = -1; + } else { + prop.advantage = 0; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js new file mode 100644 index 00000000..38c38a2a --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js @@ -0,0 +1,42 @@ +import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; + +export default function getAggregatorResult(node){ + // Work out the base value as the greater of the deining stat value or + // the damage multiplier value + let statBase = node.data.baseValue; + const damageMultiplyValue = node.data.damageMultiplyValue; + if (statBase === undefined || damageMultiplyValue > statBase){ + statBase = damageMultiplyValue; + } + // get a reference to the aggregator + const aggregator = node.data.effectAggregator; + + // Without effects just return the defining base value + if (!aggregator) return statBase; + + let base; + if (!Number.isFinite(aggregator.base)){ + base = statBase || 0; + } else if (!Number.isFinite(statBase)){ + base = aggregator.base || 0; + } else { + base = Math.max(aggregator.base, statBase); + } + let result = (base + aggregator.add) * aggregator.mul; + if (result < aggregator.min) { + result = aggregator.min; + } + if (result > aggregator.max) { + result = aggregator.max; + } + if (aggregator.set !== undefined) { + result = aggregator.set; + } + if (!node.definingProp?.decimal && Number.isFinite(result)){ + result = Math.floor(result); + } else if (Number.isFinite(result)){ + result = stripFloatingPointOddities(result); + } + + return result; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js new file mode 100644 index 00000000..9bd83f04 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -0,0 +1,33 @@ +import { CompilationContext } from '/imports/parser/parser.js'; +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; + +export default function computeCalculations(node, scope){ + // evaluate all the calculations + node.data._computationDetails?.calculations?.forEach(calcObj => { + evaluateCalculation(calcObj, scope) + }); + node.data._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { + embedInlineCalculations(inlineCalcObj); + }); +} + +function evaluateCalculation(calculation, scope){ + const context = new CompilationContext(); + const parseNode = calculation._parsedCalculation; + const fn = calculation._parseLevel || 'reduce'; + const calculationScope = {...calculation._localScope, ...scope}; + const result = parseNode[fn](calculationScope, context); + calculation.value = result; + calculation.errors = context.errors; +} + +function embedInlineCalculations(inlineCalcObj){ + const string = inlineCalcObj.text; + const calculations = inlineCalcObj.inlineCalculations; + if (!string || !calculations) return; + let index = 0; + inlineCalcObj.value = string.replace(INLINE_CALCULATION_REGEX, substring => { + let calc = calculations[index++]; + return (calc && 'value' in calc) ? calc.value : substring; + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js new file mode 100644 index 00000000..d9f0cb26 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js @@ -0,0 +1,12 @@ +export default function evaluateToggles(node){ + let prop = node.data; + let toggles = prop._computationDetails?.toggleAncestors; + if (!toggles) return; + toggles.forEach(toggle => { + if (prop.inactive || !toggle.condition) return; + if (!toggle.condition.value){ + prop.inactive = true; + prop.deactivatedByToggle = true; + } + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js deleted file mode 100644 index 486fc817..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js +++ /dev/null @@ -1,33 +0,0 @@ -import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js'; - -export default function computeVariable(graph, node){ - if (!node.data) node.data = {}; - aggregateLinks(graph, node); -} - -function aggregateLinks(graph, node){ - let definingProp; - let overridenProps = []; - graph.forEachLinkedNode( - node.id, - (linkedNode, link) => { - if (!linkedNode.data) linkedNode.data = {}; - // Ignore inactive props - if (linkedNode.data.inactive) return; - // Apply all the aggregations - let arg = {node, linkedNode, link}; - aggregate.definitions(arg); - aggregate.baseValue(arg); - aggregate.damageMultipliers(arg); - aggregate.effects(arg); - aggregate.proficiencies(arg); - aggregate.skills(arg); - aggregate.toggles(arg); - }, - true // enumerate only outbound links - ); - // store the defining and overriden props on the node - if (!node.data) node.data = {}; - node.data.definingProp = definingProp; - node.data.overridenProps = overridenProps; -} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js b/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js deleted file mode 100644 index 81217078..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js +++ /dev/null @@ -1,11 +0,0 @@ -import { CompilationContext } from '/imports/parser/parser.js'; - -export default function evaluateCalculation(calculation, scope){ - const context = new CompilationContext(); - const parseNode = calculation._parsedCalculation; - const fn = calculation._parseLevel || 'reduce'; - const calculationScope = {...calculation._localScope, ...scope}; - const result = parseNode[fn](calculationScope, context); - calculation.value = result; - calculation.errors = context.errors; -} diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index e79620bb..e9f670b1 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -1,5 +1,6 @@ -import evaluateCalculation from '/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js'; -import computeVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js'; +import computeCalculations from '/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js'; +import computeToggles from '/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js'; +import computeByType from '/imports/api/creature/computation/newEngine/computeComputation/computeByType.js'; export default function computeCreatureComputation(computation){ const stack = []; @@ -16,20 +17,16 @@ export default function computeCreatureComputation(computation){ while (stack.length){ let top = stack[stack.length - 1]; if (top.visited){ - // The object has already + // The object has already been computed, skip stack.pop(); } else if (top.visitedChildren){ // Compute the top object of the stack compute(graph, top.node, scope); - // If the node holds a variable, store it in the scope - if (!top.node.data?.type){ - scope[top.node.id] = top.node.data; - } // Mark the object as visited and remove from stack top.visited = true; stack.pop(); } else { - // Push children to graph + // Push dependencies to graph to be computed first pushDependenciesToStack(top.node.id, graph, stack); top.visitedChildren = true; } @@ -37,32 +34,20 @@ export default function computeCreatureComputation(computation){ } function compute(graph, node, scope){ - // Get the property - let prop = node.data; - - // evaluate all the calculations - if (prop?._computationDetails?.calculations){ - prop._computationDetails.calculations.forEach(calcObj => { - evaluateCalculation(calcObj, scope) - }); - } - + // Determine the prop's active status by its toggles + computeToggles(node); + computeCalculations(node, scope); // Compute the property by type - let typeCompute = propTypeComputations[prop?.type || '_variable']; - typeCompute?.(graph, node); + computeByType[node.data?.type || '_variable']?.(graph, node, scope); } -var propTypeComputations = { - '_variable': computeVariable, -}; - function pushDependenciesToStack(nodeId, graph, stack){ graph.forEachLinkedNode( nodeId, (linkedNode, link) => { - // Ignore inventory links, they can't cause dependency loops - // and are already fully computed when they are created - if (link.data === 'inventory') return; + // Ignore inventory links, they are already fully computed when they are + // created + if (link.data === 'inventory' || link.data === 'classLevel') return; stack.push({ node: linkedNode, visited: false, diff --git a/app/imports/ui/utility/stripFloatingPointOddities.js b/app/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js similarity index 100% rename from app/imports/ui/utility/stripFloatingPointOddities.js rename to app/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index aff521f4..0f4591fb 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -78,15 +78,6 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ optional: true, index: 1, }, - // Denormalised list of all properties or creatures this property depends on - dependencies: { - type: Array, - defaultValue: [], - index: 1, - }, - 'dependencies.$': { - type: String, - }, }); CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 897d2139..86453014 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -90,6 +90,11 @@ const ComputedOnlyActionSchema = createPropertySchema({ type: 'computedOnlyField', optional: true, }, + // Uses - usesUsed + usesLeft: { + type: Number, + optional: true, + }, }); const ComputedActionSchema = new SimpleSchema() diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 1913b8ee..b4d98161 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -107,6 +107,12 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ type: SimpleSchema.Integer, optional: true, }, + // Attributes with proficiency grant it to all skills based on the attribute + proficiency: { + type: Number, + allowedValues: [0.49, 0.5, 1, 2], + optional: true, + }, // The computed creature constitution modifier for hit dice constitutionMod: { type: Number, diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index 8731a591..c3721458 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -34,6 +34,12 @@ let EffectSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, + // Conditional benefits store just uncomputed text + text: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, //which stats the effect is applied to stats: { type: Array, diff --git a/app/imports/api/properties/SavingThrows.js b/app/imports/api/properties/SavingThrows.js index 23a7bbf2..4c96b558 100644 --- a/app/imports/api/properties/SavingThrows.js +++ b/app/imports/api/properties/SavingThrows.js @@ -36,6 +36,7 @@ let SavingThrowSchema = createPropertySchema({ const ComputedOnlySavingThrowSchema = createPropertySchema({ dc: { type: 'computedOnlyField', + parseLevel: 'compile', optional: true, }, }); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index ef9bc852..71846ae1 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -46,6 +46,7 @@ let SkillSchema = createPropertySchema({ baseProficiency: { type: Number, optional: true, + allowedValues: [0.49, 0.5, 1, 2], }, // The starting value, before effects baseValue: { @@ -93,7 +94,7 @@ let ComputedOnlySkillSchema = createPropertySchema({ // Computed proficiency multiplier proficiency: { type: Number, - allowedValues: [0, 0.5, 1, 2], + allowedValues: [0, 0.49, 0.5, 1, 2], defaultValue: 0, }, // Compiled text of all conditional benefits @@ -103,15 +104,6 @@ let ComputedOnlySkillSchema = createPropertySchema({ }, 'conditionalBenefits.$': { type: String, - }, - // Compiled text of all roll bonuses - rollBonuses: { - type: Array, - optional: true, - maxCount: STORAGE_LIMITS.rollBonusCount, - }, - 'rollBonuses.$': { - type: String, }, // Computed number of things forcing this skill to fail fail: { diff --git a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js index 63f16635..9acc8931 100644 --- a/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/AttributeConsumedSchema.js @@ -23,6 +23,10 @@ const AttributeConsumedSchema = createPropertySchema({ }); const ComputedOnlyAttributeConsumedSchema = createPropertySchema({ + quantity: { + type: 'computedOnlyField', + optional: true, + }, available: { type: Number, optional: true, @@ -37,10 +41,6 @@ const ComputedOnlyAttributeConsumedSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, - quantity: { - type: 'computedOnlyField', - optional: true, - }, }); const ComputedAttributeConsumedSchema = new SimpleSchema() diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index 42eeb402..1bf08904 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -36,13 +36,6 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({ type: 'computedOnlyField', optional: true, }, - // This appears both in the computed and uncomputed schema because it can be - // set by both a computation or a form - itemId: { - type: String, - regEx: SimpleSchema.RegEx.Id, - optional: true, - }, itemName: { type: String, max: STORAGE_LIMITS.name, diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js index b65a5523..8f5e4bb9 100644 --- a/app/imports/parser/parseTree/SymbolNode.js +++ b/app/imports/parser/parseTree/SymbolNode.js @@ -9,10 +9,14 @@ export default class SymbolNode extends ParseNode { toString(){ return `${this.name}` } - compile(scope){ + compile(scope, context){ let value = scope && scope[this.name]; let type = typeof value; - // For objects, get their value + // For parse nodes, compile and return + if (value instanceof ParseNode){ + return value.compile(scope, context); + } + // For objects, default to their .value if (type === 'object'){ value = value.value; type = typeof value; From 8f93179187a164dd51f1b1ce8cf6f4f8b05a5d5c Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 14 Sep 2021 16:18:36 +0200 Subject: [PATCH 14/85] Moved inventory computation to after toggles, added class levels computation --- .../buildComputation/computeInactiveStatus.js | 7 +- .../buildComputation/computeInventory.js | 98 ------------------- .../computeSlotQuantityFilled.js | 1 + .../buildComputation/linkInventory.js | 59 +++++++++++ .../buildComputation/linkTypeDependencies.js | 15 ++- .../newEngine/buildCreatureComputation.js | 19 ++-- .../computeByType/computeVariable.js | 7 +- .../aggregate/aggregateClassLevel.js | 15 +++ .../aggregate/aggregateInventory.js | 60 ++++++++++++ .../computeVariable/aggregate/index.js | 6 +- .../computeImplicitVariable.js | 44 +++++---- .../computeVariableAsAttribute.js | 2 +- .../computeVariable/computeVariableAsClass.js | 12 +++ .../computeComputation/computeCalculations.js | 3 +- .../newEngine/computeCreatureComputation.js | 7 +- app/imports/api/properties/ClassLevels.js | 8 +- app/imports/api/properties/Classes.js | 91 +++++++++++++++++ app/imports/api/properties/Slots.js | 42 ++++---- .../computedOnlyPropertySchemasIndex.js | 2 + .../computedPropertySchemasIndex.js | 2 + .../api/properties/propertySchemasIndex.js | 2 + app/imports/constants/PROPERTIES.js | 8 +- app/imports/constants/STORAGE_LIMITS.js | 3 + 23 files changed, 351 insertions(+), 162 deletions(-) delete mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js create mode 100644 app/imports/api/properties/Classes.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js index fdf651d8..ba6acda9 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js @@ -3,8 +3,11 @@ import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdo export default function computeInactiveStatus(node){ const prop = node.node; if (isActive(prop)) return; - prop.inactive = true; - prop.deactivatedBySelf = true; + // Unequipped items disable their children, but are not disabled themselves + if (prop.type !== 'item'){ + prop.inactive = true; + prop.deactivatedBySelf = true; + } // Mark children as inactive due to ancestor walkDown(node.children, child => { child.node.inactive = true; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js deleted file mode 100644 index c97f92d4..00000000 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Performs a depth first traversal of the character tree, summing the container - * and inventory contents on the way up the tree - */ -export default function computeInventory(forest, dependencyGraph){ - const data = { - weightTotal: 0, - weightEquipment: 0, - weightCarried: 0, - valueTotal: 0, - valueEquipment: 0, - valueCarried: 0, - itemsAttuned: 0, - }; - // The stack of properties to still navigate - const stack = [...forest]; - // The current containers we are inside of - const containerStack = []; - - while(stack.length){ - const top = stack[stack.length - 1]; - const prop = top.node; - if (prop._computationDetails.inventoryChildrenVisited){ - stack.pop(); - handleProp(prop, containerStack, data, dependencyGraph); - } else { - // Add all containers to the stack when we first visit them - if (prop.type === 'container'){ - containerStack.push(top.node); - setDefaultContainerData(prop); - } - // Push children onto the stack and mark this as children are visited - stack.push(...top.children); - prop._computationDetails.inventoryChildrenVisited = true; - } - } - // Store all the computed values on the dependency graph variables - for (let key in data){ - dependencyGraph.addNode(key, {engineValue: data[key]}); - } -} - -function setDefaultContainerData(container){ - container.contentsWeight = 0; - container.carriedWeight = 0; - container.contentsValue = 0; - container.carriedValue = 0; -} - -function handleProp(prop, containerStack, data, dependencyGraph){ - // Determine if this property is carried, items are carried by default - let carried = prop.type === 'container' ? prop.carried : true; - - // Weight and value for this property - const weight = (prop.weight || 0) + (prop.contentsWeight || 0); - const carriedWeight = (prop.weight || 0) + (prop.carriedWeight || 0); - const value = (prop.value || 0) + (prop.value || 0); - const carriedValue = (prop.value || 0) + (prop.carriedValue || 0); - - // Sum the item-specific data - if (prop.type === 'item'){ - dependencyGraph.addLink('itemsAttuned', prop._id, 'inventory'); - if (prop.attuned) data.itemsAttuned += 1; - if (prop.equipped){ - dependencyGraph.addLink('weightEquipment', prop._id, 'inventory'); - data.weightEquipment += weight; - dependencyGraph.addLink('valueEquipment', prop._id, 'inventory'); - data.valueEquipment += value; - } - } - - // Get the parent container - const container = containerStack[containerStack.length - 1]; - - if (container){ - // The container depends on this prop for its contents data - dependencyGraph.addLink(container._id, prop._id, 'inventory'); - // Add this property's weights and values to the container - if (!container.weightless){ - container.contentsWeight += weight; - if (carried) container.carriedWeight += carriedWeight; - } - container.contentsValue += value; - if (carried) container.carriedValue += carriedValue; - } else { - // There is no parent container, add weights/value to the character data - dependencyGraph.addLink('weightTotal', prop._id, 'inventory'); - data.weightTotal += weight; - dependencyGraph.addLink('valueTotal', prop._id, 'inventory'); - data.valueTotal += value; - if (carried){ - dependencyGraph.addLink('weightCarried', prop._id, 'inventory'); - data.weightCarried += carriedWeight; - dependencyGraph.addLink('valueCarried', prop._id, 'inventory'); - data.valueCarried += carriedValue; - } - } -} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js index 7dd1694d..b44f2681 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js @@ -4,6 +4,7 @@ */ export default function computeSlotQuantityFilled(node, dependencyGraph){ let slot = node.node; + if (slot.type !== 'propertySlot' || slot.type !== 'characterClass') return; slot.totalFilled = 0; node.children.forEach(child => { let childProp = child.node; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js new file mode 100644 index 00000000..d0b0f738 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js @@ -0,0 +1,59 @@ +/** + * Performs a depth first traversal of the character tree, summing the container + * and inventory contents on the way up the tree + */ +export default function linkInventory(forest, dependencyGraph){ + // The stack of properties to still navigate + const stack = [...forest]; + // The current containers we are inside of + const containerStack = []; + + while(stack.length){ + const top = stack[stack.length - 1]; + const prop = top.node; + if (prop._computationDetails.inventoryChildrenVisited){ + stack.pop(); + handleProp(prop, containerStack, dependencyGraph); + } else { + // Add all containers to the stack when we first visit them + if (prop.type === 'container'){ + containerStack.push(top.node); + } + // Push children onto the stack and mark this as children are visited + stack.push(...top.children); + prop._computationDetails.inventoryChildrenVisited = true; + } + } +} + +function handleProp(prop, containerStack, dependencyGraph){ + // Determine if this property is carried, items are carried by default + let carried = prop.type === 'container' ? prop.carried : true; + + // Item-specific links + if (prop.type === 'item'){ + if (prop.attuned){ + dependencyGraph.addLink('itemsAttuned', prop._id, 'attunedItem'); + } + if (prop.equipped){ + dependencyGraph.addLink('weightEquipment', prop._id, 'equippedItem'); + dependencyGraph.addLink('valueEquipment', prop._id, 'equippedItem'); + } + } + + // Get the parent container + const container = containerStack[containerStack.length - 1]; + + if (container){ + // The container depends on this prop for its contents data + dependencyGraph.addLink(container._id, prop._id, 'containerContents'); + } else { + // There is no parent container, the character totals depend on this prop + dependencyGraph.addLink('weightTotal', prop._id, 'inventoryStats'); + dependencyGraph.addLink('valueTotal', prop._id, 'inventoryStats'); + if (carried){ + dependencyGraph.addLink('weightCarried', prop._id, 'inventoryStats'); + dependencyGraph.addLink('valueCarried', prop._id, 'inventoryStats'); + } + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index ad3b231a..a16ad83c 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -2,7 +2,8 @@ const linkDependenciesByType = { action: linkResources, attack: linkResources, attribute: linkAttribute, - classLevel: linkVariableName, + characterClass: linkVariableName, + classLevel: linkClassLevel, constant: linkVariableName, damageMultiplier: linkDamageMultiplier, proficiency: linkStats, @@ -15,6 +16,18 @@ export default function linkTypeDependencies(dependencyGraph, prop){ linkDependenciesByType[prop.type]?.(prop); } +function linkClassLevel(dependencyGraph, prop){ + // The variableName of the prop depends on the prop + if (prop.variableName && prop.level){ + dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel'); + // The level variable depends on the class variableName variable + let existingLevelLink = dependencyGraph.getLink('level', prop.variableName); + if (!existingLevelLink){ + dependencyGraph.addLink('level', prop.variableName, 'level'); + } + } +} + function linkVariableName(dependencyGraph, prop){ // The variableName of the prop depends on the prop if (prop.variableName){ diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js index c1a3886e..65a20ce0 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -7,7 +7,7 @@ import computedSchemas from '/imports/api/properties/computedPropertySchemasInde import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; import { cloneDeep, unset } from 'lodash'; import createGraph from 'ngraph.graph'; -import computeInventory from '/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js'; +import linkInventory from '/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js'; import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; import parseCalculationFields from '/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js'; import computeInactiveStatus from '/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js'; @@ -36,19 +36,26 @@ import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngi */ export default function buildCreatureComputation(creatureId){ - let properties = CreatureProperties.find({ + const properties = getProperties(creatureId); + return buildComputationFromProps(properties); +} + +function getProperties(creatureId){ + return CreatureProperties.find({ 'ancestors.id': creatureId, 'removed': {$ne: true}, }, { sort: {order: 1} }); +} +export function buildComputationFromProps(properties){ // Dependency graph where edge(a, b) means a depends on b // The graph includes all dependencies even of inactive properties // such that any properties changing without changing their dependencies // can limit the recompute to connected parts of the graph // Each node's data represents a prop or a virtual prop like a variable - // Each link's data: {type: String, data: Object, requiresComputation: Boolean} + // Each link's data is a string representing the link type const dependencyGraph = createGraph(); const computation = { @@ -86,7 +93,7 @@ export default function buildCreatureComputation(creatureId){ }; // Parse all the calculations - parseCalculationFields(prop, computedSchemas) + parseCalculationFields(prop, computedSchemas); }); // Get all the properties as trees based on their ancestors @@ -98,8 +105,8 @@ export default function buildCreatureComputation(creatureId){ computeSlotQuantityFilled(node); }); - // Compute the inventory - computeInventory(forest, dependencyGraph); + // Link the inventory dependencies + linkInventory(forest, dependencyGraph); // Graph functions that rely on the props being stored first properties.forEach(prop => { diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index 35352de6..c505e81b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -2,6 +2,7 @@ import aggregate from '/imports/api/creature/computation/newEngine/computeComput import computeVariableAsAttribute from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsAttribute.js'; import computeVariableAsSkill from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsSkill.js'; import computeVariableAsConstant from '/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js'; +import computeVariableAsClass from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeVariableAsClass.js'; import computeImplicitVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeImplicitVariable.js'; export default function computeVariable(graph, node, scope){ @@ -26,9 +27,11 @@ function aggregateLinks(graph, node){ if (linkedNode.data.inactive) return; // Apply all the aggregations let arg = {node, linkedNode, link}; - aggregate.definition(arg); + aggregate.classLevel(arg); aggregate.damageMultiplier(arg); + aggregate.definition(arg); aggregate.effect(arg); + aggregate.inventory(arg); aggregate.proficiency(arg); }, true // enumerate only outbound links @@ -50,6 +53,8 @@ function computeVariableProp(node, prop, scope){ computeVariableAsSkill(node, prop, scope) } else if (prop.type === 'constant'){ computeVariableAsConstant(node, prop, scope) + } else if (prop.type === 'characterClass'){ + computeVariableAsClass(node, prop, scope) } } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js new file mode 100644 index 00000000..84db29b2 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js @@ -0,0 +1,15 @@ +export default function aggregateClassLevel({node, linkedNode, link}){ + if (link.data === 'classLevel'){ + if (node.data.inactive) return; + if (!node.data.classLevelAggregator) node.data.classLevelAggregator = { + levelsFilled: [true], // Level 0 is always filled + level: 0, + }; + let linkedProp = linkedNode.data; + let aggregator = node.data.classLevelAggregator; + if (linkedProp.level > aggregator.level) aggregator.level = linkedProp.level; + aggregator.levelsFilled[linkedProp.level] = true; + } else if (link.data === 'level'){ + node.baseValue = (node.baseValue || 0) + node.data.classLevelAggregator.level; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js new file mode 100644 index 00000000..e2df5384 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js @@ -0,0 +1,60 @@ +export default function aggregateInventory({node, linkedNode, link}){ + let linkedProp = linkedNode.data || {}; + const prop = node.data; + + switch (link.data){ + case 'attunedItem': + prop.baseValue = (prop.baseValue || 0) + 1; + return; + + case 'equippedItem': + if (node.id === 'weightEquipment'){ + prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); + } else if (node.id === 'valueEquipment'){ + prop.baseValue = (prop.baseValue || 0) + value(linkedProp); + } + return; + + case 'containerContents': + // Add this property's weights and values to the container + if (!prop.weightless){ + prop.contentsWeight = (prop.contentsWeight || 0) + weight(linkedProp); + if (prop.carried){ + prop.carriedWeight = (prop.carriedWeight || 0) + carriedWeight(linkedProp); + } + } + prop.contentsValue = (prop.contentsValue || 0) + value(linkedProp); + if (prop.carried) { + prop.carriedValue = (prop.carriedValue || 0) + carriedValue(linkedProp); + } + return; + + case 'inventoryStats': + if (node.id === 'weightTotal'){ + prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); + } else if (node.id === 'valueTotal'){ + prop.baseValue = (prop.baseValue || 0) + value(linkedProp); + } else if (node.did === 'weightCarried'){ + prop.baseValue = (prop.baseValue || 0) + carriedWeight(linkedProp); + } else if (node.did === 'valueCarried'){ + prop.carriedValue = (prop.carriedValue || 0) + carriedValue(linkedProp); + } + return; + } +} + +function weight(prop){ + return (prop.weight || 0) + (prop.contentsWeight || 0); +} + +function carriedWeight(prop){ + return (prop.weight || 0) + (prop.carriedWeight || 0); +} + +function value (prop){ + return (prop.value || 0) + (prop.value || 0); +} + +function carriedValue (prop){ + return (prop.value || 0) + (prop.carriedValue || 0); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js index 5cb17b1c..4b455afa 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js @@ -2,10 +2,14 @@ import definition from './aggregateDefinition.js'; import damageMultiplier from './aggregateDamageMultiplier.js'; import effect from './aggregateEffect.js'; import proficiency from './aggregateProficiency.js'; +import classLevel from './aggregateClassLevel.js'; +import inventory from './aggregateInventory.js'; export default Object.freeze({ - definition, + classLevel, damageMultiplier, + definition, effect, + inventory, proficiency, }); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js index 5d1d365c..1f15ff23 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -11,25 +11,33 @@ import getAggregatorResult from '/imports/api/creature/computation/newEngine/com prop.value = result; prop.proficiency = node.data.proficiency; - // denormalise the aggregator fields - const aggregator = node.data.effectAggregator; - if (aggregator.advantage && !aggregator.disadvantage){ - prop.advantage = 1; - } else if (aggregator.disadvantage && !aggregator.advantage){ - prop.advantage = -1; - } else { - prop.advantage = 0; + // denormalise class level aggregator + let classLevelAgg = node.data.classLevelAggregator; + if (classLevelAgg){ + prop.level = classLevelAgg.level; + } + + // denormalise the effect aggregator fields + 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; + } + // Passive bonus + prop.passiveBonus = aggregator.passiveAdd; + // conditional benefits + prop.conditionalBenefits = aggregator.conditional; + // Roll bonuses + prop.rollBonus = aggregator.rollBonus; + // Forced to fail + prop.fail = aggregator.fail; + // Rollbonus + prop.rollBonuses = aggregator.rollBonus; } - // Passive bonus - prop.passiveBonus = aggregator.passiveAdd; - // conditional benefits - prop.conditionalBenefits = aggregator.conditional; - // Roll bonuses - prop.rollBonus = aggregator.rollBonus; - // Forced to fail - prop.fail = aggregator.fail; - // Rollbonus - prop.rollBonuses = aggregator.rollBonus; return prop; } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 93b1aefe..81f4c73b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -10,7 +10,7 @@ export default function computeVariableAsAttribute(node, prop, scope){ // Ability scores get modifiers if (prop.attributeType === 'ability'){ - prop.modifier = Math.floor((prop.currentValue - 10) / 2); + prop.modifier = Math.floor((prop.value - 10) / 2); } // Hit dice denormalise constitution modifier diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js new file mode 100644 index 00000000..d48839f2 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js @@ -0,0 +1,12 @@ +export default function computeVariableAsAttribute(node, prop){ + let classLevelAgg = node.data.classLevelAggregator; + if (!classLevelAgg) return; + prop.level = classLevelAgg.level; + classLevelAgg.levelsFilled.forEach((filled, index) => { + if (!filled){ + if (!prop.missingLevels) prop.missingLevels = []; + prop.missingLevels.push(index); + } + }); + prop.missingLevels?.sort((a, b) => a - b); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js index 9bd83f04..34a61124 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -16,8 +16,7 @@ function evaluateCalculation(calculation, scope){ const parseNode = calculation._parsedCalculation; const fn = calculation._parseLevel || 'reduce'; const calculationScope = {...calculation._localScope, ...scope}; - const result = parseNode[fn](calculationScope, context); - calculation.value = result; + calculation.value = parseNode[fn](calculationScope, context); calculation.errors = context.errors; } diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index e9f670b1..b5261550 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -4,7 +4,7 @@ import computeByType from '/imports/api/creature/computation/newEngine/computeCo export default function computeCreatureComputation(computation){ const stack = []; - // dict of computed nodes by id + // Computation scope of {variableName: prop} const scope = {}; const graph = computation.dependencyGraph; // Add all nodes to the stack @@ -44,10 +44,7 @@ function compute(graph, node, scope){ function pushDependenciesToStack(nodeId, graph, stack){ graph.forEachLinkedNode( nodeId, - (linkedNode, link) => { - // Ignore inventory links, they are already fully computed when they are - // created - if (link.data === 'inventory' || link.data === 'classLevel') return; + (linkedNode) => { stack.push({ node: linkedNode, visited: false, diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index 37224f12..850f9851 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -9,7 +9,6 @@ let ClassLevelSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, - // Only used by slot filling dialog, not computed description: { type: 'inlineCalculationFieldToCompute', optional: true, @@ -24,12 +23,7 @@ let ClassLevelSchema = createPropertySchema({ level: { type: SimpleSchema.Integer, defaultValue: 1, - }, - // Same as in SlotFillers.js - slotFillerCondition: { - type: String, - optional: true, - max: STORAGE_LIMITS.calculation, + max: STORAGE_LIMITS.levelMax, }, }); diff --git a/app/imports/api/properties/Classes.js b/app/imports/api/properties/Classes.js new file mode 100644 index 00000000..efa5ac17 --- /dev/null +++ b/app/imports/api/properties/Classes.js @@ -0,0 +1,91 @@ +import SimpleSchema from 'simpl-schema'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; + +// Classes are like slots, except they only take class levels and enforce that +// lower levels are taken before higher levels +let ClassSchema = createPropertySchema({ + name: { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + // Only `classLevel`s with the same variable name can fill the class + variableName: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + classType: { + type: String, + allowedValues: ['startingClass', 'multiClass'], + defaultValue: 'startingClass', + }, + // Same tag format as Slots to match library classLevels against + slotTags: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'slotTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + extraTags: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.extraTagsCount, + }, + 'extraTags.$': { + type: Object, + }, + 'extraTags.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'extraTags.$.operation': { + type: String, + allowedValues: ['OR', 'NOT'], + defaultValue: 'OR', + }, + 'extraTags.$.tags': { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'extraTags.$.tags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, + }); + +const ComputedOnlyClassSchema = createPropertySchema({ + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, + level: { + type: SimpleSchema.Integer, + optional: true, + }, + missingLevels: { + type: Array, + optional: true, + }, + 'missingLevels.$': { + type: SimpleSchema.Integer, + }, + }); + +const ComputedClassSchema = new SimpleSchema() + .extend(ClassSchema) + .extend(ComputedOnlyClassSchema); + +export { ClassSchema, ComputedOnlyClassSchema, ComputedClassSchema }; diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 1d388969..8fb7c986 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -17,44 +17,44 @@ let SlotSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.variableName, }, - slotTags: { + slotTags: { type: Array, - defaultValue: [], + defaultValue: [], maxCount: STORAGE_LIMITS.tagCount, }, - 'slotTags.$': { - type: String, + 'slotTags.$': { + type: String, max: STORAGE_LIMITS.tagLength, - }, - extraTags: { + }, + extraTags: { type: Array, - defaultValue: [], + defaultValue: [], maxCount: STORAGE_LIMITS.extraTagsCount, }, - 'extraTags.$': { - type: Object, - }, + 'extraTags.$': { + type: Object, + }, 'extraTags.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, autoValue(){ if (!this.isSet) return Random.id(); } - }, + }, 'extraTags.$.operation': { - type: String, + type: String, allowedValues: ['OR', 'NOT'], defaultValue: 'OR', - }, + }, 'extraTags.$.tags': { - type: Array, + type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.tagCount, - }, + }, 'extraTags.$.tags.$': { - type: String, + type: String, max: STORAGE_LIMITS.tagLength, - }, + }, quantityExpected: { type: 'fieldToCompute', optional: true, @@ -88,6 +88,10 @@ let SlotSchema = createPropertySchema({ const ComputedOnlySlotSchema = createPropertySchema({ // Computed fields + description: { + type: 'inlineCalculationFieldToCompute', + optional: true, + }, quantityExpected: { type: 'computedOnlyField', optional: true, @@ -109,7 +113,7 @@ const ComputedOnlySlotSchema = createPropertySchema({ }); const ComputedSlotSchema = new SimpleSchema() - .extend(ComputedOnlySlotSchema) - .extend(SlotSchema); + .extend(ComputedOnlySlotSchema) + .extend(SlotSchema); export { SlotSchema, ComputedSlotSchema, ComputedOnlySlotSchema }; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 09ce6265..9d929cc2 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -4,6 +4,7 @@ import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustment import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; +import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js'; @@ -31,6 +32,7 @@ const propertySchemasIndex = { attack: ComputedOnlyAttackSchema, attribute: ComputedOnlyAttributeSchema, buff: ComputedOnlyBuffSchema, + characterClass: ComputedOnlyClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, container: ComputedOnlyContainerSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 02366c05..f30cd1c2 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -4,6 +4,7 @@ import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js import { ComputedAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; +import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { ComputedContainerSchema } from '/imports/api/properties/Containers.js'; @@ -31,6 +32,7 @@ const propertySchemasIndex = { attack: ComputedAttackSchema, attribute: ComputedAttributeSchema, buff: ComputedBuffSchema, + characterClass: ComputedClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, damage: ComputedDamageSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 3ad0c4d9..00cc1644 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -4,6 +4,7 @@ import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js'; import { AttackSchema } from '/imports/api/properties/Attacks.js'; import { AttributeSchema } from '/imports/api/properties/Attributes.js'; import { BuffSchema } from '/imports/api/properties/Buffs.js'; +import { ClassSchema } from '/imports/api/properties/Classes.js'; import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { DamageSchema } from '/imports/api/properties/Damages.js'; @@ -31,6 +32,7 @@ const propertySchemasIndex = { attack: AttackSchema, attribute: AttributeSchema, buff: BuffSchema, + characterClass: ClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, damage: DamageSchema, diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index d1ee0bb7..38692a85 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -30,11 +30,17 @@ const PROPERTIES = Object.freeze({ helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.', suggestedParents: ['action', 'attack', 'savingThrow', 'spell'], }, + characterClass: { + icon: 'mdi-card-account-details', + name: 'Class', + helpText: 'Your character should ideally have one starting class. Classes hold class levels', + suggestedParents: ['class'], + }, classLevel: { icon: '$vuetify.icons.class_level', name: 'Class level', helpText: 'Class levels represent a single level gained in a class', - suggestedParents: ['class'], + suggestedParents: [], }, constant: { icon: 'mdi-anchor', diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index 065089ca..d3e4251e 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -13,6 +13,9 @@ const STORAGE_LIMITS = Object.freeze({ url: 256, variableName: 64, + // Number limits + levelMax: 128, + //Array counts ancestorCount: 100, damageTypeCount: 32, From 856fc41429b3668357fcd811c4a82581c4216ec7 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 14 Sep 2021 22:48:30 +0200 Subject: [PATCH 15/85] Began the long road of testing computations --- .../linkCalculationDependencies.js | 2 +- .../buildCreatureComputation.test.js | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js index d08fa7c2..4240f1ad 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -1,6 +1,6 @@ import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; -import findAncestorByType from 'imports/api/creature/computation/newEngine/utility/findAncestorByType.js'; +import findAncestorByType from '/imports/api/creature/computation/newEngine/utility/findAncestorByType.js'; export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js new file mode 100644 index 00000000..2df7e2ce --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js @@ -0,0 +1,22 @@ +import { buildComputationFromProps } from './buildCreatureComputation.js'; +import { assert } from 'chai'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; + +describe('buildComputation', function(){ + it('Builds something', function(){ + let computation = buildComputationFromProps(testProperties); + console.log(computation); + }); +}); + +var testProperties = [ + clean({ + _id: 'attributeId123', + type: 'attribute', + }), +]; + +function clean(prop){ + let schema = CreatureProperties.simpleSchema(prop); + return schema.clean(prop); +} From dfd7ad4af56d9919009999a7b5bc072e1f7771a4 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 15 Sep 2021 15:15:18 +0200 Subject: [PATCH 16/85] Got tests running on single property character --- .../computation/engine/combineStat.js | 2 +- .../buildComputation/CreatureComputation.js | 36 ++++++++++ .../linkCalculationDependencies.js | 2 +- .../buildComputation/linkInventory.js | 2 + .../buildComputation/linkTypeDependencies.js | 2 +- .../parseCalculationFields.js | 49 ++++++++++--- .../buildComputation/removeSchemaFields.js | 13 ++++ .../newEngine/buildCreatureComputation.js | 71 +++++-------------- .../buildCreatureComputation.test.js | 12 +++- .../computeByType/computeVariable.js | 19 ++--- .../computeImplicitVariable.js | 2 +- .../computeVariableAsAttribute.js | 3 +- .../computeComputation/computeCalculations.js | 3 +- .../computeComputation/computeToggles.js | 1 + .../newEngine/computeCreatureComputation.js | 32 +++------ .../computeCreatureComputation.test.js | 34 +++++++++ .../newEngine/utility/applyFnToKey.js | 17 +++-- .../newEngine/utility/applyFnToKey.test.js | 60 ++++++++++++++++ app/imports/api/properties/Actions.js | 1 + .../subSchemas/inlineCalculationField.js | 26 +++++-- .../server/2.0-beta.33-dbv1.test.js | 3 - .../characterSheetTabs/InventoryTab.vue | 2 +- .../components/inventory/ContainerCard.vue | 2 +- .../ui/properties/viewers/ItemViewer.vue | 2 +- 24 files changed, 277 insertions(+), 119 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js create mode 100644 app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js create mode 100644 app/imports/api/creature/computation/newEngine/utility/applyFnToKey.test.js diff --git a/app/imports/api/creature/computation/engine/combineStat.js b/app/imports/api/creature/computation/engine/combineStat.js index db612d78..f2c10e8f 100644 --- a/app/imports/api/creature/computation/engine/combineStat.js +++ b/app/imports/api/creature/computation/engine/combineStat.js @@ -1,7 +1,7 @@ import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js'; import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; import { union } from 'lodash'; export default function combineStat(stat, aggregator, memo){ diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js new file mode 100644 index 00000000..d4836b47 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js @@ -0,0 +1,36 @@ +import { cloneDeep } from 'lodash'; +import createGraph from 'ngraph.graph'; + +export default class CreatureComputation { + constructor(properties){ + // Set up fields + this.originalPropsById = {}; + this.propsById = {}; + this.propsByType = {}; + this.propsByVariableName = {}; + this.props = properties; + this.dependencyGraph = createGraph(); + + // Store properties for easy access later + properties.forEach(prop => { + // Store a copy of the unmodified prop + this.originalPropsById[prop._id] = cloneDeep(prop); + + // Store by id + this.propsById[prop._id] = prop; + + // Store by type + this.propsByType[prop.type] ? + this.propsByType[prop.type].push(prop) : + this.propsByType[prop.type] = [prop]; + + // Store by variableName + this.propsByVariableName[prop.variableName] ? + this.propsByVariableName[prop.variableName].push(prop) : + this.propsByVariableName[prop.variableName]= [prop]; + + // Store the prop in the dependency graph + this.dependencyGraph.addNode(prop._id, prop); + }); + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js index 4240f1ad..e6081faa 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -9,7 +9,7 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop // ancestors: {} //this gets added if there are resolved ancestors }; // Traverse the parsed calculation looking for variable names - calcObj._parsedCalculation.travese(node => { + calcObj._parsedCalculation.traverse(node => { // Skip nodes that aren't symbols or accessors if (!(node instanceof SymbolNode || node instanceof AccessorNode)) return; // Link ancestor references as direct property dependencies diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js index d0b0f738..c0c9747c 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js @@ -27,6 +27,8 @@ export default function linkInventory(forest, dependencyGraph){ } function handleProp(prop, containerStack, dependencyGraph){ + // Skip props that aren't part of the inventory + if (prop.type !== 'inventory' && prop.type !== 'container') return; // Determine if this property is carried, items are carried by default let carried = prop.type === 'container' ? prop.carried : true; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index a16ad83c..b6342447 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -13,7 +13,7 @@ const linkDependenciesByType = { } export default function linkTypeDependencies(dependencyGraph, prop){ - linkDependenciesByType[prop.type]?.(prop); + linkDependenciesByType[prop.type]?.(dependencyGraph, prop); } function linkClassLevel(dependencyGraph, prop){ diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js index df451a3b..de80c789 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -1,20 +1,54 @@ +import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import { prettifyParseError, parse } from '/imports/parser/parser.js'; import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; +import { get } from 'lodash'; export default function parseCalculationFields(prop, schemas){ + parseInlineCalculationFields(prop, schemas); + parseDirectCalculationFields(prop, schemas); +} + +function parseInlineCalculationFields(prop, schemas){ + // For each key in the schema + schemas[prop.type]._schemaKeys.forEach( key => { + // That ends in .inlineCalculations + if (key.slice(-19) === '.inlineCalculations'){ + const inlineCalcKey = key.slice(0, -19); + applyFnToKey(prop, inlineCalcKey, (prop, key) => { + const inlineCalcObj = get(prop, key); + if (!inlineCalcObj) return; + // Store a reference to all the inline calculations + prop._computationDetails.inlineCalculations.push(inlineCalcObj); + // Extract the calculations and store them on the property + let string = inlineCalcObj.text; + inlineCalcObj.inlineCalculations = []; + let matches = string.matchAll(INLINE_CALCULATION_REGEX); + for (let match of matches){ + let calculation = match[1]; + inlineCalcObj.inlineCalculations.push({ + calculation, + }); + } + }); + } + }); +} + +function parseDirectCalculationFields(prop, schemas){ // For each key in the schema schemas[prop.type]._schemaKeys.forEach( key => { // that ends in '.calculation' if (key.slice(-12) === '.calculation'){ - const calcKey = key.sclice(0, -12); - + const calcKey = key.slice(0, -12); // Determine the level the calculation should compute down to - let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce'; // For all fields matching they keys // supports `keys.$.with.$.arrays` - applyFnToKey(prop, calcKey, calcObj => { + applyFnToKey(prop, calcKey, (prop, key) => { + const calcObj = get(prop, key); + if (!calcObj) return; // Store a reference to all the calculations prop._computationDetails.calculations.push(calcObj); // Store the level to compute down to later @@ -23,14 +57,7 @@ export default function parseCalculationFields(prop, schemas){ parseCalculation(calcObj); }); // Or that ends in .inlineCalculations - } else if (key.slice(-19) === '.inlineCalculations'){ - const inlineCalcKey = key.sclice(0, -19); - applyFnToKey(prop, inlineCalcKey, inlineCalcObj => { - // Store a reference to all the inline calculations - prop._computationDetails.inlineCalculations.push(inlineCalcObj); - }); } - }); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js new file mode 100644 index 00000000..ab1c3eb7 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js @@ -0,0 +1,13 @@ +import applyFnToKey from '../utility/applyFnToKey.js'; +import { unset } from 'lodash'; + +export default function removeSchemaFields(schemas, prop){ + schemas.forEach(schema => { + schema._schemaKeys.forEach(key => { + // Skip object keys + if (schema.getQuickTypeForKey(key) === 'object') return; + // Unset other computed only keys + applyFnToKey(prop, key, unset) + }); + }); +} diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js index 65a20ce0..e699c8fc 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -4,17 +4,16 @@ import CreatureProperties, from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; -import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; -import { cloneDeep, unset } from 'lodash'; -import createGraph from 'ngraph.graph'; -import linkInventory from '/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js'; -import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; -import parseCalculationFields from '/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js'; -import computeInactiveStatus from '/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js'; -import computeToggleDependencies from '/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js'; -import linkCalculationDependencies from '/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js'; -import linkTypeDependencies from '/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js'; -import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js'; +import linkInventory from './buildComputation/linkInventory.js'; +import walkDown from './utility/walkdown.js'; +import parseCalculationFields from './buildComputation/parseCalculationFields.js'; +import computeInactiveStatus from './buildComputation/computeInactiveStatus.js'; +import computeToggleDependencies from './buildComputation/computeToggleDependencies.js'; +import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; +import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; +import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; +import CreatureComputation from './buildComputation/CreatureComputation.js'; +import removeSchemaFields from './buildComputation/removeSchemaFields.js'; /** * Store index of properties @@ -50,50 +49,32 @@ function getProperties(creatureId){ } export function buildComputationFromProps(properties){ + + const computation = new CreatureComputation(properties); // Dependency graph where edge(a, b) means a depends on b // The graph includes all dependencies even of inactive properties // such that any properties changing without changing their dependencies // can limit the recompute to connected parts of the graph // Each node's data represents a prop or a virtual prop like a variable // Each link's data is a string representing the link type - const dependencyGraph = createGraph(); - - const computation = { - originalPropsById: {}, - propsById: {}, - propsByType: {}, - propsByVariableName: {}, - props: properties, - dependencyGraph, - }; + const dependencyGraph = computation.dependencyGraph; // Process the properties one by one properties.forEach(prop => { - // Store the prop in the memo by type, variableName and id - storePropInMemo(prop, computation) - - // Store the prop in the dependency graph - dependencyGraph.addNode(prop._id, prop); - - // Remove old computed only fields - computedOnlySchemas[prop.type]._schemaKeys.forEach(key => - applyFnToKey(prop, key, unset) - ); - - // Remove old denormalised fields - denormSchema._schemaKeys.forEach(key => - applyFnToKey(prop, key, unset) - ); + let computedSchema = computedOnlySchemas[prop.type]; + removeSchemaFields([computedSchema, denormSchema], prop); // Add a place to store all the computation details prop._computationDetails = { calculations: [], + inlineCalculations: [], toggleAncestors: [], }; // Parse all the calculations parseCalculationFields(prop, computedSchemas); + }); // Get all the properties as trees based on their ancestors @@ -108,7 +89,7 @@ export function buildComputationFromProps(properties){ // Link the inventory dependencies linkInventory(forest, dependencyGraph); - // Graph functions that rely on the props being stored first + // Link functions that require the above to be complete properties.forEach(prop => { linkTypeDependencies(dependencyGraph, prop, computation); linkCalculationDependencies(dependencyGraph, prop, computation); @@ -116,19 +97,3 @@ export function buildComputationFromProps(properties){ return computation; } - -function storePropInMemo(prop, memo){ - // Store dicts for easy access later - // Store a copy of the unmodified prop - memo.originalPropsById[prop._id] = cloneDeep(prop); - // Store by id - memo.propsById[prop._id] = prop; - // Store by type - memo.propsByType[prop.type] ? - memo.propsByType[prop.type].push(prop) : - memo.propsByType[prop.type] = [prop]; - // Store by variableName - memo.propsByVariableName[prop.variableName] ? - memo.propsByVariableName[prop.variableName].push(prop) : - memo.propsByVariableName[prop.variableName]= [prop]; -} diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js index 2df7e2ce..4a99644d 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js @@ -3,9 +3,9 @@ import { assert } from 'chai'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; describe('buildComputation', function(){ - it('Builds something', function(){ + it('Builds something at all', function(){ let computation = buildComputationFromProps(testProperties); - console.log(computation); + assert.exists(computation); }); }); @@ -13,6 +13,14 @@ var testProperties = [ clean({ _id: 'attributeId123', type: 'attribute', + variableName: 'strength', + attributeType: 'ability', + baseValue: { + calculation: '1 + 2 + 3', + }, + description: { + text: 'strength is {strength}' + } }), ]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index c505e81b..e5f2041b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -1,9 +1,9 @@ -import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/aggregate/index.js'; -import computeVariableAsAttribute from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsAttribute.js'; -import computeVariableAsSkill from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsSkill.js'; -import computeVariableAsConstant from '/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js'; -import computeVariableAsClass from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeVariableAsClass.js'; -import computeImplicitVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeImplicitVariable.js'; +import aggregate from './computeVariable/aggregate/index.js'; +import computeVariableAsAttribute from './computeVariable/computeVariableAsAttribute.js'; +import computeVariableAsSkill from './computeVariable/computeVariableAsSkill.js'; +import computeVariableAsConstant from './computeVariable/computeVariableAsConstant.js'; +import computeVariableAsClass from './computeVariable/computeVariableAsClass.js'; +import computeImplicitVariable from './computeVariable/computeImplicitVariable.js'; export default function computeVariable(graph, node, scope){ if (!node.data) node.data = {}; @@ -16,6 +16,7 @@ export default function computeVariable(graph, node, scope){ // Otherwise add an implicit variable to the scope scope[node.id] = computeImplicitVariable(node, scope); } + console.log('computed variable ', node); } function aggregateLinks(graph, node){ @@ -40,13 +41,14 @@ function aggregateLinks(graph, node){ function combineAggregations(node, scope){ combineMultiplierAggregator(node); - node.overridenProps.forEach(prop => { + node.data.overridenProps?.forEach(prop => { computeVariableProp(node, prop, scope); }); - computeVariableProp(node, node.definingProp, scope); + computeVariableProp(node, node.data.definingProp, scope); } function computeVariableProp(node, prop, scope){ + if (!prop) return; if (prop.type === 'attribute'){ computeVariableAsAttribute(node, prop, scope) } else if (prop.type === 'skill'){ @@ -61,6 +63,7 @@ function computeVariableProp(node, prop, scope){ function combineMultiplierAggregator(node){ // get a reference to the aggregator const aggregator = node.data.multiplierAggregator; + if (!aggregator) return; // Combine let value; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js index 1f15ff23..1ed6e530 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -1,4 +1,4 @@ -import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult.js'; /* * Variables with effects, proficiencies, or damage multipliers but no defining diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 81f4c73b..032b0840 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -1,7 +1,8 @@ -import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js'; +import getAggregatorResult from './getAggregatorResult.js'; export default function computeVariableAsAttribute(node, prop, scope){ let result = getAggregatorResult(node); + console.log('computing variable as attribure ', node); prop.total = result; prop.value = prop.total - (prop.damage || 0); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js index 34a61124..7631dc9b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -2,6 +2,7 @@ import { CompilationContext } from '/imports/parser/parser.js'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; export default function computeCalculations(node, scope){ + if (!node.data) return; // evaluate all the calculations node.data._computationDetails?.calculations?.forEach(calcObj => { evaluateCalculation(calcObj, scope) @@ -14,7 +15,7 @@ export default function computeCalculations(node, scope){ function evaluateCalculation(calculation, scope){ const context = new CompilationContext(); const parseNode = calculation._parsedCalculation; - const fn = calculation._parseLevel || 'reduce'; + const fn = calculation._parseLevel; const calculationScope = {...calculation._localScope, ...scope}; calculation.value = parseNode[fn](calculationScope, context); calculation.errors = context.errors; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js index d9f0cb26..50fff1db 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js @@ -1,5 +1,6 @@ export default function evaluateToggles(node){ let prop = node.data; + if (!prop) return; let toggles = prop._computationDetails?.toggleAncestors; if (!toggles) return; toggles.forEach(toggle => { diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index b5261550..c819e7dc 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -8,27 +8,23 @@ export default function computeCreatureComputation(computation){ const scope = {}; const graph = computation.dependencyGraph; // Add all nodes to the stack - graph.forEachNode(node => stack.push({ - node, - visited: false, - visitedChildren: false, - })); + graph.forEachNode(node => stack.push(node)); // Depth first traversal of nodes while (stack.length){ let top = stack[stack.length - 1]; - if (top.visited){ + if (top._visited){ // The object has already been computed, skip stack.pop(); - } else if (top.visitedChildren){ - // Compute the top object of the stack - compute(graph, top.node, scope); + } else if (top._visitedChildren){ // Mark the object as visited and remove from stack - top.visited = true; + top._visited = true; stack.pop(); + // Compute the top object of the stack + compute(graph, top, scope); } else { + top._visitedChildren = true; // Push dependencies to graph to be computed first - pushDependenciesToStack(top.node.id, graph, stack); - top.visitedChildren = true; + pushDependenciesToStack(top.id, graph, stack); } } } @@ -42,15 +38,5 @@ function compute(graph, node, scope){ } function pushDependenciesToStack(nodeId, graph, stack){ - graph.forEachLinkedNode( - nodeId, - (linkedNode) => { - stack.push({ - node: linkedNode, - visited: false, - visitedChildren: false, - }); - }, - true // enumerate only outbound links - ); + graph.forEachLinkedNode(nodeId, linkedNode => stack.push(linkedNode), true); } diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js new file mode 100644 index 00000000..6bdc1ac6 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js @@ -0,0 +1,34 @@ +import computeCreatureComputation from './computeCreatureComputation.js'; +import { buildComputationFromProps } from './buildCreatureComputation.js'; +import { assert } from 'chai'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; + +describe('Compute compuation', function(){ + it('Computes something at all', function(){ + console.time('compute'); + let computation = buildComputationFromProps(testProperties); + computeCreatureComputation(computation); + console.timeEnd('compute'); + assert.exists(computation); + }); +}); + +var testProperties = [ + clean({ + _id: 'attributeId123', + type: 'attribute', + variableName: 'strength', + attributeType: 'ability', + baseValue: { + calculation: '1 + 2 + 3', + }, + description: { + text: 'strength is {strength}' + } + }), +]; + +function clean(prop){ + let schema = CreatureProperties.simpleSchema(prop); + return schema.clean(prop); +} diff --git a/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js b/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js index 3e3780b4..0d4005af 100644 --- a/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js +++ b/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js @@ -1,7 +1,7 @@ import { get } from 'lodash'; -export function applyFnToKey(doc, key, fn){ - if (key.includes('$.')){ +export default function applyFnToKey(doc, key, fn){ + if (key.includes('.$')){ applyToArrayKey(doc, key, fn); } else { applyToSingleKey(doc, key, fn); @@ -16,26 +16,31 @@ function applyToSingleKey(doc, key, fn){ /** * Applies the given function to all instances in a document key * key.$.with.$.subdocs will apply to all key[i...n].with[j...m].subdocs + * Warning: Order might be confusing, it will traverse the deepest array in order + * but the shallower arrays in reverse order */ function applyToArrayKey(doc, key, fn){ const keySplit = key.split('.$'); - // Stack based depth first traversal of arrays + const array = get(doc, keySplit[0]); + if (!array) return; const stack = [{ - array: get(doc, keySplit[0]), + array, paths: keySplit.slice(1), currentPath: keySplit[0], indices: [], }]; while(stack.length){ const state = stack.pop(); - for (let index in state.array.length){ + for (let index in state.array){ const currentPath = `${state.currentPath}[${index}]${state.paths[0]}` if (state.paths.length == 1){ applyToSingleKey(doc, currentPath, fn); } else { + const array = get(doc, currentPath); + if (!array) return; stack.push({ - array: get(doc, currentPath), + array, paths: state.paths.slice(1), currentPath, indices: [...state.indices, index], diff --git a/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.test.js b/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.test.js new file mode 100644 index 00000000..5e1b3a3c --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.test.js @@ -0,0 +1,60 @@ +import applyFnToKey from './applyFnToKey.js'; +import { assert } from 'chai'; +import { get } from 'lodash'; + +describe('apply function to key', function(){ + it('uses a basic key correctly', function(){ + let obj = getStartingObject(); + applyFnToKey(obj, 'fox.name', (doc, key) => { + assert.equal(obj, doc); + assert.equal(key, 'fox.name'); + assert.equal(get(doc, key), 'foxy'); + }); + }); + it('uses a single nested key correctly', function(){ + let obj = getStartingObject(); + let foxSounds = []; + applyFnToKey(obj, 'fox.sound.$', (doc, key) => { + foxSounds.push(get(doc, key)); + }); + assert.include(foxSounds, 'wah'); + assert.include(foxSounds, 'tjoef'); + assert.include(foxSounds, 'kek'); + }); + it('uses a double nested key correctly', function(){ + let obj = getStartingObject(); + let birdSounds = []; + applyFnToKey(obj, 'birds.$.sound.$', (doc, key) => { + birdSounds.push(get(doc, key)); + }); + assert.include(birdSounds, 'koer'); + assert.include(birdSounds, 'hello'); + assert.include(birdSounds, 'squawk'); + }); +}); + +function getStartingObject(){ + return { + fox: { + name: 'foxy', + sound: [ + 'tjoef', + 'kek', + 'wah' + ] + }, + birds: [{ + name: 'pigeon', + sound: [ + 'koer', + ] + },{ + name: 'parrot', + sound: [ + 'hello', + 'cracker?', + 'squawk', + ] + }] + } +} diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 86453014..bfaf28a1 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -6,6 +6,7 @@ import { } from '/imports/api/properties/subSchemas/ResourcesSchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +SimpleSchema.extendOptions(['parseLevel']); /* * Actions are things a character can do diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.js index 93bfbf23..f9f67242 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.js +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; -import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Get schemas that apply fields directly so they can be gracefully extended @@ -26,18 +26,36 @@ function computedOnlyInlineCalculationField(field){ type: Object, optional: true, }, + [`${field}.value`]: { + type: String, + optional: true, + max: STORAGE_LIMITS.inlineCalculationField, + }, [`${field}.inlineCalculations`]: { type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.inlineCalculationCount, }, [`${field}.inlineCalculations.$`]: { - type: InlineComputationSchema, + type: Object, }, - [`${field}.value`]: { + // The part between bracers {} + [`${field}.inlineCalculations.$.calculation`]: { type: String, + max: STORAGE_LIMITS.calculation, + }, + [`${field}.inlineCalculations.$.value`]: { + type: SimpleSchema.oneOf(String, Number), optional: true, - max: STORAGE_LIMITS.inlineCalculationField, + max: STORAGE_LIMITS.calculation, + }, + [`${field}.inlineCalculations.$.errors`]: { + type: Array, + optional: true, + maxCount: STORAGE_LIMITS.errorCount, + }, + [`${field}.inlineCalculations.$.errors.$`]: { + type: ErrorSchema, }, }); } diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.test.js b/app/imports/migrations/server/2.0-beta.33-dbv1.test.js index d74c1896..ce3512a7 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.test.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.test.js @@ -38,9 +38,6 @@ const exampleAction = { 'usesUsed':0, 'description':'Starting at 1st level, you gain the ability to place a baleful curse on someone. As a bonus action, choose one creature you can see within 30 feet of you. The target is cursed for 1 minute. The curse ends early if the target dies, you die, or you are incapacitated. Until the curse ends, you gain the following benefits:\n\n- You gain a bonus to damage rolls against the cursed target. The bonus equals your proficiency bonus.\n- Any attack roll you make against the cursed target is a critical hit on a roll of 19 or 20 on the d20.\n- If the cursed target dies, you regain hit points equal to your warlock level + your Charisma modifier (minimum of 1 hit point). \n{warlock.level <10 ? "" :"- 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."} \nYou can\\\'t use this feature again until you finish a short or long rest.', 'color':'#8e24aa', - 'dependencies':[ - '4eM4YkgAaoCJfCfQ8', - ], 'descriptionCalculations':[ { 'calculation':'warlock.level <10 ? "" :"- 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."', diff --git a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue b/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue index 4fb921e4..46a56e55 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue @@ -107,7 +107,7 @@ import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; import CoinValue from '/imports/ui/components/CoinValue.vue'; -import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; export default { components: { diff --git a/app/imports/ui/properties/components/inventory/ContainerCard.vue b/app/imports/ui/properties/components/inventory/ContainerCard.vue index 22492234..f95ab1bd 100644 --- a/app/imports/ui/properties/components/inventory/ContainerCard.vue +++ b/app/imports/ui/properties/components/inventory/ContainerCard.vue @@ -49,7 +49,7 @@ import ToolbarCard from '/imports/ui/components/ToolbarCard.vue'; import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CoinValue from '/imports/ui/components/CoinValue.vue'; -import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; export default { components: { diff --git a/app/imports/ui/properties/viewers/ItemViewer.vue b/app/imports/ui/properties/viewers/ItemViewer.vue index 90af478d..99291985 100644 --- a/app/imports/ui/properties/viewers/ItemViewer.vue +++ b/app/imports/ui/properties/viewers/ItemViewer.vue @@ -134,7 +134,7 @@ import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyV import CoinValue from '/imports/ui/components/CoinValue.vue'; import IncrementButton from '/imports/ui/components/IncrementButton.vue'; import adjustQuantity from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js'; -import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; export default { components:{ From a660ccc458c7dbe6a4f1da4251ccdffffcc3a302 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 16 Sep 2021 14:31:28 +0200 Subject: [PATCH 17/85] Lots of progress testing and fixing computation engine --- .../buildComputation/CreatureComputation.js | 1 + .../buildComputation/computeInactiveStatus.js | 3 +- .../computeSlotQuantityFilled.js | 11 +- .../buildComputation/linkInventory.js | 3 +- .../buildComputation/linkTypeDependencies.js | 17 ++- .../parseCalculationFields.js | 8 +- .../buildComputation/removeSchemaFields.js | 7 +- .../tests/computeInactiveStatus.testFn.js | 144 ++++++++++++++++++ .../tests/computeSlotQuantityFilled.testFn.js | 36 +++++ .../tests/computeToggleDependencies.testFn.js | 74 +++++++++ .../linkCalculationDependencies.testFn.js | 54 +++++++ .../tests/linkInventory.testFn.js | 81 ++++++++++ .../tests/linkTypeDependencies.testfn.js | 27 ++++ .../newEngine/buildCreatureComputation.js | 6 +- .../buildCreatureComputation.test.js | 12 ++ .../computeByType/computeAction.js | 1 + .../computeAction/computeResources.js | 5 +- .../computeByType/computeVariable.js | 5 +- .../computeVariableAsAttribute.js | 1 - .../computeComputation/computeCalculations.js | 8 +- .../tests/computeAction.testFn.js | 104 +++++++++++++ .../newEngine/computeCreatureComputation.js | 8 +- .../computeCreatureComputation.test.js | 4 +- .../newEngine/utility/cleanProp.testFn.js | 6 + app/imports/api/properties/Actions.js | 138 ++++++++++++++--- app/imports/api/properties/ClassLevels.js | 6 +- app/imports/api/properties/Constants.js | 4 +- .../api/properties/DamageMultipliers.js | 4 +- app/imports/api/properties/Folders.js | 4 +- app/imports/api/properties/Proficiencies.js | 4 +- app/imports/api/properties/References.js | 4 +- app/imports/api/properties/SlotFillers.js | 4 +- .../computedOnlyPropertySchemasIndex.js | 28 ++-- .../properties/subSchemas/computedField.js | 43 ++++-- 34 files changed, 775 insertions(+), 90 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/utility/cleanProp.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js index d4836b47..2192b273 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js @@ -8,6 +8,7 @@ export default class CreatureComputation { this.propsById = {}; this.propsByType = {}; this.propsByVariableName = {}; + this.scope = {}; this.props = properties; this.dependencyGraph = createGraph(); diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js index ba6acda9..6645067d 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js @@ -4,7 +4,8 @@ export default function computeInactiveStatus(node){ const prop = node.node; if (isActive(prop)) return; // Unequipped items disable their children, but are not disabled themselves - if (prop.type !== 'item'){ + // All notes do the same + if (prop.type !== 'item' && prop.type !== 'note' ){ prop.inactive = true; prop.deactivatedBySelf = true; } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js index b44f2681..e3adabc8 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js @@ -4,13 +4,16 @@ */ export default function computeSlotQuantityFilled(node, dependencyGraph){ let slot = node.node; - if (slot.type !== 'propertySlot' || slot.type !== 'characterClass') return; + if (slot.type !== 'propertySlot') return; slot.totalFilled = 0; node.children.forEach(child => { let childProp = child.node; - dependencyGraph.addLink(slot._id, childProp._id, 'slotFill') - if (childProp.type === 'slotFiller'){ - slot.totalFilled += child.slotQuantityFilled; + 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/creature/computation/newEngine/buildComputation/linkInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js index c0c9747c..ae092bd5 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js @@ -12,6 +12,7 @@ export default function linkInventory(forest, dependencyGraph){ const top = stack[stack.length - 1]; const prop = top.node; if (prop._computationDetails.inventoryChildrenVisited){ + if (prop.type === 'container') containerStack.pop(); stack.pop(); handleProp(prop, containerStack, dependencyGraph); } else { @@ -28,7 +29,7 @@ export default function linkInventory(forest, dependencyGraph){ function handleProp(prop, containerStack, dependencyGraph){ // Skip props that aren't part of the inventory - if (prop.type !== 'inventory' && prop.type !== 'container') return; + if (prop.type !== 'item' && prop.type !== 'container') return; // Determine if this property is carried, items are carried by default let carried = prop.type === 'container' ? prop.carried : true; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index b6342447..01f1d0b7 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -12,8 +12,8 @@ const linkDependenciesByType = { spell: linkResources, } -export default function linkTypeDependencies(dependencyGraph, prop){ - linkDependenciesByType[prop.type]?.(dependencyGraph, prop); +export default function linkTypeDependencies(dependencyGraph, prop, computation){ + linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation); } function linkClassLevel(dependencyGraph, prop){ @@ -36,19 +36,20 @@ function linkVariableName(dependencyGraph, prop){ } function linkResources(dependencyGraph, prop, {propsById}){ + if (!prop.resources) return; prop.resources.itemsConsumed.forEach(itemConsumed => { if (!itemConsumed.itemId) return; const item = propsById[itemConsumed.itemId]; - if (!item.equipped) { + if (!item || item.inactive){ + // Unlink if the item doesn't exist or is inactive itemConsumed.itemId = undefined; return; } - if (!item) return; // none of these dependencies are computed, we can use them immediately - prop.available = item.quantity; - prop.itemName = item.name; - prop.itemIcon = item.icon; - prop.itemColor = item.color; + itemConsumed.available = item.quantity; + itemConsumed.itemName = item.name; + itemConsumed.itemIcon = item.icon; + itemConsumed.itemColor = item.color; dependencyGraph.addLink(prop._id, item._id, 'inventory'); }); prop.resources.attributesConsumed.forEach(attConsumed => { diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js index de80c789..eecc7641 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -5,11 +5,11 @@ import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/ap import { get } from 'lodash'; export default function parseCalculationFields(prop, schemas){ - parseInlineCalculationFields(prop, schemas); - parseDirectCalculationFields(prop, schemas); + discoverInlineCalculationFields(prop, schemas); + parseAllCalculationFields(prop, schemas); } -function parseInlineCalculationFields(prop, schemas){ +function discoverInlineCalculationFields(prop, schemas){ // For each key in the schema schemas[prop.type]._schemaKeys.forEach( key => { // That ends in .inlineCalculations @@ -35,7 +35,7 @@ function parseInlineCalculationFields(prop, schemas){ }); } -function parseDirectCalculationFields(prop, schemas){ +function parseAllCalculationFields(prop, schemas){ // For each key in the schema schemas[prop.type]._schemaKeys.forEach( key => { // that ends in '.calculation' diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js index ab1c3eb7..885d3abf 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js @@ -4,8 +4,11 @@ import { unset } from 'lodash'; export default function removeSchemaFields(schemas, prop){ schemas.forEach(schema => { schema._schemaKeys.forEach(key => { - // Skip object keys - if (schema.getQuickTypeForKey(key) === 'object') return; + // Skip object and array keys + if ( + schema.getQuickTypeForKey(key) === 'object' || + schema.getQuickTypeForKey(key) === 'objectArray' + ) return; // Unset other computed only keys applyFnToKey(prop, key, unset) }); diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js new file mode 100644 index 00000000..780716e3 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js @@ -0,0 +1,144 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { assert } from 'chai'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function(){ + let computation = buildComputationFromProps(testProperties); + const bySelf = (propId, note) => assertDeactivatedBySelf(computation, propId, note); + const byAncestor = (propId, note) => assertDeactivatedByAncestor(computation, propId, note); + const active = (propId, note) => assertActive(computation, propId, note); + + // Buffs + bySelf('buffNotAppliedId'); + byAncestor('buffNotAppliedChildId'); + active('buffAppliedId'); + active('buffAppliedChildId'); + + // Items + active('itemUnequippedId', 'Unequipped items should be active'); + byAncestor('itemUnequippedChildId', 'Children of unequipped items should be inactive'); + active('itemEquippedId'); + active('itemEquippedChildId'); + + // Spells + active('spellPreparedId'); + active('spellPreparedChildId'); + active('spellAlwaysPreparedId'); + active('spellAlwaysPreparedChildId'); + bySelf('spellUnpreparedId'); + byAncestor('spellUnpreparedChildId'); + + // Notes + active('NoteId', 'Notes should be active'); + byAncestor('NoteChildId', 'children of notes should always be inactive'); +} + +function assertDeactivatedBySelf(computation, propId, note){ + const prop = computation.propsById[propId]; + assert.isTrue(prop.deactivatedBySelf, note); + assert.isTrue(prop.inactive, 'The property should be inactive'); +} + +function assertDeactivatedByAncestor(computation, propId, note){ + const prop = computation.propsById[propId]; + assert.isTrue(prop.deactivatedByAncestor, note); + assert.isTrue(prop.inactive, 'The property should be inactive'); +} + +function assertActive(computation, propId, note){ + const prop = computation.propsById[propId]; + assert.isNotTrue(prop.inactive, note); + assert.isNotTrue(prop.deactivatedBySelf); + assert.isNotTrue(prop.deactivatedBySelf); +} + +var testProperties = [ + // Buffs + clean({ + _id: 'buffNotAppliedId', + type: 'buff', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'buffNotAppliedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'buffNotAppliedId'}], + }), + clean({ + _id: 'buffAppliedId', + type: 'buff', + applied: true, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'buffAppliedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'buffAppliedId'}], + }), + // Items + clean({ + _id: 'itemUnequippedId', + type: 'item', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'itemUnequippedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'itemUnequippedId'}], + }), + clean({ + _id: 'itemEquippedId', + type: 'item', + equipped: true, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'itemEquippedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'itemEquippedId'}], + }), + // Spells + clean({ + _id: 'spellPreparedId', + type: 'spell', + ancestors: [{id: 'charId'}], + prepared: true, + }), + clean({ + _id: 'spellPreparedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'spellPreparedId'}], + }), + clean({ + _id: 'spellAlwaysPreparedId', + type: 'spell', + ancestors: [{id: 'charId'}], + alwaysPrepared: true, + }), + clean({ + _id: 'spellAlwaysPreparedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'spellAlwaysPreparedId'}], + }), + clean({ + _id: 'spellUnpreparedId', + type: 'spell', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'spellUnpreparedChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'spellUnpreparedId'}], + }), + // Notes + clean({ + _id: 'NoteId', + type: 'note', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'NoteChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'NoteId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js new file mode 100644 index 00000000..1a6bbba4 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js @@ -0,0 +1,36 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { assert } from 'chai'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function(){ + const computation = buildComputationFromProps(testProperties); + const totalFilled = computation.propsById['slotId'].totalFilled; + assert.equal(totalFilled, 4); +} + +var testProperties = [ + // Slots + clean({ + _id: 'slotId', + type: 'propertySlot', + ancestors: [{id: 'charId'}], + }), + // Children + clean({ + _id: 'slotFillerId', + type: 'slotFiller', + slotQuantityFilled: 3, + slotFillerType: 'item', + ancestors: [{id: 'charId'}, {id: 'slotId'}], + }), + clean({ + _id: 'slotChildId', + type: 'item', + ancestors: [{id: 'charId'}, {id: 'slotId'}], + }), + clean({ + _id: 'slotGrandchildId', + type: 'effect', + ancestors: [{id: 'charId'}, {id: 'slotId'}, {id: 'slotChildId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js new file mode 100644 index 00000000..3b6028c4 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js @@ -0,0 +1,74 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { assert } from 'chai'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function(){ + const computation = buildComputationFromProps(testProperties); + const hasLink = computation.dependencyGraph.hasLink; + assert.include( + computation.propsById['conditionToggleChildId']._computationDetails.toggleAncestors, + computation.propsById['conditionToggleId'], + 'Children of toggles should store a reference to their toggle ancestor' + ) + assert.isTrue( + !!hasLink('conditionToggleChildId', 'conditionToggleId'), + 'Children of the condition toggle should depend on it' + ); + assert.isTrue( + !!hasLink('conditionToggleGrandChildId', 'conditionToggleId'), + 'Descendants of the condition toggle should depend on it' + ); + assert.isFalse( + !!hasLink('disabledToggleId', 'disabledToggleChildId'), + 'The dependency should not be reversed' + ); + assert.isFalse( + !!hasLink('disabledToggleChildId', 'disabledToggleId'), + 'Children of disabled toggle should not depend on it' + ); + assert.isFalse( + !!hasLink('enabledToggleChildId', 'enabledToggleId'), + 'Children of enabled toggle should not depend on it' + ); +} + +var testProperties = [ + clean({ + _id: 'enabledToggleId', + type: 'toggle', + enabled: true, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'disabledToggleId', + type: 'toggle', + disabled: true, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'conditionToggleId', + type: 'toggle', + ancestors: [{id: 'charId'}], + }), + // Children + clean({ + _id: 'enabledToggleChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'enabledToggleId'}], + }), + clean({ + _id: 'disabledToggleChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'disabledToggleId'}], + }), + clean({ + _id: 'conditionToggleChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'conditionToggleId'}], + }), + clean({ + _id: 'conditionToggleGrandChildId', + type: 'folder', + ancestors: [{id: 'charId'}, {id: 'conditionToggleId'}, {id: 'conditionToggleChildId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js new file mode 100644 index 00000000..918767ba --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -0,0 +1,54 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { assert } from 'chai'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function(){ + const computation = buildComputationFromProps(testProperties); + const hasLink = computation.dependencyGraph.hasLink; + assert.isTrue( + !!hasLink('childId', 'spellListId'), + 'Ancestor references of parent in inline calculations should create dependency' + ); + assert.isTrue( + !!hasLink('grandchildId', 'spellListId'), + 'References to higher ancestor should create dependency' + ); + assert.isTrue( + !!hasLink('grandchildId', 'strength'), + 'Variable references create dependencies' + ); + assert.isTrue( + !!hasLink('grandchildId', 'wisdom'), + 'Variable references create dependencies even if the attributes don\'t exist' + ); +} + +var testProperties = [ + clean({ + _id: 'spellListId', + type: 'spellList', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'childId', + type: 'spell', + description: { + text: 'DC {#spellList.dc} save or suck' + }, + ancestors: [{id: 'charId'}, {id: 'spellListId'}], + }), + clean({ + _id: 'grandchildId', + type: 'savingThrow', + dc: { + calculation: '#spellList.dc + strength + wisdom.modifier' + }, + ancestors: [{id: 'charId'}, {id: 'spellListId'}, {id: 'childId'}], + }), + clean({ + _id: 'strengthId', + type: 'attribute', + variableName: 'strength', + ancestors: [{id: 'charId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js new file mode 100644 index 00000000..9a5e244e --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js @@ -0,0 +1,81 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { assert } from 'chai'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function(){ + const computation = buildComputationFromProps(testProperties); + const hasLink = computation.dependencyGraph.hasLink; + + assert.isTrue( + !!hasLink('weightEquipment', 'equippedAttunedItemId'), + 'weight of equipment depends on equipped items' + ); + assert.isTrue( + !!hasLink('valueEquipment', 'equippedAttunedItemId'), + 'value of equipment depends on equipped items' + ); + assert.isTrue( + !!hasLink('weightTotal', 'equippedAttunedItemId'), + 'weightTotal depends on equipped items' + ); + assert.isTrue( + !!hasLink('valueTotal', 'equippedAttunedItemId'), + 'valueTotal depends on equipped items' + ); + assert.isTrue( + !!hasLink('weightCarried', 'equippedAttunedItemId'), + 'weightCarried depends on equipped items' + ); + assert.isTrue( + !!hasLink('valueCarried', 'equippedAttunedItemId'), + 'valueCarried depends on equipped items' + ); + assert.isTrue( + !!hasLink('weightCarried', 'containerId'), + 'weightCarried depends on top level containers' + ); + assert.isTrue( + !!hasLink('valueCarried', 'containerId'), + 'valueCarried depends on top level containers' + ); + assert.isFalse( + !!hasLink('weightCarried', 'childContainerId'), + 'weightCarried does not depend on nested containers' + ); + assert.isFalse( + !!hasLink('valueCarried', 'childContainerId'), + 'valueCarried does not depend on nested containers' + ); +} + +var testProperties = [ + clean({ + _id: 'equippedAttunedItemId', + type: 'item', + equipped: true, + attuned: true, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'containerId', + type: 'container', + carried: true, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'childContainerId', + type: 'container', + carried: true, + ancestors: [{id: 'charId'}, {id: 'containerId'}], + }), + clean({ + _id: 'childItemId', + type: 'item', + ancestors: [{id: 'charId'}, {id: 'containerId'}], + }), + clean({ + _id: 'grandchildItemId', + type: 'item', + ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js new file mode 100644 index 00000000..5733bd4a --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js @@ -0,0 +1,27 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { assert } from 'chai'; +import clean from '../../utility/cleanProp.testFn.js'; + +export default function(){ + const computation = buildComputationFromProps(testProperties); + const getLink = computation.dependencyGraph.hasLink; + const getNode = computation.dependencyGraph.getNode; + + assert.equal( + getLink('strength', 'strengthId').data, 'definition', + 'Links variable names to props that define them' + ); + assert.exists( + getNode('strength'), + 'Creates variable name nodes when attributes define them' + ); +} + +var testProperties = [ + clean({ + _id: 'strengthId', + type: 'attribute', + variableName: 'strength', + ancestors: [{id: 'charId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js index e699c8fc..c577459c 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js @@ -82,14 +82,14 @@ export function buildComputationFromProps(properties){ // Walk the property trees computing things that need to be inherited walkDown(forest, node => { computeInactiveStatus(node); - computeToggleDependencies(node); - computeSlotQuantityFilled(node); + computeToggleDependencies(node, dependencyGraph); + computeSlotQuantityFilled(node, dependencyGraph); }); // Link the inventory dependencies linkInventory(forest, dependencyGraph); - // Link functions that require the above to be complete + // Link functions that require the above to be complete properties.forEach(prop => { linkTypeDependencies(dependencyGraph, prop, computation); linkCalculationDependencies(dependencyGraph, prop, computation); diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js index 4a99644d..2df19c08 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js +++ b/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js @@ -1,12 +1,24 @@ import { buildComputationFromProps } from './buildCreatureComputation.js'; import { assert } from 'chai'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import computeInactiveStatus from './buildComputation/tests/computeInactiveStatus.testFn.js'; +import computeSlotQuantityFilled from './buildComputation/tests/computeSlotQuantityFilled.testFn.js'; +import computeToggleDependencies from './buildComputation/tests/computeToggleDependencies.testFn.js'; +import linkCalculationDependencies from './buildComputation/tests/linkCalculationDependencies.testFn.js'; +import linkInventory from './buildComputation/tests/linkInventory.testFn.js'; +import linkTypeDependencies from './buildComputation/tests/linkTypeDependencies.testFn.js'; describe('buildComputation', function(){ it('Builds something at all', function(){ let computation = buildComputationFromProps(testProperties); assert.exists(computation); }); + it('Computes inactive status', computeInactiveStatus); + it('Computes slot fill quantity', computeSlotQuantityFilled); + it('Links toggle dependencies', computeToggleDependencies); + it('Links calculation dependencies', linkCalculationDependencies); + it('Links inventory stats', linkInventory); + it('Links type dependencies', linkTypeDependencies); }); var testProperties = [ diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js index f8d201b4..e1f0d379 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js @@ -6,6 +6,7 @@ export default function computeAction(graph, node, scope){ prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); } computeResources(graph, node, scope); + if (!prop.resources) return; prop.resources.itemsConsumed.forEach(itemConsumed => { if (!itemConsumed.itemId) return; if (itemConsumed.available < itemConsumed.quantity.value){ diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js index ccda0235..26dbe9a3 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js @@ -1,6 +1,7 @@ export default function computeResources(graph, node, scope){ - const prop = node.data; - prop.resources.attributesConsumed.forEach(attConsumed => { + const resources = node.data?.resources; + if (!resources) return; + resources.attributesConsumed.forEach(attConsumed => { if (!attConsumed.variableName) return; const att = scope[attConsumed.variableName]; attConsumed.available = att.value; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index e5f2041b..b66cc075 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -9,14 +9,13 @@ export default function computeVariable(graph, node, scope){ if (!node.data) node.data = {}; aggregateLinks(graph, node); combineAggregations(node, scope); - if (node.definingProp){ + if (node.data.definingProp){ // Add the defining variable to the scope - scope[node.id] = node.definingProp + scope[node.id] = node.data.definingProp } else { // Otherwise add an implicit variable to the scope scope[node.id] = computeImplicitVariable(node, scope); } - console.log('computed variable ', node); } function aggregateLinks(graph, node){ diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index 032b0840..b5048202 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -2,7 +2,6 @@ import getAggregatorResult from './getAggregatorResult.js'; export default function computeVariableAsAttribute(node, prop, scope){ let result = getAggregatorResult(node); - console.log('computing variable as attribure ', node); prop.total = result; prop.value = prop.total - (prop.damage || 0); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js index 7631dc9b..129ffb5b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -1,5 +1,6 @@ import { CompilationContext } from '/imports/parser/parser.js'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; +import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; export default function computeCalculations(node, scope){ if (!node.data) return; @@ -17,7 +18,12 @@ function evaluateCalculation(calculation, scope){ const parseNode = calculation._parsedCalculation; const fn = calculation._parseLevel; const calculationScope = {...calculation._localScope, ...scope}; - calculation.value = parseNode[fn](calculationScope, context); + const resultNode = parseNode[fn](calculationScope, context); + if (resultNode instanceof ConstantNode){ + calculation.value = resultNode.value; + } else { + calculation.value = NaN; + } calculation.errors = context.errors; } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js new file mode 100644 index 00000000..0365b200 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js @@ -0,0 +1,104 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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 = computation.propsById['actionId']; + assert.equal(prop.summary.value, 'test summary 3 without referencing anything 7'); + assert.equal(prop.description.value, 'test description 12 with reference 0.25 prop'); + assert.equal(prop.uses.value, 7); + assert.equal(prop.usesLeft, 2); + + const itemConsumed = prop.resources.itemsConsumed[0]; + assert.equal(itemConsumed.quantity.value, 3); + assert.equal(itemConsumed.available, 27); + assert.equal(itemConsumed.itemName, 'Arrow'); + assert.equal(itemConsumed.itemIcon, 'itemIcon'); + assert.equal(itemConsumed.itemColor, 'itemColor'); + + const attConsumed = prop.resources.attributesConsumed[0]; + assert.equal(attConsumed.quantity.value, 4); + assert.equal(attConsumed.available, 9); + assert.equal(attConsumed.statId, 'resourceVarId'); + assert.equal(attConsumed.statName, 'Resource Var'); +} + +var testProperties = [ + clean({ + _id: 'actionId', + type: 'action', + ancestors: [{id: 'charId'}], + summary: { + text: 'test summary {1 + 2} without referencing anything {3 + 4}', + }, + description: { + text: 'test description {inlineRef * 2} with reference {1/4} prop', + }, + resources: { + itemsConsumed: [{ + _id: 'itemConsumedId', + itemId: 'arrowId', + tag: 'arrow', + quantity: { + calculation: 'itemConsumedQuantity', + }, + }], + attributesConsumed: [{ + _id: 'attConsumedId', + variableName: 'resourceVar', + quantity: { + calculation: 'resourceConsumedQuantity' + } + }], + }, + uses: { + calculation: 'nonExistantProperty + 7', + }, + usesUsed: 5, + }), + clean({ + _id: 'numItemsConumedId', + type: 'attribute', + variableName: 'itemConsumedQuantity', + baseValue: { + calculation: '3', + }, + }), + clean({ + _id: 'numResourceConumedId', + type: 'attribute', + variableName: 'resourceConsumedQuantity', + baseValue: { + calculation: '4', + }, + }), + clean({ + _id: 'resourceVarId', + name: 'Resource Var', + type: 'attribute', + variableName: 'resourceVar', + baseValue: { + calculation: '9', + }, + }), + clean({ + _id: 'inlineRefResourceId', + type: 'attribute', + variableName: 'inlineRef', + baseValue: { + calculation: '1 + 5', + }, + }), + clean({ + _id: 'arrowId', + type: 'item', + name: 'Arrow', + quantity: 27, + icon: 'itemIcon', + color: 'itemColor', + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index c819e7dc..df8d39a7 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -5,10 +5,14 @@ import computeByType from '/imports/api/creature/computation/newEngine/computeCo export default function computeCreatureComputation(computation){ const stack = []; // Computation scope of {variableName: prop} - const scope = {}; + const scope = computation.scope; const graph = computation.dependencyGraph; // Add all nodes to the stack - graph.forEachNode(node => stack.push(node)); + graph.forEachNode(node => { + node._visited = false; + node._visitedChildren = false; + stack.push(node) + }); // Depth first traversal of nodes while (stack.length){ let top = stack[stack.length - 1]; diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js index 6bdc1ac6..66eeaa82 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js @@ -2,15 +2,15 @@ import computeCreatureComputation from './computeCreatureComputation.js'; import { buildComputationFromProps } from './buildCreatureComputation.js'; import { assert } from 'chai'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import computeAction from './computeComputation/tests/computeAction.testFn.js'; describe('Compute compuation', function(){ it('Computes something at all', function(){ - console.time('compute'); let computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); - console.timeEnd('compute'); assert.exists(computation); }); + it('Computes actions', computeAction); }); var testProperties = [ diff --git a/app/imports/api/creature/computation/newEngine/utility/cleanProp.testFn.js b/app/imports/api/creature/computation/newEngine/utility/cleanProp.testFn.js new file mode 100644 index 00000000..1c845150 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/utility/cleanProp.testFn.js @@ -0,0 +1,6 @@ +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; + +export default function cleanProp(prop){ + let schema = CreatureProperties.simpleSchema(prop); + return schema.clean(prop); +} diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index bfaf28a1..6fc08695 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -1,10 +1,6 @@ import SimpleSchema from 'simpl-schema'; -import { - ResourcesSchema, - ResourcesComputedOnlySchema, - ResourcesComputedSchema, -} from '/imports/api/properties/subSchemas/ResourcesSchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; +import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; SimpleSchema.extendOptions(['parseLevel']); @@ -45,11 +41,6 @@ let ActionSchema = createPropertySchema({ 'multipleTargets', ], }, - // Resources schema changes for between standard, computed, and computedOnly - resources: { - type: ResourcesSchema, - defaultValue: {}, - }, // Calculation of how many times this action can be used uses: { type: 'fieldToCompute', @@ -66,6 +57,61 @@ let ActionSchema = createPropertySchema({ allowedValues: ['longRest', 'shortRest'], optional: true, }, + // Resources + resources: { + type: Object, + defaultValue: {}, + }, + 'resources.itemsConsumed': { + type: Array, + defaultValue: [], + }, + 'resources.itemsConsumed.$': { + type: Object, + }, + 'resources.itemsConsumed.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'resources.itemsConsumed.$.tag': { + type: String, + optional: true, + }, + 'resources.itemsConsumed.$.quantity': { + type: 'fieldToCompute', + optional: true, + }, + 'resources.itemsConsumed.$.itemId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + 'resources.attributesConsumed': { + type: Array, + defaultValue: [], + }, + 'resources.attributesConsumed.$': { + type: Object, + }, + 'resources.attributesConsumed.$._id': { + type: String, + regEx: SimpleSchema.RegEx.Id, + autoValue(){ + if (!this.isSet) return Random.id(); + } + }, + 'resources.attributesConsumed.$.variableName': { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + 'resources.attributesConsumed.$.quantity': { + type: 'fieldToCompute', + optional: true, + }, }); const ComputedOnlyActionSchema = createPropertySchema({ @@ -77,10 +123,6 @@ const ComputedOnlyActionSchema = createPropertySchema({ type: 'computedOnlyInlineCalculationField', optional: true, }, - resources: { - type: ResourcesComputedOnlySchema, - defaultValue: {}, - }, // True if the uses left is zero, or any item or attribute consumed is // insufficient insufficientResources: { @@ -96,16 +138,70 @@ const ComputedOnlyActionSchema = createPropertySchema({ type: Number, optional: true, }, + // Resources + resources: { + type: Object, + defaultValue: {}, + }, + 'resources.itemsConsumed': { + type: Array, + defaultValue: [], + }, + 'resources.itemsConsumed.$': { + type: Object, + }, + 'resources.itemsConsumed.$.available': { + type: Number, + optional: true, + }, + 'resources.itemsConsumed.$.quantity': { + type: 'computedOnlyField', + optional: true, + }, + 'resources.itemsConsumed.$.itemName': { + type: String, + max: STORAGE_LIMITS.name, + optional: true, + }, + 'resources.itemsConsumed.$.itemIcon': { + type: storedIconsSchema, + optional: true, + max: STORAGE_LIMITS.icon, + }, + 'resources.itemsConsumed.$.itemColor': { + type: String, + optional: true, + max: STORAGE_LIMITS.color, + }, + 'resources.attributesConsumed': { + type: Array, + defaultValue: [], + }, + 'resources.attributesConsumed.$': { + type: Object, + }, + 'resources.attributesConsumed.$.quantity': { + type: 'computedOnlyField', + optional: true, + }, + 'resources.attributesConsumed.$.available': { + type: Number, + optional: true, + }, + 'resources.attributesConsumed.$.statId': { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + 'resources.attributesConsumed.$.statName': { + type: String, + optional: true, + max: STORAGE_LIMITS.name, + }, }); const ComputedActionSchema = new SimpleSchema() .extend(ActionSchema) - .extend(ComputedOnlyActionSchema) - .extend({ - resources: { - type: ResourcesComputedSchema, - defaultValue: {}, - }, - }); + .extend(ComputedOnlyActionSchema); export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema}; diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index 850f9851..14c361b0 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -3,7 +3,7 @@ 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'; -let ClassLevelSchema = createPropertySchema({ +const ClassLevelSchema = createPropertySchema({ name: { type: String, optional: true, @@ -27,4 +27,6 @@ let ClassLevelSchema = createPropertySchema({ }, }); -export { ClassLevelSchema }; +const ComputedOnlyClassLevelSchema = new SimpleSchema({}); + +export { ClassLevelSchema, ComputedOnlyClassLevelSchema }; diff --git a/app/imports/api/properties/Constants.js b/app/imports/api/properties/Constants.js index 8796eaf1..4b1f382f 100644 --- a/app/imports/api/properties/Constants.js +++ b/app/imports/api/properties/Constants.js @@ -85,4 +85,6 @@ function parseString(string, fn = 'compile'){ return {result, context} } -export { ConstantSchema }; +const ComputedOnlyConstantSchema = new SimpleSchema({}); + +export { ConstantSchema, ComputedOnlyConstantSchema }; diff --git a/app/imports/api/properties/DamageMultipliers.js b/app/imports/api/properties/DamageMultipliers.js index 4ed9c52f..d40f08be 100644 --- a/app/imports/api/properties/DamageMultipliers.js +++ b/app/imports/api/properties/DamageMultipliers.js @@ -49,4 +49,6 @@ let DamageMultiplierSchema = new SimpleSchema({ }, }); -export { DamageMultiplierSchema }; +const ComputedOnlyDamageMultiplierSchema = new SimpleSchema({}); + +export { DamageMultiplierSchema, ComputedOnlyDamageMultiplierSchema }; diff --git a/app/imports/api/properties/Folders.js b/app/imports/api/properties/Folders.js index c8ee9c12..da4e5386 100644 --- a/app/imports/api/properties/Folders.js +++ b/app/imports/api/properties/Folders.js @@ -10,4 +10,6 @@ let FolderSchema = new SimpleSchema({ }, }); -export { FolderSchema }; +const ComputedOnlyFolderSchema = new SimpleSchema({}); + +export { FolderSchema, ComputedOnlyFolderSchema }; diff --git a/app/imports/api/properties/Proficiencies.js b/app/imports/api/properties/Proficiencies.js index 1a339a64..8e8246ac 100644 --- a/app/imports/api/properties/Proficiencies.js +++ b/app/imports/api/properties/Proficiencies.js @@ -26,4 +26,6 @@ let ProficiencySchema = new SimpleSchema({ }, }); -export { ProficiencySchema }; +const ComputedOnlyProficiencySchema = new SimpleSchema({}); + +export { ProficiencySchema, ComputedOnlyProficiencySchema }; diff --git a/app/imports/api/properties/References.js b/app/imports/api/properties/References.js index 4a86007c..31ae8288 100644 --- a/app/imports/api/properties/References.js +++ b/app/imports/api/properties/References.js @@ -58,4 +58,6 @@ let ReferenceSchema = new SimpleSchema({ }, }); -export { ReferenceSchema }; +const ComputedOnlyReferenceSchema = new SimpleSchema({}); + +export { ReferenceSchema, ComputedOnlyReferenceSchema }; diff --git a/app/imports/api/properties/SlotFillers.js b/app/imports/api/properties/SlotFillers.js index c3ac0d3b..185e7ba2 100644 --- a/app/imports/api/properties/SlotFillers.js +++ b/app/imports/api/properties/SlotFillers.js @@ -39,4 +39,6 @@ let SlotFillerSchema = new SimpleSchema({ }, }); -export { SlotFillerSchema }; +const ComputedOnlySlotFillerSchema = new SimpleSchema({}); + +export { SlotFillerSchema, ComputedOnlySlotFillerSchema }; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 9d929cc2..2f7cf1ee 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -5,23 +5,23 @@ import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; -import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; -import { ConstantSchema } from '/imports/api/properties/Constants.js'; +import { ComputedOnlyClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; +import { ComputedOnlyConstantSchema } from '/imports/api/properties/Constants.js'; import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js'; import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages.js'; -import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; +import { ComputedOnlyDamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js'; import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js'; -import { FolderSchema } from '/imports/api/properties/Folders.js'; +import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; -import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; -import { ReferenceSchema } from '/imports/api/properties/References.js'; +import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js'; +import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js'; 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 { SlotFillerSchema } from '/imports/api/properties/SlotFillers.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'; @@ -33,23 +33,23 @@ const propertySchemasIndex = { attribute: ComputedOnlyAttributeSchema, buff: ComputedOnlyBuffSchema, characterClass: ComputedOnlyClassSchema, - classLevel: ClassLevelSchema, - constant: ConstantSchema, + classLevel: ComputedOnlyClassLevelSchema, + constant: ComputedOnlyConstantSchema, container: ComputedOnlyContainerSchema, damage: ComputedOnlyDamageSchema, - damageMultiplier: DamageMultiplierSchema, + damageMultiplier: ComputedOnlyDamageMultiplierSchema, effect: ComputedOnlyEffectSchema, feature: ComputedOnlyFeatureSchema, - folder: FolderSchema, + folder: ComputedOnlyFolderSchema, item: ComputedOnlyItemSchema, note: ComputedOnlyNoteSchema, - proficiency: ProficiencySchema, + proficiency: ComputedOnlyProficiencySchema, propertySlot: ComputedOnlySlotSchema, - reference: ReferenceSchema, + reference: ComputedOnlyReferenceSchema, roll: ComputedOnlyRollSchema, savingThrow: ComputedOnlySavingThrowSchema, skill: ComputedOnlySkillSchema, - slotFiller: SlotFillerSchema, + slotFiller: ComputedOnlySlotFillerSchema, spellList: ComputedOnlySpellListSchema, spell: ComputedOnlySpellSchema, toggle: ComputedOnlyToggleSchema, diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 2455c237..72fb4e8a 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -5,27 +5,20 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Get schemas that apply fields directly so they can be gracefully extended // because {type: Schema} fields can't be extended function fieldToCompute(field){ - return new SimpleSchema({ - // The object should already be set, but set again just in case - [field]: { - type: Object, - optional: true, - }, + const schemaObj = { // This is required, if we don't have a calculation delete the whole object [`${field}.calculation`]: { type: String, max: STORAGE_LIMITS.calculation, }, - }); + } + // If the field is an array, we need to include those fields as well + includeParentFields(field, schemaObj); + return new SimpleSchema(schemaObj); } function computedOnlyField(field){ - return new SimpleSchema({ - // The object should already be set, but set again just in case - [field]: { - type: Object, - optional: true, - }, + const schemaObj = { [`${field}.value`]: { type: SimpleSchema.oneOf(String, Number), optional: true, @@ -38,6 +31,30 @@ function computedOnlyField(field){ [`${field}.errors.$`]:{ type: ErrorSchema, }, + } + includeParentFields(field, schemaObj); + return new SimpleSchema(schemaObj); +} + +// We must include parent array and object fields for the schema to be valid +function includeParentFields(field, schemaObj){ + const splitField = field.split('.'); + if (splitField.length === 1){ + schemaObj[field] = {type: Object}; + return; + } + let key = ''; + splitField.push(''); + splitField.forEach(value => { + if (key){ + if (value === '$'){ + schemaObj[key] = {type: Array}; + } else { + schemaObj[key] = {type: Object}; + } + key += '.'; + } + key += value; }); } From 1270e320ce7298ce1332361ef88d739e0caf1e1c Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 17 Sep 2021 15:11:29 +0200 Subject: [PATCH 18/85] Basic testing for attribute calculations --- .../computeComputation/computeByType.js | 2 + .../computeByType/computeAction.js | 14 +++- .../computeAction/computeResources.js | 11 --- .../computeByType/computeAttribute.js | 9 +++ .../aggregate/aggregateDamageMultiplier.js | 4 - .../aggregate/aggregateDefinition.js | 3 +- .../computeVariableAsAttribute.js | 3 +- .../computeVariable/getAggregatorResult.js | 2 + .../tests/computeAttribute.testFn.js | 77 +++++++++++++++++++ .../computeComputation/tests/index.js | 10 +++ .../computeCreatureComputation.test.js | 4 +- app/imports/api/properties/Attributes.js | 4 +- 12 files changed, 120 insertions(+), 23 deletions(-) delete mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js index 4e3b4b3f..382a26c2 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js @@ -1,10 +1,12 @@ import _variable from './computeByType/computeVariable.js'; import action from './computeByType/computeAction.js'; +import attribute from './computeByType/computeAttribute.js'; import slot from './computeByType/computeSlot.js'; export default Object.freeze({ _variable, action, attack: action, + attribute, slot, }); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js index e1f0d379..903e7cfb 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js @@ -1,5 +1,3 @@ -import computeResources from './computeAction/computeResources.js'; - export default function computeAction(graph, node, scope){ const prop = node.data; if (prop.uses){ @@ -20,3 +18,15 @@ export default function computeAction(graph, node, scope){ } }); } + +function computeResources(graph, node, scope){ + const resources = node.data?.resources; + if (!resources) return; + resources.attributesConsumed.forEach(attConsumed => { + if (!attConsumed.variableName) return; + const att = scope[attConsumed.variableName]; + attConsumed.available = att.value; + attConsumed.statId = att._id; + attConsumed.statName = att.name; + }); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js deleted file mode 100644 index 26dbe9a3..00000000 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction/computeResources.js +++ /dev/null @@ -1,11 +0,0 @@ -export default function computeResources(graph, node, scope){ - const resources = node.data?.resources; - if (!resources) return; - resources.attributesConsumed.forEach(attConsumed => { - if (!attConsumed.variableName) return; - const att = scope[attConsumed.variableName]; - attConsumed.available = att.value; - attConsumed.statId = att._id; - attConsumed.statName = att.name; - }); -} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js new file mode 100644 index 00000000..86d13e11 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js @@ -0,0 +1,9 @@ +// If we compute this attribute without a variable name, it just +// uses its base value and damage since no effects can target it +// If this attribute does have a variable name, it is recomputed later +// by computeVariableAsAttribute +export default function computeAttribute(graph, node){ + const prop = node.data; + prop.total = prop.baseValue?.value || 0; + prop.value = prop.total - (prop.damage || 0); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js index 256a5c48..15b5309c 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js @@ -1,7 +1,3 @@ - -/** - * - */ export default function aggregateDamageMultipliers({node, linkedNode, link}){ if (link.data !== 'damageMultiplier') return; const multiplierValue = linkedNode.data.value; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index b4fabd7f..1d94f61c 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -18,7 +18,8 @@ export default function aggregateDefinition({node, linkedNode, link}){ } // Aggregate the base value due to the defining properties - const propBaseValue = linkedNode.data.baseValue?.value; + const propBaseValue = prop.baseValue?.value; + if (propBaseValue === undefined) return; if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ node.data.baseValue = propBaseValue; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index b5048202..fc486d03 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -1,7 +1,8 @@ import getAggregatorResult from './getAggregatorResult.js'; export default function computeVariableAsAttribute(node, prop, scope){ - let result = getAggregatorResult(node); + let result = getAggregatorResult(node, prop) || 0; + prop.total = result; prop.value = prop.total - (prop.damage || 0); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js index 38c38a2a..573a5766 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js @@ -3,7 +3,9 @@ import stripFloatingPointOddities from '/imports/api/creature/computation/newEng export default function getAggregatorResult(node){ // Work out the base value as the greater of the deining stat value or // the damage multiplier value + // This baseValue comes from aggregating definitions let statBase = node.data.baseValue; + const damageMultiplyValue = node.data.damageMultiplyValue; if (statBase === undefined || damageMultiplyValue > statBase){ statBase = damageMultiplyValue; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js new file mode 100644 index 00000000..160e0615 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js @@ -0,0 +1,77 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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('emptyId').value, 0); + assert.equal(prop('noVariableNameId').value, 8); + assert.equal(prop('strengthId').value, 12); + assert.equal(prop('strengthId').modifier, 1); + assert.equal(prop('referencesDexId').value, 4); + assert.equal(prop('hitDiceId').constitutionMod, 5); +} + +var testProperties = [ + clean({ + _id: 'emptyId', + type: 'attribute', + attributeType: 'ability', + }), + clean({ + _id: 'noVariableNameId', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '8' + }, + }), + clean({ + _id: 'strengthId', + variableName: 'strength', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '12' + }, + }), + clean({ + _id: 'dexterityId', + variableName: 'dexterity', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '15' + }, + }), + clean({ + _id: 'constitutionId', + variableName: 'constitution', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '21' + }, + }), + clean({ + _id: 'referencesDexId', + variableName: 'refDex', + type: 'attribute', + baseValue: { + calculation: 'dexterity.modifier + 2' + }, + }), + clean({ + _id: 'hitDiceId', + variableName: 'hd', + type: 'attribute', + attributeType: 'hitDice', + hitDiceSize: 'd8', + baseValue: { + calculation: '4' + }, + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js new file mode 100644 index 00000000..8c92682e --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -0,0 +1,10 @@ +import computeAction from './computeAction.testFn.js'; +import computeAttribute from './computeAttribute.testFn.js'; + +export default [{ + text: 'Computes actions', + fn: computeAction, +},{ + text: 'Computes attributes', + fn: computeAttribute, +},] diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js index 66eeaa82..f64e6e7f 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js @@ -2,7 +2,7 @@ import computeCreatureComputation from './computeCreatureComputation.js'; import { buildComputationFromProps } from './buildCreatureComputation.js'; import { assert } from 'chai'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import computeAction from './computeComputation/tests/computeAction.testFn.js'; +import computeTests from './computeComputation/tests/index.js'; describe('Compute compuation', function(){ it('Computes something at all', function(){ @@ -10,7 +10,7 @@ describe('Compute compuation', function(){ computeCreatureComputation(computation); assert.exists(computation); }); - it('Computes actions', computeAction); + computeTests.forEach(test => it(test.text, test.fn)); }); var testProperties = [ diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index b4d98161..b55194f4 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -9,15 +9,15 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope let AttributeSchema = createPropertySchema({ name: { type: String, - defaultValue: 'New Attribute', + optional: true, max: STORAGE_LIMITS.name, }, // The technical, lowercase, single-word name used in formulae variableName: { type: String, + optional: true, regEx: VARIABLE_NAME_REGEX, min: 2, - defaultValue: 'newAttribute', max: STORAGE_LIMITS.variableName, }, // How it is displayed and computed is determined by type From 347bd8e4766d41e37c9ac0c35e71819d44f2deff Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 11:41:15 +0200 Subject: [PATCH 19/85] Tested and fixed class level computations --- .../buildComputation/linkTypeDependencies.js | 2 +- .../computeByType/computeVariable.js | 2 +- .../aggregate/aggregateClassLevel.js | 3 +- .../computeVariable/computeVariableAsClass.js | 5 +- .../tests/computeClasses.testFn.js | 60 +++++++++++++++++++ .../computeComputation/tests/index.js | 6 +- .../newEngine/computeCreatureComputation.js | 4 +- .../computedOnlyPropertySchemasIndex.js | 2 +- .../computedPropertySchemasIndex.js | 2 +- .../api/properties/propertySchemasIndex.js | 2 +- app/imports/constants/PROPERTIES.js | 2 +- 11 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index 01f1d0b7..d58edd84 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -2,7 +2,7 @@ const linkDependenciesByType = { action: linkResources, attack: linkResources, attribute: linkAttribute, - characterClass: linkVariableName, + class: linkVariableName, classLevel: linkClassLevel, constant: linkVariableName, damageMultiplier: linkDamageMultiplier, diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index b66cc075..26ac02de 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -54,7 +54,7 @@ function computeVariableProp(node, prop, scope){ computeVariableAsSkill(node, prop, scope) } else if (prop.type === 'constant'){ computeVariableAsConstant(node, prop, scope) - } else if (prop.type === 'characterClass'){ + } else if (prop.type === 'class'){ computeVariableAsClass(node, prop, scope) } } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js index 84db29b2..22197dc2 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js @@ -10,6 +10,7 @@ export default function aggregateClassLevel({node, linkedNode, link}){ if (linkedProp.level > aggregator.level) aggregator.level = linkedProp.level; aggregator.levelsFilled[linkedProp.level] = true; } else if (link.data === 'level'){ - node.baseValue = (node.baseValue || 0) + node.data.classLevelAggregator.level; + node.data.baseValue = (node.data.baseValue || 0) + + linkedNode.data.classLevelAggregator.level; } } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js index d48839f2..4a0b4d77 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js @@ -2,11 +2,12 @@ export default function computeVariableAsAttribute(node, prop){ let classLevelAgg = node.data.classLevelAggregator; if (!classLevelAgg) return; prop.level = classLevelAgg.level; - classLevelAgg.levelsFilled.forEach((filled, index) => { + for (let index = 1; index < classLevelAgg.level; index++ ){ + const filled = classLevelAgg.levelsFilled[index]; if (!filled){ if (!prop.missingLevels) prop.missingLevels = []; prop.missingLevels.push(index); } - }); + } prop.missingLevels?.sort((a, b) => a - b); } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js new file mode 100644 index 00000000..d49011e2 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js @@ -0,0 +1,60 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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 scope = id => computation.scope[id]; + const prop = id => computation.propsById[id]; + assert.equal(scope('level').value, 5); + assert.equal(scope('wizard').level, 4); + assert.equal(prop('wizzardId').level, 4); + assert.deepEqual(prop('wizzardId').missingLevels, [3]); +} + +var testProperties = [ + clean({ + _id: 'wizzardId', + type: 'class', + variableName: 'wizard', + classType: 'startingClass', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'rangerId', + type: 'class', + variableName: 'ranger', + classType: 'multiClass', + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'wiz1Id', + type: 'classLevel', + variableName: 'wizard', + level: 1, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'wiz2Id', + type: 'classLevel', + variableName: 'wizard', + level: 2, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'wiz4Id', + type: 'classLevel', + variableName: 'wizard', + level: 4, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'rang1Id', + type: 'classLevel', + variableName: 'ranger', + level: 1, + ancestors: [{id: 'charId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js index 8c92682e..1ab60e82 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -1,5 +1,6 @@ import computeAction from './computeAction.testFn.js'; import computeAttribute from './computeAttribute.testFn.js'; +import computeClasses from './computeClasses.testFn.js'; export default [{ text: 'Computes actions', @@ -7,4 +8,7 @@ export default [{ },{ text: 'Computes attributes', fn: computeAttribute, -},] +},{ + text: 'Computes classes', + fn: computeClasses, +}]; diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index df8d39a7..a230f30c 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -42,5 +42,7 @@ function compute(graph, node, scope){ } function pushDependenciesToStack(nodeId, graph, stack){ - graph.forEachLinkedNode(nodeId, linkedNode => stack.push(linkedNode), true); + graph.forEachLinkedNode(nodeId, linkedNode => { + stack.push(linkedNode); + }, true); } diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index 2f7cf1ee..b01bd38d 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -32,7 +32,7 @@ const propertySchemasIndex = { attack: ComputedOnlyAttackSchema, attribute: ComputedOnlyAttributeSchema, buff: ComputedOnlyBuffSchema, - characterClass: ComputedOnlyClassSchema, + class: ComputedOnlyClassSchema, classLevel: ComputedOnlyClassLevelSchema, constant: ComputedOnlyConstantSchema, container: ComputedOnlyContainerSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index f30cd1c2..41fe5084 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -32,7 +32,7 @@ const propertySchemasIndex = { attack: ComputedAttackSchema, attribute: ComputedAttributeSchema, buff: ComputedBuffSchema, - characterClass: ComputedClassSchema, + class: ComputedClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, damage: ComputedDamageSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 00cc1644..c4c9eec8 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -32,7 +32,7 @@ const propertySchemasIndex = { attack: AttackSchema, attribute: AttributeSchema, buff: BuffSchema, - characterClass: ClassSchema, + class: ClassSchema, classLevel: ClassLevelSchema, constant: ConstantSchema, damage: DamageSchema, diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index 38692a85..0a4a296f 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -30,7 +30,7 @@ const PROPERTIES = Object.freeze({ helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.', suggestedParents: ['action', 'attack', 'savingThrow', 'spell'], }, - characterClass: { + class: { icon: 'mdi-card-account-details', name: 'Class', helpText: 'Your character should ideally have one starting class. Classes hold class levels', From e63dd2560af096ff3c28ad225079a8f3f20b0413 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 12:03:25 +0200 Subject: [PATCH 20/85] tested and fixed constant node computations --- .../tests/computeConstants.testFn.js | 28 +++++++++++++++++++ .../computeComputation/tests/index.js | 4 +++ app/imports/parser/parseTree/SymbolNode.js | 16 +++++++---- 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js new file mode 100644 index 00000000..8bf8b91d --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js @@ -0,0 +1,28 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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('attId').value, 6); +} + +var testProperties = [ + clean({ + _id: 'constId', + type: 'constant', + variableName: 'arrayConstant', + calculation: '[2, 4, 6, 8, 10]', + }), + clean({ + _id: 'attId', + type: 'attribute', + baseValue: { + calculation: 'arrayConstant[3]', + }, + ancestors: [{id: 'charId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js index 1ab60e82..490272e0 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -1,6 +1,7 @@ import computeAction from './computeAction.testFn.js'; import computeAttribute from './computeAttribute.testFn.js'; import computeClasses from './computeClasses.testFn.js'; +import computeConstants from './computeConstants.testFn.js'; export default [{ text: 'Computes actions', @@ -11,4 +12,7 @@ export default [{ },{ text: 'Computes classes', fn: computeClasses, +},{ + text: 'Computes constants', + fn: computeConstants, }]; diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js index 8f5e4bb9..814ea261 100644 --- a/app/imports/parser/parseTree/SymbolNode.js +++ b/app/imports/parser/parseTree/SymbolNode.js @@ -9,18 +9,22 @@ export default class SymbolNode extends ParseNode { toString(){ return `${this.name}` } - compile(scope, context){ + compile(scope, context, calledFromReduce = false){ let value = scope && scope[this.name]; let type = typeof value; - // For parse nodes, compile and return - if (value instanceof ParseNode){ - return value.compile(scope, context); - } // For objects, default to their .value if (type === 'object'){ value = value.value; type = typeof value; } + // For parse nodes, compile and return + if (value instanceof ParseNode){ + if (calledFromReduce){ + return value.reduce(scope, context); + } else { + return value.compile(scope, context); + } + } if (type === 'string' || type === 'number' || type === 'boolean'){ return new ConstantNode({value, type}); } else if (type === 'undefined'){ @@ -32,7 +36,7 @@ export default class SymbolNode extends ParseNode { } } reduce(scope, context){ - let result = this.compile(scope); + let result = this.compile(scope, context, true); if (result instanceof SymbolNode){ if (context) context.storeError({ type: 'info', From 734df59fd1582e537397acf5c8f7b47af964ee69 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 12:41:03 +0200 Subject: [PATCH 21/85] Tested and fixed inventory computation --- .../tests/linkInventory.testFn.js | 10 ++- .../computeComputation/computeByType.js | 2 + .../computeByType/computeContainer.js | 20 ++++++ .../aggregate/aggregateInventory.js | 8 +-- .../tests/computeInventory.testFn.js | 70 +++++++++++++++++++ .../computeComputation/tests/index.js | 6 +- 6 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js index 9a5e244e..5bda55ab 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js @@ -5,7 +5,7 @@ import clean from '../../utility/cleanProp.testFn.js'; export default function(){ const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; - + assert.isTrue( !!hasLink('weightEquipment', 'equippedAttunedItemId'), 'weight of equipment depends on equipped items' @@ -46,6 +46,14 @@ export default function(){ !!hasLink('valueCarried', 'childContainerId'), 'valueCarried does not depend on nested containers' ); + assert.isTrue( + !!hasLink('containerId', 'childContainerId'), + 'containers depend on their child containers' + ); + assert.isTrue( + !!hasLink('childContainerId', 'grandchildItemId'), + 'containers depend on their child items' + ); } var testProperties = [ diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js index 382a26c2..522a5c5c 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js @@ -2,11 +2,13 @@ import _variable from './computeByType/computeVariable.js'; import action from './computeByType/computeAction.js'; import attribute from './computeByType/computeAttribute.js'; import slot from './computeByType/computeSlot.js'; +import container from './computeByType/computeContainer.js'; export default Object.freeze({ _variable, action, attack: action, attribute, + container, slot, }); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js new file mode 100644 index 00000000..58df1508 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js @@ -0,0 +1,20 @@ +import aggregate from './computeVariable/aggregate/index.js'; + +export default function computeContainer(graph, node){ + if (!node.data) node.data = {}; + aggregateLinks(graph, node); +} + +function aggregateLinks(graph, node){ + graph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + if (!linkedNode.data) linkedNode.data = {}; + // Ignore inactive props + if (linkedNode.data.inactive) return; + // Aggregate inventory links + aggregate.inventory({node, linkedNode, link}); + }, + true // enumerate only outbound links + ); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js index e2df5384..dc36e1d8 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js @@ -34,10 +34,10 @@ export default function aggregateInventory({node, linkedNode, link}){ prop.baseValue = (prop.baseValue || 0) + weight(linkedProp); } else if (node.id === 'valueTotal'){ prop.baseValue = (prop.baseValue || 0) + value(linkedProp); - } else if (node.did === 'weightCarried'){ + } else if (node.id === 'weightCarried'){ prop.baseValue = (prop.baseValue || 0) + carriedWeight(linkedProp); - } else if (node.did === 'valueCarried'){ - prop.carriedValue = (prop.carriedValue || 0) + carriedValue(linkedProp); + } else if (node.id === 'valueCarried'){ + prop.baseValue = (prop.baseValue || 0) + carriedValue(linkedProp); } return; } @@ -52,7 +52,7 @@ function carriedWeight(prop){ } function value (prop){ - return (prop.value || 0) + (prop.value || 0); + return (prop.value || 0) + (prop.contentsValue || 0); } function carriedValue (prop){ diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js new file mode 100644 index 00000000..4b6b6a78 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js @@ -0,0 +1,70 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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]; + const scope = id => computation.scope[id].value; + console.log(computation.scope); + + assert.equal(scope('weightEquipment'), 2); + assert.equal(scope('valueEquipment'), 3); + + assert.equal(scope('itemsAttuned'), 1); + + assert.equal(prop('childContainerId').carriedWeight, 23); + assert.equal(prop('childContainerId').contentsWeight, 23); + + assert.equal(scope('weightCarried'), 58); + + assert.equal(scope('weightCarried'), 58); + assert.equal(scope('valueCarried'), 71); + + assert.equal(scope('weightTotal'), 58); + assert.equal(scope('valueTotal'), 71); +} + +var testProperties = [ + clean({ + _id: 'equippedAttunedItemId', + type: 'item', + equipped: true, + attuned: true, + weight: 2, + value: 3, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'containerId', + type: 'container', + carried: true, + weight: 5, + value: 7, + ancestors: [{id: 'charId'}], + }), + clean({ + _id: 'childContainerId', + type: 'container', + carried: true, + weight: 11, + value: 13, + ancestors: [{id: 'charId'}, {id: 'containerId'}], + }), + clean({ + _id: 'childItemId', + type: 'item', + weight: 17, + value: 19, + ancestors: [{id: 'charId'}, {id: 'containerId'}], + }), + clean({ + _id: 'grandchildItemId', + type: 'item', + weight: 23, + value: 29, + ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js index 490272e0..ff62d6aa 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -2,8 +2,9 @@ import computeAction from './computeAction.testFn.js'; import computeAttribute from './computeAttribute.testFn.js'; import computeClasses from './computeClasses.testFn.js'; import computeConstants from './computeConstants.testFn.js'; +import computeInventory from './computeInventory.testFn.js'; -export default [{ +export default [/*{ text: 'Computes actions', fn: computeAction, },{ @@ -15,4 +16,7 @@ export default [{ },{ text: 'Computes constants', fn: computeConstants, +},*/{ + text: 'Computes inventory', + fn: computeInventory, }]; From 2f893710e2064c64c24b4b2089f5f02a1a0e9f13 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 13:40:11 +0200 Subject: [PATCH 22/85] Tested and fixed damage multiplier computations --- .../computation/engine/combineStat.js | 4 +- .../buildComputation/linkTypeDependencies.js | 4 +- .../computeByType/computeVariable.js | 5 +-- .../tests/computeDamageMultipliers.testFn.js | 40 +++++++++++++++++++ .../tests/computeInventory.testFn.js | 1 - .../computeComputation/tests/index.js | 8 +++- 6 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js diff --git a/app/imports/api/creature/computation/engine/combineStat.js b/app/imports/api/creature/computation/engine/combineStat.js index f2c10e8f..e69d9c27 100644 --- a/app/imports/api/creature/computation/engine/combineStat.js +++ b/app/imports/api/creature/computation/engine/combineStat.js @@ -186,9 +186,9 @@ function combineSkill(stat, aggregator, memo){ function combineDamageMultiplier(stat){ if (stat.immunityCount) return 0; let result; - if (stat.ressistanceCount && !stat.vulnerabilityCount){ + if (stat.resistanceCount && !stat.vulnerabilityCount){ result = 0.5; - } else if (!stat.ressistanceCount && stat.vulnerabilityCount){ + } else if (!stat.resistanceCount && stat.vulnerabilityCount){ result = 2; } else { result = 1; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index d58edd84..ae68ba30 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -68,7 +68,9 @@ function linkAttribute(dependencyGraph, prop){ function linkDamageMultiplier(dependencyGraph, prop){ prop.damageTypes.forEach(damageType => { - dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, prop.type); + // Remove all non-letter characters from the damage name + const damageName = damageType.replace(/[^a-z]/gi, '') + dependencyGraph.addLink(`${damageName}Multiplier`, prop._id, prop.type); }); } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index 26ac02de..9cf43842 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -69,18 +69,17 @@ function combineMultiplierAggregator(node){ if (aggregator.immunityCount){ value = 0; } else if ( - aggregator.ressistanceCount && + aggregator.resistanceCount && !aggregator.vulnerabilityCount ){ value = 0.5; } else if ( - !aggregator.ressistanceCount && + !aggregator.resistanceCount && aggregator.vulnerabilityCount ){ value = 2; } else { value = 1; } - node.data.damageMultiplyValue = value; } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js new file mode 100644 index 00000000..94ce0b24 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js @@ -0,0 +1,40 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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 scope = id => computation.scope[id]; + assert.equal(scope('blugeoningMultiplier').value, 1); + assert.equal(scope('customDamageMultiplier').value, 0.5); + assert.equal(scope('slashingMultiplier').value, 0); +} + +var testProperties = [ + clean({ + _id: 'resistanceId', + type: 'damageMultiplier', + damageTypes: ['blugeoning', 'customDamage'], + value: 0.5, + }), + clean({ + _id: 'vulnerabilityId', + type: 'damageMultiplier', + damageTypes: ['blugeoning'], + value: 2, + }), + clean({ + _id: 'slashResistId', + type: 'damageMultiplier', + damageTypes: ['slashing'], + value: 0.5, + }), + clean({ + _id: 'slashInvulnId', + type: 'damageMultiplier', + damageTypes: ['slashing'], + value: 0, + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js index 4b6b6a78..d77b5b59 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js @@ -8,7 +8,6 @@ export default function(){ computeCreatureComputation(computation); const prop = id => computation.propsById[id]; const scope = id => computation.scope[id].value; - console.log(computation.scope); assert.equal(scope('weightEquipment'), 2); assert.equal(scope('valueEquipment'), 3); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js index ff62d6aa..d8d61cf0 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -3,8 +3,9 @@ import computeAttribute from './computeAttribute.testFn.js'; import computeClasses from './computeClasses.testFn.js'; import computeConstants from './computeConstants.testFn.js'; import computeInventory from './computeInventory.testFn.js'; +import computeDamageMultipliers from './computeDamageMultipliers.testFn.js'; -export default [/*{ +export default [{ text: 'Computes actions', fn: computeAction, },{ @@ -16,7 +17,10 @@ export default [/*{ },{ text: 'Computes constants', fn: computeConstants, -},*/{ +},{ text: 'Computes inventory', fn: computeInventory, +},{ + text: 'Computes damage multipliers', + fn: computeDamageMultipliers, }]; From c0a9a1251d0d9f9de53f596d3d0452be48124c68 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 15:22:01 +0200 Subject: [PATCH 23/85] Tested and fixed effect computations --- .../computeByType/computeAction.js | 8 +-- .../computeByType/computeAttribute.js | 2 +- .../computeByType/computeContainer.js | 8 +-- .../computeByType/computeSlot.js | 2 +- .../computeByType/computeVariable.js | 29 ++++----- .../computeVariableAsAttribute.js | 4 +- .../computeVariable/computeVariableAsClass.js | 2 +- .../computeVariableAsConstant.js | 2 +- .../computeComputation/computeCalculations.js | 4 +- .../computeComputation/computeToggles.js | 2 +- .../tests/computeEffects.testFn.js | 59 +++++++++++++++++++ .../computeComputation/tests/index.js | 4 ++ .../newEngine/computeCreatureComputation.js | 11 ++-- app/imports/api/properties/Effects.js | 1 - 14 files changed, 100 insertions(+), 38 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js index 903e7cfb..dde680fc 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js @@ -1,9 +1,9 @@ -export default function computeAction(graph, node, scope){ +export default function computeAction(computation, node){ const prop = node.data; if (prop.uses){ prop.usesLeft = prop.uses.value - (prop.usesUsed || 0); } - computeResources(graph, node, scope); + computeResources(computation, node); if (!prop.resources) return; prop.resources.itemsConsumed.forEach(itemConsumed => { if (!itemConsumed.itemId) return; @@ -19,12 +19,12 @@ export default function computeAction(graph, node, scope){ }); } -function computeResources(graph, node, scope){ +function computeResources(computation, node){ const resources = node.data?.resources; if (!resources) return; resources.attributesConsumed.forEach(attConsumed => { if (!attConsumed.variableName) return; - const att = scope[attConsumed.variableName]; + const att = computation.scope[attConsumed.variableName]; attConsumed.available = att.value; attConsumed.statId = att._id; attConsumed.statName = att.name; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js index 86d13e11..13c43072 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js @@ -2,7 +2,7 @@ // uses its base value and damage since no effects can target it // If this attribute does have a variable name, it is recomputed later // by computeVariableAsAttribute -export default function computeAttribute(graph, node){ +export default function computeAttribute(computation, node){ const prop = node.data; prop.total = prop.baseValue?.value || 0; prop.value = prop.total - (prop.damage || 0); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js index 58df1508..61b4b79a 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js @@ -1,12 +1,12 @@ import aggregate from './computeVariable/aggregate/index.js'; -export default function computeContainer(graph, node){ +export default function computeContainer(computation, node){ if (!node.data) node.data = {}; - aggregateLinks(graph, node); + aggregateLinks(computation, node); } -function aggregateLinks(graph, node){ - graph.forEachLinkedNode( +function aggregateLinks(computation, node){ + computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { if (!linkedNode.data) linkedNode.data = {}; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js index 66c9e790..654655ba 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js @@ -1,4 +1,4 @@ -export default function computSlot(graph, node){ +export default function computSlot(computation, node){ const prop = node.data; if (prop.quantityExpected){ prop.spaceLeft = prop.quantityExpected - prop.totalFilled; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js index 9cf43842..ee9685db 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js @@ -5,21 +5,22 @@ import computeVariableAsConstant from './computeVariable/computeVariableAsConsta import computeVariableAsClass from './computeVariable/computeVariableAsClass.js'; import computeImplicitVariable from './computeVariable/computeImplicitVariable.js'; -export default function computeVariable(graph, node, scope){ +export default function computeVariable(computation, node){ + const scope = computation.scope; if (!node.data) node.data = {}; - aggregateLinks(graph, node); - combineAggregations(node, scope); + aggregateLinks(computation, node); + combineAggregations(computation, node); if (node.data.definingProp){ // Add the defining variable to the scope scope[node.id] = node.data.definingProp } else { // Otherwise add an implicit variable to the scope - scope[node.id] = computeImplicitVariable(node, scope); + scope[node.id] = computeImplicitVariable(node); } } -function aggregateLinks(graph, node){ - graph.forEachLinkedNode( +function aggregateLinks(computation, node){ + computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { if (!linkedNode.data) linkedNode.data = {}; @@ -38,24 +39,24 @@ function aggregateLinks(graph, node){ ); } -function combineAggregations(node, scope){ +function combineAggregations(computation, node){ combineMultiplierAggregator(node); node.data.overridenProps?.forEach(prop => { - computeVariableProp(node, prop, scope); + computeVariableProp(computation, node, prop); }); - computeVariableProp(node, node.data.definingProp, scope); + computeVariableProp(computation, node, node.data.definingProp); } -function computeVariableProp(node, prop, scope){ +function computeVariableProp(computation, node, prop){ if (!prop) return; if (prop.type === 'attribute'){ - computeVariableAsAttribute(node, prop, scope) + computeVariableAsAttribute(computation, node, prop) } else if (prop.type === 'skill'){ - computeVariableAsSkill(node, prop, scope) + computeVariableAsSkill(computation, node, prop) } else if (prop.type === 'constant'){ - computeVariableAsConstant(node, prop, scope) + computeVariableAsConstant(computation, node, prop) } else if (prop.type === 'class'){ - computeVariableAsClass(node, prop, scope) + computeVariableAsClass(computation, node, prop) } } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js index fc486d03..3a7c2982 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -1,6 +1,6 @@ import getAggregatorResult from './getAggregatorResult.js'; -export default function computeVariableAsAttribute(node, prop, scope){ +export default function computeVariableAsAttribute(computation, node, prop){ let result = getAggregatorResult(node, prop) || 0; prop.total = result; @@ -16,7 +16,7 @@ export default function computeVariableAsAttribute(node, prop, scope){ // Hit dice denormalise constitution modifier if (prop.attributeType === 'hitDice') { - prop.constitutionMod = scope['constitution']?.modifier || 0; + prop.constitutionMod = computation.scope['constitution']?.modifier || 0; } // Stats that have no effects or base value can be hidden diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js index 4a0b4d77..187072fc 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js @@ -1,4 +1,4 @@ -export default function computeVariableAsAttribute(node, prop){ +export default function computeVariableAsAttribute(computation, node, prop){ let classLevelAgg = node.data.classLevelAggregator; if (!classLevelAgg) return; prop.level = classLevelAgg.level; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js index c9070937..898d2c5e 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js @@ -1,6 +1,6 @@ import { parse } from '/imports/parser/parser.js'; -export default function computeVariableAsConstant(node, prop){ +export default function computeVariableAsConstant(computation, node, prop){ let string = prop.calculation; if (!string) return; let parseNode; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js index 129ffb5b..04212b13 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js @@ -2,11 +2,11 @@ import { CompilationContext } from '/imports/parser/parser.js'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -export default function computeCalculations(node, scope){ +export default function computeCalculations(computation, node){ if (!node.data) return; // evaluate all the calculations node.data._computationDetails?.calculations?.forEach(calcObj => { - evaluateCalculation(calcObj, scope) + evaluateCalculation(calcObj, computation.scope) }); node.data._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { embedInlineCalculations(inlineCalcObj); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js index 50fff1db..3a49fe53 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js @@ -1,4 +1,4 @@ -export default function evaluateToggles(node){ +export default function evaluateToggles(computation, node){ let prop = node.data; if (!prop) return; let toggles = prop._computationDetails?.toggleAncestors; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js new file mode 100644 index 00000000..dea3f464 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js @@ -0,0 +1,59 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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, 26); +} + +var testProperties = [ + clean({ + _id: 'strengthId', + variableName: 'strength', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '8' + }, + }), + clean({ + _id: 'strength2Id', + variableName: 'strength', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '10' + }, + }), + clean({ + _id: 'strengthBaseId', + type: 'effect', + operation: 'base', + amount: { + calculation: '10 + 2' + }, + stats: ['strength'], + }), + clean({ + _id: 'strengthAddId', + type: 'effect', + operation: 'add', + amount: { + calculation: '1' + }, + stats: ['strength'], + }), + clean({ + _id: 'strengthMulId', + type: 'effect', + operation: 'mul', + amount: { + calculation: '2' + }, + stats: ['strength'], + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js index d8d61cf0..26920321 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -4,6 +4,7 @@ import computeClasses from './computeClasses.testFn.js'; import computeConstants from './computeConstants.testFn.js'; import computeInventory from './computeInventory.testFn.js'; import computeDamageMultipliers from './computeDamageMultipliers.testFn.js'; +import computeEffects from './computeEffects.testFn.js'; export default [{ text: 'Computes actions', @@ -23,4 +24,7 @@ export default [{ },{ text: 'Computes damage multipliers', fn: computeDamageMultipliers, +},{ + text: 'Computes effects', + fn: computeEffects, }]; diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js index a230f30c..a317e383 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -5,7 +5,6 @@ import computeByType from '/imports/api/creature/computation/newEngine/computeCo export default function computeCreatureComputation(computation){ const stack = []; // Computation scope of {variableName: prop} - const scope = computation.scope; const graph = computation.dependencyGraph; // Add all nodes to the stack graph.forEachNode(node => { @@ -24,7 +23,7 @@ export default function computeCreatureComputation(computation){ top._visited = true; stack.pop(); // Compute the top object of the stack - compute(graph, top, scope); + compute(computation, top); } else { top._visitedChildren = true; // Push dependencies to graph to be computed first @@ -33,12 +32,12 @@ export default function computeCreatureComputation(computation){ } } -function compute(graph, node, scope){ +function compute(computation, node){ // Determine the prop's active status by its toggles - computeToggles(node); - computeCalculations(node, scope); + computeToggles(computation, node); + computeCalculations(computation, node); // Compute the property by type - computeByType[node.data?.type || '_variable']?.(graph, node, scope); + computeByType[node.data?.type || '_variable']?.(computation, node); } function pushDependenciesToStack(nodeId, graph, stack){ diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index c3721458..f901ba50 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -27,7 +27,6 @@ let EffectSchema = createPropertySchema({ 'passiveAdd', 'fail', 'conditional', - 'rollBonus', ], }, amount: { From 8ea04fc78698b2b0db401be5a47da816639f8bd9 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 15:22:28 +0200 Subject: [PATCH 24/85] Implemented effect and proficiency inheritance from abilities to skills --- .../computeVariable/computeVariableAsSkill.js | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index e9ae5ae1..88251218 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -1,14 +1,24 @@ -export default function computeVariableAsSkill(node, prop, scope){ +import aggregate from './aggregate/index.js'; + +export default function computeVariableAsSkill(computation, node, prop){ // Skills are based on some ability Modifier - let ability = scope[prop.ability]; + let ability = computation.scope[prop.ability]; prop.abilityMod = ability?.modifier || 0; - // TODO: Use this ability's skill effects/profs iff this skill is not a save + + // Inherit the ability's skill effects and proficiencies if skill is not a save + if (prop.skillType !== 'save' && ability){ + aggregateAbilityEffects({ + computation, + skillNode: node, + abilityNode: computation.dependencyGraph.getNode(prop.ability) + }); + } // Proficiency prop.proficiency = node.data.proficiency; // Get the character's proficiency bonus to apply - let profBonus = scope['proficiencyBonus']?.value || 0; + let profBonus = computation.scope['proficiencyBonus']?.value || 0; // Multiply the proficiency bonus by the actual proficiency if(prop.proficiency === 0.49){ @@ -61,3 +71,28 @@ export default function computeVariableAsSkill(node, prop, scope){ // Rollbonus prop.rollBonuses = aggregator.rollBonus; } + +function aggregateAbilityEffects({computation, skillNode, abilityNode}){ + computation.dependencyGraph.forEachLinkedNode( + abilityNode.id, + (linkedNode, link) => { + if (!linkedNode.data) linkedNode.data = {}; + // Ignore inactive props + 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 (![ + 'advantage', 'disadvantage', 'passiveAdd', 'fail' + ].includes(linkedNode.data.operation)){ + return; + } + } + // Apply the aggregations + let arg = {node: skillNode, linkedNode, link}; + aggregate.effect(arg); + aggregate.proficiency(arg); + }, + true // enumerate only outbound links + ); +} From 2cb14146d48af2d572b082fd7f5eae0d832d2776 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 23 Sep 2021 15:44:50 +0200 Subject: [PATCH 25/85] Tested and fixed skill compuations --- .../computeVariable/computeVariableAsSkill.js | 2 +- .../tests/computeSkills.testFn.js | 84 +++++++++++++++++++ .../computeComputation/tests/index.js | 4 + app/imports/api/properties/Attributes.js | 2 +- 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js index 88251218..da0ed54b 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js @@ -37,7 +37,7 @@ export default function computeVariableAsSkill(computation, node, prop){ prop.hide = statBase === undefined && prop.proficiency == 0 || undefined; - prop.value = statBase; + prop.value = (statBase || 0) + prop.abilityMod + profBonus; return; } // Combine aggregator diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js new file mode 100644 index 00000000..fa6c86a8 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js @@ -0,0 +1,84 @@ +import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/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('atheleticsId').proficiency, 2, 'Inherits proficiency from ability'); + assert.equal(prop('atheleticsId').value, 7, 'calculates value correctly'); + assert.equal(prop('atheleticsId').advantage, 1, 'Inherits advantage from ability'); + assert.equal(prop('strengthSaveId').advantage, undefined, 'Saves don\'t inherit effects'); + assert.equal(prop('strengthSaveId').value, 4, 'Saves calculate correctly'); + + assert.equal(prop('acrobaticsId').value, 1); + assert.equal(prop('toolsId').value, 7); +} + +var testProperties = [ + clean({ + _id: 'strengthId', + variableName: 'strength', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '12' + }, + }), + clean({ + _id: 'profBonusId', + variableName: 'proficiencyBonus', + type: 'attribute', + attributeType: 'modifier', + baseValue: { + calculation: '3' + }, + }), + clean({ + _id: 'atheleticsId', + variableName: 'athletics', + type: 'skill', + skillType: 'skill', + ability: 'strength', + }), + clean({ + _id: 'acrobaticsId', + variableName: 'acrobatics', + type: 'skill', + skillType: 'skill', + baseProficiency: 0.49, + }), + clean({ + _id: 'toolsId', + variableName: 'tools', + type: 'skill', + skillType: 'tool', + baseProficiency: 0.5, + baseValue: { + calculation: '5', + } + }), + clean({ + _id: 'strengthAdvantageId', + type: 'effect', + operation: 'advantage', + stats: ['strength'], + }), + clean({ + _id: 'strengthProficiencyId', + type: 'proficiency', + value: 2, + stats: ['strength'], + }), + clean({ + _id: 'strengthSaveId', + variableName: 'strengthSave', + type: 'skill', + skillType: 'save', + ability: 'strength', + baseProficiency: 1, + }), +]; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js index 26920321..b9c8baf7 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js +++ b/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js @@ -5,6 +5,7 @@ import computeConstants from './computeConstants.testFn.js'; import computeInventory from './computeInventory.testFn.js'; import computeDamageMultipliers from './computeDamageMultipliers.testFn.js'; import computeEffects from './computeEffects.testFn.js'; +import computeSkills from './computeSkills.testFn.js'; export default [{ text: 'Computes actions', @@ -27,4 +28,7 @@ export default [{ },{ text: 'Computes effects', fn: computeEffects, +},{ + text: 'Computes skills', + fn: computeSkills, }]; diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index b55194f4..df50be09 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -26,7 +26,7 @@ let AttributeSchema = createPropertySchema({ allowedValues: [ 'ability', //Strength, Dex, Con, etc. 'stat', // Speed, Armor Class - 'modifier', // Proficiency Bonus, Initiative + 'modifier', // Proficiency Bonus, displayed as +x 'hitDice', // d12 hit dice 'healthBar', // Hitpoints, Temporary Hitpoints, can take damage 'bar', // Displayed as a health bar, can't take damage From 875b2f7c04877b6a1adda0aa8dabeb1df33ac839 Mon Sep 17 00:00:00 2001 From: Weslley Nascimento Rocha Date: Sat, 25 Sep 2021 16:36:59 -0300 Subject: [PATCH 26/85] tableLookup function was returning a string tableLookup function was returning a string instead of number --- app/imports/parser/functions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index b1ec5a91..3d5b9c11 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -106,7 +106,7 @@ export default { fn: function tableLookup(arrayNode, number){ for(let i in arrayNode.values){ let node = arrayNode.values[i]; - if (node.value > number) return i; + if (node.value > number) return Number(i); } return arrayNode.values.length; } From 706303862fc3bcc0ec5e093b5adbf81034f5fb60 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 11:06:48 +0200 Subject: [PATCH 27/85] Fixed class level not having computed description --- app/imports/api/properties/ClassLevels.js | 13 +++++++++++-- .../api/properties/computedPropertySchemasIndex.js | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/imports/api/properties/ClassLevels.js b/app/imports/api/properties/ClassLevels.js index 14c361b0..9b42c7b9 100644 --- a/app/imports/api/properties/ClassLevels.js +++ b/app/imports/api/properties/ClassLevels.js @@ -27,6 +27,15 @@ const ClassLevelSchema = createPropertySchema({ }, }); -const ComputedOnlyClassLevelSchema = new SimpleSchema({}); +const ComputedOnlyClassLevelSchema = new SimpleSchema({ + description: { + type: 'computedOnlyInlineCalculationField', + optional: true, + }, +}); -export { ClassLevelSchema, ComputedOnlyClassLevelSchema }; +const ComputedClassLevelSchema = new SimpleSchema() + .extend(ComputedOnlyClassLevelSchema) + .extend(ClassLevelSchema); + +export { ClassLevelSchema, ComputedOnlyClassLevelSchema, ComputedClassLevelSchema }; diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 41fe5084..e9dc3fea 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -5,7 +5,7 @@ import { ComputedAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; -import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; +import { ComputedClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { ConstantSchema } from '/imports/api/properties/Constants.js'; import { ComputedContainerSchema } from '/imports/api/properties/Containers.js'; import { ComputedDamageSchema } from '/imports/api/properties/Damages.js'; @@ -33,7 +33,7 @@ const propertySchemasIndex = { attribute: ComputedAttributeSchema, buff: ComputedBuffSchema, class: ComputedClassSchema, - classLevel: ClassLevelSchema, + classLevel: ComputedClassLevelSchema, constant: ConstantSchema, damage: ComputedDamageSchema, damageMultiplier: DamageMultiplierSchema, From 091e367d2790e6d00b9534088e4029b9f00a431d Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 11:20:40 +0200 Subject: [PATCH 28/85] Fixed suggested parent of class levels to be class --- app/imports/constants/PROPERTIES.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index 0a4a296f..5704ada9 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -34,13 +34,13 @@ const PROPERTIES = Object.freeze({ icon: 'mdi-card-account-details', name: 'Class', helpText: 'Your character should ideally have one starting class. Classes hold class levels', - suggestedParents: ['class'], + suggestedParents: [], }, classLevel: { icon: '$vuetify.icons.class_level', name: 'Class level', helpText: 'Class levels represent a single level gained in a class', - suggestedParents: [], + suggestedParents: ['class'], }, constant: { icon: 'mdi-anchor', From 5e83a88132c57b5c1937e7923b1c8e99f6372961 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 11:21:10 +0200 Subject: [PATCH 29/85] Added migrations for all properties --- .../migrations/server/2.0-beta.33-dbv1.js | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js index da930803..071a8ccc 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -66,6 +66,69 @@ const transformsByPropType = { ...actionTransforms, ...getComputedPropertyTransforms('rollBonus'), ], + 'attribute': [ + ...getComputedPropertyTransforms('baseValue'), + ...getComputedPropertyTransforms('spellSlotLevel'), + ...getInlineComputationTransforms('description'), + {from: 'value', to: 'total'}, + ], + 'buff': [ + ...getComputedPropertyTransforms('duration'), + ...getComputedPropertyTransforms('spellSlotLevel'), + ...getInlineComputationTransforms('description'), + {from: 'value', to: 'total'}, + ], + 'classLevel': [ + ...getInlineComputationTransforms('description'), + ], + 'container': [ + ...getInlineComputationTransforms('description'), + ], + 'damage': [ + ...getComputedPropertyTransforms('amount'), + ], + 'effect': [ + {from: 'calculation', to: 'amount.calculation'}, + {from: 'result', to: 'amount.value'}, + {from: 'errors', to: 'amount.errors'}, + ], + 'feature': [ + ...getInlineComputationTransforms('summary'), + ...getInlineComputationTransforms('description'), + ], + 'item': [ + ...getInlineComputationTransforms('description'), + ], + 'note': [ + ...getInlineComputationTransforms('summary'), + ...getInlineComputationTransforms('description'), + ], + 'roll': [ + ...getComputedPropertyTransforms('roll'), + ], + 'savingThrow': [ + ...getComputedPropertyTransforms('dc'), + ], + 'skill': [ + ...getComputedPropertyTransforms('baseValue'), + ...getInlineComputationTransforms('description'), + ], + 'propertySlot': [ + ...getComputedPropertyTransforms('quantityExpected'), + ...getComputedPropertyTransforms('slotCondition'), + ...getInlineComputationTransforms('description'), + ], + 'spellList': [ + ...getComputedPropertyTransforms('maxPrepared'), + ...getComputedPropertyTransforms('dc'), + ...getComputedPropertyTransforms('attackRollBonus'), + ...getInlineComputationTransforms('description'), + ], + 'toggle': [ + {from: 'condition', to: 'condition.calculation'}, + {from: 'toggleResult', to: 'condition.value'}, + {from: 'errors', to: 'condition.errors'}, + ], }; function getComputedPropertyTransforms(key){ From fe83b5cbc53abe730885275223ca114b3b208a4c Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 13:42:37 +0200 Subject: [PATCH 30/85] Fixed migration errors when tested against a copy of the live dataset --- app/imports/api/properties/Attributes.js | 3 +- app/imports/api/properties/Effects.js | 2 +- app/imports/api/properties/Skills.js | 1 + app/imports/api/properties/Spells.js | 8 +- .../properties/subSchemas/computedField.js | 8 +- .../subSchemas/createPropertySchema.js | 1 + app/imports/constants/STORAGE_LIMITS.js | 11 +-- .../migrations/server/2.0-beta.33-dbv1.js | 77 ++++++++++++++++--- .../migrations/server/transformFields.js | 3 +- .../server/config/simpleSchemaDebug.js | 2 +- 10 files changed, 87 insertions(+), 29 deletions(-) diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index df50be09..a45b5c98 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -58,11 +58,10 @@ let AttributeSchema = createPropertySchema({ type: 'inlineCalculationFieldToCompute', optional: true, }, - // The damage done to the attribute, always positive + // The damage done to the attribute, should always compute as positive damage: { type: SimpleSchema.Integer, optional: true, - min: 0, }, // Can the value be decimal? decimal: { diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index f901ba50..542d344d 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -37,7 +37,7 @@ let EffectSchema = createPropertySchema({ text: { type: String, optional: true, - max: STORAGE_LIMITS.name, + max: STORAGE_LIMITS.effectCondition, }, //which stats the effect is applied to stats: { diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index 71846ae1..b53119f3 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -65,6 +65,7 @@ let ComputedOnlySkillSchema = createPropertySchema({ value: { type: Number, defaultValue: 0, + optional: true, }, // The result of baseValueCalculation baseValue: { diff --git a/app/imports/api/properties/Spells.js b/app/imports/api/properties/Spells.js index d1d97fef..304e16a3 100644 --- a/app/imports/api/properties/Spells.js +++ b/app/imports/api/properties/Spells.js @@ -44,18 +44,18 @@ let SpellSchema = new SimpleSchema({}) type: String, optional: true, defaultValue: 'action', - max: STORAGE_LIMITS.name, + max: STORAGE_LIMITS.spellDetail, }, range: { type: String, optional: true, - max: STORAGE_LIMITS.name, + max: STORAGE_LIMITS.spellDetail, }, duration: { type: String, optional: true, defaultValue: 'Instantaneous', - max: STORAGE_LIMITS.name, + max: STORAGE_LIMITS.spellDetail, }, verbal: { type: Boolean, @@ -72,7 +72,7 @@ let SpellSchema = new SimpleSchema({}) material: { type: String, optional: true, - max: STORAGE_LIMITS.name, + max: STORAGE_LIMITS.spellDetail, }, ritual: { type: Boolean, diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 72fb4e8a..925912da 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -6,10 +6,10 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // because {type: Schema} fields can't be extended function fieldToCompute(field){ const schemaObj = { - // This is required, if we don't have a calculation delete the whole object [`${field}.calculation`]: { type: String, max: STORAGE_LIMITS.calculation, + optional: true, }, } // If the field is an array, we need to include those fields as well @@ -40,7 +40,7 @@ function computedOnlyField(field){ function includeParentFields(field, schemaObj){ const splitField = field.split('.'); if (splitField.length === 1){ - schemaObj[field] = {type: Object}; + schemaObj[field] = {type: Object, optional: true}; return; } let key = ''; @@ -48,9 +48,9 @@ function includeParentFields(field, schemaObj){ splitField.forEach(value => { if (key){ if (value === '$'){ - schemaObj[key] = {type: Array}; + schemaObj[key] = {type: Array, optional: true}; } else { - schemaObj[key] = {type: Object}; + schemaObj[key] = {type: Object, optional: true}; } key += '.'; } diff --git a/app/imports/api/properties/subSchemas/createPropertySchema.js b/app/imports/api/properties/subSchemas/createPropertySchema.js index 7e7b1c36..f4e206fe 100644 --- a/app/imports/api/properties/subSchemas/createPropertySchema.js +++ b/app/imports/api/properties/subSchemas/createPropertySchema.js @@ -78,6 +78,7 @@ function applyDefaultCalculationValue(definition, key){ definition[calcField] = { type: String, defaultValue, + optional: true, }; } delete def.defaultValue; diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index d3e4251e..11cc55d0 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -1,6 +1,6 @@ const STORAGE_LIMITS = Object.freeze({ // String lengths - calculation: 256, + calculation: 1024, collectionName: 64, color: 10000, description: 49473, //the length of the Bee Movie script @@ -10,8 +10,10 @@ const STORAGE_LIMITS = Object.freeze({ name: 128, summary: 10000, tagLength: 128, - url: 256, + url: 1024, variableName: 64, + spellDetail: 512, + effectText: 512, // Number limits levelMax: 128, @@ -22,13 +24,12 @@ const STORAGE_LIMITS = Object.freeze({ diceRollValuesCount: 100, errorCount: 32, extraTagsCount: 5, - inlineCalculationCount: 32, + inlineCalculationCount: 64, logContentCount: 32, readersCount: 50, resourcesCount: 32, rollCount: 64, - rollBonusCount: 32, - statsToTarget: 32, + statsToTarget: 64, tagCount: 64, writersCount: 20, }); diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js index 071a8ccc..112e22ce 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -5,6 +5,7 @@ import { get } from 'lodash'; import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js'; import transformFields from '/imports/migrations/server/transformFields.js'; import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; // Git version 2.0-beta.33 // Database version 1 @@ -34,7 +35,10 @@ function migrateCollection({collection, reversed}){ } export default function migrateProperty({collection, reversed, prop}){ - const transforms = transformsByPropType[prop.type]; + const transforms = [ + ...(transformsByPropType[prop.type] || []), + {from: 'dependencies'} + ]; let migratedProp = transformFields(prop, transforms, reversed); const schema = collection.simpleSchema({type: prop.type}); // Only clean if the schema version matches our destination version @@ -43,7 +47,15 @@ export default function migrateProperty({collection, reversed, prop}){ migratedProp = schema.clean(migratedProp); schema.validate(migratedProp); } catch(e){ - console.warn(e); + if (e.details[0]?.type === 'maxString'){ + + console.log({ + prop: prop, + details: e.details, + }); + } else { + console.warn({prop, error: e}); + } } } return migratedProp; @@ -70,13 +82,13 @@ const transformsByPropType = { ...getComputedPropertyTransforms('baseValue'), ...getComputedPropertyTransforms('spellSlotLevel'), ...getInlineComputationTransforms('description'), - {from: 'value', to: 'total'}, + {from: 'value', to: 'total', up: nanToNull}, + {from: 'proficiency', to: 'proficiency', up: stripZero}, ], 'buff': [ ...getComputedPropertyTransforms('duration'), - ...getComputedPropertyTransforms('spellSlotLevel'), ...getInlineComputationTransforms('description'), - {from: 'value', to: 'total'}, + {from: 'value', to: 'total', up: nanToNull}, ], 'classLevel': [ ...getInlineComputationTransforms('description'), @@ -89,8 +101,16 @@ const transformsByPropType = { ], 'effect': [ {from: 'calculation', to: 'amount.calculation'}, - {from: 'result', to: 'amount.value'}, - {from: 'errors', to: 'amount.errors'}, + {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'), @@ -112,6 +132,15 @@ const transformsByPropType = { 'skill': [ ...getComputedPropertyTransforms('baseValue'), ...getInlineComputationTransforms('description'), + {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}, ], 'propertySlot': [ ...getComputedPropertyTransforms('quantityExpected'), @@ -126,16 +155,16 @@ const transformsByPropType = { ], 'toggle': [ {from: 'condition', to: 'condition.calculation'}, - {from: 'toggleResult', to: 'condition.value'}, - {from: 'errors', to: 'condition.errors'}, + {from: 'toggleResult', to: 'condition.value', up: nanToNull}, + {from: 'errors', to: 'condition.errors', up: trimErrors}, ], }; function getComputedPropertyTransforms(key){ return [ {from: key, to: `${key}.calculation`}, - {from: `${key}Result`, to: `${key}.value`}, - {from: `${key}Errors`, to: `${key}.errors`}, + {from: `${key}Result`, to: `${key}.value`, up: nanToNull}, + {from: `${key}Errors`, to: `${key}.errors`, up: trimErrors}, ]; } @@ -149,3 +178,29 @@ function getInlineComputationTransforms(key){ {from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`}, ]; } + +function nanToNull(val){ + if (Number.isNaN(val)){ + return null; + } else { + return val; + } +} + +function stripZero(val){ + if (val === 0){ + return undefined; + } else { + return val; + } +} + +function trimErrors(arr){ + if(!arr) return arr; + arr.forEach(e => { + if (e.message.length > STORAGE_LIMITS.errorMessage){ + e.message = e.message.slice(0, STORAGE_LIMITS.errorMessage); + } + }); + return arr; +} diff --git a/app/imports/migrations/server/transformFields.js b/app/imports/migrations/server/transformFields.js index 5fc68b51..60bd9094 100644 --- a/app/imports/migrations/server/transformFields.js +++ b/app/imports/migrations/server/transformFields.js @@ -1,6 +1,7 @@ import { get, set, unset, forEachRight, cloneDeep } from 'lodash'; export default function transformFields(src, transformList, reversed = false){ + if (!transformList) return src; // don't bash the old document during the transforms let doc = cloneDeep(src); for(let originalTransform of transformList){ @@ -80,7 +81,7 @@ function transformArrayField(src, doc, {from, to, up}, reversed){ if (reversed){ forEachRight(state.array, iterate(stack, state, src, doc, toSplit, up)); } else { - state.array.forEach(iterate(stack, state, src, doc, toSplit, up)); + state.array?.forEach(iterate(stack, state, src, doc, toSplit, up)); } } } diff --git a/app/imports/server/config/simpleSchemaDebug.js b/app/imports/server/config/simpleSchemaDebug.js index 1bf2e900..ce81e01d 100644 --- a/app/imports/server/config/simpleSchemaDebug.js +++ b/app/imports/server/config/simpleSchemaDebug.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; if (Meteor.isDevelopment){ - SimpleSchema.debug = true + //SimpleSchema.debug = true } From 2228802dd338be2107d2f096103150a288309b1e Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 13:44:43 +0200 Subject: [PATCH 31/85] moved v1 migrations to the migrations folder --- .../{api/creature => migrations}/v1Migration/migrateCharacter.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/imports/{api/creature => migrations}/v1Migration/migrateCharacter.js (100%) diff --git a/app/imports/api/creature/v1Migration/migrateCharacter.js b/app/imports/migrations/v1Migration/migrateCharacter.js similarity index 100% rename from app/imports/api/creature/v1Migration/migrateCharacter.js rename to app/imports/migrations/v1Migration/migrateCharacter.js From fdea74844178d92a62f6197dcc5e3b4062485079 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 14:28:32 +0200 Subject: [PATCH 32/85] Tore out the old engine, left some wounds --- .../embedInlineCalculations.js | 11 - .../afterComputation/evaluateString.js | 67 ---- .../computation/engine/ComputationMemo.js | 289 ------------------ .../computation/engine/EffectAggregator.js | 78 ----- .../computation/engine/applyToggles.js | 25 -- .../computation/engine/combineStat.js | 197 ------------ .../computation/engine/computeConstant.js | 12 - .../computation/engine/computeEffect.js | 55 ---- .../engine/computeEndStepProperty.js | 129 -------- .../engine/computeInlineCalculations.js | 40 --- .../computation/engine/computeLevels.js | 66 ---- .../computation/engine/computeMemo.js | 37 --- .../computation/engine/computeProficiency.js | 23 -- .../computation/engine/computeStat.js | 162 ---------- .../computation/engine/computeToggle.js | 55 ---- .../computation/engine/evaluateCalculation.js | 137 --------- .../computation/engine/findAncestorByType.js | 10 - .../engine/getComputationProperties.js | 24 -- .../engine/getDependentProperties.js | 51 ---- .../engine/writeAlteredProperties.js | 118 ------- .../engine/writeCreatureVariables.js | 59 ---- .../computation/methods/recomputeCreature.js | 120 -------- .../creatureProperties/CreatureProperties.js | 4 +- .../methods/adjustQuantity.js | 6 +- .../methods/damagePropertiesByName.js | 4 +- .../methods/damageProperty.js | 4 +- .../creatureProperties/methods/dealDamage.js | 8 +- .../methods/duplicateProperty.js | 12 +- .../creatureProperties/methods/equipItem.js | 8 +- .../methods/insertProperty.js | 13 +- .../methods/insertPropertyFromLibraryNode.js | 10 +- .../methods/pullFromProperty.js | 4 +- .../methods/pushToProperty.js | 5 +- .../methods/restoreProperty.js | 10 +- .../methods/selectAmmoItem.js | 4 +- .../methods/softRemoveProperty.js | 7 +- .../methods/updateCreatureProperty.js | 17 +- .../recomputeCreaturesByProperty.js | 4 +- .../creatures/methods/restCreature.js | 4 +- .../denormalise/recomputeDamageMultipliers.js | 78 ----- .../recomputeInactiveProperties.js | 75 ----- .../denormalise/recomputeInventory.js | 111 ------- .../denormalise/recomputeSlotFullness.js | 43 --- .../api/creature/experience/Experiences.js | 4 +- .../creature/mixins/recomputeCreatureMixin.js | 4 +- .../computation}/CreatureComputation.js | 0 .../buildComputation/computeInactiveStatus.js | 2 +- .../computeSlotQuantityFilled.js | 0 .../computeToggleDependencies.js | 2 +- .../linkCalculationDependencies.js | 2 +- .../buildComputation/linkInventory.js | 0 .../buildComputation/linkTypeDependencies.js | 0 .../parseCalculationFields.js | 2 +- .../buildComputation/removeSchemaFields.js | 0 .../tests/computeInactiveStatus.testFn.js | 2 +- .../tests/computeSlotQuantityFilled.testFn.js | 2 +- .../tests/computeToggleDependencies.testFn.js | 2 +- .../linkCalculationDependencies.testFn.js | 2 +- .../tests/linkInventory.testFn.js | 2 +- .../tests/linkTypeDependencies.testfn.js | 2 +- .../computation}/buildCreatureComputation.js | 2 +- .../buildCreatureComputation.test.js | 0 .../computeComputation/computeByType.js | 0 .../computeByType/computeAction.js | 0 .../computeByType/computeAttribute.js | 0 .../computeByType/computeContainer.js | 0 .../computeByType/computeSlot.js | 0 .../computeByType/computeVariable.js | 0 .../aggregate/aggregateClassLevel.js | 0 .../aggregate/aggregateDamageMultiplier.js | 0 .../aggregate/aggregateDefinition.js | 0 .../aggregate/aggregateEffect.js | 0 .../aggregate/aggregateInventory.js | 0 .../aggregate/aggregateProficiency.js | 0 .../computeVariable/aggregate/index.js | 0 .../computeImplicitVariable.js | 0 .../computeVariableAsAttribute.js | 0 .../computeVariable/computeVariableAsClass.js | 0 .../computeVariableAsConstant.js | 0 .../computeVariable/computeVariableAsSkill.js | 0 .../computeVariable/getAggregatorResult.js | 2 +- .../computeComputation/computeCalculations.js | 0 .../computeComputation/computeToggles.js | 0 .../tests/computeAction.testFn.js | 2 +- .../tests/computeAttribute.testFn.js | 2 +- .../tests/computeClasses.testFn.js | 2 +- .../tests/computeConstants.testFn.js | 2 +- .../tests/computeDamageMultipliers.testFn.js | 2 +- .../tests/computeEffects.testFn.js | 2 +- .../tests/computeInventory.testFn.js | 2 +- .../tests/computeSkills.testFn.js | 2 +- .../computeComputation/tests/index.js | 0 .../computeCreatureComputation.js | 6 +- .../computeCreatureComputation.test.js | 0 .../computation}/utility/applyFnToKey.js | 0 .../computation}/utility/applyFnToKey.test.js | 0 .../computation}/utility/cleanProp.testFn.js | 0 .../utility/findAncestorByType.js | 0 .../utility/stripFloatingPointOddities.js | 0 .../computation}/utility/walkdown.js | 0 app/imports/api/engine/computeCreature.js | 17 ++ .../oldActions}/applyAction.js | 4 +- .../oldActions}/applyAdjustment.js | 0 .../oldActions}/applyAttack.js | 0 .../oldActions}/applyBuff.js | 0 .../oldActions}/applyDamage.js | 0 .../oldActions}/applyProperties.js | 0 .../oldActions}/applyRoll.js | 0 .../oldActions}/applySave.js | 0 .../oldActions}/applyToggle.js | 0 .../oldActions}/castSpellWithSlot.js | 2 +- .../actions => engine/oldActions}/doAction.js | 16 +- .../actions => engine/oldActions}/doCheck.js | 0 .../oldActions}/getAncestorContext.js | 0 .../oldActions}/spendResources.js | 0 app/imports/api/parenting/organizeMethods.js | 15 +- .../migrations/server/2.0-beta.33-dbv1.js | 5 - .../server/publications/singleCharacter.js | 6 +- .../computation/EmbedInlineComputations.vue | 4 +- .../characterSheetTabs/InventoryTab.vue | 2 +- .../character/characterSheetTabs/StatsTab.vue | 2 +- .../components/actions/ActionCard.vue | 2 +- .../components/inventory/ContainerCard.vue | 2 +- .../ui/properties/viewers/ActionViewer.vue | 8 +- .../ui/properties/viewers/ItemViewer.vue | 2 +- 125 files changed, 102 insertions(+), 2235 deletions(-) delete mode 100644 app/imports/api/creature/computation/afterComputation/embedInlineCalculations.js delete mode 100644 app/imports/api/creature/computation/afterComputation/evaluateString.js delete mode 100644 app/imports/api/creature/computation/engine/ComputationMemo.js delete mode 100644 app/imports/api/creature/computation/engine/EffectAggregator.js delete mode 100644 app/imports/api/creature/computation/engine/applyToggles.js delete mode 100644 app/imports/api/creature/computation/engine/combineStat.js delete mode 100644 app/imports/api/creature/computation/engine/computeConstant.js delete mode 100644 app/imports/api/creature/computation/engine/computeEffect.js delete mode 100644 app/imports/api/creature/computation/engine/computeEndStepProperty.js delete mode 100644 app/imports/api/creature/computation/engine/computeInlineCalculations.js delete mode 100644 app/imports/api/creature/computation/engine/computeLevels.js delete mode 100644 app/imports/api/creature/computation/engine/computeMemo.js delete mode 100644 app/imports/api/creature/computation/engine/computeProficiency.js delete mode 100644 app/imports/api/creature/computation/engine/computeStat.js delete mode 100644 app/imports/api/creature/computation/engine/computeToggle.js delete mode 100644 app/imports/api/creature/computation/engine/evaluateCalculation.js delete mode 100644 app/imports/api/creature/computation/engine/findAncestorByType.js delete mode 100644 app/imports/api/creature/computation/engine/getComputationProperties.js delete mode 100644 app/imports/api/creature/computation/engine/getDependentProperties.js delete mode 100644 app/imports/api/creature/computation/engine/writeAlteredProperties.js delete mode 100644 app/imports/api/creature/computation/engine/writeCreatureVariables.js delete mode 100644 app/imports/api/creature/computation/methods/recomputeCreature.js delete mode 100644 app/imports/api/creature/denormalise/recomputeDamageMultipliers.js delete mode 100644 app/imports/api/creature/denormalise/recomputeInactiveProperties.js delete mode 100644 app/imports/api/creature/denormalise/recomputeInventory.js delete mode 100644 app/imports/api/creature/denormalise/recomputeSlotFullness.js rename app/imports/api/{creature/computation/newEngine/buildComputation => engine/computation}/CreatureComputation.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/computeInactiveStatus.js (90%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/computeSlotQuantityFilled.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/computeToggleDependencies.js (84%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/linkCalculationDependencies.js (93%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/linkInventory.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/linkTypeDependencies.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/parseCalculationFields.js (96%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/removeSchemaFields.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/tests/computeInactiveStatus.testFn.js (96%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/tests/computeSlotQuantityFilled.testFn.js (87%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/tests/computeToggleDependencies.testFn.js (94%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/tests/linkCalculationDependencies.testFn.js (92%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/tests/linkInventory.testFn.js (95%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildComputation/tests/linkTypeDependencies.testfn.js (85%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildCreatureComputation.js (97%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/buildCreatureComputation.test.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeAction.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeAttribute.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeContainer.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeSlot.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/aggregate/index.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/computeImplicitVariable.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/computeVariableAsClass.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeByType/computeVariable/getAggregatorResult.js (91%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeCalculations.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/computeToggles.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeAction.testFn.js (95%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeAttribute.testFn.js (93%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeClasses.testFn.js (92%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeConstants.testFn.js (85%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeDamageMultipliers.testFn.js (89%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeEffects.testFn.js (91%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeInventory.testFn.js (93%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/computeSkills.testFn.js (94%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeComputation/tests/index.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeCreatureComputation.js (79%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/computeCreatureComputation.test.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/utility/applyFnToKey.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/utility/applyFnToKey.test.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/utility/cleanProp.testFn.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/utility/findAncestorByType.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/utility/stripFloatingPointOddities.js (100%) rename app/imports/api/{creature/computation/newEngine => engine/computation}/utility/walkdown.js (100%) create mode 100644 app/imports/api/engine/computeCreature.js rename app/imports/api/{creature/actions => engine/oldActions}/applyAction.js (74%) rename app/imports/api/{creature/actions => engine/oldActions}/applyAdjustment.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applyAttack.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applyBuff.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applyDamage.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applyProperties.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applyRoll.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applySave.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/applyToggle.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/castSpellWithSlot.js (96%) rename app/imports/api/{creature/actions => engine/oldActions}/doAction.js (80%) rename app/imports/api/{creature/actions => engine/oldActions}/doCheck.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/getAncestorContext.js (100%) rename app/imports/api/{creature/actions => engine/oldActions}/spendResources.js (100%) diff --git a/app/imports/api/creature/computation/afterComputation/embedInlineCalculations.js b/app/imports/api/creature/computation/afterComputation/embedInlineCalculations.js deleted file mode 100644 index 10841dba..00000000 --- a/app/imports/api/creature/computation/afterComputation/embedInlineCalculations.js +++ /dev/null @@ -1,11 +0,0 @@ -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; - -export default function embedInlineCalculations(string, calculations){ - if (!string) return ''; - if (!calculations) return string; - let index = 0; - return string.replace(INLINE_CALCULATION_REGEX, substring => { - let comp = calculations && calculations[index++]; - return (comp && 'result' in comp) ? comp.result : substring; - }); -} diff --git a/app/imports/api/creature/computation/afterComputation/evaluateString.js b/app/imports/api/creature/computation/afterComputation/evaluateString.js deleted file mode 100644 index f193d341..00000000 --- a/app/imports/api/creature/computation/afterComputation/evaluateString.js +++ /dev/null @@ -1,67 +0,0 @@ -import { parse, CompilationContext } from '/imports/parser/parser.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; - -//TODO replace constants with their parsed node - -export default function evaluateString({string, scope, fn = 'compile', context}){ - if (!context){ - context = new CompilationContext({}); - } - if (!string){ - context.storeError('No string provided'); - return {result: {value: string}, context}; - } - - if (!scope) context.storeError('No scope provided'); - - // Parse the string using mathjs - let node; - try { - node = parse(string); - } catch (e) { - context.storeError(e); - return {result: {value: string}, context}; - } - node = replaceConstants({calc: node, context, scope}); - let result = node[fn](scope, context); - return {result, context}; -} - -// Replace constants in the calc with the right ParseNodes -function replaceConstants({calc, context, scope}){ - let constFailed = []; - calc = calc.replaceNodes(node => { - if (!(node instanceof SymbolNode)) return; - let constant = scope[node.name]; - // replace constants that aren't overridden by stats or disabled by a toggle - if (constant && constant.type === 'constant'){ - // Fail if the constant has errors - if (constant.errors && constant.errors.length){ - constFailed.push(node.name); - return; - } - let parsedConstantNode; - try { - parsedConstantNode = parse(constant.calculation); - } catch(e){ - constFailed.push(node.name); - return; - } - if (!parsedConstantNode) constFailed.push(node.name); - return parsedConstantNode; - } - }); - constFailed.forEach(name => { - context.storeError({ - type: 'error', - message: `${name} is a constant property with parsing errors` - }); - }); - let failed = !!constFailed.length; - if (failed){ - calc = new ErrorNode({error: 'Failed to replace constants'}); - } - return calc; -} diff --git a/app/imports/api/creature/computation/engine/ComputationMemo.js b/app/imports/api/creature/computation/engine/ComputationMemo.js deleted file mode 100644 index 75d270da..00000000 --- a/app/imports/api/creature/computation/engine/ComputationMemo.js +++ /dev/null @@ -1,289 +0,0 @@ -import { includes, cloneDeep } from 'lodash'; -import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js'; - -// The computation memo is an in-memory data structure used only during the -// computation process -export default class ComputationMemo { - constructor(props, creature){ - this.statsByVariableName = {}; - this.constantsByVariableName = {}; - this.constantsById = {}; - this.extraStatsByVariableName = {}; - this.statsById = {}; - this.originalPropsById = {}; - this.propsById = {}; - this.skillsByAbility = {}; - this.unassignedEffects = []; - this.classLevelsById = {}; - this.classes = {}; - this.togglesById = {}; - this.toggleIds = new Set(); - // Equipped items that might be used as ammo - this.equipmentById = {}; - // Properties that have calculations, but don't impact other properties - this.endStepPropsById = {}; - // First note all the ids of all the toggles - props.forEach((prop) => { - if ( - prop.type === 'toggle' - ) { - this.toggleIds.add(prop._id); - } - }); - props.filter((prop) => { - if ( - prop.type === 'toggle' - ) { - this.addToggle(prop); - } else { - return true; - } - }).filter((prop) => { - if ( - prop.type === 'attribute' || - prop.type === 'skill' - ) { - // Add all the stats - this.addStat(prop); - } else if ( - prop.type === 'item' - ) { - this.addEquipment(prop); - } else { - return true; - } - }).forEach((prop) => { - // Now add everything else - if (prop.type === 'effect'){ - this.addEffect(prop); - } else if (prop.type === 'proficiency') { - this.addProficiency(prop); - } else if (prop.type === 'classLevel'){ - this.addClassLevel(prop); - } else if (prop.type === 'constant'){ - this.addConstant(prop); - } else { - this.addEndStepProp(prop); - } - }); - for (let name in creature.denormalizedStats){ - if (!this.statsByVariableName[name]){ - this.statsByVariableName[name] = { - variableName: name, - value: creature.denormalizedStats[name], - computationDetails: propDetailsByType.denormalizedStat(), - } - } - } - } - addConstant(prop){ - prop = this.registerProperty(prop); - this.constantsById[prop._id] = prop; - } - registerProperty(prop){ - this.originalPropsById[prop._id] = cloneDeep(prop); - this.propsById[prop._id] = prop; - prop.dependencies = []; - prop.computationDetails = propDetails(prop); - prop.ancestors.forEach(ancestor => { - if (this.toggleIds.has(ancestor.id)){ - prop.computationDetails.toggleAncestors.push(ancestor.id); - } - }); - return prop; - } - addToggle(prop){ - prop = this.registerProperty(prop); - this.togglesById[prop._id] = prop; - } - addClassLevel(prop){ - prop = this.registerProperty(prop); - this.classLevelsById[prop._id] = prop; - } - addStat(prop){ - let variableName = prop.variableName; - if (!variableName) return; - let existingStat = this.statsByVariableName[variableName]; - prop = this.registerProperty(prop); - if (existingStat){ - existingStat.computationDetails.idsOfSameName.push(prop._id); - } else { - this.statsById[prop._id] = prop; - this.statsByVariableName[variableName] = prop; - if ( - prop.type === 'skill' && - isSkillCheck(prop) && - prop.ability - ){ - this.addSkillToAbility(prop, prop.ability) - } - } - } - addSkillToAbility(prop, ability){ - if (!this.skillsByAbility[ability]){ - this.skillsByAbility[ability] = []; - } - this.skillsByAbility[ability].push(prop); - } - addEffect(prop){ - prop = this.registerProperty(prop); - let targets = this.getEffectTargets(prop); - targets.forEach(target => { - if (target.computationDetails && target.computationDetails.effects){ - target.computationDetails.effects.push(prop); - } - }); - if (!targets.size){ - this.unassignedEffects.push(prop); - } - } - getEffectTargets(prop){ - let targets = new Set(); - if (!prop.stats) return targets; - prop.stats.forEach((statName) => { - let target; - if (statName[0] === '#'){ - target = findAncestorByType({ - type: statName.slice(1), - prop, - memo: this - }); - } else { - target = this.statsByVariableName[statName]; - } - if (!target) return; - targets.add(target); - if (isSkillOperation(prop) && isAbility(target)){ - let extras = this.skillsByAbility[statName] || []; - extras.forEach(ex =>{ - // Only pass on ability effects to skills and checks - if (ex.skillType === 'skill' || ex.skillType === 'check'){ - targets.add(ex) - } - }); - } - }); - return targets; - } - addProficiency(prop){ - prop = this.registerProperty(prop); - let targets = this.getProficiencyTargets(prop); - targets.forEach(target => { - if(target.computationDetails.proficiencies){ - target.computationDetails.proficiencies.push(prop); - } - }); - } - getProficiencyTargets(prop){ - let targets = new Set(); - if (!prop.stats) return targets; - prop.stats.forEach(statName => { - let target = this.statsByVariableName[statName]; - if (!target) return; - targets.add(target); - if (isAbility(target)) { - let extras = this.skillsByAbility[statName] || []; - extras.forEach(ex =>{ - // Only pass on ability proficiencies to skills and checks - if (ex.skillType === 'skill' || ex.skillType === 'check'){ - targets.add(ex) - } - }); - } - }); - return targets; - } - addEquipment(prop){ - prop = this.registerProperty(prop); - this.equipmentById[prop._id] = prop; - } - addEndStepProp(prop){ - prop = this.registerProperty(prop); - this.endStepPropsById[prop._id] = prop; - } -} - -function isAbility(prop){ - return prop.type === 'attribute' && - prop.attributeType === 'ability' -} - -function isSkillCheck(prop){ - return includes(['skill', 'check', 'save', 'utility'], prop.skillType); -} - -const skillOperations = [ - 'advantage', - 'disadvantage', - 'passiveAdd', - 'fail', - 'conditional', - 'rollBonus', -]; - -function isSkillOperation(prop){ - return skillOperations.includes(prop.operation); -} - -function propDetails(prop){ - return propDetailsByType[prop.type] && propDetailsByType[prop.type]() || - propDetailsByType.default(); -} - -const propDetailsByType = { - default(){ - return { - toggleAncestors: [], - }; - }, - toggle(){ - return { - computed: false, - busyComputing: false, - toggleAncestors: [], - }; - }, - attribute(){ - return { - computed: false, - busyComputing: false, - effects: [], - proficiencies: [], - toggleAncestors: [], - idsOfSameName: [], - }; - }, - skill(){ - return { - computed: false, - busyComputing: false, - effects: [], - proficiencies: [], - toggleAncestors: [], - idsOfSameName: [], - }; - }, - effect(){ - return { - computed: false, - busyComputing: false, - toggleAncestors: [], - }; - }, - classLevel(){ - return { - computed: true, - toggleAncestors: [], - }; - }, - proficiency(){ - return { - toggleAncestors: [], - }; - }, - denormalizedStat(){ - return { - toggleAncestors: [], - }; - } -} diff --git a/app/imports/api/creature/computation/engine/EffectAggregator.js b/app/imports/api/creature/computation/engine/EffectAggregator.js deleted file mode 100644 index 83e30f32..00000000 --- a/app/imports/api/creature/computation/engine/EffectAggregator.js +++ /dev/null @@ -1,78 +0,0 @@ -export default class EffectAggregator{ - constructor(){ - this.base = undefined; - this.add = 0; - this.mul = 1; - this.min = Number.NEGATIVE_INFINITY; - this.max = Number.POSITIVE_INFINITY; - this.advantage = 0; - this.disadvantage = 0; - this.passiveAdd = undefined; - this.fail = 0; - this.set = undefined; - this.conditional = []; - this.rollBonus = []; - this.hasNoEffects = true; - } - addEffect(effect){ - let result = effect.result; - if (this.hasNoEffects) this.hasNoEffects = false; - switch(effect.operation){ - case 'base': - // Take the largest base value - if (Number.isFinite(result)){ - if(Number.isFinite(this.base)){ - this.base = Math.max(this.base, result); - } else { - this.base = result; - } - } - break; - case 'add': - // Add all adds together - this.add += result; - break; - case 'mul': - // Multiply the muls together - this.mul *= result; - break; - case 'min': - // Take the largest min value - this.min = result > this.min ? result : this.min; - break; - case 'max': - // Take the smallest max value - this.max = result < this.max ? result : this.max; - break; - case 'set': - // Take the highest set value - this.set = this.set === undefined || result > this.set ? result : this.set; - break; - case 'advantage': - // Sum number of advantages - this.advantage++; - break; - case 'disadvantage': - // Sum number of disadvantages - this.disadvantage++; - break; - case 'passiveAdd': - // Add all passive adds together - if (this.passiveAdd === undefined) this.passiveAdd = 0; - this.passiveAdd += result; - break; - case 'fail': - // Sum number of fails - this.fail++; - break; - case 'conditional': - // Store array of conditionals - this.conditional.push(result); - break; - case 'rollBonus': - // Store array of roll bonuses - this.rollBonus.push(result); - break; - } - } -} diff --git a/app/imports/api/creature/computation/engine/applyToggles.js b/app/imports/api/creature/computation/engine/applyToggles.js deleted file mode 100644 index 74839c35..00000000 --- a/app/imports/api/creature/computation/engine/applyToggles.js +++ /dev/null @@ -1,25 +0,0 @@ -import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js'; -import { union } from 'lodash'; - -export default function applyToggles(prop, memo){ - // If it used to be inactive delete those fields - if (prop.inactive) prop.inactive = undefined; - if (prop.deactivatedByAncestor) prop.deactivatedByAncestor = undefined; - if (prop.deactivatedByToggle) prop.deactivatedByToggle = undefined; - // Iterate through the toggle ancestors from oldest to nearest - prop.computationDetails.toggleAncestors.forEach(toggleId => { - let toggle = memo.togglesById[toggleId]; - computeToggle(toggle, memo); - prop.dependencies = union( - prop.dependencies, - [toggle._id], - toggle.dependencies, - ); - // Deactivate if the toggle is false - if (!toggle.toggleResult){ - prop.inactive = true; - prop.deactivatedByAncestor = true; - prop.deactivatedByToggle = true; - } - }); -} diff --git a/app/imports/api/creature/computation/engine/combineStat.js b/app/imports/api/creature/computation/engine/combineStat.js deleted file mode 100644 index e69d9c27..00000000 --- a/app/imports/api/creature/computation/engine/combineStat.js +++ /dev/null @@ -1,197 +0,0 @@ -import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; -import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js'; -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; -import { union } from 'lodash'; - -export default function combineStat(stat, aggregator, memo){ - if (stat.type === 'attribute'){ - combineAttribute(stat, aggregator, memo); - } else if (stat.type === 'skill'){ - combineSkill(stat, aggregator, memo); - } else if (stat.type === 'damageMultiplier'){ - combineDamageMultiplier(stat, memo); - } -} - -function getAggregatorResult(stat, aggregator){ - let base; - if (!Number.isFinite(aggregator.base)){ - base = stat.baseValue || 0; - } else if (!Number.isFinite(stat.baseValue)){ - base = aggregator.base || 0; - } else { - base = Math.max(aggregator.base, stat.baseValue); - } - let result = (base + aggregator.add) * aggregator.mul; - if (result < aggregator.min) { - result = aggregator.min; - } - if (result > aggregator.max) { - result = aggregator.max; - } - if (aggregator.set !== undefined) { - result = aggregator.set; - } - if (!stat.decimal && Number.isFinite(result)){ - result = Math.floor(result); - } else if (Number.isFinite(result)){ - result = stripFloatingPointOddities(result); - } - return result; -} - -function combineAttribute(stat, aggregator, memo){ - stat.value = getAggregatorResult(stat, aggregator); - if (stat.attributeType === 'spellSlot'){ - let { - result, - context, - dependencies - } = evaluateCalculation({ - string: stat.spellSlotLevelCalculation, - memo, - prop: stat, - }); - stat.spellSlotLevelValue = result.value; - stat.spellSlotLevelErrors = context.errors; - stat.dependencies = union(stat.dependencies, dependencies); - } - stat.currentValue = stat.value - (stat.damage || 0); - // Ability scores get modifiers - if (stat.attributeType === 'ability') { - stat.modifier = Math.floor((stat.currentValue - 10) / 2); - } else { - stat.modifier = undefined; - } - // Hit dice get constitution modifiers - stat.constitutionMod = undefined; - if (stat.attributeType === 'hitDice') { - let conStat = memo.statsByVariableName['constitution']; - if (conStat && 'modifier' in conStat){ - stat.constitutionMod = conStat.modifier; - stat.dependencies = union( - stat.dependencies, - [conStat._id], - conStat.dependencies, - ); - } - } - // Stats that have no effects can be hidden based on a sheet setting - stat.hide = aggregator.hasNoEffects && - stat.baseValue === undefined || - undefined -} - -function combineSkill(stat, aggregator, memo){ - // Skills are based on some ability Modifier - let ability = stat.ability && memo.statsByVariableName[stat.ability] - if (stat.ability && ability){ - computeStat(ability, memo); - stat.abilityMod = ability.modifier; - stat.dependencies = union( - stat.dependencies, - [ability._id], - ability.dependencies, - ); - } else { - stat.abilityMod = 0; - } - // Combine all the child proficiencies - stat.proficiency = 0; - for (let i in stat.computationDetails.proficiencies){ - let prof = stat.computationDetails.proficiencies[i]; - computeProficiency(prof, memo); - if ( - !prof.deactivatedByToggle && - prof.value > stat.proficiency - ){ - stat.proficiency = prof.value; - stat.dependencies = union( - stat.dependencies, - [prof._id], - prof.dependencies, - ); - } - } - // Get the character's proficiency bonus to apply - let profBonusStat = memo.statsByVariableName['proficiencyBonus']; - let profBonus = profBonusStat && profBonusStat.value; - - if (profBonusStat){ - stat.dependencies = union( - stat.dependencies, - [profBonusStat._id], - profBonusStat.dependencies, - ); - } - - if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){ - let levelProp = memo.statsByVariableName['level']; - let level = levelProp.value; - profBonus = Math.ceil(level / 4) + 1; - if (levelProp._id){ - stat.dependencies = union(stat.dependencies, [levelProp._id]); - } - if (levelProp.dependencies){ - stat.dependencies = union(stat.dependencies, levelProp.dependencies); - } - } - - // Multiply the proficiency bonus by the actual proficiency - if(stat.proficiency === 0.49){ - // Round down proficiency bonus in the special case - profBonus = Math.floor(profBonus * 0.5); - } else { - profBonus = Math.ceil(profBonus * stat.proficiency); - } - - // Combine everything to get the final result - let base = aggregator.base || 0; - let result = (base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul; - if (result < aggregator.min) result = aggregator.min; - if (result > aggregator.max) result = aggregator.max; - if (aggregator.set !== undefined) { - result = aggregator.set; - } - if (Number.isFinite(result)){ - result = Math.floor(result); - } - stat.value = result; - // Advantage/disadvantage - if (aggregator.advantage && !aggregator.disadvantage){ - stat.advantage = 1; - } else if (aggregator.disadvantage && !aggregator.advantage){ - stat.advantage = -1; - } else { - stat.advantage = 0; - } - // Passive bonus - stat.passiveBonus = aggregator.passiveAdd; - // conditional benefits - stat.conditionalBenefits = aggregator.conditional; - // Roll bonuses - stat.rollBonus = aggregator.rollBonus; - // Forced to fail - stat.fail = aggregator.fail; - // Rollbonus - stat.rollBonuses = aggregator.rollBonus; - // Hide - stat.hide = aggregator.hasNoEffects && - stat.baseValue === undefined && - stat.proficiency == 0 || - undefined; -} - -function combineDamageMultiplier(stat){ - if (stat.immunityCount) return 0; - let result; - if (stat.resistanceCount && !stat.vulnerabilityCount){ - result = 0.5; - } else if (!stat.resistanceCount && stat.vulnerabilityCount){ - result = 2; - } else { - result = 1; - } - stat.value = result; -} diff --git a/app/imports/api/creature/computation/engine/computeConstant.js b/app/imports/api/creature/computation/engine/computeConstant.js deleted file mode 100644 index 4d972c57..00000000 --- a/app/imports/api/creature/computation/engine/computeConstant.js +++ /dev/null @@ -1,12 +0,0 @@ -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; - -export default function computeConstant(constant, memo){ - // Apply any toggles - applyToggles(constant, memo); - if (constant.deactivatedByToggle) return; - if ( - !memo.constantsByVariableName[constant.variableName] - ){ - memo.constantsByVariableName[constant.variableName] = constant - } -} diff --git a/app/imports/api/creature/computation/engine/computeEffect.js b/app/imports/api/creature/computation/engine/computeEffect.js deleted file mode 100644 index 75d880b4..00000000 --- a/app/imports/api/creature/computation/engine/computeEffect.js +++ /dev/null @@ -1,55 +0,0 @@ -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; -import { union } from 'lodash'; - -export default function computeEffect(effect, memo){ - if (effect.computationDetails.computed) return; - if (effect.computationDetails.busyComputing){ - // Trying to compute this effect again while it is already computing. - // We must be in a dependency loop. - effect.computationDetails.computed = true; - effect.result = NaN; - effect.computationDetails.busyComputing = false; - effect.computationDetails.error = 'dependencyLoop'; - if (Meteor.isClient) console.warn('dependencyLoop', effect); - return; - } - // Before doing any work, mark this effect as busy - effect.computationDetails.busyComputing = true; - - // Apply any toggles - applyToggles(effect, memo); - - // Determine result of effect calculation - delete effect.errors; - if (!effect.calculation){ - if(effect.operation === 'add' || effect.operation === 'base'){ - effect.result = 0; - } else { - delete effect.result - } - } else if (Number.isFinite(+effect.calculation)){ - effect.result = +effect.calculation; - } else if(effect.operation === 'conditional' || effect.operation === 'rollBonus'){ - effect.result = effect.calculation; - } else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){ - effect.result = 1; - } else { - let { - result, - context, - dependencies, - } = evaluateCalculation({ - string: effect.calculation, - prop: effect, - memo - }); - effect.result = result.value; - effect.dependencies = union(effect.dependencies, dependencies); - if (context.errors.length){ - effect.errors = context.errors; - } - } - effect.computationDetails.computed = true; - effect.computationDetails.busyComputing = false; -} diff --git a/app/imports/api/creature/computation/engine/computeEndStepProperty.js b/app/imports/api/creature/computation/engine/computeEndStepProperty.js deleted file mode 100644 index fad2afd4..00000000 --- a/app/imports/api/creature/computation/engine/computeEndStepProperty.js +++ /dev/null @@ -1,129 +0,0 @@ -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; -import { union } from 'lodash'; - -export default function computeEndStepProperty(prop, memo){ - applyToggles(prop, memo); - - switch (prop.type){ - case 'action': - case 'spell': - computeAction(prop, memo); - break; - case 'adjustment': - case 'damage': - computePropertyField(prop, memo, 'amount', 'compile'); - break; - case 'attack': - computeAction(prop, memo); - computePropertyField(prop, memo, 'rollBonus'); - break; - case 'savingThrow': - computePropertyField(prop, memo, 'dc'); - break; - case 'spellList': - computePropertyField(prop, memo, 'maxPrepared'); - computePropertyField(prop, memo, 'attackRollBonus'); - computePropertyField(prop, memo, 'dc'); - break; - case 'propertySlot': - computePropertyField(prop, memo, 'quantityExpected'); - computePropertyField(prop, memo, 'slotCondition'); - break; - case 'roll': - computePropertyField(prop, memo, 'roll', 'compile'); - break; - } -} - -function computeAction(prop, memo){ - // Uses - let { - result, - context, - dependencies, - } = evaluateCalculation({ string: prop.uses, prop, memo}); - prop.usesResult = result.value; - prop.dependencies = union(prop.dependencies, dependencies); - if (context.errors.length){ - prop.usesErrors = context.errors; - } else { - delete prop.usesErrors; - } - prop.insufficientResources = undefined; - if (prop.usesUsed >= prop.usesResult){ - prop.insufficientResources = true; - } - if (!prop.resources) return; - // Attributes consumed - prop.resources.attributesConsumed.forEach((attConsumed, i) => { - if (attConsumed.variableName){ - let stat = memo.statsByVariableName[attConsumed.variableName]; - prop.resources.attributesConsumed[i].statId = stat && stat._id; - prop.resources.attributesConsumed[i].statName = stat && stat.name; - let available = stat && stat.currentValue || 0; - prop.resources.attributesConsumed[i].available = available; - if (available < attConsumed.quantity){ - prop.insufficientResources = true; - } - if (stat){ - prop.dependencies = union( - prop.dependencies, - [stat._id], - stat.dependencies - ); - } - } - }); - // Items consumed - prop.resources.itemsConsumed.forEach((itemConsumed, i) => { - let item = itemConsumed.itemId ? - memo.equipmentById[itemConsumed.itemId] : - undefined; - let available = item ? item.quantity : 0; - prop.resources.itemsConsumed[i].available = available; - if (!item || available < itemConsumed.quantity){ - prop.insufficientResources = true; - } - if (item){ - prop.resources.itemsConsumed[i].itemId = item._id; - let name = item.name; - if (item.quantity !== 1 && item.plural){ - name = item.plural; - } - if (name) prop.resources.itemsConsumed[i].itemName = name; - if (item.icon) prop.resources.itemsConsumed[i].itemIcon = item.icon; - if (item.color) prop.resources.itemsConsumed[i].itemColor = item.color; - prop.dependencies = union( - prop.dependencies, - [item._id], - item.dependencies - ); - } else { - delete prop.resources.itemsConsumed[i].itemId; - delete prop.resources.itemsConsumed[i].itemName; - delete prop.resources.itemsConsumed[i].itemIcon; - delete prop.resources.itemsConsumed[i].itemColor; - } - }); -} - -function computePropertyField(prop, memo, fieldName, fn){ - let { - result, - context, - dependencies, - } = evaluateCalculation({string: prop[fieldName], prop, memo, fn}); - if (result instanceof ConstantNode){ - prop[`${fieldName}Result`] = result.value; - } else { - prop[`${fieldName}Result`] = result.toString(); - } - prop.dependencies = union(prop.dependencies, dependencies); - if (context.errors.length){ - prop[`${fieldName}Errors`] = context.errors; - } else { - delete prop[`${fieldName}Errors`]; - } -} diff --git a/app/imports/api/creature/computation/engine/computeInlineCalculations.js b/app/imports/api/creature/computation/engine/computeInlineCalculations.js deleted file mode 100644 index eb54d998..00000000 --- a/app/imports/api/creature/computation/engine/computeInlineCalculations.js +++ /dev/null @@ -1,40 +0,0 @@ -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import { union } from 'lodash'; - -export default function computeInlineCalculations(prop, memo){ - if (prop.summary){ - computeInlineCalcsForField(prop, memo, 'summary'); - } - if (prop.description){ - computeInlineCalcsForField(prop, memo, 'description'); - } -} - -function computeInlineCalcsForField(prop, memo, field){ - let string = prop[field]; - let inlineComputations = []; - let matches = string.matchAll(INLINE_CALCULATION_REGEX); - for (let match of matches){ - let calculation = match[1]; - let { - result, - context, - dependencies, - } = evaluateCalculation({string: calculation, prop, memo, fn: 'compile'}); - if (result instanceof ErrorNode){ - result = '`Calculation Error`'; - } - let computation = { - calculation, - result: result && result.toString(), - }; - if (context.errors.length){ - computation.errors = context.errors; - } - inlineComputations.push(computation); - prop.dependencies = union(prop.dependencies, dependencies); - } - prop[`${field}Calculations`] = inlineComputations; -} diff --git a/app/imports/api/creature/computation/engine/computeLevels.js b/app/imports/api/creature/computation/engine/computeLevels.js deleted file mode 100644 index 21757025..00000000 --- a/app/imports/api/creature/computation/engine/computeLevels.js +++ /dev/null @@ -1,66 +0,0 @@ -import { forOwn, has, union } from 'lodash'; -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; - -export default function computeLevels(memo){ - computeClassLevels(memo); - computeTotalLevel(memo); -} - -function computeClassLevels(memo){ - forOwn(memo.classLevelsById, classLevel => { - applyToggles(classLevel, memo); - // class levels are mutually dependent - classLevel.dependencies = union( - classLevel.dependencies, - Object.keys(memo.classLevelsById) - ); - if (classLevel.deactivatedByToggle) return; - let name = classLevel.variableName; - let stat = memo.statsByVariableName[name]; - if (!stat){ - memo.statsByVariableName[name] = classLevel; - memo.classes[name] = classLevel; - } else if (!has(stat, 'level')){ - // Stat is overriden by an attribute - return; - } else if (stat.level < classLevel.level) { - memo.statsByVariableName[name] = classLevel; - memo.classes[name] = classLevel; - } - }); -} - -function computeTotalLevel(memo){ - let currentLevel = memo.statsByVariableName['level']; - if (!currentLevel || currentLevel.deactivatedByToggle){ - currentLevel = { - value: 0, - dependencies: [], - computationDetails: { - builtIn: true, - computed: true, - } - }; - memo.statsByVariableName['level'] = currentLevel; - } - // bail out if overriden by an attribute - if (!currentLevel.computationDetails.builtIn) return; - let level = 0; - for (let name in memo.classes){ - let cls = memo.classes[name]; - level += cls.level || 0; - if (cls._id){ - currentLevel.dependencies = union( - currentLevel.dependencies, - [cls._id] - ) - } - if (cls.dependencies){ - currentLevel.dependencies = union( - currentLevel.dependencies, - cls.dependencies, - ) - } - } - currentLevel.value = level; -} diff --git a/app/imports/api/creature/computation/engine/computeMemo.js b/app/imports/api/creature/computation/engine/computeMemo.js deleted file mode 100644 index 5eeb32e1..00000000 --- a/app/imports/api/creature/computation/engine/computeMemo.js +++ /dev/null @@ -1,37 +0,0 @@ -import { each, forOwn } from 'lodash'; -import computeLevels from '/imports/api/creature/computation/engine/computeLevels.js'; -import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; -import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js'; -import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js'; -import computeEndStepProperty from '/imports/api/creature/computation/engine/computeEndStepProperty.js'; -import computeInlineCalculations from '/imports/api/creature/computation/engine/computeInlineCalculations.js'; -import computeConstant from '/imports/api/creature/computation/engine/computeConstant.js'; - -export default function computeMemo(memo){ - // Compute level - computeLevels(memo); - // Compute all constants that could be used - forOwn(memo.constantsById, constant => { - computeConstant (constant, memo); - }); - // Compute all stats, even if they are overriden - forOwn(memo.statsById, stat => { - computeStat (stat, memo); - }); - // Compute effects which didn't end up targeting a stat - each(memo.unassignedEffects, effect => { - computeEffect(effect, memo); - }); - // Compute toggles which didn't already get computed by dependencies - forOwn(memo.togglesById, toggle => { - computeToggle(toggle, memo); - }); - // Compute end step properties - forOwn(memo.endStepPropsById, prop => { - computeEndStepProperty(prop, memo); - }); - // Compute inline calculations - forOwn(memo.propsById, prop => { - computeInlineCalculations(prop, memo); - }); -} diff --git a/app/imports/api/creature/computation/engine/computeProficiency.js b/app/imports/api/creature/computation/engine/computeProficiency.js deleted file mode 100644 index 112a594b..00000000 --- a/app/imports/api/creature/computation/engine/computeProficiency.js +++ /dev/null @@ -1,23 +0,0 @@ -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; - -export default function computeEffect(proficiency, memo){ - if (proficiency.computationDetails.computed) return; - if (proficiency.computationDetails.busyComputing){ - // Trying to compute this proficiency again while it is already computing. - // We must be in a dependency loop. - proficiency.computationDetails.computed = true; - proficiency.result = NaN; - proficiency.computationDetails.busyComputing = false; - proficiency.computationDetails.error = 'dependencyLoop'; - if (Meteor.isClient) console.warn('dependencyLoop', proficiency); - return; - } - // Before doing any work, mark this proficiency as busy - proficiency.computationDetails.busyComputing = true; - - // Apply any toggles - applyToggles(proficiency, memo); - - proficiency.computationDetails.computed = true; - proficiency.computationDetails.busyComputing = false; -} diff --git a/app/imports/api/creature/computation/engine/computeStat.js b/app/imports/api/creature/computation/engine/computeStat.js deleted file mode 100644 index a455a43a..00000000 --- a/app/imports/api/creature/computation/engine/computeStat.js +++ /dev/null @@ -1,162 +0,0 @@ -import combineStat from '/imports/api/creature/computation/engine/combineStat.js'; -import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js'; -import EffectAggregator from '/imports/api/creature/computation/engine/EffectAggregator.js'; -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; -import { each, union, without } from 'lodash'; - -export default function computeStat(stat, memo){ - // If the stat is already computed, skip it - if (stat.computationDetails.computed) return; - if (stat.computationDetails.busyComputing){ - // Trying to compute this stat again while it is already computing. - // We must be in a dependency loop. - stat.computationDetails.computed = true; - stat.value = NaN; - stat.computationDetails.busyComputing = false; - stat.computationDetails.error = 'dependencyLoop'; - if (Meteor.isClient) console.warn('dependencyLoop', stat); - return; - } - // Before doing any work, mark this stat as busy - stat.computationDetails.busyComputing = true; - - let effects = stat.computationDetails.effects || []; - let proficiencies = stat.computationDetails.proficiencies || []; - - // Get references to all the stats that share the variable name - let sameNameStats - - if (stat.computationDetails.idsOfSameName){ - sameNameStats = stat.computationDetails.idsOfSameName.map( - id => memo.propsById[id] - ); - } else { - sameNameStats = []; - } - - let allStats = [stat, ...sameNameStats]; - - // Decide which stat is the last active stat - // The last active stat is considered the cannonical stat - let lastActiveStat; - allStats.forEach(candidateStat => { - applyToggles(candidateStat, memo); - if (!candidateStat.inactive) lastActiveStat = candidateStat; - candidateStat.overridden = undefined; - }); - if (!lastActiveStat){ - delete memo.statsByVariableName[stat.variableName]; - return; - } - // Make sure the active stat has all the effects and proficiencies - lastActiveStat.computationDetails.effects = effects; - lastActiveStat.computationDetails.proficiencies = proficiencies; - - // Update the memo's stat with the chosen stat - memo.statsByVariableName[stat.variableName] = lastActiveStat; - - // Recreate list of the non-cannonical stats - sameNameStats = without(allStats, lastActiveStat); - - sameNameStats.forEach(statInstance => { - // Mark the non-cannonical stats as overridden - statInstance.overridden = true; - - // Apply the cannonical damage - statInstance.damage = lastActiveStat.damage; - }); - - let baseDependencies = []; - allStats.forEach(statInstance => { - // Add this stat and its deps to the dependencies - baseDependencies = union( - baseDependencies, - [statInstance._id], - statInstance.dependencies, - ); - - // Apply all the base proficiencies - if (statInstance.baseProficiency && !statInstance.inactive){ - proficiencies.push({ - value: statInstance.baseProficiency, - stats: [statInstance.variableName], - type: 'proficiency', - dependencies: statInstance.overridden ? - union(statInstance.dependencies, [statInstance._id]) : - [], - computationDetails: { - computed: true, - } - }); - } - - // Compute each active stat's baseValue calculation and apply it - if (!statInstance.inactive) { - delete statInstance.baseValueErrors; - let { - result, - context, - dependencies - } = evaluateCalculation({ - string: statInstance.baseValueCalculation, - prop: statInstance, - memo - }); - result.value = +result.value; - if (!isNaN(result.value)){ - statInstance.baseValue = result.value; - } else { - statInstance.baseValue = undefined; - } - statInstance.dependencies = union(statInstance.dependencies, dependencies); - if (context.errors.length){ - statInstance.baseValueErrors = context.errors; - } - // Apply all the base values - if (Number.isFinite(statInstance.baseValue)){ - effects.push({ - operation: 'base', - calculation: statInstance.baseValueCalculation, - result: statInstance.baseValue, - stats: [statInstance.variableName], - dependencies: statInstance.overridden ? - union(statInstance.dependencies, [statInstance._id]) : - [], - computationDetails: { - computed: true, - }, - }); - } - } - }); - - // Compute and aggregate all the effects - let aggregator = new EffectAggregator(); - let effectDeps = []; - each(effects, (effect) => { - // Compute - computeEffect(effect, memo); - if (effect.deactivatedByToggle) return; - - // dependencies - if (effect._id) effectDeps = union(effectDeps, [effect._id]); - effectDeps = union(effectDeps, effect.dependencies); - - // Add computed effect to aggregator - aggregator.addEffect(effect); - }); - - // Combine the effects into the stats - allStats.forEach(statInstance => { - // Conglomerate all the effects to compute the final stat values - combineStat(statInstance, aggregator, memo); - // Mark the stats as computed - statInstance.computationDetails.computed = true; - statInstance.computationDetails.busyComputing = false; - // Only the active stat instance depeneds on the effects - if (!statInstance.overridden){ - statInstance.dependencies = union(statInstance.dependencies, effectDeps); - } - }); -} diff --git a/app/imports/api/creature/computation/engine/computeToggle.js b/app/imports/api/creature/computation/engine/computeToggle.js deleted file mode 100644 index dad8174b..00000000 --- a/app/imports/api/creature/computation/engine/computeToggle.js +++ /dev/null @@ -1,55 +0,0 @@ -import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; -import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js'; -import { union } from 'lodash'; - -export default function computeToggle(toggle, memo){ - if (toggle.computationDetails.computed) return; - if (toggle.computationDetails.busyComputing){ - // Trying to compute this effect again while it is already computing. - // We must be in a dependency loop. - toggle.computationDetails.computed = true; - toggle.result = false; - toggle.computationDetails.busyComputing = false; - toggle.computationDetails.error = 'dependencyLoop'; - if (Meteor.isClient) console.warn('dependencyLoop', toggle); - return; - } - // Before doing any work, mark this toggle as busy - toggle.computationDetails.busyComputing = true; - - // Apply any parent toggles - applyToggles(toggle, memo); - - // Do work - delete toggle.errors; - if (toggle.enabled){ - toggle.toggleResult = true; - } else if (toggle.disabled){ - toggle.toggleResult = false; - } else if (!toggle.condition){ - toggle.toggleResult = false; - } else if (Number.isFinite(+toggle.condition)){ - toggle.toggleResult = !!+toggle.condition; - } else { - let { - result, - context, - dependencies, - } = evaluateCalculation({string: toggle.condition, prop: toggle, memo}); - toggle.toggleResult = !!result.value; - toggle.dependencies = union( - toggle.dependencies, - dependencies, - ); - if (context.errors.length){ - toggle.errors = context.errors; - } - } - if (!toggle.toggleResult){ - toggle.inactive = true; - toggle.deactivatedBySelf = true; - toggle.deactivatedByToggle = true; - } - toggle.computationDetails.computed = true; - toggle.computationDetails.busyComputing = false; -} diff --git a/app/imports/api/creature/computation/engine/evaluateCalculation.js b/app/imports/api/creature/computation/engine/evaluateCalculation.js deleted file mode 100644 index 9266efd3..00000000 --- a/app/imports/api/creature/computation/engine/evaluateCalculation.js +++ /dev/null @@ -1,137 +0,0 @@ -import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; -import { prettifyParseError, parse, CompilationContext } from '/imports/parser/parser.js'; -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; -import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js'; -import { union } from 'lodash'; - -/* Convert a calculation into a constant output and errors*/ -export default function evaluateCalculation({ - string, - prop, - memo, - fn = 'reduce', -}){ - let dependencies = []; - let context = new CompilationContext(); - if (!string) return { - result: new ConstantNode({value: string, type: 'string'}), - context, - dependencies, - }; - if (typeof string !== 'string'){ - string = string.toString(); - } - // Parse the string - let calc; - try { - calc = parse(string); - } catch (e) { - let error = prettifyParseError(e); - return { - result: new ErrorNode({context, error}), - context, - dependencies, - }; - } - - // Replace constants with their parsed constant - let replaceResults = replaceConstants({ - calc, memo, prop, dependencies, context - }); - dependencies = replaceResults.dependencies; - calc = replaceResults.calc; - if (replaceResults.failed){ - return { - result: new ConstantNode({value: string, type: 'string'}), - context, - dependencies, - }; - } - - // Ensure all symbol nodes are defined and computed - dependencies = computeSymbols({calc, memo, prop, dependencies}) - - // Evaluate - let result = calc[fn](memo.statsByVariableName, context); - return {result, context, dependencies}; -} - -// Replace constants in the calc with the right ParseNodes -function replaceConstants({calc, memo, prop, dependencies, context}){ - let constFailed = []; - calc = calc.replaceNodes(node => { - if (!(node instanceof SymbolNode)) return; - let stat, constant; - if (node.name[0] !== '#'){ - stat = memo.statsByVariableName[node.name] - constant = memo.constantsByVariableName[node.name]; - } else if (node.name === '#constant'){ - constant = findAncestorByType({type: 'constant', prop, memo}); - } - // replace constants that aren't overridden by stats or disabled by a toggle - if (constant && !constant.deactivatedByToggle && !stat){ - dependencies = union(dependencies, [ - constant._id, - ...constant.dependencies - ]); - // Fail if the constant has errors - if (constant.errors && constant.errors.length){ - constFailed.push(node.name); - return; - } - let parsedConstantNode; - try { - parsedConstantNode = parse(constant.calculation); - } catch(e){ - constFailed.push(node.name); - return; - } - if (!parsedConstantNode) constFailed.push(node.name); - return parsedConstantNode; - } - }); - constFailed.forEach(name => { - context.storeError({ - type: 'error', - message: `${name} is a constant property with parsing errors` - }); - }); - let failed = !!constFailed.length; - if (failed){ - calc = new ErrorNode({error: 'Failed to replace constants'}); - } - return { failed, dependencies, calc }; -} - - // Ensure all symbol nodes are defined and computed -function computeSymbols({calc, memo, prop, dependencies}){ - calc.traverse(node => { - if (node instanceof SymbolNode || node instanceof AccessorNode){ - let stat; - // References up the tree start with # - if (node.name[0] === '#'){ - stat = findAncestorByType({type: node.name.slice(1), prop, memo}); - memo.statsByVariableName[node.name] = stat; - } else { - stat = memo.statsByVariableName[node.name]; - } - if (stat && stat.computationDetails && !stat.computationDetails.computed){ - computeStat(stat, memo); - } - if (stat){ - if (stat.dependencies){ - dependencies = union(dependencies, [ - stat._id || node.name, - ...stat.dependencies - ]); - } else { - dependencies = union(dependencies, [stat._id || node.name]); - } - } - } - }); - return dependencies; -} diff --git a/app/imports/api/creature/computation/engine/findAncestorByType.js b/app/imports/api/creature/computation/engine/findAncestorByType.js deleted file mode 100644 index f8bafdf4..00000000 --- a/app/imports/api/creature/computation/engine/findAncestorByType.js +++ /dev/null @@ -1,10 +0,0 @@ -export default function findAncestorByType({type, prop, memo}){ - if (!prop || !prop.ancestors) return; - let ancestor; - for (let i = prop.ancestors.length - 1; i >= 0; i--){ - ancestor = memo.propsById[prop.ancestors[i].id]; - if (ancestor && ancestor.type === type){ - return ancestor; - } - } -} diff --git a/app/imports/api/creature/computation/engine/getComputationProperties.js b/app/imports/api/creature/computation/engine/getComputationProperties.js deleted file mode 100644 index aee7575e..00000000 --- a/app/imports/api/creature/computation/engine/getComputationProperties.js +++ /dev/null @@ -1,24 +0,0 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; - -export default function getComputationProperties(creatureId){ - // Find all the relevant properties - return CreatureProperties.find({ - 'ancestors.id': creatureId, - removed: {$ne: true}, - $or: [ - // All active properties - {inactive: {$ne: true}}, - // Unless they were deactivated because of a toggle - {deactivatedByToggle: true}, - ] - }, { - // Filter out fields never used by calculations - fields: { - icon: 0, - }, - // Obey tree order - sort: { - order: 1, - } - }).fetch(); -} diff --git a/app/imports/api/creature/computation/engine/getDependentProperties.js b/app/imports/api/creature/computation/engine/getDependentProperties.js deleted file mode 100644 index d87a70ed..00000000 --- a/app/imports/api/creature/computation/engine/getDependentProperties.js +++ /dev/null @@ -1,51 +0,0 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { union } from 'lodash'; - -export default function getDependentProperties({ - creatureId, - propertyIds, - propertiesDependedAponIds, - }){ - // find ids of all dependant toggles that have conditions, even if inactive - let toggleIds = CreatureProperties.find({ - 'ancestors.id': creatureId, - type: 'toggle', - removed: {$ne: true}, - condition: { $exists: true }, - dependencies: {$in: propertyIds}, - }, { - fields: {_id: 1}, - }).map(t => t._id); - // Find all the dependant properties - let props = CreatureProperties.find({ - 'ancestors.id': creatureId, - removed: {$ne: true}, - dependencies: {$in: propertyIds}, - $or: [ - // All active properties - {inactive: {$ne: true}}, - // All active and inactive toggles with conditions - // Same as {$in: toggleIds}, but should be slightly faster - {type: 'toggle', condition: { $exists: true }}, - // All decendents of the above toggles - {'ancestors.id': {$in: toggleIds}}, - ] - }, { fields: {_id: 1, dependencies: 1} }).fetch(); - // Add all the properties that changing props depend on, but haven't yet been - // included to make an array of every property we need - let allConnectedPropIds = [...propertyIds, ...propertiesDependedAponIds]; - props.forEach(prop => { - allConnectedPropIds = union( - allConnectedPropIds, - prop.dependencies, - [prop._id]); - }); - // Add on all the properties and the objects they depend apon - return CreatureProperties.find({ - _id: {$in: allConnectedPropIds} - }, { - // Ignore fields not used in computations - fields: {icon: 0}, - sort: {order: 1}, - }).fetch(); -} diff --git a/app/imports/api/creature/computation/engine/writeAlteredProperties.js b/app/imports/api/creature/computation/engine/writeAlteredProperties.js deleted file mode 100644 index 6927ea9e..00000000 --- a/app/imports/api/creature/computation/engine/writeAlteredProperties.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Meteor } from 'meteor/meteor' -import { isEqual, forOwn } from 'lodash'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; - -export default function writeAlteredProperties(memo){ - let bulkWriteOperations = []; - // Loop through all properties on the memo - forOwn(memo.propsById, changed => { - let schema = propertySchemasIndex[changed.type]; - if (!schema){ - console.warn('No schema for ' + changed.type); - return; - } - let id = changed._id; - let op = undefined; - let original = memo.originalPropsById[id]; - let keys = [ - 'dependencies', - 'inactive', - 'deactivatedBySelf', - 'deactivatedByAncestor', - 'deactivatedByToggle', - 'damage', - ...schema.objectKeys(), - ]; - op = addChangedKeysToOp(op, keys, original, changed); - if (op){ - bulkWriteOperations.push(op); - } - }); - writePropertiesSequentially(bulkWriteOperations); -} - -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 (!isEqual(original[key], changed[key])){ - if (!op) op = newOperation(original._id, changed.type); - let value = changed[key]; - if (value === undefined){ - // Unset values that become undefined - addUnsetOp(op, key); - } else { - // Set values that changed to something else - addSetOp(op, key, value); - } - } - } - return op; -} - -function newOperation(_id, type){ - let newOp = { - updateOne: { - filter: {_id}, - update: {}, - } - }; - if (Meteor.isClient){ - newOp.type = type; - } - return newOp; -} - -function addSetOp(op, key, value){ - if (op.updateOne.update.$set){ - op.updateOne.update.$set[key] = value; - } else { - op.updateOne.update.$set = {[key]: value}; - } -} - -function addUnsetOp(op, key){ - if (op.updateOne.update.$unset){ - op.updateOne.update.$unset[key] = 1; - } else { - op.updateOne.update.$unset = {[key]: 1}; - } -} - -// We use this instead of bulkWriteProperties because it functions with latency -// compensation without needing to roll back changes, which causes multiple -// expensive redraws of the character sheet -function writePropertiesSequentially(bulkWriteOps){ - bulkWriteOps.forEach(op => { - let updateOneOrMany = op.updateOne || op.updateMany; - CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, { - // The bulk code is bypassing validation, so do the same here - // selector: {type: op.type} // include this if bypass is off - bypassCollection2: true, - }); - }); -} - -// This is more efficient on the database, but significantly less efficient -// 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){ - if (!bulkWriteOps.length) return; - // bulkWrite is only available on the server - if (Meteor.isServer){ - CreatureProperties.rawCollection().bulkWrite( - bulkWriteOps, - {ordered : false}, - function(e){ - if (e) { - console.error('Bulk write failed: '); - console.error(e); - } - } - ); - } else { - writePropertiesSequentially(bulkWriteOps); - } -} diff --git a/app/imports/api/creature/computation/engine/writeCreatureVariables.js b/app/imports/api/creature/computation/engine/writeCreatureVariables.js deleted file mode 100644 index c8ccb7d8..00000000 --- a/app/imports/api/creature/computation/engine/writeCreatureVariables.js +++ /dev/null @@ -1,59 +0,0 @@ -import { pick, forOwn } from 'lodash'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import VERSION from '/imports/constants/VERSION.js'; - -export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) { - const fields = [ - 'ability', - 'abilityMod', - 'advantage', - 'attributeType', - 'baseProficiency', - 'baseValue', - 'calculation', - 'conditionalBenefits', - 'currentValue', - 'damage', - 'decimal', - 'fail', - 'level', - 'modifier', - 'name', - 'passiveBonus', - 'proficiency', - 'reset', - 'resetMultiplier', - 'rollBonuses', - 'skillType', - 'spellSlotLevelValue', - 'type', - 'value', - ]; - - if (fullRecompute){ - memo.creatureVariables = {}; - forOwn(memo.statsByVariableName, (stat, variableName) => { - // Don't save context variables - if (variableName[0] === '#') return; - let condensedStat = pick(stat, fields); - memo.creatureVariables[variableName] = condensedStat; - }); - forOwn(memo.constantsByVariableName, (stat, variableName) => { - let condensedStat = pick(stat, fields); - if (!memo.creatureVariables[variableName]){ - memo.creatureVariables[variableName] = condensedStat; - } - }); - Creatures.update(creatureId, {$set: { - variables: memo.creatureVariables, - computeVersion: VERSION, - }}); - } else { - let $set = {}; - forOwn(memo.statsByVariableName, (stat, variableName) => { - let condensedStat = pick(stat, fields); - $set[`variables.${variableName}`] = condensedStat; - }); - Creatures.update(creatureId, {$set}); - } -} diff --git a/app/imports/api/creature/computation/methods/recomputeCreature.js b/app/imports/api/creature/computation/methods/recomputeCreature.js deleted file mode 100644 index 781304ae..00000000 --- a/app/imports/api/creature/computation/methods/recomputeCreature.js +++ /dev/null @@ -1,120 +0,0 @@ -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import SimpleSchema from 'simpl-schema'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js'; -import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js'; -import computeMemo from '/imports/api/creature/computation/engine/computeMemo.js'; -import writeAlteredProperties from '/imports/api/creature/computation/engine/writeAlteredProperties.js'; -import writeCreatureVariables from '/imports/api/creature/computation/engine/writeCreatureVariables.js'; -import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalise/recomputeDamageMultipliers.js'; -import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js'; -import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; - -export const recomputeCreature = new ValidatedMethod({ - - name: 'creatures.recomputeCreature', - - validate: new SimpleSchema({ - charId: { type: String } - }).validator(), - - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - - run({charId}) { - let creature = Creatures.findOne(charId); - // Permission - assertEditPermission(creature, this.userId); - // Work, call this direcly if you are already in a method that has checked - // for permission to edit a given character - recomputeCreatureById(charId); - }, - -}); - -export function recomputeCreatureById(creatureId){ - let creature = Creatures.findOne(creatureId); - recomputeCreatureByDoc(creature); -} - -/** - * This function is the heart of DiceCloud. It recomputes a creature's stats, - * distilling down effects and proficiencies into the final stats that make up - * a creature. - * - * Essentially this is a depth first tree traversal algorithm that computes - * stats' dependencies before computing stats themselves, while detecting - * dependency loops. - * - * At the moment it makes no effort to limit recomputation to just what was - * changed. - * - * Attempting to implement dependency management to limit recomputation to just - * change affected stats should only happen as a last resort, when this function - * can no longer be performed more efficiently, and server resources can not be - * expanded to meet demand. - * - * A brief overview: - * - Fetch the stats of the creature and add them to - * an object for quick lookup - * - Fetch the effects and proficiencies which apply to each stat and store them with the stat - * - Fetch the class levels and store them as well - * - Mark each stat and effect as uncomputed - * - Iterate over each stat in order and compute it - * - If the stat is already computed, skip it - * - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed - * - Mark the stat as busy computing - * - Iterate over each effect which applies to the attribute - * - If the effect is not computed compute it - * - If the effect relies on another attribute, get its computed value - * - Recurse if that attribute is uncomputed - * - apply the effect to the attribute - * - Conglomerate all the effects to compute the final stat values - * - Mark the stat as computed - * - Write the computed results back to the database - */ -export function recomputeCreatureByDoc(creature){ - const creatureId = creature._id; - let props = getComputationProperties(creatureId); - let computationMemo = new ComputationMemo(props, creature); - computeMemo(computationMemo); - writeAlteredProperties(computationMemo); - writeCreatureVariables(computationMemo, creatureId); - recomputeDamageMultipliersById(creatureId); - recomputeSlotFullness(creatureId); - return computationMemo; -} - -export function recomputePropertyDependencies(property){ - let creature = getRootCreatureAncestor(property); - recomputeCreatureByDependencies({ - creature, - propertyIds: [property._id], - propertiesDependedAponIds: property.dependencies, - }); -} - -export function recomputeCreatureByDependencies({ - creature, - propertyIds, - propertiesDependedAponIds -}){ - let props = getDependentProperties({ - creatureId: creature._id, - propertyIds, - propertiesDependedAponIds, - }); - let computationMemo = new ComputationMemo(props, creature); - computeMemo(computationMemo); - writeAlteredProperties(computationMemo); - writeCreatureVariables(computationMemo, creature._id, false) - recomputeInactiveProperties(creature._id); - return computationMemo; -} diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index 0f4591fb..0b2a491f 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -95,8 +95,8 @@ for (let key in propertySchemasIndex){ } import '/imports/api/creature/creatureProperties/methods/index.js'; -import '/imports/api/creature/actions/doAction.js'; -import '/imports/api/creature/actions/castSpellWithSlot.js'; +//import '/imports/api/creature/actions/doAction.js'; +//import '/imports/api/creature/actions/castSpellWithSlot.js'; export default CreatureProperties; export { diff --git a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js index 430e37fe..b9958fcb 100644 --- a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js +++ b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js @@ -4,8 +4,7 @@ import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; const adjustQuantity = new ValidatedMethod({ name: 'creatureProperties.adjustQuantity', @@ -33,8 +32,7 @@ const adjustQuantity = new ValidatedMethod({ // Changing quantity does not change dependencies, but recomputing the // inventory changes many deps at once, so recompute fully - recomputeCreatureByDoc(rootCreature); - recomputeInventory(rootCreature._id); + computeCreature(rootCreature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js index 2a67299c..673f71ad 100644 --- a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js +++ b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js @@ -5,7 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js'; const damagePropertiesByName = new ValidatedMethod({ name: 'CreatureProperties.damagePropertiesByName', @@ -50,7 +50,7 @@ const damagePropertiesByName = new ValidatedMethod({ damagePropertyWork({property, operation, value}); lastProperty = property; }); - if (lastProperty) recomputePropertyDependencies(lastProperty); + if (lastProperty) computeCreatureDependencyGroup(lastProperty); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js index da6b64da..d00ea3c4 100644 --- a/app/imports/api/creature/creatureProperties/methods/damageProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js @@ -4,7 +4,7 @@ import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js'; const damageProperty = new ValidatedMethod({ name: 'creatureProperties.damage', @@ -39,7 +39,7 @@ const damageProperty = new ValidatedMethod({ } let result = damagePropertyWork({property, operation, value}); // Dependencies can't be changed through damage, only recompute deps - recomputePropertyDependencies(property); + computeCreatureDependencyGroup(property); return result; }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/dealDamage.js b/app/imports/api/creature/creatureProperties/methods/dealDamage.js index ea3f3607..173132c2 100644 --- a/app/imports/api/creature/creatureProperties/methods/dealDamage.js +++ b/app/imports/api/creature/creatureProperties/methods/dealDamage.js @@ -5,7 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; -import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; const dealDamage = new ValidatedMethod({ name: 'creatureProperties.dealDamage', @@ -61,11 +61,7 @@ const dealDamage = new ValidatedMethod({ propertyIds.push(healthBar._id); propertiesDependedAponIds.push(...healthBar.dependencies); }); - recomputeCreatureByDependencies({ - creature, - propertyIds, - propertiesDependedAponIds, - }); + computeCreature(creatureId); return totalDamage; }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js index 60761aa8..03c505c2 100644 --- a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js @@ -8,10 +8,8 @@ import { setLineageOfDocs, renewDocIds } from '/imports/api/parenting/parenting.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; import { reorderDocs } from '/imports/api/parenting/order.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; var snackbar; if (Meteor.isClient){ snackbar = require( @@ -89,14 +87,8 @@ const duplicateProperty = new ValidatedMethod({ ancestorId: property.ancestors[0].id, }); - // Inserting the active status of the property needs to be denormalised - recomputeInactiveProperties(creature._id); - - // Recompute the inventory - recomputeInventory(creature._id); - // Inserting a creature property invalidates dependencies: full recompute - recomputeCreatureByDoc(creature); + computeCreature(creature._id); return propertyId; }, diff --git a/app/imports/api/creature/creatureProperties/methods/equipItem.js b/app/imports/api/creature/creatureProperties/methods/equipItem.js index ba1a0444..24cb51fa 100644 --- a/app/imports/api/creature/creatureProperties/methods/equipItem.js +++ b/app/imports/api/creature/creatureProperties/methods/equipItem.js @@ -4,9 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { organizeDoc } from '/imports/api/parenting/organizeMethods.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; @@ -49,9 +47,7 @@ const equipItem = new ValidatedMethod({ skipRecompute: true, }); - recomputeInactiveProperties(creature._id); - recomputeInventory(creature._id); - recomputeCreatureByDoc(creature); + computeCreature(creature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/insertProperty.js b/app/imports/api/creature/creatureProperties/methods/insertProperty.js index 1dec5491..4535e1fe 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/insertProperty.js @@ -5,9 +5,7 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge import SimpleSchema from 'simpl-schema'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { reorderDocs } from '/imports/api/parenting/order.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; import { getAncestry } from '/imports/api/parenting/parenting.js'; import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js'; import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; @@ -140,15 +138,8 @@ export function insertPropertyWork({property, creature}){ collection: CreatureProperties, ancestorId: creature._id, }); - // Inserting the active status of the property needs to be denormalised - recomputeInactiveProperties(creature._id); - - // Recompute the inventory if it has changed - if (property.type === 'item' || property.type === 'container'){ - recomputeInventory(creature._id); - } // Inserting a creature property invalidates dependencies: full recompute - recomputeCreatureByDoc(creature); + computeCreature(creature._id); return _id; } diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 1cb44914..e73cbe33 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -5,8 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { setLineageOfDocs, @@ -15,7 +14,6 @@ import { } from '/imports/api/parenting/parenting.js'; import { reorderDocs } from '/imports/api/parenting/order.js'; import { setDocToLastOrder } from '/imports/api/parenting/order.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; const insertPropertyFromLibraryNode = new ValidatedMethod({ @@ -74,12 +72,8 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({ ancestorId: rootCreature._id, }); - // The library properties need to denormalise which of them are inactive - recomputeInactiveProperties(rootCreature._id); - // Some of the library properties may be items or containers - recomputeInventory(rootCreature._id); // Inserting a creature property invalidates dependencies: full recompute - recomputeCreatureByDoc(rootCreature); + computeCreature(rootCreature._id); // Return the docId of the last property, the inserted root property return rootId; }, diff --git a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js index a5ad2929..fa2acf78 100644 --- a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js @@ -3,7 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; const pullFromProperty = new ValidatedMethod({ name: 'creatureProperties.pull', @@ -28,7 +28,7 @@ const pullFromProperty = new ValidatedMethod({ }); // TODO figure out if this method can change deps or not - recomputeCreatureByDoc(rootCreature); + computeCreature(rootCreature._id); // recomputePropertyDependencies(property); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js index c1054490..e730065c 100644 --- a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js @@ -3,7 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; import { get } from 'lodash'; const pushToProperty = new ValidatedMethod({ @@ -45,8 +45,7 @@ const pushToProperty = new ValidatedMethod({ }); // TODO figure out if this method can change deps or not - recomputeCreatureByDoc(rootCreature); - // recomputePropertyDependencies(property); + computeCreature(rootCreature._id); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js index 864bdf36..b612e6c7 100644 --- a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js @@ -5,9 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { restore } from '/imports/api/parenting/softRemove.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; const restoreProperty = new ValidatedMethod({ name: 'creatureProperties.restore', @@ -28,12 +26,8 @@ const restoreProperty = new ValidatedMethod({ // Do work restore({_id, collection: CreatureProperties}); - // Items and containers might be restored - recomputeInventory(rootCreature._id); - // Parents active status may have changed while it was deleted - recomputeInactiveProperties(rootCreature._id); // Changes dependency tree by restoring children - recomputeCreatureByDoc(rootCreature); + computeCreature(rootCreature._id); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js index 3c735ca1..c1e4baa3 100644 --- a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js +++ b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js @@ -4,7 +4,7 @@ import SimpleSchema from 'simpl-schema'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; const selectAmmoItem = new ValidatedMethod({ name: 'creatureProperties.selectAmmoItem', @@ -45,7 +45,7 @@ const selectAmmoItem = new ValidatedMethod({ // Changing the linked item does change the dependency tree // TODO: We can predict exactly which deps will be affected instead of // recomputing the entire creature - recomputeCreatureByDoc(rootCreature); + computeCreature(rootCreature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js index 328d3d8e..86df9247 100644 --- a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js @@ -5,8 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { softRemove } from '/imports/api/parenting/softRemove.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; const softRemoveProperty = new ValidatedMethod({ name: 'creatureProperties.softRemove', @@ -27,10 +26,8 @@ const softRemoveProperty = new ValidatedMethod({ // Do work softRemove({_id, collection: CreatureProperties}); - // Potentially changes items and containers - recomputeInventory(rootCreature._id); // Changes dependency tree by removing children - recomputeCreatureByDoc(rootCreature); + computeCreature(rootCreature._id); } }); diff --git a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js index 19ff8047..5d0bbfb4 100644 --- a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js +++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js @@ -3,9 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; const updateCreatureProperty = new ValidatedMethod({ name: 'creatureProperties.update', @@ -47,20 +45,9 @@ const updateCreatureProperty = new ValidatedMethod({ selector: {type: property.type}, }); - // Some updates might cause other properties to become inactive - if ([ - 'applied', 'equipped', 'prepared', 'alwaysPrepared', 'disabled' - ].includes(path[0])){ - recomputeInactiveProperties(rootCreature._id); - } - - if (property.type === 'item' || property.type === 'container'){ - // Potentially changes items and containers - recomputeInventory(rootCreature._id); - } // Updating a property is likely to change dependencies, do a full recompute // denormalised stats might change, so fetch the creature again - recomputeCreatureById(rootCreature._id); + computeCreature(rootCreature._id); }, }); diff --git a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js index aa71e48f..ec8fb927 100644 --- a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js +++ b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js @@ -1,4 +1,4 @@ -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; /** * Recomputes all ancestor creatures of this property @@ -6,7 +6,7 @@ import { recomputeCreatureById } from '/imports/api/creature/computation/methods export default function recomputeCreaturesByProperty(property){ for (let ref of property.ancestors){ if (ref.collection === 'creatures') { - recomputeCreatureById.call(ref.id); + computeCreature.call(ref.id); } } } diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index a0464a00..a82f331b 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; const restCreature = new ValidatedMethod({ name: 'creature.methods.longRest', @@ -109,7 +109,7 @@ const restCreature = new ValidatedMethod({ }); }); } - recomputeCreatureById(creatureId); + computeCreature(creatureId); }, }); diff --git a/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js b/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js deleted file mode 100644 index b31b1ece..00000000 --- a/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js +++ /dev/null @@ -1,78 +0,0 @@ -import { ValidatedMethod } from 'meteor/mdg:validated-method'; -import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import SimpleSchema from 'simpl-schema'; -import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; - -export const recomputeDamageMultipliers = new ValidatedMethod({ - - name: 'creatures.recomputeDamageMultipliers', - - validate: new SimpleSchema({ - creatureId: { type: String } - }).validator(), - - mixins: [RateLimiterMixin], - rateLimit: { - numRequests: 5, - timeInterval: 5000, - }, - - run({creatureId}) { - // Permission - assertEditPermission(creatureId, this.userId); - // Work, call this direcly if you are already in a method that has checked - // for permission to edit a given character - recomputeDamageMultipliersById(creatureId); - }, - -}); - -export function recomputeDamageMultipliersById(creatureId){ - if (!creatureId) throw 'Creature ID is required'; - let props = CreatureProperties.find({ - 'ancestors.id': creatureId, - type: 'damageMultiplier', - removed: {$ne: true}, - inactive: {$ne: true}, - }, { - sort: {order: 1} - }); - - // Count of how many weakness, resistances and immunities each damage type has - let multipliersByName = {}; - props.forEach(dm => { - dm.damageTypes.forEach(damageType => { - if (!multipliersByName[damageType]){ - multipliersByName[damageType] = { - weaknesses: 0, - resistances: 0, - immunities: 0, - }; - } - if (dm.value === 0){ - multipliersByName[damageType].immunities++; - } else if (dm.value === 0.5){ - multipliersByName[damageType].resistances++; - } else if (dm.value === 2){ - multipliersByName[damageType].weaknesses++; - } - }); - }); - // Make an Object with keys of all the damage types that have a resulting - // immunity, weakness, or resistance - let damageMultipliers = {}; - for (let damageType in multipliersByName){ - let multiplier = multipliersByName[damageType]; - if (multiplier.immunities){ - damageMultipliers[damageType] = 0; - } else if (multiplier.resistances && !multiplier.weaknesses){ - damageMultipliers[damageType] = 0.5; - } else if (multiplier.weaknesses && !multiplier.resistances){ - damageMultipliers[damageType] = 2; - } - } - // Store the Object on the creature document - Creatures.update(creatureId, {$set: {damageMultipliers}}); -} diff --git a/app/imports/api/creature/denormalise/recomputeInactiveProperties.js b/app/imports/api/creature/denormalise/recomputeInactiveProperties.js deleted file mode 100644 index 8cff24a5..00000000 --- a/app/imports/api/creature/denormalise/recomputeInactiveProperties.js +++ /dev/null @@ -1,75 +0,0 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; - -export default function recomputeInactiveProperties(ancestorId){ - let disabledFilter = { - 'ancestors.id': ancestorId, - $or: [ - {disabled: true}, // Everything can be disabled - {type: 'buff', applied: false}, // Buffs can be applied - {type: 'item', equipped: {$ne: true}}, - {type: 'spell', prepared: {$ne: true}, alwaysPrepared: {$ne: true}}, - ], - }; - let disabledIds = CreatureProperties.find(disabledFilter, { - fields: {_id: 1}, - }).map(prop => prop._id); - - // Deactivate relevant properties - // Inactive properties - CreatureProperties.update({ - 'ancestors.id': ancestorId, - '_id': {$in: disabledIds}, - $or: [ - {inactive: {$ne: true}}, - {deactivatedBySelf: {$ne: true}}, - {deactivatedByAncestor: true}, - ], - }, { - $set: { - inactive: true, - deactivatedBySelf: true, - }, - $unset: {deactivatedByAncestor: 1}, - }, { - multi: true, - selector: {type: 'any'}, - }); - // Decendants of inactive properties - CreatureProperties.update({ - 'ancestors.id': {$eq: ancestorId, $in: disabledIds}, - $or: [ - {inactive: {$ne: true}}, - {deactivatedByAncestor: {$ne: true}}, - ], - }, { - $set: { - inactive: true, - deactivatedByAncestor: true, - }, - }, { - multi: true, - selector: {type: 'any'}, - }); - - // Remove inactive from all the properties that are inactive but shouldn't be - CreatureProperties.update({ - 'ancestors.id': {$eq: ancestorId, $nin: disabledIds}, - '_id': {$nin: disabledIds}, - // if it was a toggle responsible, we leave it alone - deactivatedByToggle: {$ne: true}, - $or: [ - {inactive: true}, - {deactivatedByAncestor: true}, - {deactivatedBySelf: true} - ], - }, { - $unset: { - inactive: 1, - deactivatedByAncestor: 1, - deactivatedBySelf: 1, - }, - }, { - multi: true, - selector: {type: 'any'}, - }); -} diff --git a/app/imports/api/creature/denormalise/recomputeInventory.js b/app/imports/api/creature/denormalise/recomputeInventory.js deleted file mode 100644 index 3bf7d1a2..00000000 --- a/app/imports/api/creature/denormalise/recomputeInventory.js +++ /dev/null @@ -1,111 +0,0 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import nodesToTree from '/imports/api/parenting/nodesToTree.js'; - -export default function recomputeInventory(creatureId){ - let inventoryForest = nodesToTree({ - collection: CreatureProperties, - ancestorId: creatureId, - filter: { - type: {$in: ['container', 'item']}, - }, - deactivatedByAncestor: {$ne: true}, - }); - let containersToWrite = []; - let data = getChildrenInventoryData(inventoryForest, containersToWrite); - containersToWrite.forEach(container => { - CreatureProperties.update(container._id, {$set: { - contentsWeight: container.contentsWeight, - contentsValue: container.contentsValue, - }}, {selector: {type: 'container'}}); - }); - Creatures.update(creatureId, {$set: { - 'denormalizedStats.weightTotal': data.weightTotal, - 'denormalizedStats.weightEquipment': data.weightEquipment, - 'denormalizedStats.weightCarried': data.weightCarried, - 'denormalizedStats.valueTotal': data.valueTotal, - 'denormalizedStats.valueEquipment': data.valueEquipment, - 'denormalizedStats.valueCarried': data.valueCarried, - 'denormalizedStats.itemsAttuned': data.itemsAttuned, - }}); - return data; -} - -function getChildrenInventoryData(forest, containersToWrite){ - let data = { - weightTotal: 0, - weightEquipment: 0, - weightCarried: 0, - valueTotal: 0, - valueEquipment: 0, - valueCarried: 0, - itemsAttuned: 0, - } - forest.forEach(tree => { - let treeData = getInventoryData(tree, containersToWrite); - for (let key in data){ - data[key] += treeData[key] || 0; - } - }); - return data; -} - -function getInventoryData(tree, containersToWrite){ - let data = { - weightTotal: 0, - weightEquipment: 0, - weightCarried: 0, - valueTotal: 0, - valueEquipment: 0, - valueCarried: 0, - itemsAttuned: 0, - } - let childData = getChildrenInventoryData(tree.children, containersToWrite); - let node = tree.node; - if (node.type === 'container'){ - data.weightTotal += node.weight || 0; - data.valueTotal += node.value || 0; - data.weightCarried += node.weight || 0; - data.valueCarried += node.value || 0; - storeContentsData(node, childData, containersToWrite); - } else if (node.type === 'item'){ - data.weightTotal += (node.weight * node.quantity) || 0; - data.valueTotal += (node.value * node.quantity) || 0; - data.weightCarried += (node.weight * node.quantity) || 0; - data.valueCarried += (node.value * node.quantity) || 0; - if (node.equipped){ - data.weightEquipment += (node.weight * node.quantity) || 0; - data.valueEquipment += (node.value * node.quantity) || 0; - } - if (node.attuned){ - data.itemsAttuned += 1; - } - } - for (let key in data){ - data[key] += childData[key]; - } - if (node.contentsWeightless){ - data.weightCarried = node.weight; - } - if (node.carried === false){ - data.weightCarried = 0; - data.valueCarried = 0; - } - return data -} - -function storeContentsData(node, childData, containersToWrite){ - let newContentsWeight = childData.weightCarried - if (node.contentsWeight !== newContentsWeight){ - node.contentsWeight = newContentsWeight; - node.contentsWeightChanged = true; - } - let newContentsValue = childData.valueCarried; - if (node.contentsValue !== newContentsValue){ - node.contentsValue = newContentsValue; - node.contentsValueChanged = true; - } - if (node.contentsWeightChanged || node.contentsValueChanged){ - containersToWrite.push(node); - } -} diff --git a/app/imports/api/creature/denormalise/recomputeSlotFullness.js b/app/imports/api/creature/denormalise/recomputeSlotFullness.js deleted file mode 100644 index a99731c7..00000000 --- a/app/imports/api/creature/denormalise/recomputeSlotFullness.js +++ /dev/null @@ -1,43 +0,0 @@ -import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -// n + 1 database queries + n potential updates for n slots. Could be sped up. -export default function recomputeSlotFullness(ancestorId){ - CreatureProperties.find({ - 'ancestors.id': ancestorId, - type: 'propertySlot', - }).forEach(slot => { - let children = CreatureProperties.find({ - 'parent.id': slot._id, - removed: {$ne: true}, - }, { - fields: { - slotQuantityFilled: 1, - type: 1 - } - }).fetch(); - let totalFilled = 0; - children.forEach(child => { - if (child.type === 'slotFiller'){ - totalFilled += child.slotQuantityFilled; - } else { - totalFilled++; - } - }); - let spaceLeft; - let expected = slot.quantityExpectedResult; - if (typeof expected !== 'number'){ - expected = 1; - } - if (expected === 0){ - spaceLeft = null; - } else { - spaceLeft = expected - totalFilled; - } - if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){ - CreatureProperties.update(slot._id, { - $set: {totalFilled, spaceLeft}, - }, { - selector: {type: 'propertySlot'} - }); - } - }); -} diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index 100e87c3..89304e2b 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js'; -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; let Experiences = new Mongo.Collection('experiences'); @@ -175,7 +175,7 @@ const recomputeExperiences = new ValidatedMethod({ 'denormalizedStats.xp': xp, 'denormalizedStats.milestoneLevels': milestoneLevels }}); - recomputeCreatureById(creatureId); + computeCreature(creatureId); }, }); diff --git a/app/imports/api/creature/mixins/recomputeCreatureMixin.js b/app/imports/api/creature/mixins/recomputeCreatureMixin.js index 05c0abde..5aaf87ae 100644 --- a/app/imports/api/creature/mixins/recomputeCreatureMixin.js +++ b/app/imports/api/creature/mixins/recomputeCreatureMixin.js @@ -1,4 +1,4 @@ -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; export default function recomputeCreatureMixin(methodOptions){ let runFunc = methodOptions.run; @@ -10,7 +10,7 @@ export default function recomputeCreatureMixin(methodOptions){ ) { return result; } - recomputeCreatureById(charId); + computeCreature(charId); return result; }; return methodOptions; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/buildComputation/CreatureComputation.js rename to app/imports/api/engine/computation/CreatureComputation.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js similarity index 90% rename from app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js rename to app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js index 6645067d..0ec51aa4 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js @@ -1,4 +1,4 @@ -import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; +import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; export default function computeInactiveStatus(node){ const prop = node.node; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js rename to app/imports/api/engine/computation/buildComputation/computeSlotQuantityFilled.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js similarity index 84% rename from app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js rename to app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js index 40d77ea5..6b168c3c 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js @@ -1,4 +1,4 @@ -import walkDown from '/imports/api/creature/computation/newEngine/utility/walkdown.js'; +import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; export default function computeToggleDependencies(node, dependencyGraph){ const prop = node.node; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js similarity index 93% rename from app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js rename to app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index e6081faa..69f747ee 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -1,6 +1,6 @@ import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; -import findAncestorByType from '/imports/api/creature/computation/newEngine/utility/findAncestorByType.js'; +import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js'; export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js b/app/imports/api/engine/computation/buildComputation/linkInventory.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/buildComputation/linkInventory.js rename to app/imports/api/engine/computation/buildComputation/linkInventory.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js rename to app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js similarity index 96% rename from app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js rename to app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index eecc7641..6c55e290 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,7 +1,7 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import { prettifyParseError, parse } from '/imports/parser/parser.js'; import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import applyFnToKey from '/imports/api/creature/computation/newEngine/utility/applyFnToKey.js'; +import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; import { get } from 'lodash'; export default function parseCalculationFields(prop, schemas){ diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/buildComputation/removeSchemaFields.js rename to app/imports/api/engine/computation/buildComputation/removeSchemaFields.js diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js similarity index 96% rename from app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js rename to app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js index 780716e3..19754fb9 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeInactiveStatus.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js similarity index 87% rename from app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js rename to app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js index 1a6bbba4..8a8af817 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeSlotQuantityFilled.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeSlotQuantityFilled.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js similarity index 94% rename from app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js rename to app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js index 3b6028c4..268ea2ef 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/computeToggleDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeToggleDependencies.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js similarity index 92% rename from app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js rename to app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js index 918767ba..ca81ae6e 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkCalculationDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js similarity index 95% rename from app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js rename to app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js index 5bda55ab..cb22544a 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkInventory.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkInventory.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js b/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js similarity index 85% rename from app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js rename to app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js index 5733bd4a..b8f9d4f1 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/tests/linkTypeDependencies.testfn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkTypeDependencies.testfn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +import { buildComputationFromProps } from '/imports/api/engine/computation/buildCreatureComputation.js'; import { assert } from 'chai'; import clean from '../../utility/cleanProp.testFn.js'; diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js similarity index 97% rename from app/imports/api/creature/computation/newEngine/buildCreatureComputation.js rename to app/imports/api/engine/computation/buildCreatureComputation.js index c577459c..af4d7fbb 100644 --- a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; -import CreatureComputation from './buildComputation/CreatureComputation.js'; +import CreatureComputation from './CreatureComputation.js'; import removeSchemaFields from './buildComputation/removeSchemaFields.js'; /** diff --git a/app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js b/app/imports/api/engine/computation/buildCreatureComputation.test.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/buildCreatureComputation.test.js rename to app/imports/api/engine/computation/buildCreatureComputation.test.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType.js rename to app/imports/api/engine/computation/computeComputation/computeByType.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAction.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeAttribute.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeAttribute.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeAttribute.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeContainer.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeContainer.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeSlot.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeSlot.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateClassLevel.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDamageMultiplier.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateInventory.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateProficiency.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/aggregate/index.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/index.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeImplicitVariable.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsClass.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsClass.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsClass.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsSkill.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js similarity index 91% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js rename to app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js index 573a5766..95905904 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/getAggregatorResult.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/getAggregatorResult.js @@ -1,4 +1,4 @@ -import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js'; +import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js'; export default function getAggregatorResult(node){ // Work out the base value as the greater of the deining stat value or diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js rename to app/imports/api/engine/computation/computeComputation/computeCalculations.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js b/app/imports/api/engine/computation/computeComputation/computeToggles.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js rename to app/imports/api/engine/computation/computeComputation/computeToggles.js diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js similarity index 95% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js index 0365b200..2fc425bc 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAction.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js similarity index 93% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index 160e0615..37c30504 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js similarity index 92% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js index d49011e2..2c6bdc8f 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeClasses.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeClasses.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js similarity index 85% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js index 8bf8b91d..94fd414e 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeConstants.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeConstants.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js similarity index 89% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js index 94ce0b24..ef1ec555 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeDamageMultipliers.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeDamageMultipliers.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js similarity index 91% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js index dea3f464..8650a9e2 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeEffects.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeEffects.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js similarity index 93% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js index d77b5b59..d2cc2f1d 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeInventory.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeInventory.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js similarity index 94% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js rename to app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js index fa6c86a8..2557296a 100644 --- a/app/imports/api/creature/computation/newEngine/computeComputation/tests/computeSkills.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeSkills.testFn.js @@ -1,4 +1,4 @@ -import { buildComputationFromProps } from '/imports/api/creature/computation/newEngine/buildCreatureComputation.js'; +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'; diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js b/app/imports/api/engine/computation/computeComputation/tests/index.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeComputation/tests/index.js rename to app/imports/api/engine/computation/computeComputation/tests/index.js diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/engine/computation/computeCreatureComputation.js similarity index 79% rename from app/imports/api/creature/computation/newEngine/computeCreatureComputation.js rename to app/imports/api/engine/computation/computeCreatureComputation.js index a317e383..2c542c92 100644 --- a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.js @@ -1,6 +1,6 @@ -import computeCalculations from '/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js'; -import computeToggles from '/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js'; -import computeByType from '/imports/api/creature/computation/newEngine/computeComputation/computeByType.js'; +import computeCalculations from '/imports/api/engine/computation/computeComputation/computeCalculations.js'; +import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js'; +import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js'; export default function computeCreatureComputation(computation){ const stack = []; diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js b/app/imports/api/engine/computation/computeCreatureComputation.test.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/computeCreatureComputation.test.js rename to app/imports/api/engine/computation/computeCreatureComputation.test.js diff --git a/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js b/app/imports/api/engine/computation/utility/applyFnToKey.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/utility/applyFnToKey.js rename to app/imports/api/engine/computation/utility/applyFnToKey.js diff --git a/app/imports/api/creature/computation/newEngine/utility/applyFnToKey.test.js b/app/imports/api/engine/computation/utility/applyFnToKey.test.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/utility/applyFnToKey.test.js rename to app/imports/api/engine/computation/utility/applyFnToKey.test.js diff --git a/app/imports/api/creature/computation/newEngine/utility/cleanProp.testFn.js b/app/imports/api/engine/computation/utility/cleanProp.testFn.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/utility/cleanProp.testFn.js rename to app/imports/api/engine/computation/utility/cleanProp.testFn.js diff --git a/app/imports/api/creature/computation/newEngine/utility/findAncestorByType.js b/app/imports/api/engine/computation/utility/findAncestorByType.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/utility/findAncestorByType.js rename to app/imports/api/engine/computation/utility/findAncestorByType.js diff --git a/app/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js b/app/imports/api/engine/computation/utility/stripFloatingPointOddities.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js rename to app/imports/api/engine/computation/utility/stripFloatingPointOddities.js diff --git a/app/imports/api/creature/computation/newEngine/utility/walkdown.js b/app/imports/api/engine/computation/utility/walkdown.js similarity index 100% rename from app/imports/api/creature/computation/newEngine/utility/walkdown.js rename to app/imports/api/engine/computation/utility/walkdown.js diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js new file mode 100644 index 00000000..2a5192d5 --- /dev/null +++ b/app/imports/api/engine/computeCreature.js @@ -0,0 +1,17 @@ +import buildCreatureComputation from './computation/buildCreatureComputation.js'; +import computeCreatureComputation from './computation/computeCreatureComputation.js'; + +export default function computeCreature(creatureId){ + const computation = buildCreatureComputation(creatureId); + computeCreatureComputation(computation); + // TODO: writeCreatureComputation(computation); +} + +// For now just recompute the whole creature, later only recompute a single +// connected section of the depdendency graph +export function computeCreatureDependencyGroup(property){ + let creatureId = property.ancestors[0].id; + const computation = buildCreatureComputation(creatureId); + computeCreatureComputation(computation); + // TODO: writeCreatureComputation(computation); +} diff --git a/app/imports/api/creature/actions/applyAction.js b/app/imports/api/engine/oldActions/applyAction.js similarity index 74% rename from app/imports/api/creature/actions/applyAction.js rename to app/imports/api/engine/oldActions/applyAction.js index 1ffb7990..1611a764 100644 --- a/app/imports/api/creature/actions/applyAction.js +++ b/app/imports/api/engine/oldActions/applyAction.js @@ -1,13 +1,13 @@ import spendResources from '/imports/api/creature/actions/spendResources.js' -import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js'; export default function applyAction({prop, log}){ let content = { name: prop.name }; + /* if (prop.summary){ content.value = embedInlineCalculations( prop.summary, prop.summaryCalculations ); - } + }*/ log.content.push(content); spendResources({prop, log}); } diff --git a/app/imports/api/creature/actions/applyAdjustment.js b/app/imports/api/engine/oldActions/applyAdjustment.js similarity index 100% rename from app/imports/api/creature/actions/applyAdjustment.js rename to app/imports/api/engine/oldActions/applyAdjustment.js diff --git a/app/imports/api/creature/actions/applyAttack.js b/app/imports/api/engine/oldActions/applyAttack.js similarity index 100% rename from app/imports/api/creature/actions/applyAttack.js rename to app/imports/api/engine/oldActions/applyAttack.js diff --git a/app/imports/api/creature/actions/applyBuff.js b/app/imports/api/engine/oldActions/applyBuff.js similarity index 100% rename from app/imports/api/creature/actions/applyBuff.js rename to app/imports/api/engine/oldActions/applyBuff.js diff --git a/app/imports/api/creature/actions/applyDamage.js b/app/imports/api/engine/oldActions/applyDamage.js similarity index 100% rename from app/imports/api/creature/actions/applyDamage.js rename to app/imports/api/engine/oldActions/applyDamage.js diff --git a/app/imports/api/creature/actions/applyProperties.js b/app/imports/api/engine/oldActions/applyProperties.js similarity index 100% rename from app/imports/api/creature/actions/applyProperties.js rename to app/imports/api/engine/oldActions/applyProperties.js diff --git a/app/imports/api/creature/actions/applyRoll.js b/app/imports/api/engine/oldActions/applyRoll.js similarity index 100% rename from app/imports/api/creature/actions/applyRoll.js rename to app/imports/api/engine/oldActions/applyRoll.js diff --git a/app/imports/api/creature/actions/applySave.js b/app/imports/api/engine/oldActions/applySave.js similarity index 100% rename from app/imports/api/creature/actions/applySave.js rename to app/imports/api/engine/oldActions/applySave.js diff --git a/app/imports/api/creature/actions/applyToggle.js b/app/imports/api/engine/oldActions/applyToggle.js similarity index 100% rename from app/imports/api/creature/actions/applyToggle.js rename to app/imports/api/engine/oldActions/applyToggle.js diff --git a/app/imports/api/creature/actions/castSpellWithSlot.js b/app/imports/api/engine/oldActions/castSpellWithSlot.js similarity index 96% rename from app/imports/api/creature/actions/castSpellWithSlot.js rename to app/imports/api/engine/oldActions/castSpellWithSlot.js index 9e2b3570..54d4686c 100644 --- a/app/imports/api/creature/actions/castSpellWithSlot.js +++ b/app/imports/api/engine/oldActions/castSpellWithSlot.js @@ -5,7 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; import { doActionWork } from '/imports/api/creature/actions/doAction.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js'; diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/engine/oldActions/doAction.js similarity index 80% rename from app/imports/api/creature/actions/doAction.js rename to app/imports/api/engine/oldActions/doAction.js index 8cbe6690..cfea79e2 100644 --- a/app/imports/api/creature/actions/doAction.js +++ b/app/imports/api/engine/oldActions/doAction.js @@ -6,11 +6,9 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js'; import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; import nodesToTree from '/imports/api/parenting/nodesToTree.js'; import applyProperties from '/imports/api/creature/actions/applyProperties.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js'; const doAction = new ValidatedMethod({ @@ -50,17 +48,11 @@ const doAction = new ValidatedMethod({ }); doActionWork({action, creature, targets, actionContext, method: this}); - // The acting creature might have used ammo - recomputeInventory(creature._id); - - // The action might add properties which need to be activated - recomputeInactiveProperties(creature._id); - // recompute creatures - recomputeCreatureByDoc(creature); + computeCreature(creature._id); + targets.forEach(target => { - recomputeInactiveProperties(target._id); - recomputeCreatureByDoc(target); + computeCreature(target._id); }); }, }); diff --git a/app/imports/api/creature/actions/doCheck.js b/app/imports/api/engine/oldActions/doCheck.js similarity index 100% rename from app/imports/api/creature/actions/doCheck.js rename to app/imports/api/engine/oldActions/doCheck.js diff --git a/app/imports/api/creature/actions/getAncestorContext.js b/app/imports/api/engine/oldActions/getAncestorContext.js similarity index 100% rename from app/imports/api/creature/actions/getAncestorContext.js rename to app/imports/api/engine/oldActions/getAncestorContext.js diff --git a/app/imports/api/creature/actions/spendResources.js b/app/imports/api/engine/oldActions/spendResources.js similarity index 100% rename from app/imports/api/creature/actions/spendResources.js rename to app/imports/api/engine/oldActions/spendResources.js diff --git a/app/imports/api/parenting/organizeMethods.js b/app/imports/api/parenting/organizeMethods.js index 624a2525..e67fdc45 100644 --- a/app/imports/api/parenting/organizeMethods.js +++ b/app/imports/api/parenting/organizeMethods.js @@ -8,9 +8,8 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js'; import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; -import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js'; -import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; + const organizeDoc = new ValidatedMethod({ name: 'organize.organizeDoc', validate: new SimpleSchema({ @@ -60,14 +59,8 @@ const organizeDoc = new ValidatedMethod({ let creaturesToRecompute = union(docCreatures, parentCreatures); // Recompute the creatures creaturesToRecompute.forEach(id => { - // The active status of some properties might change due to a change in - // ancestry - recomputeInactiveProperties(id); - if (doc.type === 'container' || doc.type === 'item'){ - recomputeInventory(id); - } // Some Dependencies depend on ancestry, so a full recompute is needed - recomputeCreatureById(id); + computeCreature(id); }); } }, @@ -93,7 +86,7 @@ const reorderDoc = new ValidatedMethod({ safeUpdateDocOrder({docRef, order}); // Recompute the affected creatures getCreatureAncestors(doc).forEach(id => { - recomputeCreatureById(id); + computeCreature(id); }); }, }); diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js index 112e22ce..d5ed8489 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -1,8 +1,6 @@ import { Migrations } from 'meteor/percolate:migrations'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js'; -import { get } from 'lodash'; -import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js'; import transformFields from '/imports/migrations/server/transformFields.js'; import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; @@ -172,9 +170,6 @@ function getInlineComputationTransforms(key){ return [ {from: key, to: `${key}.text`}, {from: `${key}Calculations`, to: `${key}.inlineCalculations`}, - {to: `${key}.value`, up: (val, doc) => - embedInlineCalculations(get(doc, key), get(doc, `${key}Calculations`)) - }, {from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`}, ]; } diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index 105ffed2..d879de52 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -3,8 +3,7 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; -import recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js'; -import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; +import { computeCreature } from '/imports/api/engine/computeCreature.js'; import VERSION from '/imports/constants/VERSION.js'; let schema = new SimpleSchema({ @@ -27,8 +26,7 @@ Meteor.publish('singleCharacter', function(creatureId){ catch(e){ return [] } if (creature.computeVersion !== VERSION){ try { - recomputeInvetory(creatureId); - recomputeCreatureById(creatureId) + computeCreature(creatureId) } catch(e){ console.error(e) } } diff --git a/app/imports/ui/components/computation/EmbedInlineComputations.vue b/app/imports/ui/components/computation/EmbedInlineComputations.vue index befe1c41..c624c566 100644 --- a/app/imports/ui/components/computation/EmbedInlineComputations.vue +++ b/app/imports/ui/components/computation/EmbedInlineComputations.vue @@ -6,7 +6,7 @@ - - diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index 40b2585b..d3c86223 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -187,7 +187,7 @@ import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue' -import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; +// import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js'; import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js' import Libraries from '/imports/api/library/Libraries.js'; import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue'; diff --git a/app/imports/ui/properties/components/features/FeatureCard.vue b/app/imports/ui/properties/components/features/FeatureCard.vue index 79062699..b4d086ea 100644 --- a/app/imports/ui/properties/components/features/FeatureCard.vue +++ b/app/imports/ui/properties/components/features/FeatureCard.vue @@ -11,10 +11,8 @@ - @@ -22,12 +20,12 @@ + + From 85e8756d1d1e123ed50042b0867ce38616dfc00d Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 16:39:44 +0200 Subject: [PATCH 34/85] Fixed parse errors not showing up on calculations --- .../tests/linkCalculationDependencies.testFn.js | 9 +++++++++ .../computeComputation/computeCalculations.js | 6 +++++- .../tests/computeAttribute.testFn.js | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js index ca81ae6e..3996b5a3 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -5,6 +5,8 @@ import clean from '../../utility/cleanProp.testFn.js'; export default function(){ const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; + const prop = (id) => computation.propsById[id]; + assert.isTrue( !!hasLink('childId', 'spellListId'), 'Ancestor references of parent in inline calculations should create dependency' @@ -21,6 +23,10 @@ export default function(){ !!hasLink('grandchildId', 'wisdom'), 'Variable references create dependencies even if the attributes don\'t exist' ); + assert.equal( + prop('strengthId').baseValue.errors.length, 1, + 'Parse errors should be added to calculation errors' + ); } var testProperties = [ @@ -49,6 +55,9 @@ var testProperties = [ _id: 'strengthId', type: 'attribute', variableName: 'strength', + baseValue: { + calculation: '15 + ', + }, ancestors: [{id: 'charId'}], }), ]; diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js index 04212b13..b78bc5fc 100644 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ b/app/imports/api/engine/computation/computeComputation/computeCalculations.js @@ -24,7 +24,11 @@ function evaluateCalculation(calculation, scope){ } else { calculation.value = NaN; } - calculation.errors = context.errors; + if (calculation.errors){ + calculation.errors = [...calculation.errors, ...context.errors] + } else { + calculation.errors = context.errors + } } function embedInlineCalculations(inlineCalcObj){ diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index 37c30504..7cc0a32e 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -13,6 +13,10 @@ export default function(){ assert.equal(prop('strengthId').modifier, 1); assert.equal(prop('referencesDexId').value, 4); assert.equal(prop('hitDiceId').constitutionMod, 5); + assert.equal( + prop('parseErrorId').baseValue.errors.length, 1, + 'Parse errors should be added to calculation errors' + ); } var testProperties = [ @@ -74,4 +78,13 @@ var testProperties = [ calculation: '4' }, }), + clean({ + _id: 'parseErrorId', + variableName: 'parseError', + type: 'attribute', + attributeType: 'ability', + baseValue: { + calculation: '12 +' + }, + }), ]; From b0ee5cd3042481a5489da051f2fbc01d2d1a8c3e Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 27 Sep 2021 17:26:52 +0200 Subject: [PATCH 35/85] Continued iterating on calculations, added failing test for bugs found --- .../buildComputation/removeSchemaFields.js | 7 +++-- .../computeComputation/computeCalculations.js | 3 ++ .../tests/computeAction.testFn.js | 13 ++++++++- .../writeAlteredProperties.js | 9 ++++-- app/imports/api/properties/Damages.js | 2 ++ .../subSchemas/inlineCalculationField.js | 1 + .../server/publications/singleCharacter.js | 4 +-- .../ui/properties/forms/AttributeForm.vue | 15 ++++------ .../properties/forms/shared/ComputedField.vue | 29 +++++++++++++++++++ 9 files changed, 67 insertions(+), 16 deletions(-) create mode 100644 app/imports/ui/properties/forms/shared/ComputedField.vue diff --git a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js index 885d3abf..bf0a0af7 100644 --- a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js +++ b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js @@ -4,10 +4,13 @@ import { unset } from 'lodash'; export default function removeSchemaFields(schemas, prop){ schemas.forEach(schema => { schema._schemaKeys.forEach(key => { - // Skip object and array keys + // Skip object and array keys, except the errors array if ( schema.getQuickTypeForKey(key) === 'object' || - schema.getQuickTypeForKey(key) === 'objectArray' + ( + schema.getQuickTypeForKey(key) === 'objectArray' && + key.slice(-6)!== 'errors' + ) ) return; // Unset other computed only keys applyFnToKey(prop, key, unset) diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js index b78bc5fc..2b06135c 100644 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ b/app/imports/api/engine/computation/computeComputation/computeCalculations.js @@ -29,6 +29,9 @@ function evaluateCalculation(calculation, scope){ } else { calculation.errors = context.errors } + // remove the working fields + delete calculation._parseLevel; + delete calculation._parsedCalculation; } function embedInlineCalculations(inlineCalcObj){ diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js index 2fc425bc..6d6395ac 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js @@ -6,13 +6,16 @@ import clean from '../../utility/cleanProp.testFn.js'; export default function(){ const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); - + const prop = computation.propsById['actionId']; assert.equal(prop.summary.value, 'test summary 3 without referencing anything 7'); assert.equal(prop.description.value, 'test description 12 with reference 0.25 prop'); assert.equal(prop.uses.value, 7); assert.equal(prop.usesLeft, 2); + const rolled = computation.propsById['rolledDescriptionId']; + assert.equal(rolled.summary.value, 'test roll gets compiled {1d4 + 4} properly'); + const itemConsumed = prop.resources.itemsConsumed[0]; assert.equal(itemConsumed.quantity.value, 3); assert.equal(itemConsumed.available, 27); @@ -60,6 +63,14 @@ var testProperties = [ }, usesUsed: 5, }), + clean({ + _id: 'rolledDescriptionId', + type: 'action', + ancestors: [{id: 'charId'}], + summary: { + text: 'test roll gets compiled {1d4 + (2 + 2)} properly', + }, + }), clean({ _id: 'numItemsConumedId', type: 'attribute', diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index 408d1a34..0d7bcbe6 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor' -import { isEqual, forOwn } from 'lodash'; +import { isEqual } from 'lodash'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; export default function writeAlteredProperties(computation){ let bulkWriteOperations = []; // Loop through all properties on the memo - forOwn(computation.propsById, changed => { + computation.props.forEach(changed => { let schema = propertySchemasIndex[changed.type]; if (!schema){ console.warn('No schema for ' + changed.type); @@ -36,6 +36,9 @@ function addChangedKeysToOp(op, keys, original, changed) { // and compile an operation that sets all those keys for (let key of keys){ if (!isEqual(original[key], changed[key])){ + console.log('not equal: ', key); + console.log(original[key]) + console.log(changed[key]) if (!op) op = newOperation(original._id, changed.type); let value = changed[key]; if (value === undefined){ @@ -83,6 +86,7 @@ function addUnsetOp(op, key){ // compensation without needing to roll back changes, which causes multiple // expensive redraws of the character sheet function writePropertiesSequentially(bulkWriteOps){ + console.log({opsLength: bulkWriteOps.length}); bulkWriteOps.forEach(op => { let updateOneOrMany = op.updateOne || op.updateMany; CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, { @@ -91,6 +95,7 @@ function writePropertiesSequentially(bulkWriteOps){ bypassCollection2: true, }); }); + console.log('finished writing ops'); } // This is more efficient on the database, but significantly less efficient diff --git a/app/imports/api/properties/Damages.js b/app/imports/api/properties/Damages.js index 8b4881f9..bc790b13 100644 --- a/app/imports/api/properties/Damages.js +++ b/app/imports/api/properties/Damages.js @@ -9,6 +9,7 @@ const DamageSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, defaultValue: '1d8 + strength.modifier', + parseLevel: 'compile', }, // Who this damage applies to target: { @@ -31,6 +32,7 @@ const ComputedOnlyDamageSchema = createPropertySchema({ amount: { type: 'computedOnlyField', optional: true, + parseLevel: 'compile', }, }); diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.js index f9f67242..50e31224 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.js +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.js @@ -38,6 +38,7 @@ function computedOnlyInlineCalculationField(field){ }, [`${field}.inlineCalculations.$`]: { type: Object, + parseLevel: 'compile', }, // The part between bracers {} [`${field}.inlineCalculations.$.calculation`]: { diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index ce1257fb..69fff4f7 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -15,7 +15,7 @@ let schema = new SimpleSchema({ Meteor.publish('singleCharacter', function(creatureId){ schema.validate({ creatureId }); - this.autorun(function (){ + this.autorun(function (computation){ let userId = this.userId; let creatureCursor creatureCursor = Creatures.find({ @@ -24,7 +24,7 @@ Meteor.publish('singleCharacter', function(creatureId){ let creature = creatureCursor.fetch()[0]; try { assertViewPermission(creature, userId) } catch(e){ return [] } - if (creature.computeVersion !== VERSION){ + if (creature.computeVersion !== VERSION && computation.firstRun){ try { computeCreature(creatureId) } diff --git a/app/imports/ui/properties/forms/AttributeForm.vue b/app/imports/ui/properties/forms/AttributeForm.vue index 23e48e88..0fdcfafe 100644 --- a/app/imports/ui/properties/forms/AttributeForm.vue +++ b/app/imports/ui/properties/forms/AttributeForm.vue @@ -1,18 +1,17 @@ + + diff --git a/app/imports/ui/properties/forms/shared/ComputedField.vue b/app/imports/ui/properties/forms/shared/ComputedField.vue index 51719474..c7d5b8a2 100644 --- a/app/imports/ui/properties/forms/shared/ComputedField.vue +++ b/app/imports/ui/properties/forms/shared/ComputedField.vue @@ -2,9 +2,17 @@
+ > + +
@@ -19,7 +27,7 @@ export default { props: { model: { type: Object, - default: () => {}, + default: () => ({}), }, }, } From cb10b53a1062a4926560038f01c2ee30ba6c7dae Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 28 Sep 2021 14:25:11 +0200 Subject: [PATCH 38/85] Updated forms and some UI for new data structure --- .../tests/computeAttribute.testFn.js | 2 + .../writeAlteredProperties.js | 6 +-- app/imports/api/engine/computeCreature.js | 2 + .../api/engine/oldActions/applyAttack.js | 2 +- .../engine/oldActions/castSpellWithSlot.js | 2 +- app/imports/api/properties/Attacks.js | 2 - .../character/CharacterSheetToolbar.vue | 5 --- .../character/characterSheetTabs/StatsTab.vue | 2 +- .../components/actions/ActionCard.vue | 18 +++----- .../actions/AttributeConsumedView.vue | 2 +- .../components/actions/ItemConsumedView.vue | 11 +++-- .../components/attributes/AbilityListTile.vue | 8 ++-- .../components/attributes/AttributeCard.vue | 4 +- .../components/attributes/AttributeEffect.vue | 15 +++++-- .../components/attributes/HealthBarCard.vue | 4 +- .../components/attributes/HitDiceListTile.vue | 11 ++--- .../components/attributes/ResourceCard.vue | 30 ++++++------- .../attributes/SpellSlotListTile.vue | 13 +++--- .../components/features/FeatureCard.vue | 2 +- .../components/persona/NoteCard.vue | 9 ++-- .../ui/properties/forms/ActionForm.vue | 38 ++++++++--------- .../ui/properties/forms/AdjustmentForm.vue | 7 ++-- .../forms/AttributeConsumedForm.vue | 8 ++-- .../ui/properties/forms/AttributeForm.vue | 21 +++++----- app/imports/ui/properties/forms/BuffForm.vue | 22 +++++----- .../ui/properties/forms/ClassLevelForm.vue | 12 ++---- .../ui/properties/forms/ContainerForm.vue | 11 +++-- .../ui/properties/forms/DamageForm.vue | 8 ++-- .../ui/properties/forms/EffectForm.vue | 9 ++-- .../ui/properties/forms/FeatureForm.vue | 20 ++++----- .../ui/properties/forms/ItemConsumedForm.vue | 7 ++-- app/imports/ui/properties/forms/ItemForm.vue | 11 +++-- app/imports/ui/properties/forms/NoteForm.vue | 20 ++++----- app/imports/ui/properties/forms/RollForm.vue | 10 ++--- .../ui/properties/forms/SavingThrowForm.vue | 9 ++-- app/imports/ui/properties/forms/SkillForm.vue | 21 ++++------ app/imports/ui/properties/forms/SlotForm.vue | 28 ++++++------- app/imports/ui/properties/forms/SpellForm.vue | 10 ++--- .../ui/properties/forms/SpellListForm.vue | 36 +++++++--------- .../ui/properties/forms/ToggleForm.vue | 12 ++---- .../properties/forms/shared/ComputedField.vue | 3 +- .../forms/shared/InlineComputationField.vue | 42 +++++++++++++++++++ .../forms/shared/propertyFormMixin.js | 7 ++++ .../treeNodeViews/AdjustmentTreeNode.vue | 11 +++-- .../treeNodeViews/DamageTreeNode.vue | 2 +- .../treeNodeViews/EffectTreeNode.vue | 3 +- .../ui/properties/viewers/ActionViewer.vue | 32 +++++++------- .../properties/viewers/AdjustmentViewer.vue | 8 ++-- .../ui/properties/viewers/AttributeViewer.vue | 13 +++--- .../ui/properties/viewers/BuffViewer.vue | 8 ++-- .../properties/viewers/ClassLevelViewer.vue | 4 +- .../ui/properties/viewers/SkillViewer.vue | 2 +- 52 files changed, 304 insertions(+), 301 deletions(-) create mode 100644 app/imports/ui/properties/forms/shared/InlineComputationField.vue diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index 2a9e4f3c..9eb5858a 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -7,10 +7,12 @@ export default function(){ const computation = buildComputationFromProps(testProperties); computeCreatureComputation(computation); const prop = id => computation.propsById[id]; + const scope = variableName => computation.scope[variableName]; assert.equal(prop('emptyId').value, 0); assert.equal(prop('noVariableNameId').value, 8); assert.equal(prop('strengthId').value, 12); assert.equal(prop('strengthId').modifier, 1); + assert.equal(scope('strength').modifier, 1); assert.equal(prop('referencesDexId').value, 4); assert.equal(prop('hitDiceId').constitutionMod, 5); assert.equal( diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index 0d7bcbe6..932975e6 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -36,9 +36,6 @@ function addChangedKeysToOp(op, keys, original, changed) { // and compile an operation that sets all those keys for (let key of keys){ if (!isEqual(original[key], changed[key])){ - console.log('not equal: ', key); - console.log(original[key]) - console.log(changed[key]) if (!op) op = newOperation(original._id, changed.type); let value = changed[key]; if (value === undefined){ @@ -86,7 +83,6 @@ function addUnsetOp(op, key){ // compensation without needing to roll back changes, which causes multiple // expensive redraws of the character sheet function writePropertiesSequentially(bulkWriteOps){ - console.log({opsLength: bulkWriteOps.length}); bulkWriteOps.forEach(op => { let updateOneOrMany = op.updateOne || op.updateMany; CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, { @@ -95,7 +91,7 @@ function writePropertiesSequentially(bulkWriteOps){ bypassCollection2: true, }); }); - console.log('finished writing ops'); + if (bulkWriteOps.length) console.log(`Wrote ${bulkWriteOps.length} props`); } // This is more efficient on the database, but significantly less efficient diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index 9d404289..9817f754 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -3,9 +3,11 @@ import computeCreatureComputation from './computation/computeCreatureComputation import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js'; export default function computeCreature(creatureId){ + console.time('Compute creature'); const computation = buildCreatureComputation(creatureId); computeCreatureComputation(computation); writeAlteredProperties(computation); + console.timeEnd('Compute creature'); } // For now just recompute the whole creature, TODO only recompute a single diff --git a/app/imports/api/engine/oldActions/applyAttack.js b/app/imports/api/engine/oldActions/applyAttack.js index e3774041..fb157ec5 100644 --- a/app/imports/api/engine/oldActions/applyAttack.js +++ b/app/imports/api/engine/oldActions/applyAttack.js @@ -9,7 +9,7 @@ export default function applyAttack({ let value = roll(1, 20)[0]; actionContext.attackRoll = {value}; let criticalHitTarget = creature.variables.criticalHitTarget && - creature.variables.criticalHitTarget.currentValue || 20; + creature.variables.criticalHitTarget.value || 20; let criticalHit = value >= criticalHitTarget; if (criticalHit) actionContext.criticalHit = {value: true}; let result = value + prop.rollBonusResult; diff --git a/app/imports/api/engine/oldActions/castSpellWithSlot.js b/app/imports/api/engine/oldActions/castSpellWithSlot.js index 54d4686c..74ef41c3 100644 --- a/app/imports/api/engine/oldActions/castSpellWithSlot.js +++ b/app/imports/api/engine/oldActions/castSpellWithSlot.js @@ -49,7 +49,7 @@ const castSpellWithSlot = new ValidatedMethod({ throw new Meteor.Error('No slot', 'Slot not found to cast spell'); } - if (!slot.currentValue){ + if (!slot.value){ throw new Meteor.Error('No slot', 'Slot depleted'); } diff --git a/app/imports/api/properties/Attacks.js b/app/imports/api/properties/Attacks.js index c0e947d5..ce4a6687 100644 --- a/app/imports/api/properties/Attacks.js +++ b/app/imports/api/properties/Attacks.js @@ -10,7 +10,6 @@ let AttackSchema = new SimpleSchema() // What gets added to the d20 roll rollBonus: { type: 'fieldToCompute', - parseLevel: 'compile', optional: true, defaultValue: 'strength.modifier + proficiencyBonus', }, @@ -36,7 +35,6 @@ const ComputedOnlyAttackSchema = new SimpleSchema() .extend(createPropertySchema({ rollBonus: { type: 'computedOnlyField', - parseLevel: 'compile', optional: true, }, })); diff --git a/app/imports/ui/creature/character/CharacterSheetToolbar.vue b/app/imports/ui/creature/character/CharacterSheetToolbar.vue index d8b4c549..ab087d56 100644 --- a/app/imports/ui/creature/character/CharacterSheetToolbar.vue +++ b/app/imports/ui/creature/character/CharacterSheetToolbar.vue @@ -225,11 +225,6 @@ export default { }, }, meteor: { - $subscribe: { - 'singleCharacter'(){ - return [this.creatureId]; - }, - }, creature(){ return Creatures.findOne(this.creatureId); }, diff --git a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue index 678f55ca..e36938e8 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue @@ -142,7 +142,7 @@ class="resource" > {{ model.actionType }} -
- {{ usesLeft }} uses +
+ {{ model.usesLeft }} uses
@@ -71,9 +71,7 @@ diff --git a/app/imports/ui/properties/viewers/SkillViewer.vue b/app/imports/ui/properties/viewers/SkillViewer.vue index df8be16a..8f78a69b 100644 --- a/app/imports/ui/properties/viewers/SkillViewer.vue +++ b/app/imports/ui/properties/viewers/SkillViewer.vue @@ -222,7 +222,7 @@ export default { let creature = Creatures.findOne(creatureId) return creature && creature.variables.proficiencyBonus && - creature.variables.proficiencyBonus.currentValue; + creature.variables.proficiencyBonus.value; }, }, } From cb1fd38df3b8527b5ef62f968064d534824bad13 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 29 Sep 2021 15:54:14 +0200 Subject: [PATCH 39/85] Optimized some slow parts of the engine. Last low hanging fruit: parsing is slow, cache parsed calculations --- .../creatureProperties/CreatureProperties.js | 4 + .../engine/computation/CreatureComputation.js | 18 +--- .../parseCalculationFields.js | 85 ++++++++----------- .../buildComputation/removeSchemaFields.js | 15 +--- .../computation/buildCreatureComputation.js | 8 +- .../buildCreatureComputation.test.js | 1 + .../computeImplicitVariable.js | 9 +- .../computeComputation/computeCalculations.js | 1 + .../writeAlteredProperties.js | 6 +- app/imports/api/engine/computeCreature.js | 2 - app/imports/api/properties/Actions.js | 10 ++- app/imports/api/properties/Attributes.js | 7 ++ app/imports/api/properties/Buffs.js | 1 + app/imports/api/properties/Classes.js | 2 + app/imports/api/properties/Containers.js | 4 + app/imports/api/properties/Skills.js | 9 ++ app/imports/api/properties/Slots.js | 2 + .../properties/subSchemas/computedField.js | 24 +++++- .../subSchemas/createPropertySchema.js | 5 ++ .../subSchemas/inlineCalculationField.js | 5 ++ app/imports/api/simpleSchemaConfig.js | 29 ++++++- 21 files changed, 151 insertions(+), 96 deletions(-) diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js index 0b2a491f..b70d96d5 100644 --- a/app/imports/api/creature/creatureProperties/CreatureProperties.js +++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js @@ -55,6 +55,7 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ type: Boolean, optional: true, index: 1, + removeBeforeCompute: true, }, // Denormalised flag if this property was made inactive by an inactive // ancestor. True if this property has an inactive ancestor even if this @@ -63,6 +64,7 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ type: Boolean, optional: true, index: 1, + removeBeforeCompute: true, }, // Denormalised flag if this property was made inactive because of its own // state @@ -70,6 +72,7 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ type: Boolean, optional: true, index: 1, + removeBeforeCompute: true, }, // Denormalised flag if this property was made inactive because of a toggle // calculation. Either an ancestor toggle calculation or its own. @@ -77,6 +80,7 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({ type: Boolean, optional: true, index: 1, + removeBeforeCompute: true, }, }); diff --git a/app/imports/api/engine/computation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js index 2192b273..416ef7c7 100644 --- a/app/imports/api/engine/computation/CreatureComputation.js +++ b/app/imports/api/engine/computation/CreatureComputation.js @@ -1,4 +1,4 @@ -import { cloneDeep } from 'lodash'; +import { EJSON } from 'meteor/ejson'; import createGraph from 'ngraph.graph'; export default class CreatureComputation { @@ -6,8 +6,6 @@ export default class CreatureComputation { // Set up fields this.originalPropsById = {}; this.propsById = {}; - this.propsByType = {}; - this.propsByVariableName = {}; this.scope = {}; this.props = properties; this.dependencyGraph = createGraph(); @@ -15,21 +13,11 @@ export default class CreatureComputation { // Store properties for easy access later properties.forEach(prop => { // Store a copy of the unmodified prop - this.originalPropsById[prop._id] = cloneDeep(prop); - + // EJSON clone is ~4x faster than lodash cloneDeep for EJSONable objects + this.originalPropsById[prop._id] = EJSON.clone(prop); // Store by id this.propsById[prop._id] = prop; - // Store by type - this.propsByType[prop.type] ? - this.propsByType[prop.type].push(prop) : - this.propsByType[prop.type] = [prop]; - - // Store by variableName - this.propsByVariableName[prop.variableName] ? - this.propsByVariableName[prop.variableName].push(prop) : - this.propsByVariableName[prop.variableName]= [prop]; - // Store the prop in the dependency graph this.dependencyGraph.addNode(prop._id, prop); }); diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 6dbc90a4..72e698f3 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -11,66 +11,53 @@ export default function parseCalculationFields(prop, schemas){ function discoverInlineCalculationFields(prop, schemas){ // For each key in the schema - schemas[prop.type]._schemaKeys.forEach( key => { + schemas[prop.type].inlineCalculationFields().forEach( calcKey => { // That ends in .inlineCalculations - if (key.slice(-19) === '.inlineCalculations'){ - const inlineCalcKey = key.slice(0, -19); - applyFnToKey(prop, inlineCalcKey, (prop, key) => { - const inlineCalcObj = get(prop, key); - if (!inlineCalcObj) return; - // Store a reference to all the inline calculations - prop._computationDetails.inlineCalculations.push(inlineCalcObj); - // Extract the calculations and store them on the property - let string = inlineCalcObj.text; - if (!string) return; - inlineCalcObj.inlineCalculations = []; - let matches = string.matchAll(INLINE_CALCULATION_REGEX); - for (let match of matches){ - let calculation = match[1]; - inlineCalcObj.inlineCalculations.push({ - calculation, - }); - } - }); - } + applyFnToKey(prop, calcKey, (prop, key) => { + const inlineCalcObj = get(prop, key); + if (!inlineCalcObj) return; + // Store a reference to all the inline calculations + prop._computationDetails.inlineCalculations.push(inlineCalcObj); + // Extract the calculations and store them on the property + let string = inlineCalcObj.text; + if (!string) return; + inlineCalcObj.inlineCalculations = []; + let matches = string.matchAll(INLINE_CALCULATION_REGEX); + for (let match of matches){ + let calculation = match[1]; + inlineCalcObj.inlineCalculations.push({ + calculation, + }); + } + }); }); } function parseAllCalculationFields(prop, schemas){ - // For each key in the schema - schemas[prop.type]._schemaKeys.forEach( key => { - // that ends in '.calculation' - if (key.slice(-12) === '.calculation'){ - const calcKey = key.slice(0, -12); - // Determine the level the calculation should compute down to - let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce'; + // For each computed key in the schema + schemas[prop.type].computedFields().forEach( calcKey => { + // Determine the level the calculation should compute down to + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce'; - // For all fields matching they keys - // supports `keys.$.with.$.arrays` - applyFnToKey(prop, calcKey, (prop, key) => { - const calcObj = get(prop, key); - if (!calcObj) return; - // If the calculation isn't set, delete the whole object - if (!calcObj.calculation){ - unset(prop, key); - return; - } - // Store a reference to all the calculations - prop._computationDetails.calculations.push(calcObj); - // Store the level to compute down to later - calcObj._parseLevel = parseLevel; - // Parse the calculation - parseCalculation(calcObj); - }); - // Or that ends in .inlineCalculations - } + // For all fields matching they keys + // supports `keys.$.with.$.arrays` + applyFnToKey(prop, calcKey, (prop, key) => { + const calcObj = get(prop, key); + if (!calcObj) return; + // Store a reference to all the calculations + prop._computationDetails.calculations.push(calcObj); + // Store the level to compute down to later + calcObj._parseLevel = parseLevel; + // Parse the calculation + parseCalculation(calcObj); + }); }); } function parseCalculation(calcObj){ - let calculation = calcObj.calculation || ''; + if (!calcObj.calculation) return; try { - calcObj._parsedCalculation = parse(calculation); + calcObj._parsedCalculation = parse(calcObj.calculation); } catch (e) { let error = { type: 'evaluation', diff --git a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js index bf0a0af7..9c783df0 100644 --- a/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js +++ b/app/imports/api/engine/computation/buildComputation/removeSchemaFields.js @@ -3,17 +3,8 @@ import { unset } from 'lodash'; export default function removeSchemaFields(schemas, prop){ schemas.forEach(schema => { - schema._schemaKeys.forEach(key => { - // Skip object and array keys, except the errors array - if ( - schema.getQuickTypeForKey(key) === 'object' || - ( - schema.getQuickTypeForKey(key) === 'objectArray' && - key.slice(-6)!== 'errors' - ) - ) return; - // Unset other computed only keys - applyFnToKey(prop, key, unset) - }); + schema.removeBeforeComputeFields().forEach( + key => applyFnToKey(prop, key, unset) + ); }); } diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index d1160ee8..7cb54d30 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -29,14 +29,10 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; * computed toggles */ -/** - * TODO - * compute class levels - */ - export default function buildCreatureComputation(creatureId){ const properties = getProperties(creatureId); - return buildComputationFromProps(properties); + const computation = buildComputationFromProps(properties); + return computation; } function getProperties(creatureId){ diff --git a/app/imports/api/engine/computation/buildCreatureComputation.test.js b/app/imports/api/engine/computation/buildCreatureComputation.test.js index 2df19c08..33b3830b 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.test.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.test.js @@ -1,3 +1,4 @@ +import '/imports/api/simpleSchemaConfig.js'; import { buildComputationFromProps } from './buildCreatureComputation.js'; import { assert } from 'chai'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js index 1ed6e530..c10c164d 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeImplicitVariable.js @@ -7,9 +7,12 @@ import getAggregatorResult from './getAggregatorResult.js'; export default function computeImplicitVariable(node){ const prop = {}; const result = getAggregatorResult(node); - prop.total = result; - prop.value = result; - prop.proficiency = node.data.proficiency; + if (result !== undefined){ + prop.value = result; + } + if (node.data.proficiency !== undefined){ + prop.proficiency = node.data.proficiency; + } // denormalise class level aggregator let classLevelAgg = node.data.classLevelAggregator; diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js index 7cdeb18a..4034b5eb 100644 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ b/app/imports/api/engine/computation/computeComputation/computeCalculations.js @@ -35,6 +35,7 @@ function evaluateCalculation(calculation, scope){ // remove the working fields delete calculation._parseLevel; delete calculation._parsedCalculation; + delete calculation._localScope; } function embedInlineCalculations(inlineCalcObj){ diff --git a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js index 932975e6..cd0372cb 100644 --- a/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js +++ b/app/imports/api/engine/computation/writeComputation/writeAlteredProperties.js @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor' -import { isEqual } from 'lodash'; +import { EJSON } from 'meteor/ejson'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js'; @@ -35,7 +35,7 @@ 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 (!isEqual(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){ @@ -91,7 +91,7 @@ function writePropertiesSequentially(bulkWriteOps){ bypassCollection2: true, }); }); - if (bulkWriteOps.length) console.log(`Wrote ${bulkWriteOps.length} props`); + //if (bulkWriteOps.length) console.log(`Wrote ${bulkWriteOps.length} props`); } // This is more efficient on the database, but significantly less efficient diff --git a/app/imports/api/engine/computeCreature.js b/app/imports/api/engine/computeCreature.js index 9817f754..9d404289 100644 --- a/app/imports/api/engine/computeCreature.js +++ b/app/imports/api/engine/computeCreature.js @@ -3,11 +3,9 @@ import computeCreatureComputation from './computation/computeCreatureComputation import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js'; export default function computeCreature(creatureId){ - console.time('Compute creature'); const computation = buildCreatureComputation(creatureId); computeCreatureComputation(computation); writeAlteredProperties(computation); - console.timeEnd('Compute creature'); } // For now just recompute the whole creature, TODO only recompute a single diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 6fc08695..8f8007d3 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -2,7 +2,6 @@ import SimpleSchema from 'simpl-schema'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; -SimpleSchema.extendOptions(['parseLevel']); /* * Actions are things a character can do @@ -128,6 +127,7 @@ const ComputedOnlyActionSchema = createPropertySchema({ insufficientResources: { type: Boolean, optional: true, + removeBeforeCompute: true, }, uses: { type: 'computedOnlyField', @@ -137,6 +137,7 @@ const ComputedOnlyActionSchema = createPropertySchema({ usesLeft: { type: Number, optional: true, + removeBeforeCompute: true, }, // Resources resources: { @@ -153,6 +154,7 @@ const ComputedOnlyActionSchema = createPropertySchema({ 'resources.itemsConsumed.$.available': { type: Number, optional: true, + removeBeforeCompute: true, }, 'resources.itemsConsumed.$.quantity': { type: 'computedOnlyField', @@ -162,16 +164,19 @@ const ComputedOnlyActionSchema = createPropertySchema({ type: String, max: STORAGE_LIMITS.name, optional: true, + removeBeforeCompute: true, }, 'resources.itemsConsumed.$.itemIcon': { type: storedIconsSchema, optional: true, max: STORAGE_LIMITS.icon, + removeBeforeCompute: true, }, 'resources.itemsConsumed.$.itemColor': { type: String, optional: true, max: STORAGE_LIMITS.color, + removeBeforeCompute: true, }, 'resources.attributesConsumed': { type: Array, @@ -187,16 +192,19 @@ const ComputedOnlyActionSchema = createPropertySchema({ 'resources.attributesConsumed.$.available': { type: Number, optional: true, + removeBeforeCompute: true, }, 'resources.attributesConsumed.$.statId': { type: String, regEx: SimpleSchema.RegEx.Id, optional: true, + removeBeforeCompute: true, }, 'resources.attributesConsumed.$.statName': { type: String, optional: true, max: STORAGE_LIMITS.name, + removeBeforeCompute: true, }, }); diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index a45b5c98..3c57d982 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -94,38 +94,45 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ type: SimpleSchema.oneOf(Number, String, Boolean), defaultValue: 0, optional: true, + removeBeforeCompute: true, }, // The computed value of the attribute minus the damage value: { type: SimpleSchema.oneOf(Number, String, Boolean), defaultValue: 0, optional: true, + removeBeforeCompute: true, }, // The computed modifier, provided the attribute type is `ability` modifier: { type: SimpleSchema.Integer, optional: true, + removeBeforeCompute: true, }, // Attributes with proficiency grant it to all skills based on the attribute proficiency: { type: Number, allowedValues: [0.49, 0.5, 1, 2], optional: true, + removeBeforeCompute: true, }, // The computed creature constitution modifier for hit dice constitutionMod: { type: Number, optional: true, + removeBeforeCompute: true, }, // Should this attribute hide hide: { type: Boolean, optional: true, + removeBeforeCompute: true, }, // Denormalised tag if stat is overridden by one with the same variable name overridden: { type: Boolean, optional: true, + removeBeforeCompute: true, }, }); diff --git a/app/imports/api/properties/Buffs.js b/app/imports/api/properties/Buffs.js index 50b334cd..ff13bd96 100644 --- a/app/imports/api/properties/Buffs.js +++ b/app/imports/api/properties/Buffs.js @@ -47,6 +47,7 @@ let ComputedOnlyBuffSchema = createPropertySchema({ type: Number, optional: true, min: 0, + removeBeforeCompute: true, }, appliedBy: { type: Object, diff --git a/app/imports/api/properties/Classes.js b/app/imports/api/properties/Classes.js index efa5ac17..e2b89e87 100644 --- a/app/imports/api/properties/Classes.js +++ b/app/imports/api/properties/Classes.js @@ -74,10 +74,12 @@ const ComputedOnlyClassSchema = createPropertySchema({ level: { type: SimpleSchema.Integer, optional: true, + removeBeforeCompute: true, }, missingLevels: { type: Array, optional: true, + removeBeforeCompute: true, }, 'missingLevels.$': { type: SimpleSchema.Integer, diff --git a/app/imports/api/properties/Containers.js b/app/imports/api/properties/Containers.js index ca911e67..402a3e83 100644 --- a/app/imports/api/properties/Containers.js +++ b/app/imports/api/properties/Containers.js @@ -43,20 +43,24 @@ const ComputedOnlyContainerSchema = createPropertySchema({ contentsWeight:{ type: Number, optional: true, + removeBeforeCompute: true, }, // Weight of all the carried contents (some sub-containers might not be carried) // zero if `contentsWeightless` is true carriedWeight:{ type: Number, optional: true, + removeBeforeCompute: true, }, contentsValue:{ type: Number, optional: true, + removeBeforeCompute: true, }, carriedValue:{ type: Number, optional: true, + removeBeforeCompute: true, }, }); diff --git a/app/imports/api/properties/Skills.js b/app/imports/api/properties/Skills.js index b53119f3..82a4b165 100644 --- a/app/imports/api/properties/Skills.js +++ b/app/imports/api/properties/Skills.js @@ -66,6 +66,7 @@ let ComputedOnlySkillSchema = createPropertySchema({ type: Number, defaultValue: 0, optional: true, + removeBeforeCompute: true, }, // The result of baseValueCalculation baseValue: { @@ -80,28 +81,33 @@ let ComputedOnlySkillSchema = createPropertySchema({ abilityMod: { type: SimpleSchema.Integer, optional: true, + removeBeforeCompute: true, }, // Computed advantage/disadvantage advantage: { type: SimpleSchema.Integer, optional: true, allowedValues: [-1, 0, 1], + removeBeforeCompute: true, }, // Computed bonus to passive checks passiveBonus: { type: Number, optional: true, + removeBeforeCompute: true, }, // Computed proficiency multiplier proficiency: { type: Number, allowedValues: [0, 0.49, 0.5, 1, 2], defaultValue: 0, + removeBeforeCompute: true, }, // Compiled text of all conditional benefits conditionalBenefits: { type: Array, optional: true, + removeBeforeCompute: true, }, 'conditionalBenefits.$': { type: String, @@ -110,16 +116,19 @@ let ComputedOnlySkillSchema = createPropertySchema({ fail: { type: SimpleSchema.Integer, optional: true, + removeBeforeCompute: true, }, // Should this attribute hide hide: { type: Boolean, optional: true, + removeBeforeCompute: true, }, // Denormalised tag if stat is overridden by one with the same variable name overridden: { type: Boolean, optional: true, + removeBeforeCompute: true, }, }) diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js index 8fb7c986..b19defd3 100644 --- a/app/imports/api/properties/Slots.js +++ b/app/imports/api/properties/Slots.js @@ -105,10 +105,12 @@ const ComputedOnlySlotSchema = createPropertySchema({ totalFilled: { type: SimpleSchema.Integer, defaultValue: 0, + removeBeforeCompute: true, }, spaceLeft: { type: SimpleSchema.Integer, optional: true, + removeBeforeCompute: true, }, }); diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 925912da..28b538c6 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -22,11 +22,13 @@ function computedOnlyField(field){ [`${field}.value`]: { type: SimpleSchema.oneOf(String, Number), optional: true, + removeBeforeCompute: true, }, [`${field}.errors`]: { type: Array, optional: true, maxCount: STORAGE_LIMITS.errorCount, + removeBeforeCompute: true, }, [`${field}.errors.$`]:{ type: ErrorSchema, @@ -40,17 +42,31 @@ function computedOnlyField(field){ function includeParentFields(field, schemaObj){ const splitField = field.split('.'); if (splitField.length === 1){ - schemaObj[field] = {type: Object, optional: true}; + schemaObj[field] = { + type: Object, + optional: true, + computedField: true, + }; return; } let key = ''; splitField.push(''); - splitField.forEach(value => { + splitField.forEach((value, index) => { if (key){ if (value === '$'){ - schemaObj[key] = {type: Array, optional: true}; + schemaObj[key] = { + type: Array, + optional: true + }; } else { - schemaObj[key] = {type: Object, optional: true}; + schemaObj[key] = { + type: Object, + optional: true, + }; + // the last object is the computed field + if (index === splitField.length - 1){ + schemaObj[key].computedField = true; + } } key += '.'; } diff --git a/app/imports/api/properties/subSchemas/createPropertySchema.js b/app/imports/api/properties/subSchemas/createPropertySchema.js index f4e206fe..a866295c 100644 --- a/app/imports/api/properties/subSchemas/createPropertySchema.js +++ b/app/imports/api/properties/subSchemas/createPropertySchema.js @@ -31,6 +31,11 @@ export default function createPropertySchema(definition){ `computed field: '${key}' of '${def.type}' is expected to be optional` ); } + if (def.removeBeforeCompute){ + console.warn( + `computed field: '${key}' of '${def.type}' should not be removed before computation` + ) + } } } diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.js index 50e31224..71b49d50 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.js +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.js @@ -10,6 +10,7 @@ function inlineCalculationFieldToCompute(field){ [field]: { type: Object, optional: true, + inlineCalculationField: true, }, [`${field}.text`]: { type: String, @@ -25,20 +26,24 @@ function computedOnlyInlineCalculationField(field){ [field]: { type: Object, optional: true, + inlineCalculationField: true, }, [`${field}.value`]: { type: String, optional: true, max: STORAGE_LIMITS.inlineCalculationField, + removeBeforeCompute: true, }, [`${field}.inlineCalculations`]: { type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.inlineCalculationCount, + removeBeforeCompute: true, }, [`${field}.inlineCalculations.$`]: { type: Object, parseLevel: 'compile', + computedField: true, }, // The part between bracers {} [`${field}.inlineCalculations.$.calculation`]: { diff --git a/app/imports/api/simpleSchemaConfig.js b/app/imports/api/simpleSchemaConfig.js index d3c4e059..aff36261 100644 --- a/app/imports/api/simpleSchemaConfig.js +++ b/app/imports/api/simpleSchemaConfig.js @@ -1,3 +1,30 @@ import SimpleSchema from 'simpl-schema'; -SimpleSchema.extendOptions(['parseLevel']); +SimpleSchema.extendOptions([ + 'parseLevel', + 'removeBeforeCompute', + 'inlineCalculationField', + 'computedField', +]); + +// Store a quick way of referencing keys that have specific tags === true +function storeTaggedKeys(tag, fnName){ + SimpleSchema.prototype[fnName] = function(){ + if (!this['_' + fnName]){ + this['_' + fnName] = []; + for (const key in this._schema){ + if (this._schema[key][tag]){ + this['_' + fnName].push(key); + } + } + } + return this['_' + fnName]; + } +} + +// Keys that should be deleted at the start of a computation +storeTaggedKeys('removeBeforeCompute', 'removeBeforeComputeFields'); +// Keys that represent inline calculation objects +storeTaggedKeys('inlineCalculationField', 'inlineCalculationFields'); +// Keys that represent computed field objects +storeTaggedKeys('computedField', 'computedFields'); From 7dac0cf3f3f4ab67377a123d480a2f6d4eb54e0d Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 29 Sep 2021 18:32:12 +0200 Subject: [PATCH 40/85] Removed color max length, using regex instead --- app/imports/api/properties/Actions.js | 2 +- app/imports/api/properties/subSchemas/ItemConsumedSchema.js | 2 +- app/imports/constants/STORAGE_LIMITS.js | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 8f8007d3..caa3c51e 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -175,7 +175,7 @@ const ComputedOnlyActionSchema = createPropertySchema({ 'resources.itemsConsumed.$.itemColor': { type: String, optional: true, - max: STORAGE_LIMITS.color, + regEx: /^#([a-f0-9]{3}){1,2}\b$/i, removeBeforeCompute: true, }, 'resources.attributesConsumed': { diff --git a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js index 1bf08904..2290649d 100644 --- a/app/imports/api/properties/subSchemas/ItemConsumedSchema.js +++ b/app/imports/api/properties/subSchemas/ItemConsumedSchema.js @@ -49,7 +49,7 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({ itemColor: { type: String, optional: true, - max: STORAGE_LIMITS.color, + regEx: /^#([a-f0-9]{3}){1,2}\b$/i, }, }); diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js index 11cc55d0..f2bd1e1f 100644 --- a/app/imports/constants/STORAGE_LIMITS.js +++ b/app/imports/constants/STORAGE_LIMITS.js @@ -2,7 +2,6 @@ const STORAGE_LIMITS = Object.freeze({ // String lengths calculation: 1024, collectionName: 64, - color: 10000, description: 49473, //the length of the Bee Movie script inlineCalculationField: 49473, errorMessage: 256, From feffa45cf7eb32c7f541e6972b4ef94e1c5f6478 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 1 Oct 2021 13:41:22 +0200 Subject: [PATCH 41/85] Began work on rewriting parser without object orientation Parsing is expensive, if the parse tree can be stored on the DB it can save a lot of compute time, but mongo can't store Classes, so we re-write without classes --- .../api/engine/computation/utility/collate.js | 12 ++ app/imports/parser/ResolveContext.js | 20 +++ app/imports/parser/compute.js | 1 - app/imports/parser/parseTree/AccessorNode.js | 54 ------- app/imports/parser/parseTree/ArrayNode.js | 39 ----- app/imports/parser/parseTree/CallNode.js | 126 --------------- app/imports/parser/parseTree/ConstantNode.js | 22 --- app/imports/parser/parseTree/ErrorNode.js | 21 --- app/imports/parser/parseTree/IfNode.js | 42 ----- app/imports/parser/parseTree/_index.js | 5 + app/imports/parser/parseTree/accessor.js | 66 ++++++++ app/imports/parser/parseTree/array.js | 46 ++++++ app/imports/parser/parseTree/call.js | 146 ++++++++++++++++++ app/imports/parser/parseTree/constant.js | 18 +++ app/imports/parser/parseTree/error.js | 17 ++ app/imports/parser/parseTree/if.js | 59 +++++++ app/imports/parser/parseTree/index.js | 76 +++++++++ app/imports/parser/parser.test.js | 11 ++ app/imports/parser/resolve.js | 45 ++++++ 19 files changed, 521 insertions(+), 305 deletions(-) create mode 100644 app/imports/api/engine/computation/utility/collate.js create mode 100644 app/imports/parser/ResolveContext.js delete mode 100644 app/imports/parser/compute.js delete mode 100644 app/imports/parser/parseTree/AccessorNode.js delete mode 100644 app/imports/parser/parseTree/ArrayNode.js delete mode 100644 app/imports/parser/parseTree/CallNode.js delete mode 100644 app/imports/parser/parseTree/ConstantNode.js delete mode 100644 app/imports/parser/parseTree/ErrorNode.js delete mode 100644 app/imports/parser/parseTree/IfNode.js create mode 100644 app/imports/parser/parseTree/_index.js create mode 100644 app/imports/parser/parseTree/accessor.js create mode 100644 app/imports/parser/parseTree/array.js create mode 100644 app/imports/parser/parseTree/call.js create mode 100644 app/imports/parser/parseTree/constant.js create mode 100644 app/imports/parser/parseTree/error.js create mode 100644 app/imports/parser/parseTree/if.js create mode 100644 app/imports/parser/parseTree/index.js create mode 100644 app/imports/parser/parser.test.js create mode 100644 app/imports/parser/resolve.js diff --git a/app/imports/api/engine/computation/utility/collate.js b/app/imports/api/engine/computation/utility/collate.js new file mode 100644 index 00000000..d6599d65 --- /dev/null +++ b/app/imports/api/engine/computation/utility/collate.js @@ -0,0 +1,12 @@ +// Collate the array with the given value or array of values, creating the +// array if it doesn't exist +export default function collate(array, toAdd){ + if (Array.isArray(toAdd) && toAdd.length){ + if (!array) array = []; + array.push(...toAdd); + } else if (toAdd) { + if (!array) array = []; + array.push(toAdd); + } + return array; +} diff --git a/app/imports/parser/ResolveContext.js b/app/imports/parser/ResolveContext.js new file mode 100644 index 00000000..3edd2fa8 --- /dev/null +++ b/app/imports/parser/ResolveContext.js @@ -0,0 +1,20 @@ +export default class Context { + constructor({errors = [], rolls = []}){ + this.errors = errors; + this.rolls = rolls; + } + error(e){ + if (!e) return; + if (typeof e === 'string'){ + this.errors.push({ + type: 'error', + message: e, + }); + } else { + this.errors.push(e); + } + } + roll(r){ + this.rolls.push(r); + } +} diff --git a/app/imports/parser/compute.js b/app/imports/parser/compute.js deleted file mode 100644 index 5386218b..00000000 --- a/app/imports/parser/compute.js +++ /dev/null @@ -1 +0,0 @@ -// Takes a parse tree and computes it down as far as possible into a real number diff --git a/app/imports/parser/parseTree/AccessorNode.js b/app/imports/parser/parseTree/AccessorNode.js deleted file mode 100644 index ad2d7cad..00000000 --- a/app/imports/parser/parseTree/AccessorNode.js +++ /dev/null @@ -1,54 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class AccessorNode extends ParseNode { - constructor({name, path}) { - super(...arguments); - this.name = name; - this.path = path; - } - compile(scope, context){ - let value = scope && scope[this.name]; - // For objects, get their value - this.path.forEach(name => { - if (value === undefined) return; - value = value[name]; - }); - let type = typeof value; - if (type === 'string' || type === 'number' || type === 'boolean'){ - return new ConstantNode({value, type}); - } else if (type === 'undefined'){ - return new AccessorNode({ - name: this.name, - path: this.path, - }); - } else { - if (context) context.storeError({ - type: 'error', - message: `${this.name} returned an unexpected type` - }); - return new AccessorNode({ - name: this.name, - path: this.path, - }); - } - } - reduce(scope, context){ - let result = this.compile(scope, context); - if (result instanceof AccessorNode){ - if (context) context.storeError({ - type: 'info', - message: `${result.toString()} not found, set to 0` - }); - return new ConstantNode({ - type: 'number', - value: 0, - }); - } else { - return result; - } - } - toString(){ - return `${this.name}.${this.path.join('.')}`; - } -} diff --git a/app/imports/parser/parseTree/ArrayNode.js b/app/imports/parser/parseTree/ArrayNode.js deleted file mode 100644 index 2f31fa69..00000000 --- a/app/imports/parser/parseTree/ArrayNode.js +++ /dev/null @@ -1,39 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class ArrayNode extends ParseNode { - constructor({values}) { - super(...arguments); - this.values = values; - } - static fromConstantArray(array){ - let values = array.map( value => { - let type = typeof value; - if ( - type === 'string' || - type === 'number' || - type === 'boolean' || - type === 'undefined' - ){ - return new ConstantNode({value, type}); - } else { - throw `Unexpected type in constant array: ${type}` - } - }); - return new ArrayNode({values}); - } - resolve(fn, scope, context){ - let values = this.values.map(node => node[fn](scope, context)); - return new ArrayNode({values}); - } - toString(){ - return `[${this.values.map(node => node.toString()).join(', ')}]`; - } - traverse(fn){ - fn(this); - this.values.forEach(value => value.traverse(fn)); - } - replaceChildren(fn){ - this.values = this.values.map(node => node.replaceNodes(fn)); - } -} diff --git a/app/imports/parser/parseTree/CallNode.js b/app/imports/parser/parseTree/CallNode.js deleted file mode 100644 index 59c9e9ff..00000000 --- a/app/imports/parser/parseTree/CallNode.js +++ /dev/null @@ -1,126 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -import functions from '/imports/parser/functions.js'; - -export default class CallNode extends ParseNode { - constructor({functionName, args}) { - super(...arguments); - this.functionName = functionName; - this.args = args; - } - resolve(fn, scope, context){ - let func = functions[this.functionName]; - // Check that the function exists - if (!func) return new ErrorNode({ - node: this, - error: `${this.functionName} is not a supported function`, - context, - }); - - // Resolve the arguments - let resolvedArgs = this.args.map(node => node[fn](scope, context)); - // Check that the arguments match what is expected - let checkFailed = this.checkArugments({ - fn, - context, - resolvedArgs, - argumentsExpected: func.arguments - }); - - if (checkFailed){ - if (fn === 'reduce'){ - return new ErrorNode({ - node: this, - error: `Invalid arguments to ${this.functionName} function`, - }); - } else { - return new CallNode({ - functionName: this.functionName, - args: resolvedArgs, - }); - } - } - - // Map contant nodes to constants before attempting to run the function - let mappedArgs = resolvedArgs.map(node => { - if (node instanceof ConstantNode){ - return node.value; - } else { - return node; - } - }); - - try { - // Run the function - let value = func.fn.apply(null, mappedArgs); - - let type = typeof value; - if (type === 'number' || type === 'string' || type === 'boolean'){ - // Convert constant results into constant nodes - return new ConstantNode({ value, type }); - } else { - return value; - } - } catch (error) { - return new ErrorNode({ - node: this, - error: error.message || error, - context, - }); - } - } - toString(){ - return `${this.functionName}(${this.args.map(node => node.toString()).join(', ')})`; - } - traverse(fn){ - fn(this); - this.args.forEach(arg => arg.traverse(fn)); - } - replaceChildren(fn){ - this.args = this.args.map(arg => arg.replaceNodes(fn)); - } - checkArugments({fn, context, argumentsExpected, resolvedArgs}){ - // Check that the number of arguments matches the number expected - if ( - !argumentsExpected.anyLength && - argumentsExpected.length !== resolvedArgs.length - ){ - context.storeError({ - type: 'error', - message: 'Incorrect number of arguments ' + - `to ${this.functionName} function, ` + - `expected ${argumentsExpected.length} got ${resolvedArgs.length}` - }); - return true; - } - - let failed = false; - // Check that each argument is of the correct type - resolvedArgs.forEach((node, index) => { - let type; - if (argumentsExpected.anyLength){ - type = argumentsExpected[0]; - } else { - type = argumentsExpected[index]; - } - if (typeof type === 'string'){ - // Type being a string means a constant node with matching type - if (node.type !== type) failed = true; - } else { - // Otherwise check that the node is an instance of the given type - if (!(node instanceof type)) failed = true; - } - if (failed && fn === 'reduce'){ - let typeName = typeof type === 'string' ? type : type.constructor.name; - let nodeName = node.type || node.constructor.name - context.storeError({ - type: 'error', - message: `Incorrect arguments to ${this.functionName} function` + - `expected ${typeName} got ${nodeName}` - }); - } - }); - return failed; - } -} diff --git a/app/imports/parser/parseTree/ConstantNode.js b/app/imports/parser/parseTree/ConstantNode.js deleted file mode 100644 index 708bfaba..00000000 --- a/app/imports/parser/parseTree/ConstantNode.js +++ /dev/null @@ -1,22 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; - -export default class ConstantNode extends ParseNode { - constructor({value, type}){ - super(...arguments); - // string, number, boolean, uncompiledNode - this.type = type; - this.value = value; - } - compile(){ - return this; - } - toString(){ - return `${this.value}`; - } - get isNumber(){ - return this.type === 'number'; - } - get isInteger(){ - return this.type === 'number' && Number.isInteger(this.value); - } -} diff --git a/app/imports/parser/parseTree/ErrorNode.js b/app/imports/parser/parseTree/ErrorNode.js deleted file mode 100644 index ff9565ee..00000000 --- a/app/imports/parser/parseTree/ErrorNode.js +++ /dev/null @@ -1,21 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; - -export default class ErrorNode extends ParseNode { - constructor({node, error, context}) { - super(...arguments); - this.node = node; - this.error = error; - if (context){ - context.storeError({ - type: 'error', - message: error, - }); - } - } - compile(){ - return this; - } - toString(){ - return this.error.toString(); - } -} diff --git a/app/imports/parser/parseTree/IfNode.js b/app/imports/parser/parseTree/IfNode.js deleted file mode 100644 index a94f21c2..00000000 --- a/app/imports/parser/parseTree/IfNode.js +++ /dev/null @@ -1,42 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class IfNode extends ParseNode { - constructor({condition, consequent, alternative}){ - super(...arguments); - this.condition = condition; - this.consequent = consequent; - this.alternative = alternative; - } - toString(){ - let {condition, consequent, alternative} = this; - return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}` - } - resolve(fn, scope, context){ - let condition = this.condition[fn](scope, context); - if (condition instanceof ConstantNode){ - if (condition.value){ - return this.consequent[fn](scope, context); - } else { - return this.alternative[fn](scope, context); - } - } else { - return new IfNode({ - condition: condition, - consequent: this.consequent, - alternative: this.alternative, - }); - } - } - traverse(fn){ - fn(this); - this.condition.traverse(fn); - this.consequent.traverse(fn); - this.alternative.traverse(fn); - } - replaceChildren(fn){ - this.condition = this.condition.replaceNodes(fn); - this.consequent = this.consequent.replaceNodes(fn); - this.alternative = this.alternative.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/_index.js b/app/imports/parser/parseTree/_index.js new file mode 100644 index 00000000..571f7e7a --- /dev/null +++ b/app/imports/parser/parseTree/_index.js @@ -0,0 +1,5 @@ +import accessor from './accessor.js'; + +export default Object.freeze({ + accessor, +}); diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js new file mode 100644 index 00000000..5ebbf246 --- /dev/null +++ b/app/imports/parser/parseTree/accessor.js @@ -0,0 +1,66 @@ +import constant from './constant.js'; + +const accessor = { + create({name, path}) { + return { + type: 'accessor', + path, + name, + }; + }, + compile(node, scope, context){ + let value = scope && scope[node.name]; + // For objects, get their value + node.path.forEach(name => { + if (value === undefined) return; + value = value[name]; + }); + let valueType = typeof value; + if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){ + return { + result: constant.create({ + value, + valueType + }), + context, + }; + } else if (valueType === 'undefined'){ + return { + result: accessor.create({ + name: node.name, + path: node.path, + }), + context, + }; + } else { + context.error(`${node.name} returned an unexpected type`); + return { + result: accessor.create({ + name: node.name, + path: node.path, + }), + context, + }; + } + }, + reduce(node, scope, context){ + let { result } = accessor.compile(node, scope, context); + if (result.type === 'accessor'){ + context.error(`${accessor.toString(result)} not found, set to 0`); + return { + result: constant.create({ + value: 0, + valueType: 'number', + }), + context + }; + } else { + return {result, context}; + } + }, + toString(node){ + return `${node.name}.${node.path.join('.')}`; + } +} + +export default accessor; diff --git a/app/imports/parser/parseTree/array.js b/app/imports/parser/parseTree/array.js new file mode 100644 index 00000000..407ab92a --- /dev/null +++ b/app/imports/parser/parseTree/array.js @@ -0,0 +1,46 @@ +import constant from './constant.js'; +import resolve, { toString, traverse } from '../resolve.js'; + +const array = { + create({values}) { + return { + type: 'array', + values, + }; + }, + fromConstantArray(array){ + let values = array.map( value => { + let valueType = typeof value; + if ( + valueType === 'string' || + valueType === 'number' || + valueType === 'boolean' || + valueType === 'undefined' + ){ + return constant.create({value, valueType}); + } else { + throw `Unexpected type in constant array: ${valueType}` + } + }); + return array.create({values}); + }, + resolve(fn, node, scope){ + let values = node.values.map(node => { + let { result } = resolve(fn, node, scope, context); + return result; + }); + return { + result: array.create({values}), + context, + }; + }, + toString(node){ + return `[${node.values.map(value => toString(value)).join(', ')}]`; + }, + traverse(node, fn){ + fn(node); + node.values.forEach(value => traverse(value, fn)); + }, +} + +export default array; diff --git a/app/imports/parser/parseTree/call.js b/app/imports/parser/parseTree/call.js new file mode 100644 index 00000000..6893dbf4 --- /dev/null +++ b/app/imports/parser/parseTree/call.js @@ -0,0 +1,146 @@ +import error from './error.js'; +import constant from './constant.js'; +import functions from '/imports/parser/functions.js'; +import resolve, { toString, traverse, mergeResolvedNodes } from '../resolve.js'; + +const call = { + create({functionName, args}) { + return { + type: 'call', + functionName, + args, + } + }, + resolve(fn, node, scope, context){ + let func = functions[node.functionName]; + // Check that the function exists + if (!func) { + context.error(`${node.functionName} is not a supported function`); + return { + result: error.create({ + node: node, + error: `${node.functionName} is not a supported function`, + }), + context, + }; + } + + // Resolve the arguments + let resolvedArgs = node.args.map(arg => { + let { result } = resolve(fn, arg, scope, context); + return result; + }); + + // Check that the arguments match what is expected + let checkFailed = call.checkArugments({ + fn, + resolvedArgs, + argumentsExpected: func.arguments, + context, + }); + + if (checkFailed){ + if (fn === 'reduce'){ + context.error(`Invalid arguments to ${node.functionName} function`); + return { + result: error.create({ + node: node, + error: `Invalid arguments to ${node.functionName} function`, + }), + context, + }; + } else { + return { + result: call.create({ + functionName: node.functionName, + args: resolvedArgs, + }), + context, + }; + } + } + + // Map contant nodes to constants before attempting to run the function + let mappedArgs = resolvedArgs.map(arg => { + if (arg.type === 'constant'){ + return arg.value; + } else { + return arg; + } + }); + + try { + // Run the function + let value = func.fn.apply(null, mappedArgs); + + let valueType = typeof value; + if (valueType === 'number' || valueType === 'string' || valueType === 'boolean'){ + // Convert constant results into constant nodes + return { + result: constant.create({ value, valueType }), + context, + }; + } else { + return { + result: value, + context, + }; + } + } catch (error) { + context.error(error.message || error); + return { + result: error.create({ + node: node, + error: error.message || error, + }), + context, + } + } + }, + toString(node){ + return `${node.functionName}(${node.args.map(arg => toString(arg)).join(', ')})`; + }, + traverse(node, fn){ + fn(node); + node.args.forEach(arg => traverse(arg, fn)); + }, + checkArugments({node, fn, argumentsExpected, resolvedArgs, context}){ + // Check that the number of arguments matches the number expected + if ( + !argumentsExpected.anyLength && + argumentsExpected.length !== resolvedArgs.length + ){ + context.error('Incorrect number of arguments ' + + `to ${node.functionName} function, ` + + `expected ${argumentsExpected.length} got ${resolvedArgs.length}`); + return true; + } + + let failed = false; + // Check that each argument is of the correct type + resolvedArgs.forEach((node, index) => { + let type; + if (argumentsExpected.anyLength){ + type = argumentsExpected[0]; + } else { + type = argumentsExpected[index]; + } + if (typeof type === 'string'){ + // Type being a string means a constant node with matching type + if (node.valueType !== type) failed = true; + } else { + // Otherwise check that the node is an instance of the given type + if (!(node instanceof type)) failed = true; + } + if (failed && fn === 'reduce'){ + let typeName = typeof type === 'string' ? type : type.constructor.name; + let nodeName = node.type; + context.error(`Incorrect arguments to ${node.functionName} function` + + `expected ${typeName} got ${nodeName}`); + } + }); + return failed; + } +} + +export default call; diff --git a/app/imports/parser/parseTree/constant.js b/app/imports/parser/parseTree/constant.js new file mode 100644 index 00000000..4220bc96 --- /dev/null +++ b/app/imports/parser/parseTree/constant.js @@ -0,0 +1,18 @@ +const constant = { + create({value, valueType}){ + if (!valueType) throw `Expected valueType to be set, got ${valueType}` + return { + type: 'constant', + valueType, + value, + } + }, + compile(node){ + return node; + }, + toString(node){ + return `${node.value}`; + }, +} + +export default constant; diff --git a/app/imports/parser/parseTree/error.js b/app/imports/parser/parseTree/error.js new file mode 100644 index 00000000..8533061b --- /dev/null +++ b/app/imports/parser/parseTree/error.js @@ -0,0 +1,17 @@ +const error = { + create({node, error}) { + return { + type: 'error', + node, + error, + } + }, + compile(node){ + return node; + }, + toString(node){ + return node.error.toString(); + }, +} + +export default error; diff --git a/app/imports/parser/parseTree/if.js b/app/imports/parser/parseTree/if.js new file mode 100644 index 00000000..369ff924 --- /dev/null +++ b/app/imports/parser/parseTree/if.js @@ -0,0 +1,59 @@ +import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve'; +import collate from '/imports/api/engine/computation/utility/collate.js'; + +const ifNode = { + create({condition, consequent, alternative}){ + return { + type: 'if', + condition, + consequent, + alternative, + }; + }, + toString(node){ + let {condition, consequent, alternative} = node; + return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}` + }, + resolve(fn, node, scope){ + let rest, condition, consequent, alternative; + let resolved = {}; + + ({result: condition, ...rest} = resolve(fn, node.condition, scope)); + mergeResolvedNodes(resolved, rest); + + if (condition.type === 'constant'){ + if (condition.value){ + ({result: consequent, ...rest} = resolve(fn, node.consequent, scope)); + mergeResolvedNodes(resolved, rest); + return { + result: consequent, + ...resolved + }; + } else { + ({result: alternative, ...rest} = resolve(fn, node.alternative, scope)); + mergeResolvedNodes(resolved, rest); + return { + result: alternative, + ...resolved + }; + } + } else { + return { + result: ifNode.create({ + condition: condition, + consequent: node.consequent, + alternative: node.alternative, + }), + ...resolved + }; + } + }, + traverse(node, fn){ + fn(node); + traverse(node.condition, fn); + traverse(node.consequent, fn); + traverse(node.alternative, fn); + }, +} + +export default ifNode; diff --git a/app/imports/parser/parseTree/index.js b/app/imports/parser/parseTree/index.js new file mode 100644 index 00000000..dcb19e2d --- /dev/null +++ b/app/imports/parser/parseTree/index.js @@ -0,0 +1,76 @@ +import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve'; + +const index = { + create({array, index}) { + return { + type: 'index', + array, + index, + } + }, + resolve(fn, node, scope){ + let index, array, rest; + let resolved = {}; + ({result: index, ...rest} = resolve(fn, node.index, scope)); + mergeResolvedNodes(resolved, rest); + ({result: array, ...rest} = resolve(fn, node.array, scope)); + mergeResolvedNodes(resolved, rest); + + if ( + index.valueType === 'number' && + Number.isInteger(index.value) && + array.type === 'array' + ){ + if (index.value < 1 || index.value > array.values.length){ + mergeResolvedNodes(resolved, { + errors: [{ + type: 'warning', + message: `Index of ${index.value} is out of range for an array` + + ` of length ${array.values.length}`, + }] + }); + } + let selection = array.values[index.value - 1]; + if (selection){ + let result; + ({result, ...rest} = resolve(fn, selection, scope)); + mergeResolvedNodes(resolved, rest) + return result; + } + } else if (fn === 'reduce'){ + if (!(array instanceof ArrayNode)){ + return new ErrorNode({ + node: node, + error: 'Can not get the index of a non-array node: ' + + node.array.toString() + ' = ' + array.toString(), + context, + }); + } else if (!index.isInteger){ + return new ErrorNode({ + node: node, + error: array.toString() + ' is not an integer index of the array', + context, + }); + } + } + return new IndexNode({ + index, + array, + previousNodes: [node], + }); + }, + toString(){ + return `${node.array.toString()}[${node.index.toString()}]`; + }, + traverse(fn){ + fn(node); + node.array.traverse(fn); + node.index.traverse(fn); + }, + replaceChildren(fn){ + node.array = node.array.replaceNodes(fn); + node.index = node.index.replaceNodes(fn); + } +} + +export default index; diff --git a/app/imports/parser/parser.test.js b/app/imports/parser/parser.test.js new file mode 100644 index 00000000..30f0b0b9 --- /dev/null +++ b/app/imports/parser/parser.test.js @@ -0,0 +1,11 @@ +import { parse } from './parser'; +import { assert } from 'chai'; + +describe('Parser', function(){ + it('parses valid text without error', function(){ + assert.typeOf(parse('1'), 'object'); + }); + it('parses various operations', function(){ + assert.typeOf(parse('1 + 2 * 3 / 4 * 1d8'), 'object'); + }); +}); diff --git a/app/imports/parser/resolve.js b/app/imports/parser/resolve.js new file mode 100644 index 00000000..86d2fad6 --- /dev/null +++ b/app/imports/parser/resolve.js @@ -0,0 +1,45 @@ +import nodeTypeIndex from './parseTree/index.js'; +import collate from '/imports/api/engine/computation/utility/collate.js'; +import Context from './ResolveContext.js'; + +// Takes a parse ndoe and computes it to a set detail level +// returns {result, context} +export default function resolve(fn, node, scope, context = new Context()){ + let type = nodeTypeIndex[node.type]; + if (!type){ + throw new Meteor.Error(`Parse node type: ${node.type} not implemented`); + } + if (type.resolve){ + return type.resolve(fn, node, scope, context); + } else if (type[fn]) { + return type[fn](node, scope, context); + } else if (fn === 'reduce' && type.roll) { + return type.roll(node, scope, context) + } else if (type.compile){ + return type.compile(node, scope, context) + } else { + throw new Meteor.Error('Compile not implemented on ' + node.type); + } +} + +export function toString(node){ + let type = nodeTypeIndex[node.type]; + if (!type.toString){ + throw new Meteor.Error('toString not implemented on ' + node.type); + } + return type.toString(node); +} + +export function traverse(node, fn){ + let type = nodeTypeIndex[node.type]; + if (type.traverse){ + return type.traverse(node, fn); + } + return fn(node); +} + +export function mergeResolvedNodes(main, other){ + main.errors = collate(main.errors, other.errors); + main.rolls = collate(main.rolls, other.rolls); + return main; +} From b78517b61fa6b21f08f03bac4abc260a0804e337 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sun, 3 Oct 2021 13:54:17 +0200 Subject: [PATCH 42/85] Finished migrating parser to be object orientation free. All tests pass --- .../linkCalculationDependencies.js | 7 +- .../parseCalculationFields.js | 6 +- .../computeComputation/computeCalculations.js | 13 ++- app/imports/api/properties/Constants.js | 20 ++--- app/imports/parser/ResolveContext.js | 20 ----- app/imports/parser/TextField.vue | 38 +++++++++ app/imports/parser/functions.js | 4 +- app/imports/parser/grammar.js | 47 ++++------ app/imports/parser/grammar.ne | 45 ++++------ app/imports/parser/parseTree/IndexNode.js | 64 -------------- .../parser/parseTree/NotOperatorNode.js | 34 -------- app/imports/parser/parseTree/OperatorNode.js | 71 ---------------- .../parser/parseTree/ParenthesisNode.js | 30 ------- app/imports/parser/parseTree/ParseNode.js | 43 ---------- app/imports/parser/parseTree/RollArrayNode.js | 22 ----- app/imports/parser/parseTree/RollNode.js | 70 --------------- app/imports/parser/parseTree/SymbolNode.js | 53 ------------ .../parser/parseTree/UnaryOperatorNode.js | 40 --------- app/imports/parser/parseTree/_index.js | 26 ++++++ app/imports/parser/parseTree/accessor.js | 5 +- app/imports/parser/parseTree/array.js | 6 +- app/imports/parser/parseTree/call.js | 16 ++-- app/imports/parser/parseTree/constant.js | 11 ++- app/imports/parser/parseTree/error.js | 6 +- app/imports/parser/parseTree/if.js | 31 ++----- app/imports/parser/parseTree/index.js | 85 +++++++++---------- app/imports/parser/parseTree/not.js | 37 ++++++++ app/imports/parser/parseTree/operator.js | 76 +++++++++++++++++ app/imports/parser/parseTree/parenthesis.js | 34 ++++++++ app/imports/parser/parseTree/roll.js | 82 ++++++++++++++++++ app/imports/parser/parseTree/rollArray.js | 35 ++++++++ app/imports/parser/parseTree/symbol.js | 61 +++++++++++++ app/imports/parser/parseTree/unaryOperator.js | 46 ++++++++++ app/imports/parser/parser.js | 14 --- app/imports/parser/resolve.js | 45 +++++++--- 35 files changed, 589 insertions(+), 654 deletions(-) delete mode 100644 app/imports/parser/ResolveContext.js create mode 100644 app/imports/parser/TextField.vue delete mode 100644 app/imports/parser/parseTree/IndexNode.js delete mode 100644 app/imports/parser/parseTree/NotOperatorNode.js delete mode 100644 app/imports/parser/parseTree/OperatorNode.js delete mode 100644 app/imports/parser/parseTree/ParenthesisNode.js delete mode 100644 app/imports/parser/parseTree/ParseNode.js delete mode 100644 app/imports/parser/parseTree/RollArrayNode.js delete mode 100644 app/imports/parser/parseTree/RollNode.js delete mode 100644 app/imports/parser/parseTree/SymbolNode.js delete mode 100644 app/imports/parser/parseTree/UnaryOperatorNode.js create mode 100644 app/imports/parser/parseTree/not.js create mode 100644 app/imports/parser/parseTree/operator.js create mode 100644 app/imports/parser/parseTree/parenthesis.js create mode 100644 app/imports/parser/parseTree/roll.js create mode 100644 app/imports/parser/parseTree/rollArray.js create mode 100644 app/imports/parser/parseTree/symbol.js create mode 100644 app/imports/parser/parseTree/unaryOperator.js diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 69f747ee..0e91f833 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -1,6 +1,5 @@ -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; -import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js'; +import { traverse } from '/imports/parser/resolve.js'; export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { @@ -9,9 +8,9 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop // ancestors: {} //this gets added if there are resolved ancestors }; // Traverse the parsed calculation looking for variable names - calcObj._parsedCalculation.traverse(node => { + traverse(calcObj._parsedCalculation, node => { // Skip nodes that aren't symbols or accessors - if (!(node instanceof SymbolNode || node instanceof AccessorNode)) return; + if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return; // Link ancestor references as direct property dependencies if (node.name[0] === '#'){ let ancestorProp = getAncestorProp( diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 72e698f3..1d824e85 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,8 +1,8 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import { prettifyParseError, parse } from '/imports/parser/parser.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { get, unset } from 'lodash'; +import { get } from 'lodash'; +import errorNode from '/imports/parser/parseTree/error.js' export default function parseCalculationFields(prop, schemas){ discoverInlineCalculationFields(prop, schemas); @@ -66,6 +66,6 @@ function parseCalculation(calcObj){ calcObj.errors ? calcObj.errors.push(error) : calcObj.errors = [error]; - calcObj._parsedCalculation = new ErrorNode({error}); + calcObj._parsedCalculation = errorNode.create({error}); } } diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js index 4034b5eb..ccbaf79f 100644 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ b/app/imports/api/engine/computation/computeComputation/computeCalculations.js @@ -1,7 +1,5 @@ -import { CompilationContext } from '/imports/parser/parser.js'; +import resolve, { toString } from '/imports/parser/resolve.js'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; export default function computeCalculations(computation, node){ if (!node.data) return; @@ -15,17 +13,16 @@ export default function computeCalculations(computation, node){ } function evaluateCalculation(calculation, scope){ - const context = new CompilationContext(); const parseNode = calculation._parsedCalculation; const fn = calculation._parseLevel; const calculationScope = {...calculation._localScope, ...scope}; - const resultNode = parseNode[fn](calculationScope, context); - if (resultNode instanceof ConstantNode){ + const {result: resultNode, context} = resolve(fn, parseNode, calculationScope); + if (resultNode.parseType === 'constant'){ calculation.value = resultNode.value; - } else if (resultNode instanceof ErrorNode){ + } else if (resultNode.parseType === 'error'){ calculation.value = null; } else { - calculation.value = resultNode.toString(); + calculation.value = toString(resultNode); } if (calculation.errors){ calculation.errors = [...calculation.errors, ...context.errors] diff --git a/app/imports/api/properties/Constants.js b/app/imports/api/properties/Constants.js index 4b1f382f..0995ccd9 100644 --- a/app/imports/api/properties/Constants.js +++ b/app/imports/api/properties/Constants.js @@ -3,12 +3,10 @@ import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import { parse, - CompilationContext, prettifyParseError, } from '/imports/parser/parser.js'; -import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import resolve, { Context, traverse } from '/imports/parser/resolve.js'; /* * Constants are primitive values that can be used elsewhere in computations @@ -50,12 +48,9 @@ let ConstantSchema = new SimpleSchema({ // Any existing errors will result in an early failure if (context && context.errors.length) return context.errors; // Ban variables in constants if necessary - result && result.traverse(node => { - if (node instanceof SymbolNode || node instanceof AccessorNode){ - context.storeError({ - type: 'error', - message: 'Variables can\'t be used to define a constant' - }); + result && traverse(result, node => { + if (node.parseType === 'symbol' || node.parseType === 'accessor'){ + context.error('Variables can\'t be used to define a constant'); } }); return context && context.errors || []; @@ -67,7 +62,7 @@ let ConstantSchema = new SimpleSchema({ }); function parseString(string, fn = 'compile'){ - let context = new CompilationContext(); + let context = new Context(); if (!string){ return {result: string, context}; } @@ -78,10 +73,11 @@ function parseString(string, fn = 'compile'){ node = parse(string); } catch (e) { let message = prettifyParseError(e); - context.storeError({type: 'error', message}); + context.error(message); return {context}; } - let result = node[fn]({/*empty scope*/}, context); + if (!node) return {context}; + let {result} = resolve(fn, node, {/*empty scope*/}, context); return {result, context} } diff --git a/app/imports/parser/ResolveContext.js b/app/imports/parser/ResolveContext.js deleted file mode 100644 index 3edd2fa8..00000000 --- a/app/imports/parser/ResolveContext.js +++ /dev/null @@ -1,20 +0,0 @@ -export default class Context { - constructor({errors = [], rolls = []}){ - this.errors = errors; - this.rolls = rolls; - } - error(e){ - if (!e) return; - if (typeof e === 'string'){ - this.errors.push({ - type: 'error', - message: e, - }); - } else { - this.errors.push(e); - } - } - roll(r){ - this.rolls.push(r); - } -} diff --git a/app/imports/parser/TextField.vue b/app/imports/parser/TextField.vue new file mode 100644 index 00000000..9c9f0919 --- /dev/null +++ b/app/imports/parser/TextField.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index b1ec5a91..9213665e 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -1,5 +1,3 @@ -import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; - export default { 'abs': { comment: 'Returns the absolute value of a number', @@ -101,7 +99,7 @@ export default { {input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'}, {input: 'tableLookup([100, 300], 594)', result: '2'}, ], - arguments: [ArrayNode, 'number'], + arguments: ['array', 'number'], resultType: 'number', fn: function tableLookup(arrayNode, number){ for(let i in arrayNode.values){ diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index 8e29fb16..064fc2f2 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -1,19 +1,8 @@ -// Generated automatically by nearley, version 2.16.0 +// Generated automatically by nearley, version 2.20.1 // http://github.com/Hardmath123/nearley function id(x) { return x[0]; } - import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; - import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; - import CallNode from '/imports/parser/parseTree/CallNode.js'; - import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - import IfNode from '/imports/parser/parseTree/IfNode.js'; - import IndexNode from '/imports/parser/parseTree/IndexNode.js'; - import OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; - import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js'; - import RollNode from '/imports/parser/parseTree/RollNode.js'; - import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; - import UnaryOperatorNode from '/imports/parser/parseTree/UnaryOperatorNode.js'; - import NotOperatorNode from '/imports/parser/parseTree/NotOperatorNode.js'; + import node from './parseTree/_index.js'; import moo from 'moo'; @@ -53,7 +42,7 @@ function id(x) { return x[0]; } function nuller() { return null; } function operator([left, _1, operator, _2, right], fn){ - return new OperatorNode({ + return node.operator.create({ left, right, operator: operator.value, @@ -65,7 +54,7 @@ let ParserRules = [ {"name": "spacedExpression", "symbols": ["_", "expression", "_"], "postprocess": d => d[1]}, {"name": "expression", "symbols": ["ifStatement"], "postprocess": id}, {"name": "ifStatement", "symbols": ["orExpression", "_", (lexer.has("ifOperator") ? {type: "ifOperator"} : ifOperator), "_", "orExpression", "_", (lexer.has("elseOperator") ? {type: "elseOperator"} : elseOperator), "_", "ifStatement"], "postprocess": - d => new IfNode({condition: d[0], consequent: d[4], alternative: d[8]}) + d => node.if.create({condition: d[0], consequent: d[4], alternative: d[8]}) }, {"name": "ifStatement", "symbols": ["orExpression"], "postprocess": id}, {"name": "orExpression", "symbols": ["orExpression", "_", (lexer.has("orOperator") ? {type: "orOperator"} : orOperator), "_", "andExpression"], "postprocess": d => operator(d, 'or')}, @@ -82,18 +71,18 @@ let ParserRules = [ {"name": "remainderExpression", "symbols": ["multiplicativeExpression"], "postprocess": id}, {"name": "multiplicativeExpression", "symbols": ["multiplicativeExpression", "_", (lexer.has("multiplicativeOperator") ? {type: "multiplicativeOperator"} : multiplicativeOperator), "_", "rollExpression"], "postprocess": d => operator(d, 'multiply')}, {"name": "multiplicativeExpression", "symbols": ["rollExpression"], "postprocess": id}, - {"name": "rollExpression", "symbols": ["rollExpression", "_", (lexer.has("diceOperator") ? {type: "diceOperator"} : diceOperator), "_", "exponentExpression"], "postprocess": d => new RollNode({left: d[0], right: d[4]})}, + {"name": "rollExpression", "symbols": ["rollExpression", "_", (lexer.has("diceOperator") ? {type: "diceOperator"} : diceOperator), "_", "exponentExpression"], "postprocess": d => node.roll.create({left: d[0], right: d[4]})}, {"name": "rollExpression", "symbols": ["singleRollExpression"], "postprocess": id}, - {"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "singleRollExpression"], "postprocess": d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]})}, + {"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "singleRollExpression"], "postprocess": d => node.roll.create({left: node.constant.create({value: 1}), right: d[2]})}, {"name": "singleRollExpression", "symbols": ["exponentExpression"], "postprocess": id}, {"name": "exponentExpression", "symbols": ["callExpression", "_", (lexer.has("exponentOperator") ? {type: "exponentOperator"} : exponentOperator), "_", "exponentExpression"], "postprocess": d => operator(d, 'exponent')}, {"name": "exponentExpression", "symbols": ["unaryExpression"], "postprocess": id}, - {"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => new UnaryOperatorNode({operator: d[0].value, right: d[2]})}, + {"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => node.unaryOperator.create({operator: d[0].value, right: d[2]})}, {"name": "unaryExpression", "symbols": ["notExpression"], "postprocess": id}, - {"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => new NotOperatorNode({right: d[2]})}, + {"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => node.notOperator.create({right: d[2]})}, {"name": "notExpression", "symbols": ["callExpression"], "postprocess": id}, {"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess": - d => new CallNode ({functionName: d[0].name, args: d[2]}) + d => node.call.create({functionName: d[0].name, args: d[2]}) }, {"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id}, {"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, @@ -105,7 +94,7 @@ let ParserRules = [ {"name": "arguments", "symbols": [{"literal":"("}, "_", "arguments$ebnf$1", "arguments$ebnf$2", "_", {"literal":")"}], "postprocess": d => [d[2], ...d[3]] }, - {"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => new IndexNode ({array: d[0], index: d[3]})}, + {"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => node.index.create({array: d[0], index: d[3]})}, {"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id}, {"name": "arrayExpression$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, {"name": "arrayExpression$ebnf$1", "symbols": ["arrayExpression$ebnf$1$subexpression$1"], "postprocess": id}, @@ -114,10 +103,10 @@ let ParserRules = [ {"name": "arrayExpression$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]}, {"name": "arrayExpression$ebnf$2", "symbols": ["arrayExpression$ebnf$2", "arrayExpression$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, {"name": "arrayExpression", "symbols": [{"literal":"["}, "_", "arrayExpression$ebnf$1", "arrayExpression$ebnf$2", "_", {"literal":"]"}], "postprocess": - d => new ArrayNode({values: d[2] ? [d[2], ...d[3]] : []}) + d => node.array.create({values: d[2] ? [d[2], ...d[3]] : []}) }, {"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id}, - {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => new ParenthesisNode({content: d[2]})}, + {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => node.parenthesis.create({content: d[2]})}, {"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id}, {"name": "accessorExpression$subexpression$1", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[0].value}, {"name": "accessorExpression$ebnf$1$subexpression$1", "symbols": [{"literal":"."}, (lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[1].value}, @@ -125,18 +114,18 @@ let ParserRules = [ {"name": "accessorExpression$ebnf$1$subexpression$2", "symbols": [{"literal":"."}, (lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[1].value}, {"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1", "accessorExpression$ebnf$1$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, {"name": "accessorExpression", "symbols": ["accessorExpression$subexpression$1", "accessorExpression$ebnf$1"], "postprocess": - d=> new AccessorNode({name: d[0], path: d[1]}) + d=> node.accessor.create({name: d[0], path: d[1]}) }, {"name": "accessorExpression", "symbols": ["valueExpression"], "postprocess": id}, {"name": "valueExpression", "symbols": ["name"], "postprocess": id}, {"name": "valueExpression", "symbols": ["number"], "postprocess": id}, {"name": "valueExpression", "symbols": ["string"], "postprocess": id}, {"name": "valueExpression", "symbols": ["boolean"], "postprocess": id}, - {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode({value: +d[0].value, type: 'number'})}, - {"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => new SymbolNode({name: d[0].value})}, - {"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => new ConstantNode({value: d[0].value, type: 'string'})}, - {"name": "boolean", "symbols": [{"literal":"true"}], "postprocess": d => new ConstantNode({value: true, type: 'boolean'})}, - {"name": "boolean", "symbols": [{"literal":"false"}], "postprocess": d => new ConstantNode({value: false, type: 'boolean'})}, + {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => node.constant.create({value: +d[0].value})}, + {"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => node.symbol.create({name: d[0].value})}, + {"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => node.constant.create({value: d[0].value})}, + {"name": "boolean", "symbols": [{"literal":"true"}], "postprocess": d => node.constant.create({value: true})}, + {"name": "boolean", "symbols": [{"literal":"false"}], "postprocess": d => node.constant.create({value: false})}, {"name": "_", "symbols": []}, {"name": "_", "symbols": [(lexer.has("space") ? {type: "space"} : space)], "postprocess": nuller} ]; diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index cd278bab..82681659 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -1,17 +1,6 @@ @preprocessor esmodule @{% - import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; - import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; - import CallNode from '/imports/parser/parseTree/CallNode.js'; - import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - import IfNode from '/imports/parser/parseTree/IfNode.js'; - import IndexNode from '/imports/parser/parseTree/IndexNode.js'; - import OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; - import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js'; - import RollNode from '/imports/parser/parseTree/RollNode.js'; - import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; - import UnaryOperatorNode from '/imports/parser/parseTree/UnaryOperatorNode.js'; - import NotOperatorNode from '/imports/parser/parseTree/NotOperatorNode.js'; + import node from './parseTree/_index.js'; import moo from 'moo'; @@ -51,7 +40,7 @@ function nuller() { return null; } function operator([left, _1, operator, _2, right], fn){ - return new OperatorNode({ + return node.operator.create({ left, right, operator: operator.value, @@ -71,7 +60,7 @@ expression -> ifStatement -> orExpression _ %ifOperator _ orExpression _ %elseOperator _ ifStatement {% - d => new IfNode({condition: d[0], consequent: d[4], alternative: d[8]}) + d => node.if.create({condition: d[0], consequent: d[4], alternative: d[8]}) %} | orExpression {% id %} @@ -104,11 +93,11 @@ multiplicativeExpression -> | rollExpression {% id %} rollExpression -> - rollExpression _ %diceOperator _ exponentExpression {% d => new RollNode({left: d[0], right: d[4]}) %} + rollExpression _ %diceOperator _ exponentExpression {% d => node.roll.create({left: d[0], right: d[4]}) %} | singleRollExpression {% id %} singleRollExpression -> - "d" _ singleRollExpression {% d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]}) %} + "d" _ singleRollExpression {% d => node.roll.create({left: node.constant.create({value: 1}), right: d[2]}) %} | exponentExpression {% id %} exponentExpression -> @@ -116,16 +105,16 @@ exponentExpression -> | unaryExpression {% id %} unaryExpression -> - %additiveOperator _ unaryExpression {% d => new UnaryOperatorNode({operator: d[0].value, right: d[2]})%} + %additiveOperator _ unaryExpression {% d => node.unaryOperator.create({operator: d[0].value, right: d[2]})%} | notExpression {% id %} notExpression -> - %notOperator _ notExpression {% d => new NotOperatorNode({right: d[2]})%} + %notOperator _ notExpression {% d => node.notOperator.create({right: d[2]})%} | callExpression {% id %} callExpression -> name _ arguments {% - d => new CallNode ({functionName: d[0].name, args: d[2]}) + d => node.call.create({functionName: d[0].name, args: d[2]}) %} | indexExpression {% id %} @@ -135,22 +124,22 @@ arguments -> %} indexExpression -> - arrayExpression "[" _ expression _ "]" {% d => new IndexNode ({array: d[0], index: d[3]}) %} + arrayExpression "[" _ expression _ "]" {% d => node.index.create({array: d[0], index: d[3]}) %} | arrayExpression {% id %} arrayExpression -> "[" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ "]" {% - d => new ArrayNode({values: d[2] ? [d[2], ...d[3]] : []}) + d => node.array.create({values: d[2] ? [d[2], ...d[3]] : []}) %} | parenthesizedExpression {% id %} parenthesizedExpression -> - "(" _ expression _ ")" {% d => new ParenthesisNode({content: d[2]}) %} + "(" _ expression _ ")" {% d => node.parenthesis.create({content: d[2]}) %} | accessorExpression {% id %} accessorExpression -> (%name {% d => d[0].value %}) ( "." %name {% d => d[1].value %} ):+ {% - d=> new AccessorNode({name: d[0], path: d[1]}) + d=> node.accessor.create({name: d[0], path: d[1]}) %} | valueExpression {% id %} @@ -162,17 +151,17 @@ valueExpression -> # A number or a function of a number number -> - %number {% d => new ConstantNode({value: +d[0].value, type: 'number'}) %} + %number {% d => node.constant.create({value: +d[0].value}) %} name -> - %name {% d => new SymbolNode({name: d[0].value}) %} + %name {% d => node.symbol.create({name: d[0].value}) %} string -> - %string {% d => new ConstantNode({value: d[0].value, type: 'string'}) %} + %string {% d => node.constant.create({value: d[0].value}) %} boolean -> - "true" {% d => new ConstantNode({value: true, type: 'boolean'}) %} -| "false" {% d => new ConstantNode({value: false, type: 'boolean'}) %} + "true" {% d => node.constant.create({value: true}) %} +| "false" {% d => node.constant.create({value: false}) %} _ -> null diff --git a/app/imports/parser/parseTree/IndexNode.js b/app/imports/parser/parseTree/IndexNode.js deleted file mode 100644 index fdd6c6c6..00000000 --- a/app/imports/parser/parseTree/IndexNode.js +++ /dev/null @@ -1,64 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; - -export default class IndexNode extends ParseNode { - constructor({array, index}) { - super(...arguments); - this.array = array; - this.index = index; - } - resolve(fn, scope, context){ - let index = this.index[fn](scope, context); - let array = this.array[fn](scope, context); - - if (index.isInteger && array instanceof ArrayNode){ - if (index.value < 1 || index.value > array.values.length){ - if (context){ - context.storeError({ - type: 'warning', - message: `Index of ${index.value} is out of range for an array` + - ` of length ${array.values.length}`, - }); - } - } - let selection = array.values[index.value - 1]; - if (selection){ - let result = selection[fn](scope, context); - return result; - } - } else if (fn === 'reduce'){ - if (!(array instanceof ArrayNode)){ - return new ErrorNode({ - node: this, - error: 'Can not get the index of a non-array node: ' + - this.array.toString() + ' = ' + array.toString(), - context, - }); - } else if (!index.isInteger){ - return new ErrorNode({ - node: this, - error: array.toString() + ' is not an integer index of the array', - context, - }); - } - } - return new IndexNode({ - index, - array, - previousNodes: [this], - }); - } - toString(){ - return `${this.array.toString()}[${this.index.toString()}]`; - } - traverse(fn){ - fn(this); - this.array.traverse(fn); - this.index.traverse(fn); - } - replaceChildren(fn){ - this.array = this.array.replaceNodes(fn); - this.index = this.index.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/NotOperatorNode.js b/app/imports/parser/parseTree/NotOperatorNode.js deleted file mode 100644 index 47aca511..00000000 --- a/app/imports/parser/parseTree/NotOperatorNode.js +++ /dev/null @@ -1,34 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class NotOperatorNode extends ParseNode { - constructor({right}) { - super(...arguments); - this.right = right; - } - resolve(fn, scope, context){ - let rightNode = this.right[fn](scope, context); - if (!(rightNode instanceof ConstantNode)){ - return new NotOperatorNode({ - right: rightNode, - }); - } - let right = rightNode.value; - let result = !right; - return new ConstantNode({ - value: result, - type: typeof result, - }); - } - toString(){ - let {right} = this; - return `!${right.toString()}`; - } - traverse(fn){ - fn(this); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/OperatorNode.js b/app/imports/parser/parseTree/OperatorNode.js deleted file mode 100644 index 56633993..00000000 --- a/app/imports/parser/parseTree/OperatorNode.js +++ /dev/null @@ -1,71 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class OperatorNode extends ParseNode { - constructor({left, right, operator, fn}) { - super(...arguments); - this.left = left; - this.right = right; - this.fn = fn; - this.operator = operator; - } - resolve(fn, scope, context){ - let leftNode = this.left[fn](scope, context); - let rightNode = this.right[fn](scope, context); - let left, right; - if (!(leftNode instanceof ConstantNode) || !(rightNode instanceof ConstantNode)){ - return new OperatorNode({ - left: leftNode, - right: rightNode, - operator: this.operator, - fn: this.fn, - }); - } else { - left = leftNode.value; - right = rightNode.value; - } - let result; - switch(this.operator){ - case '+': result = left + right; break; - case '-': result = left - right; break; - case '*': result = left * right; break; - case '/': result = left / right; break; - case '^': result = Math.pow(left, right); break; - case '%': result = left % right; break; - case '&': - case '&&': result = left && right; break; - case '|': - case '||': result = left || right; break; - case '=': - case '==': result = left == right; break; - case '===': result = left === right; break; - case '!=': result = left != right; break; - case '!==': result = left !== right; break; - case '>': result = left > right; break; - case '<': result = left < right; break; - case '>=': result = left >= right; break; - case '<=': result = left <= right; break; - } - return new ConstantNode({ - value: result, - type: typeof result, - }); - } - toString(){ - let {left, right, operator} = this; - // special case of adding a negative number - if (operator === '+' && right.isNumber && right.value < 0){ - return `${left.toString()} - ${-right.value}` - } - return `${left.toString()} ${operator} ${right.toString()}`; - } - traverse(fn){ - fn(this); - this.left.traverse(fn); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.left = this.left.replaceNodes(fn); - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/ParenthesisNode.js b/app/imports/parser/parseTree/ParenthesisNode.js deleted file mode 100644 index db6b5e14..00000000 --- a/app/imports/parser/parseTree/ParenthesisNode.js +++ /dev/null @@ -1,30 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; - -export default class ParenthesisNode extends ParseNode { - constructor({content}) { - super(...arguments); - this.content = content; - } - resolve(fn, scope, context){ - let content = this.content[fn](scope, context); - if ( - fn === 'reduce' || - content.constructor.name === 'ConstantNode' || - content.constructor.name === 'ErrorNode' - ){ - return content; - } else { - return new ParenthesisNode({content}); - } - } - toString(){ - return `(${this.content.toString()})`; - } - traverse(fn){ - fn(this); - this.content.traverse(fn); - } - replaceChildren(fn){ - this.content = this.content.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/ParseNode.js b/app/imports/parser/parseTree/ParseNode.js deleted file mode 100644 index 740c7b14..00000000 --- a/app/imports/parser/parseTree/ParseNode.js +++ /dev/null @@ -1,43 +0,0 @@ -export default class ParseNode { - toString(){ - throw new Meteor.Error('toString not implemented on ' + this.constructor.name); - } - compile(scope, context){ - // Returns a ParseNode, a ConstantNode if possible - if(this.resolve) { - return this.resolve('compile', scope, context); - } else { - throw new Meteor.Error('Compile not implemented on ' + this.constructor.name); - } - } - // Compile, but turn rolls into arrays - roll(scope, context){ - if (this.resolve){ - return this.resolve('roll', scope, context); - } else { - return this.compile(scope, context); - } - } - // Compile, turn rolls into arrays, and reduce those arrays into single values - reduce(scope, context){ - if (this.resolve){ - return this.resolve('reduce', scope, context); - } else { - return this.roll(scope, context); - } - } - // If traverse isn't implemented, just apply it to the current node - traverse(fn){ - fn(this); - } - // replace nodes, only replace nodes if fn returns a value - replaceNodes(fn){ - let newNode = fn(this); - if (newNode) { - return newNode; - } else { - if (this.replaceChildren) this.replaceChildren(fn) - return this; - } - } -} diff --git a/app/imports/parser/parseTree/RollArrayNode.js b/app/imports/parser/parseTree/RollArrayNode.js deleted file mode 100644 index 978c20d6..00000000 --- a/app/imports/parser/parseTree/RollArrayNode.js +++ /dev/null @@ -1,22 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class RollArrayNode extends ParseNode { - constructor({values}) { - super(...arguments); - this.values = values; - } - compile(){ - return this; - } - toString(){ - return `[${this.values.join(', ')}]`; - } - reduce(){ - let total = this.values.reduce((a, b) => a + b); - return new ConstantNode({ - value: total, - type: 'number', - }); - } -} diff --git a/app/imports/parser/parseTree/RollNode.js b/app/imports/parser/parseTree/RollNode.js deleted file mode 100644 index 5e1d178f..00000000 --- a/app/imports/parser/parseTree/RollNode.js +++ /dev/null @@ -1,70 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import RollArrayNode from '/imports/parser/parseTree/RollArrayNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import roll from '/imports/parser/roll.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -export default class RollNode extends ParseNode { - constructor({left, right}) { - super(...arguments); - this.left = left; - this.right = right; - } - compile(scope, context){ - let left = this.left.compile(scope, context); - let right = this.right.compile(scope, context); - return new RollNode({left, right, previousNodes: [this]}); - } - toString(){ - if ( - this.left.isNumberNode && this.left.value === 1 - ){ - return `d${this.right.toString()}`; - } else { - return `${this.left.toString()}d${this.right.toString()}`; - } - } - roll(scope, context){ - let left = this.left.reduce(scope, context); - let right = this.right.reduce(scope, context); - if (!left.isInteger){ - return new ErrorNode({ - node: this, - error: 'Number of dice is not an integer', - }); - } - if (!right.isInteger){ - return new ErrorNode({ - node: this, - error: 'Dice size is not an integer', - }); - } - let number = left.value; - if (context.doubleRolls){ - number *= 2; - } - if (number > STORAGE_LIMITS.diceRollValuesCount) return new ErrorNode({ - node: this, - error: `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`, - context, - }); - let diceSize = right.value; - let values = roll(number, diceSize); - if (context){ - context.storeRoll({number, diceSize, values}); - } - return new RollArrayNode({values}); - } - reduce(scope, context){ - return this.roll(scope, context).reduce(scope, context); - } - traverse(fn){ - fn(this); - this.left.traverse(fn); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.left = this.left.replaceNodes(fn); - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js deleted file mode 100644 index 814ea261..00000000 --- a/app/imports/parser/parseTree/SymbolNode.js +++ /dev/null @@ -1,53 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class SymbolNode extends ParseNode { - constructor({name}){ - super(...arguments); - this.name = name; - } - toString(){ - return `${this.name}` - } - compile(scope, context, calledFromReduce = false){ - let value = scope && scope[this.name]; - let type = typeof value; - // For objects, default to their .value - if (type === 'object'){ - value = value.value; - type = typeof value; - } - // For parse nodes, compile and return - if (value instanceof ParseNode){ - if (calledFromReduce){ - return value.reduce(scope, context); - } else { - return value.compile(scope, context); - } - } - if (type === 'string' || type === 'number' || type === 'boolean'){ - return new ConstantNode({value, type}); - } else if (type === 'undefined'){ - return new SymbolNode({ - name: this.name, - }); - } else { - throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); - } - } - reduce(scope, context){ - let result = this.compile(scope, context, true); - if (result instanceof SymbolNode){ - if (context) context.storeError({ - type: 'info', - message: `${result.toString()} not found, set to 0` - }); - return new ConstantNode({ - type: 'number', - value: 0, - }); - } else { - return result; - } - } -} diff --git a/app/imports/parser/parseTree/UnaryOperatorNode.js b/app/imports/parser/parseTree/UnaryOperatorNode.js deleted file mode 100644 index c8109e6a..00000000 --- a/app/imports/parser/parseTree/UnaryOperatorNode.js +++ /dev/null @@ -1,40 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class UnaryOperatorNode extends ParseNode { - constructor({operator, right}) { - super(...arguments); - this.operator = operator; - this.right = right; - } - resolve(fn, scope, context){ - let rightNode = this.right[fn](scope, context); - if (rightNode.type !== 'number'){ - return new UnaryOperatorNode({ - operator: this.operator, - right: rightNode, - }); - } - let right = rightNode.value; - let result; - switch(this.operator){ - case '-': result = -right; break; - case '+': result = +right; break; - } - return new ConstantNode({ - value: result, - type: typeof result, - }); - } - toString(){ - let {right, operator} = this; - return `${operator}${right.toString()}`; - } - traverse(fn){ - fn(this); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/_index.js b/app/imports/parser/parseTree/_index.js index 571f7e7a..9900c381 100644 --- a/app/imports/parser/parseTree/_index.js +++ b/app/imports/parser/parseTree/_index.js @@ -1,5 +1,31 @@ import accessor from './accessor.js'; +import array from './array.js'; +import call from './call.js'; +import constant from './constant.js'; +import error from './error.js'; +import ifNode from './if.js'; +import index from './index.js'; +import not from './not.js'; +import operator from './operator.js'; +import parenthesis from './parenthesis.js'; +import roll from './roll.js'; +import rollArray from './rollArray.js'; +import symbol from './symbol.js'; +import unaryOperator from './unaryOperator.js'; export default Object.freeze({ accessor, + array, + call, + constant, + error, + if: ifNode, + index, + not, + operator, + parenthesis, + roll, + rollArray, + symbol, + unaryOperator, }); diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js index 5ebbf246..8c5b3773 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.js @@ -3,7 +3,7 @@ import constant from './constant.js'; const accessor = { create({name, path}) { return { - type: 'accessor', + parseType: 'accessor', path, name, }; @@ -45,12 +45,11 @@ const accessor = { }, reduce(node, scope, context){ let { result } = accessor.compile(node, scope, context); - if (result.type === 'accessor'){ + if (result.parseType === 'accessor'){ context.error(`${accessor.toString(result)} not found, set to 0`); return { result: constant.create({ value: 0, - valueType: 'number', }), context }; diff --git a/app/imports/parser/parseTree/array.js b/app/imports/parser/parseTree/array.js index 407ab92a..7ee711a6 100644 --- a/app/imports/parser/parseTree/array.js +++ b/app/imports/parser/parseTree/array.js @@ -1,10 +1,10 @@ -import constant from './constant.js'; import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; const array = { create({values}) { return { - type: 'array', + parseType: 'array', values, }; }, @@ -24,7 +24,7 @@ const array = { }); return array.create({values}); }, - resolve(fn, node, scope){ + resolve(fn, node, scope, context){ let values = node.values.map(node => { let { result } = resolve(fn, node, scope, context); return result; diff --git a/app/imports/parser/parseTree/call.js b/app/imports/parser/parseTree/call.js index 6893dbf4..52b16967 100644 --- a/app/imports/parser/parseTree/call.js +++ b/app/imports/parser/parseTree/call.js @@ -1,12 +1,12 @@ import error from './error.js'; import constant from './constant.js'; import functions from '/imports/parser/functions.js'; -import resolve, { toString, traverse, mergeResolvedNodes } from '../resolve.js'; +import resolve, { toString, traverse } from '../resolve.js'; const call = { create({functionName, args}) { return { - type: 'call', + parseType: 'call', functionName, args, } @@ -62,7 +62,7 @@ const call = { // Map contant nodes to constants before attempting to run the function let mappedArgs = resolvedArgs.map(arg => { - if (arg.type === 'constant'){ + if (arg.parseType === 'constant'){ return arg.value; } else { return arg; @@ -125,16 +125,10 @@ const call = { } else { type = argumentsExpected[index]; } - if (typeof type === 'string'){ - // Type being a string means a constant node with matching type - if (node.valueType !== type) failed = true; - } else { - // Otherwise check that the node is an instance of the given type - if (!(node instanceof type)) failed = true; - } + if (node.parseType !== type && node.valueType !== type) failed = true; if (failed && fn === 'reduce'){ let typeName = typeof type === 'string' ? type : type.constructor.name; - let nodeName = node.type; + let nodeName = node.parseType; context.error(`Incorrect arguments to ${node.functionName} function` + `expected ${typeName} got ${nodeName}`); } diff --git a/app/imports/parser/parseTree/constant.js b/app/imports/parser/parseTree/constant.js index 4220bc96..e382b897 100644 --- a/app/imports/parser/parseTree/constant.js +++ b/app/imports/parser/parseTree/constant.js @@ -1,14 +1,13 @@ const constant = { - create({value, valueType}){ - if (!valueType) throw `Expected valueType to be set, got ${valueType}` + create({value}){ return { - type: 'constant', - valueType, + parseType: 'constant', + valueType: typeof value, value, } }, - compile(node){ - return node; + compile(node, scope, context){ + return {result: node, context}; }, toString(node){ return `${node.value}`; diff --git a/app/imports/parser/parseTree/error.js b/app/imports/parser/parseTree/error.js index 8533061b..f79ac4af 100644 --- a/app/imports/parser/parseTree/error.js +++ b/app/imports/parser/parseTree/error.js @@ -1,13 +1,13 @@ const error = { create({node, error}) { return { - type: 'error', + parseType: 'error', node, error, } }, - compile(node){ - return node; + compile(node, scope, context){ + return {result: node, context}; }, toString(node){ return node.error.toString(); diff --git a/app/imports/parser/parseTree/if.js b/app/imports/parser/parseTree/if.js index 369ff924..fc56bf66 100644 --- a/app/imports/parser/parseTree/if.js +++ b/app/imports/parser/parseTree/if.js @@ -1,10 +1,9 @@ -import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve'; -import collate from '/imports/api/engine/computation/utility/collate.js'; +import resolve, { traverse, toString } from '../resolve'; const ifNode = { create({condition, consequent, alternative}){ return { - type: 'if', + parseType: 'if', condition, consequent, alternative, @@ -14,28 +13,14 @@ const ifNode = { let {condition, consequent, alternative} = node; return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}` }, - resolve(fn, node, scope){ - let rest, condition, consequent, alternative; - let resolved = {}; + resolve(fn, node, scope, context){ + let {result: condition} = resolve(fn, node.condition, scope, context); - ({result: condition, ...rest} = resolve(fn, node.condition, scope)); - mergeResolvedNodes(resolved, rest); - - if (condition.type === 'constant'){ + if (condition.parseType === 'constant'){ if (condition.value){ - ({result: consequent, ...rest} = resolve(fn, node.consequent, scope)); - mergeResolvedNodes(resolved, rest); - return { - result: consequent, - ...resolved - }; + return resolve(fn, node.consequent, scope, context); } else { - ({result: alternative, ...rest} = resolve(fn, node.alternative, scope)); - mergeResolvedNodes(resolved, rest); - return { - result: alternative, - ...resolved - }; + return resolve(fn, node.alternative, scope, context); } } else { return { @@ -44,7 +29,7 @@ const ifNode = { consequent: node.consequent, alternative: node.alternative, }), - ...resolved + context, }; } }, diff --git a/app/imports/parser/parseTree/index.js b/app/imports/parser/parseTree/index.js index dcb19e2d..17b00a21 100644 --- a/app/imports/parser/parseTree/index.js +++ b/app/imports/parser/parseTree/index.js @@ -1,76 +1,73 @@ -import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve'; +import resolve, { traverse, toString } from '../resolve'; +import error from './error'; const index = { create({array, index}) { return { - type: 'index', + parseType: 'index', array, index, } }, - resolve(fn, node, scope){ - let index, array, rest; - let resolved = {}; - ({result: index, ...rest} = resolve(fn, node.index, scope)); - mergeResolvedNodes(resolved, rest); - ({result: array, ...rest} = resolve(fn, node.array, scope)); - mergeResolvedNodes(resolved, rest); + resolve(fn, node, scope, context){ + let {result: index} = resolve(fn, node.index, scope, context); + let {result: array} = resolve(fn, node.array, scope, context); if ( index.valueType === 'number' && Number.isInteger(index.value) && - array.type === 'array' + array.parseType === 'array' ){ if (index.value < 1 || index.value > array.values.length){ - mergeResolvedNodes(resolved, { - errors: [{ - type: 'warning', - message: `Index of ${index.value} is out of range for an array` + - ` of length ${array.values.length}`, - }] + context.error({ + type: 'warning', + message: `Index of ${index.value} is out of range for an array` + + ` of length ${array.values.length}`, }); } let selection = array.values[index.value - 1]; if (selection){ - let result; - ({result, ...rest} = resolve(fn, selection, scope)); - mergeResolvedNodes(resolved, rest) - return result; + return resolve(fn, selection, scope, context); } } else if (fn === 'reduce'){ - if (!(array instanceof ArrayNode)){ - return new ErrorNode({ - node: node, - error: 'Can not get the index of a non-array node: ' + - node.array.toString() + ' = ' + array.toString(), + if (array.parseType !== 'array'){ + const message = `Can not get the index of a non-array node: ${node.array.toString()} = ${array.toString()}` + context.error(message); + return { + result: error.create({ + node, + error: message, + }), context, - }); + }; } else if (!index.isInteger){ - return new ErrorNode({ - node: node, - error: array.toString() + ' is not an integer index of the array', + const message = `${array.toString()} is not an integer index of the array` + context.error(message); + return { + result: error.create({ + node, + error: message, + }), context, - }); + }; } } - return new IndexNode({ - index, - array, - previousNodes: [node], - }); + return { + result: index.create({ + index, + array, + }), + context, + }; }, - toString(){ - return `${node.array.toString()}[${node.index.toString()}]`; + toString(node){ + return `${toString(node.array)}[${toString(node.index)}]`; }, - traverse(fn){ + traverse(node, fn){ fn(node); - node.array.traverse(fn); - node.index.traverse(fn); + traverse(node.array, fn); + traverse(node.index, fn); }, - replaceChildren(fn){ - node.array = node.array.replaceNodes(fn); - node.index = node.index.replaceNodes(fn); - } } export default index; diff --git a/app/imports/parser/parseTree/not.js b/app/imports/parser/parseTree/not.js new file mode 100644 index 00000000..9488a96c --- /dev/null +++ b/app/imports/parser/parseTree/not.js @@ -0,0 +1,37 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; + +const not = { + create({right}) { + return { + parseType: 'not', + right, + } + }, + resolve(fn, node, scope, context){ + const {result: right} = resolve(fn, node.right, scope, context); + if (right.parseType !== 'constant'){ + return { + result: not.create({ + right: right, + }), + context, + }; + } + return { + result: constant.create({ + value: !right.value, + }), + context, + }; + }, + toString(node){ + return `!${toString(node.right)}`; + }, + traverse(node, fn){ + fn(node); + traverse(node.right, fn); + } +} + +export default not; diff --git a/app/imports/parser/parseTree/operator.js b/app/imports/parser/parseTree/operator.js new file mode 100644 index 00000000..7cc4a099 --- /dev/null +++ b/app/imports/parser/parseTree/operator.js @@ -0,0 +1,76 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; + +const operator = { + create({left, right, operator, fn}) { + return { + parseType: 'operator', + left, + right, + operator, + fn + }; + }, + resolve(fn, node, scope, context){ + const {result: leftNode} = resolve(fn, node.left, scope, context); + const {result: rightNode} = resolve(fn, node.right, scope, context); + let left, right; + if (leftNode.parseType !== 'constant' || rightNode.parseType !== 'constant'){ + return { + result: operator.create({ + left: leftNode, + right: rightNode, + operator: node.operator, + fn: node.fn, + }), + context, + }; + } else { + left = leftNode.value; + right = rightNode.value; + } + let result; + switch(node.operator){ + case '+': result = left + right; break; + case '-': result = left - right; break; + case '*': result = left * right; break; + case '/': result = left / right; break; + case '^': result = Math.pow(left, right); break; + case '%': result = left % right; break; + case '&': + case '&&': result = left && right; break; + case '|': + case '||': result = left || right; break; + case '=': + case '==': result = left == right; break; + case '===': result = left === right; break; + case '!=': result = left != right; break; + case '!==': result = left !== right; break; + case '>': result = left > right; break; + case '<': result = left < right; break; + case '>=': result = left >= right; break; + case '<=': result = left <= right; break; + } + return { + result: constant.create({ + value: result, + }), + context, + }; + }, + toString(node){ + let {left, right, operator} = node; + // special case of adding a negative number + if (operator === '+' && right.valueType === 'number' && right.value < 0){ + return `${toString(left)} - ${-right.value}` + } + return `${toString(left)} ${operator} ${toString(right)}`; + }, + traverse(node, fn){ + fn(node); + traverse(node.left, fn); + traverse(node.right, fn); + }, +} + +export default operator; diff --git a/app/imports/parser/parseTree/parenthesis.js b/app/imports/parser/parseTree/parenthesis.js new file mode 100644 index 00000000..cd0cfa1a --- /dev/null +++ b/app/imports/parser/parseTree/parenthesis.js @@ -0,0 +1,34 @@ +import resolve, { toString, traverse } from '../resolve.js'; + +const parenthesis = { + create({content}) { + return { + parseType: 'parenthesis', + content, + }; + }, + resolve(fn, node, scope, context){ + const {result: content} = resolve(fn, node.content, scope, context); + if ( + fn === 'reduce' || + content.parseType === 'constant' || + content.parseType === 'error' + ){ + return {result: content, context}; + } else { + return { + result: parenthesis.create({content}), + context + }; + } + }, + toString(node){ + return `(${toString(node.content)})`; + }, + traverse(node, fn){ + fn(node); + traverse(node.content, fn); + } +} + +export default parenthesis; diff --git a/app/imports/parser/parseTree/roll.js b/app/imports/parser/parseTree/roll.js new file mode 100644 index 00000000..cc87cbcc --- /dev/null +++ b/app/imports/parser/parseTree/roll.js @@ -0,0 +1,82 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import error from './error.js'; +import rollArray from './rollArray.js'; +import roll from '/imports/parser/roll.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const rollNode = { + create({left, right}) { + return { + parseType: 'roll', + left, + right, + }; + }, + compile(node, scope, context){ + const {result: left} = resolve('compile', node.left, scope, context); + const {result: right} = resolve('compile', node.right, scope, context); + return { + result: rollNode.create({left, right}), + context, + }; + }, + toString(node){ + if ( + node.left.parseType === 'number' && node.left.value === 1 + ){ + return `d${toString(node.right)}`; + } else { + return `${toString(node.left)}d${toString(node.right)}`; + } + }, + roll(node, scope, context){ + const {result: left} = resolve('reduce', node.left, scope, context); + const {result: right} = resolve('reduce', node.right, scope, context); + if (left.parseType !== 'number' && !Number.isInteger(left.value)){ + return errorResult('Number of dice is not an integer', node, context); + } + if (!right.isInteger){ + return errorResult('Dice size is not an integer', node, context); + } + let number = left.value; + if (context.doubleRolls){ + number *= 2; + } + if (number > STORAGE_LIMITS.diceRollValuesCount){ + const message = `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`; + return errorResult(message, node, context); + } + let diceSize = right.value; + let values = roll(number, diceSize); + if (context){ + context.storeRoll({number, diceSize, values}); + } + return { + result: rollArray.create({ + values, + diceSize, + diceNum: left.value, + }), + context + }; + }, + reduce(node, scope, context){ + const {result} = rollNode.roll(node, scope, context); + return resolve('reduce', result, scope, context); + }, + traverse(node, fn){ + fn(node); + traverse(node.left, fn); + traverse(node.right, fn); + }, +} + +function errorResult(message, node, context){ + context.error(message); + return { + result: error.create({ node, error: message }), + context, + }; +} + +export default rollNode; diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js new file mode 100644 index 00000000..bb778947 --- /dev/null +++ b/app/imports/parser/parseTree/rollArray.js @@ -0,0 +1,35 @@ +import constant from './constant.js'; + +const rollArray = { + constructor({values, diceSize, diceNum}) { + return { + parseType: 'rollArray', + values, + diceSize, + diceNum, + }; + }, + compile(node, scope, context){ + return { + result: node, + context + }; + }, + toString(node){ + return `[${node.values.join(', ')}]`; + }, + reduce(node, scope, context){ + const total = node.values.reduce((a, b) => a + b, 0); + return { + result: constant.create({ + value: total, + }), + context, + }; + }, + traverse(node, fn){ + return fn(node); + } +} + +export default rollArray; diff --git a/app/imports/parser/parseTree/symbol.js b/app/imports/parser/parseTree/symbol.js new file mode 100644 index 00000000..208baa66 --- /dev/null +++ b/app/imports/parser/parseTree/symbol.js @@ -0,0 +1,61 @@ +import resolve from '../resolve.js'; +import constant from './constant.js'; + +const symbol = { + create({name}){ + return { + parseType: 'symbol', + name, + }; + }, + toString(node){ + return `${node.name}` + }, + compile(node, scope, context, calledFromReduce = false){ + let value = scope && scope[node.name]; + let type = typeof value; + // For objects, default to their .value + if (type === 'object'){ + value = value.value; + type = typeof value; + } + // For parse nodes, compile and return + if (value?.parseType){ + if (calledFromReduce){ + return resolve('reduce', value, scope, context); + } else { + return resolve('compile', value, scope, context); + } + } + if (type === 'string' || type === 'number' || type === 'boolean'){ + return { + result: constant.create({value}), + context, + }; + } else if (type === 'undefined'){ + return { + result: symbol.create({name: node.name}), + context, + }; + } else { + throw new Meteor.Error(`Unexpected case: ${node.name} resolved to ${value}`); + } + }, + reduce(node, scope, context){ + let {result} = symbol.compile(node, scope, context, true); + if (result.parseType === 'symbol'){ + context.error({ + type: 'info', + message: `${result.toString()} not found, set to 0` + }); + return { + result: constant.create({value: 0}), + context, + }; + } else { + return {result, context}; + } + } +} + +export default symbol; diff --git a/app/imports/parser/parseTree/unaryOperator.js b/app/imports/parser/parseTree/unaryOperator.js new file mode 100644 index 00000000..1b348f90 --- /dev/null +++ b/app/imports/parser/parseTree/unaryOperator.js @@ -0,0 +1,46 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; + +const unaryOperator = { + create({operator, right}) { + return { + parseType: 'unaryOperator', + operator, + right, + }; + }, + resolve(fn, node, scope, context){ + const {result: rightNode} = resolve(fn, node.right, scope, context); + if (rightNode.parseType !== 'number'){ + return { + result: unaryOperator.create({ + operator: node.operator, + right: rightNode, + }), + context, + }; + } + let right = rightNode.value; + let result; + switch(node.operator){ + case '-': result = -right; break; + case '+': result = +right; break; + } + return { + result: constant.create({ + value: result, + parseType: typeof result, + }), + context, + }; + }, + toString(node){ + return `${node.operator}${toString(node.right)}`; + }, + traverse(node, fn){ + fn(node); + traverse(node.right, fn); + }, +}; + +export default unaryOperator; diff --git a/app/imports/parser/parser.js b/app/imports/parser/parser.js index 13f24579..8532055b 100644 --- a/app/imports/parser/parser.js +++ b/app/imports/parser/parser.js @@ -7,20 +7,6 @@ export default function parser(){ return new nearley.Parser(nearleyGrammar); } -export class CompilationContext { - constructor({doubleRolls} = {}){ - this.errors = []; - this.rolls = []; - this.doubleRolls = doubleRolls; - } - storeError(e){ - this.errors.push(e); - } - storeRoll(r){ - this.rolls.push(r); - } -} - export function parse(string){ let parser = new nearley.Parser(nearleyGrammar); parser.feed(string); diff --git a/app/imports/parser/resolve.js b/app/imports/parser/resolve.js index 86d2fad6..add29f25 100644 --- a/app/imports/parser/resolve.js +++ b/app/imports/parser/resolve.js @@ -1,13 +1,11 @@ -import nodeTypeIndex from './parseTree/index.js'; -import collate from '/imports/api/engine/computation/utility/collate.js'; -import Context from './ResolveContext.js'; +import nodeTypeIndex from './parseTree/_index.js'; // Takes a parse ndoe and computes it to a set detail level // returns {result, context} export default function resolve(fn, node, scope, context = new Context()){ - let type = nodeTypeIndex[node.type]; + let type = nodeTypeIndex[node.parseType]; if (!type){ - throw new Meteor.Error(`Parse node type: ${node.type} not implemented`); + throw new Meteor.Error(`Parse node type: ${node.parseType} not implemented`); } if (type.resolve){ return type.resolve(fn, node, scope, context); @@ -18,28 +16,49 @@ export default function resolve(fn, node, scope, context = new Context()){ } else if (type.compile){ return type.compile(node, scope, context) } else { - throw new Meteor.Error('Compile not implemented on ' + node.type); + throw new Meteor.Error('Compile not implemented on ' + node.parseType); } } export function toString(node){ - let type = nodeTypeIndex[node.type]; + let type = nodeTypeIndex[node.parseType]; if (!type.toString){ - throw new Meteor.Error('toString not implemented on ' + node.type); + throw new Meteor.Error('toString not implemented on ' + node.parseType); } return type.toString(node); } export function traverse(node, fn){ - let type = nodeTypeIndex[node.type]; + if (!node) return; + let type = nodeTypeIndex[node.parseType]; + if (!type){ + console.error(node); + throw new Meteor.Error('Not valid parse node'); + } if (type.traverse){ return type.traverse(node, fn); } return fn(node); } -export function mergeResolvedNodes(main, other){ - main.errors = collate(main.errors, other.errors); - main.rolls = collate(main.rolls, other.rolls); - return main; +export class Context { + constructor({errors = [], rolls = [], doubleRolls} = {}){ + this.errors = errors; + this.rolls = rolls; + this.doubleRolls = doubleRolls; + } + error(e){ + if (!e) return; + if (typeof e === 'string'){ + this.errors.push({ + type: 'error', + message: e, + }); + } else { + this.errors.push(e); + } + } + roll(r){ + this.rolls.push(r); + } } From c2d430ad23d9f31468dba1e429f990d8b261903d Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sun, 3 Oct 2021 19:21:35 +0200 Subject: [PATCH 43/85] Fixed tableLookup returning string instead of number --- app/imports/parser/functions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index 9213665e..a98dded5 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -104,7 +104,7 @@ export default { fn: function tableLookup(arrayNode, number){ for(let i in arrayNode.values){ let node = arrayNode.values[i]; - if (node.value > number) return i; + if (node.value > number) return +i; } return arrayNode.values.length; } From 1a143930315498822733ec6d196ed11cfb92b512 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sun, 3 Oct 2021 20:59:04 +0200 Subject: [PATCH 44/85] Parsed calculations are now cached between calculations Parsing is one of the more expensive computations done to characters, so the parser results are now stored on the DB and only updated if they are dirty. A hash is used to determine if the calculation has changed since the last computation --- .../linkCalculationDependencies.js | 2 +- .../parseCalculationFields.js | 35 ++++++++++++++----- .../linkCalculationDependencies.testFn.js | 4 +-- .../computation/buildCreatureComputation.js | 1 - .../computeByType/computeAction.js | 1 + .../computeComputation/computeCalculations.js | 13 +++---- .../tests/computeAttribute.testFn.js | 4 --- .../api/engine/computation/utility/cyrb53.js | 14 ++++++++ .../properties/subSchemas/computedField.js | 16 +++++++++ .../subSchemas/inlineCalculationField.js | 26 +++++++++++++- app/imports/parser/parseTree/index.js | 6 ++-- app/imports/parser/resolve.js | 2 ++ 12 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 app/imports/api/engine/computation/utility/cyrb53.js diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 0e91f833..9df70b88 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -8,7 +8,7 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop // ancestors: {} //this gets added if there are resolved ancestors }; // Traverse the parsed calculation looking for variable names - traverse(calcObj._parsedCalculation, node => { + traverse(calcObj.parseNode, node => { // Skip nodes that aren't symbols or accessors if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return; // Link ancestor references as direct property dependencies diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 1d824e85..500afeee 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -2,7 +2,8 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX import { prettifyParseError, parse } from '/imports/parser/parser.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; import { get } from 'lodash'; -import errorNode from '/imports/parser/parseTree/error.js' +import errorNode from '/imports/parser/parseTree/error.js'; +import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; export default function parseCalculationFields(prop, schemas){ discoverInlineCalculationFields(prop, schemas); @@ -20,7 +21,15 @@ function discoverInlineCalculationFields(prop, schemas){ prop._computationDetails.inlineCalculations.push(inlineCalcObj); // Extract the calculations and store them on the property let string = inlineCalcObj.text; - if (!string) return; + if (!string){ + delete inlineCalcObj.hash; + return; + } + const inlineCalcHash = cyrb53(inlineCalcObj.text); + if (inlineCalcHash === inlineCalcObj.hash){ + return; + } + inlineCalcObj.hash = inlineCalcHash; inlineCalcObj.inlineCalculations = []; let matches = string.matchAll(INLINE_CALCULATION_REGEX); for (let match of matches){ @@ -55,17 +64,27 @@ function parseAllCalculationFields(prop, schemas){ } function parseCalculation(calcObj){ - if (!calcObj.calculation) return; + // If there is no calculation clear the cached parse node and error + if (!calcObj.calculation){ + delete calcObj.hash; + delete calcObj.parseError; + return; + } + const calcHash = cyrb53(calcObj.calculation); + // If the cached parse calculation is equal to the calculation, skip + if (calcHash === calcObj.hash){ + return; + } + calcObj.hash = calcHash; try { - calcObj._parsedCalculation = parse(calcObj.calculation); + calcObj.parseNode = parse(calcObj.calculation); + delete calcObj.parseError; } catch (e) { let error = { type: 'evaluation', message: prettifyParseError(e), }; - calcObj.errors ? - calcObj.errors.push(error) : - calcObj.errors = [error]; - calcObj._parsedCalculation = errorNode.create({error}); + calcObj.parseError = error; + calcObj.parseNode = errorNode.create({error}); } } diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js index 3996b5a3..f432d926 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -24,8 +24,8 @@ export default function(){ 'Variable references create dependencies even if the attributes don\'t exist' ); assert.equal( - prop('strengthId').baseValue.errors.length, 1, - 'Parse errors should be added to calculation errors' + prop('strengthId').baseValue.parseError.message, 'Unexpected end of input', + 'Parse errors should be stored on the calculation doc' ); } diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index 7cb54d30..e78b248c 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -90,6 +90,5 @@ export function buildComputationFromProps(properties){ linkTypeDependencies(dependencyGraph, prop, computation); linkCalculationDependencies(dependencyGraph, prop, computation); }); - return computation; } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js index dde680fc..9b318fa5 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeAction.js @@ -25,6 +25,7 @@ function computeResources(computation, node){ resources.attributesConsumed.forEach(attConsumed => { if (!attConsumed.variableName) return; const att = computation.scope[attConsumed.variableName]; + if (!att._id) return; attConsumed.available = att.value; attConsumed.statId = att._id; attConsumed.statName = att.name; diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js index ccbaf79f..6b74b5e3 100644 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ b/app/imports/api/engine/computation/computeComputation/computeCalculations.js @@ -13,25 +13,20 @@ export default function computeCalculations(computation, node){ } function evaluateCalculation(calculation, scope){ - const parseNode = calculation._parsedCalculation; + const parseNode = calculation.parseNode; const fn = calculation._parseLevel; const calculationScope = {...calculation._localScope, ...scope}; const {result: resultNode, context} = resolve(fn, parseNode, calculationScope); - if (resultNode.parseType === 'constant'){ + calculation.errors = context.errors; + if (resultNode?.parseType === 'constant'){ calculation.value = resultNode.value; - } else if (resultNode.parseType === 'error'){ + } else if (resultNode?.parseType === 'error'){ calculation.value = null; } else { calculation.value = toString(resultNode); } - if (calculation.errors){ - calculation.errors = [...calculation.errors, ...context.errors] - } else { - calculation.errors = context.errors - } // remove the working fields delete calculation._parseLevel; - delete calculation._parsedCalculation; delete calculation._localScope; } diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index 9eb5858a..25f298bb 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -15,10 +15,6 @@ export default function(){ assert.equal(scope('strength').modifier, 1); assert.equal(prop('referencesDexId').value, 4); assert.equal(prop('hitDiceId').constitutionMod, 5); - assert.equal( - prop('parseErrorId').baseValue.errors.length, 1, - 'Parse errors should be added to calculation errors' - ); assert.equal( prop('parseErrorId').baseValue.value, null, 'Parse errors should null the value' diff --git a/app/imports/api/engine/computation/utility/cyrb53.js b/app/imports/api/engine/computation/utility/cyrb53.js new file mode 100644 index 00000000..d93cb1c8 --- /dev/null +++ b/app/imports/api/engine/computation/utility/cyrb53.js @@ -0,0 +1,14 @@ +// Simple hash function from +// https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript +// Don't use for security +export default function(str, seed = 0) { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1>>>16), 2246822507) ^ Math.imul(h2 ^ (h2>>>13), 3266489909); + h2 = Math.imul(h2 ^ (h2>>>16), 2246822507) ^ Math.imul(h1 ^ (h1>>>13), 3266489909); + return 4294967296 * (2097151 & h2) + (h1>>>0); +} diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 28b538c6..e8cb1220 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -24,6 +24,22 @@ function computedOnlyField(field){ optional: true, removeBeforeCompute: true, }, + // A cache of the parse result of the calculation + [`${field}.parseNode`]: { + type: Object, + optional: true, + blackbox: true, + }, + // Set if there was an error parsing the calculation + [`${field}.parseError`]: { + type: ErrorSchema, + optional: true, + }, + // a hash of the calculation to see if the cached values need to be updated + [`${field}.hash`]: { + type: Number, + optional: true, + }, [`${field}.errors`]: { type: Array, optional: true, diff --git a/app/imports/api/properties/subSchemas/inlineCalculationField.js b/app/imports/api/properties/subSchemas/inlineCalculationField.js index 71b49d50..7be26ed7 100644 --- a/app/imports/api/properties/subSchemas/inlineCalculationField.js +++ b/app/imports/api/properties/subSchemas/inlineCalculationField.js @@ -28,6 +28,12 @@ function computedOnlyInlineCalculationField(field){ optional: true, inlineCalculationField: true, }, + // a hash of the text to see if the current cached values need to be updated + [`${field}.hash`]: { + type: String, + optional: true, + max: STORAGE_LIMITS.inlineCalculationField, + }, [`${field}.value`]: { type: String, optional: true, @@ -38,7 +44,6 @@ function computedOnlyInlineCalculationField(field){ type: Array, defaultValue: [], maxCount: STORAGE_LIMITS.inlineCalculationCount, - removeBeforeCompute: true, }, [`${field}.inlineCalculations.$`]: { type: Object, @@ -50,15 +55,34 @@ function computedOnlyInlineCalculationField(field){ type: String, max: STORAGE_LIMITS.calculation, }, + // The result of the calc [`${field}.inlineCalculations.$.value`]: { type: SimpleSchema.oneOf(String, Number), optional: true, max: STORAGE_LIMITS.calculation, + removeBeforeCompute: true, + }, + // A cache of the parse result of the calculation + [`${field}.inlineCalculations.$.parseNode`]: { + type: Object, + optional: true, + blackbox: true, + }, + // Set if there was an error parsing the calculation + [`${field}.inlineCalculations.$.parseError`]: { + type: ErrorSchema, + optional: true, + }, + // a hash of the calculation to see if the cached values need to be updated + [`${field}.inlineCalculations.$.hash`]: { + type: Number, + optional: true, }, [`${field}.inlineCalculations.$.errors`]: { type: Array, optional: true, maxCount: STORAGE_LIMITS.errorCount, + removeBeforeCompute: true, }, [`${field}.inlineCalculations.$.errors.$`]: { type: ErrorSchema, diff --git a/app/imports/parser/parseTree/index.js b/app/imports/parser/parseTree/index.js index 17b00a21..8fbea29d 100644 --- a/app/imports/parser/parseTree/index.js +++ b/app/imports/parser/parseTree/index.js @@ -1,7 +1,7 @@ import resolve, { traverse, toString } from '../resolve'; import error from './error'; -const index = { +const indexNode = { create({array, index}) { return { parseType: 'index', @@ -53,7 +53,7 @@ const index = { } } return { - result: index.create({ + result: indexNode.create({ index, array, }), @@ -70,4 +70,4 @@ const index = { }, } -export default index; +export default indexNode; diff --git a/app/imports/parser/resolve.js b/app/imports/parser/resolve.js index add29f25..2663dbbd 100644 --- a/app/imports/parser/resolve.js +++ b/app/imports/parser/resolve.js @@ -3,6 +3,7 @@ import nodeTypeIndex from './parseTree/_index.js'; // Takes a parse ndoe and computes it to a set detail level // returns {result, context} export default function resolve(fn, node, scope, context = new Context()){ + if (!node) return {result: undefined, context}; let type = nodeTypeIndex[node.parseType]; if (!type){ throw new Meteor.Error(`Parse node type: ${node.parseType} not implemented`); @@ -21,6 +22,7 @@ export default function resolve(fn, node, scope, context = new Context()){ } export function toString(node){ + if (!node) return ''; let type = nodeTypeIndex[node.parseType]; if (!type.toString){ throw new Meteor.Error('toString not implemented on ' + node.parseType); From b9008314a2847bacc39ea4238cb1a853e01e2b4e Mon Sep 17 00:00:00 2001 From: Weslley Rocha Date: Tue, 5 Oct 2021 10:14:40 -0300 Subject: [PATCH 45/85] converting string to number --- app/imports/parser/functions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index 3d5b9c11..bc64957f 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -106,7 +106,7 @@ export default { fn: function tableLookup(arrayNode, number){ for(let i in arrayNode.values){ let node = arrayNode.values[i]; - if (node.value > number) return Number(i); + if (node.value > number) return +i; } return arrayNode.values.length; } From 0097696cc85625d960e0242a8d645e057acdbca5 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 6 Oct 2021 14:57:13 +0200 Subject: [PATCH 46/85] Began merging attacks into actions --- .../buildComputation/linkTypeDependencies.js | 1 - .../computeComputation/computeByType.js | 2 +- app/imports/api/properties/Actions.js | 10 ++++ app/imports/api/properties/Attacks.js | 46 ------------------- .../computedOnlyPropertySchemasIndex.js | 2 - .../computedPropertySchemasIndex.js | 2 - .../api/properties/propertySchemasIndex.js | 2 - .../migrations/server/2.0-beta.33-dbv1.js | 11 +++-- .../components/actions/ActionCard.vue | 7 +-- .../ui/properties/forms/ActionForm.vue | 11 ++--- 10 files changed, 23 insertions(+), 71 deletions(-) delete mode 100644 app/imports/api/properties/Attacks.js diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index ae68ba30..d6e30222 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -1,6 +1,5 @@ const linkDependenciesByType = { action: linkResources, - attack: linkResources, attribute: linkAttribute, class: linkVariableName, classLevel: linkClassLevel, diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index 522a5c5c..45ce9316 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -7,8 +7,8 @@ import container from './computeByType/computeContainer.js'; export default Object.freeze({ _variable, action, - attack: action, attribute, container, slot, + spell: action, }); diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index caa3c51e..fef5eb67 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -40,6 +40,12 @@ let ActionSchema = createPropertySchema({ 'multipleTargets', ], }, + // Some actions have an attack roll + attackRoll: { + type: 'fieldToCompute', + optional: true, + defaultValue: 'strength.modifier + proficiencyBonus', + }, // Calculation of how many times this action can be used uses: { type: 'fieldToCompute', @@ -129,6 +135,10 @@ const ComputedOnlyActionSchema = createPropertySchema({ optional: true, removeBeforeCompute: true, }, + attackRoll: { + type: 'computedOnlyField', + optional: true, + }, uses: { type: 'computedOnlyField', optional: true, diff --git a/app/imports/api/properties/Attacks.js b/app/imports/api/properties/Attacks.js deleted file mode 100644 index ce4a6687..00000000 --- a/app/imports/api/properties/Attacks.js +++ /dev/null @@ -1,46 +0,0 @@ -import SimpleSchema from 'simpl-schema'; -import { ActionSchema, ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; -import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -// Attacks are special instances of actions -let AttackSchema = new SimpleSchema() - .extend(ActionSchema) - .extend(createPropertySchema({ - // What gets added to the d20 roll - rollBonus: { - type: 'fieldToCompute', - optional: true, - defaultValue: 'strength.modifier + proficiencyBonus', - }, - // Set better defaults for the action - actionType: { - type: String, - defaultValue: 'attack', - max: STORAGE_LIMITS.name, - }, - tags: { - type: Array, - defaultValue: ['attack'], - maxCount: STORAGE_LIMITS.tagCount, - }, - 'tags.$': { - type: String, - max: STORAGE_LIMITS.tagLength, - }, - })); - -const ComputedOnlyAttackSchema = new SimpleSchema() - .extend(ComputedOnlyActionSchema) - .extend(createPropertySchema({ - rollBonus: { - type: 'computedOnlyField', - optional: true, - }, - })); - -const ComputedAttackSchema = new SimpleSchema() - .extend(AttackSchema) - .extend(ComputedOnlyAttackSchema); - -export { AttackSchema, ComputedOnlyAttackSchema, ComputedAttackSchema }; diff --git a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js index b01bd38d..7affc14c 100644 --- a/app/imports/api/properties/computedOnlyPropertySchemasIndex.js +++ b/app/imports/api/properties/computedOnlyPropertySchemasIndex.js @@ -1,7 +1,6 @@ import SimpleSchema from 'simpl-schema'; import { ComputedOnlyActionSchema } from '/imports/api/properties/Actions.js'; import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js'; import { ComputedOnlyClassSchema } from '/imports/api/properties/Classes.js'; @@ -29,7 +28,6 @@ import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js'; const propertySchemasIndex = { action: ComputedOnlyActionSchema, adjustment: ComputedOnlyAdjustmentSchema, - attack: ComputedOnlyAttackSchema, attribute: ComputedOnlyAttributeSchema, buff: ComputedOnlyBuffSchema, class: ComputedOnlyClassSchema, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index e9dc3fea..7ede8fd8 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -1,7 +1,6 @@ import SimpleSchema from 'simpl-schema'; import { ComputedActionSchema } from '/imports/api/properties/Actions.js'; import { ComputedAdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { ComputedAttackSchema } from '/imports/api/properties/Attacks.js'; import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js'; import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js'; import { ComputedClassSchema } from '/imports/api/properties/Classes.js'; @@ -29,7 +28,6 @@ import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js'; const propertySchemasIndex = { action: ComputedActionSchema, adjustment: ComputedAdjustmentSchema, - attack: ComputedAttackSchema, attribute: ComputedAttributeSchema, buff: ComputedBuffSchema, class: ComputedClassSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index c4c9eec8..d799559d 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -1,7 +1,6 @@ import SimpleSchema from 'simpl-schema'; import { ActionSchema } from '/imports/api/properties/Actions.js'; import { AdjustmentSchema } from '/imports/api/properties/Adjustments.js'; -import { AttackSchema } from '/imports/api/properties/Attacks.js'; import { AttributeSchema } from '/imports/api/properties/Attributes.js'; import { BuffSchema } from '/imports/api/properties/Buffs.js'; import { ClassSchema } from '/imports/api/properties/Classes.js'; @@ -29,7 +28,6 @@ import { ItemSchema } from '/imports/api/properties/Items.js'; const propertySchemasIndex = { action: ActionSchema, adjustment: AdjustmentSchema, - attack: AttackSchema, attribute: AttributeSchema, buff: BuffSchema, class: ClassSchema, diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js index d5ed8489..eced0aac 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -74,7 +74,9 @@ const transformsByPropType = { ], 'attack': [ ...actionTransforms, - ...getComputedPropertyTransforms('rollBonus'), + ...getComputedPropertyTransforms('rollBonus', 'attackRoll'), + //change type to action + {from: 'type', to: 'type', up: () => 'action'}, ], 'attribute': [ ...getComputedPropertyTransforms('baseValue'), @@ -158,11 +160,12 @@ const transformsByPropType = { ], }; -function getComputedPropertyTransforms(key){ +function getComputedPropertyTransforms(key, toKey){ + if (!toKey) toKey = key; return [ {from: key, to: `${key}.calculation`}, - {from: `${key}Result`, to: `${key}.value`, up: nanToNull}, - {from: `${key}Errors`, to: `${key}.errors`, up: trimErrors}, + {from: `${key}Result`, to: `${toKey}.value`, up: nanToNull}, + {from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors}, ]; } diff --git a/app/imports/ui/properties/components/actions/ActionCard.vue b/app/imports/ui/properties/components/actions/ActionCard.vue index 5ec693fd..05a577b5 100644 --- a/app/imports/ui/properties/components/actions/ActionCard.vue +++ b/app/imports/ui/properties/components/actions/ActionCard.vue @@ -15,7 +15,7 @@ :disabled="model.insufficientResources || !context.editPermission" @click.stop="doAction" > - - @@ -91,9 +81,7 @@ diff --git a/app/imports/ui/properties/forms/AdjustmentForm.vue b/app/imports/ui/properties/forms/AdjustmentForm.vue index 5bc0e484..19387a28 100644 --- a/app/imports/ui/properties/forms/AdjustmentForm.vue +++ b/app/imports/ui/properties/forms/AdjustmentForm.vue @@ -1,25 +1,35 @@ + + + + diff --git a/app/imports/ui/properties/forms/shared/schemaFormMixin.js b/app/imports/ui/properties/forms/shared/schemaFormMixin.js index 525a585c..2cb8b332 100644 --- a/app/imports/ui/properties/forms/shared/schemaFormMixin.js +++ b/app/imports/ui/properties/forms/shared/schemaFormMixin.js @@ -4,14 +4,23 @@ */ import { get, toPath } from 'lodash'; -function resolvePath(model, path){ +function resolvePath(model, path, set){ let arrayPath = toPath(path); if (arrayPath.length === 1){ return { object: model, key: arrayPath[0] }; } - let objectPath = arrayPath.slice(0, -1); let key = arrayPath.slice(-1); - let object = get(model, objectPath); + let objectPath = arrayPath.slice(0, -1); + let object = model; + // Ensure that nested objects exist before navigating them + objectPath.forEach(pathKey => { + let newObject = object[pathKey]; + if (!newObject){ + newObject = {}; + set(object, pathKey, newObject); + } + object = newObject; + }); return {object, key}; } @@ -41,7 +50,8 @@ const schemaFormMixin = { methods: { // Sets the value at the given path change({path, value, ack}){ - let {object, key} = resolvePath(this.model, path); + let {object, key} = resolvePath(this.model, path, this.$set); + this.$set(object, key, value); if (ack) ack(); }, @@ -54,7 +64,7 @@ const schemaFormMixin = { if (ack) ack(); }, pull({path, ack}){ - let {object, key} = resolvePath(this.model, path); + let {object, key} = resolvePath(this.model, path, this.$set); if (!object || !object.splice){ throw `${path.join('.')} is ${object}, doesnt have "splice"` } diff --git a/app/imports/ui/properties/viewers/ActionViewer.vue b/app/imports/ui/properties/viewers/ActionViewer.vue index 7f1baacd..ce094084 100644 --- a/app/imports/ui/properties/viewers/ActionViewer.vue +++ b/app/imports/ui/properties/viewers/ActionViewer.vue @@ -119,7 +119,7 @@ diff --git a/app/imports/ui/tabletop/TabletopRightDrawer.vue b/app/imports/ui/tabletop/TabletopRightDrawer.vue new file mode 100644 index 00000000..9d5fc55f --- /dev/null +++ b/app/imports/ui/tabletop/TabletopRightDrawer.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/app/imports/ui/tabletop/TabletopToolbar.vue b/app/imports/ui/tabletop/TabletopToolbar.vue index 6d735619..6c0d0af3 100644 --- a/app/imports/ui/tabletop/TabletopToolbar.vue +++ b/app/imports/ui/tabletop/TabletopToolbar.vue @@ -1,16 +1,18 @@ diff --git a/app/imports/ui/properties/viewers/SkillViewer.vue b/app/imports/ui/properties/viewers/SkillViewer.vue index 8f78a69b..91c3d47e 100644 --- a/app/imports/ui/properties/viewers/SkillViewer.vue +++ b/app/imports/ui/properties/viewers/SkillViewer.vue @@ -1,84 +1,114 @@ @@ -100,6 +130,15 @@ export default { inject: { context: { default: {} } }, + data(){return { + proficiencyText: { + 0: 'Not proficient', + 1: 'Proficient', + 0.49: 'Half proficiency bonus rounded down', + 0.5: 'Half proficiency bonus rounded up', + 2: 'Double proficiency bonus', + }, + }}, computed: { displayedModifier(){ let mod = this.model.value; @@ -139,23 +178,23 @@ export default { name: 'Skill base value', operation: 'base', calculation: prop.baseValueCalculation, - result: prop.baseValue, + amount: {value: prop.baseValue?.value}, stats: [prop.variableName], ancestors: prop.ancestors, - }) ).filter(effect => effect.result); + }) ).filter(effect => effect.amount?.value); } else { return []; } }, effects(){ - if (this.context.creatureId){ + if (this.context.creatureId && this.model.variableName){ let creatureId = this.context.creatureId; return CreatureProperties.find({ 'ancestors.id': creatureId, stats: this.model.variableName, type: 'effect', removed: {$ne: true}, - }); + }).fetch(); } else { return []; } @@ -189,7 +228,7 @@ export default { type: 'proficiency', removed: {$ne: true}, inactive: {$ne: true}, - }); + }).fetch(); } else { return []; } @@ -211,7 +250,7 @@ export default { _id: abilityProp._id, name: abilityProp.name, operation: 'base', - result: abilityProp.modifier, + amount: {value: abilityProp.modifier}, stats: [this.model.variableName], ancestors: abilityProp.ancestors, } diff --git a/app/imports/ui/properties/viewers/shared/PropertyDescription.vue b/app/imports/ui/properties/viewers/shared/PropertyDescription.vue index 7ea9f5f2..f1a87521 100644 --- a/app/imports/ui/properties/viewers/shared/PropertyDescription.vue +++ b/app/imports/ui/properties/viewers/shared/PropertyDescription.vue @@ -1,21 +1,32 @@ From f2369277f100fe6d10aaaf82f24937598c7e4421 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 18 Oct 2021 11:27:43 +0200 Subject: [PATCH 54/85] Fixed bug from misspelling .overridden as .overriden --- .../computeVariable/aggregate/aggregateDefinition.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index 1d94f61c..66fbe79a 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -3,7 +3,7 @@ export default function aggregateDefinition({node, linkedNode, link}){ // Look at all definition links if (link.data !== 'definition') return; - // Store which property is THE defining property and which are overriden + // Store which property is THE defining property and which are overridden const prop = linkedNode.data; // get current defining prop const definingProp = node.data.definingProp; @@ -28,7 +28,7 @@ export default function aggregateDefinition({node, linkedNode, link}){ function overrideProp(prop, node){ if (!prop) return; - prop.overriden = true; - if (!node.data.overridenProps) node.data.overridenProps = []; - node.data.overridenProps.push(prop); + prop.overridden = true; + if (!node.data.overriddenProps) node.data.overriddenProps = []; + node.data.overriddenProps.push(prop); } From 7a11a4aa2266e0f97a6a78d5cd982ad9f1517b82 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 18 Oct 2021 12:48:26 +0200 Subject: [PATCH 55/85] Improved action viewer, fixed bugs found along the way --- .../applyPropertyByType/applyAction.js | 17 +- app/imports/ui/components/propertyToolbar.vue | 7 +- .../CreaturePropertyDialog.vue | 36 +-- .../components/actions/ItemConsumedView.vue | 17 +- .../ui/properties/viewers/ActionViewer.vue | 214 +++++++++--------- .../ui/properties/viewers/AttributeViewer.vue | 4 +- .../ui/properties/viewers/SkillViewer.vue | 11 +- .../viewers/shared/PropertyDescription.vue | 4 +- .../viewers/shared/PropertyField.vue | 5 +- 9 files changed, 167 insertions(+), 148 deletions(-) diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index 3b6ac32e..776e2f15 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -147,20 +147,23 @@ function spendResources({prop, log, scope}){ if (!item.equipped){ throw 'The selected ammo is not equipped'; } - if (!itemConsumed.quantity) return; + if ( + !itemConsumed.quantity.value || + !isFinite(itemConsumed.quantity.value) + ) return; itemQuantityAdjustments.push({ property: item, operation: 'increment', - value: itemConsumed.quantity, + value: itemConsumed.quantity.value, }); let logName = item.name; - if (itemConsumed.quantity > 1 || itemConsumed.quantity < -1){ + if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){ logName = item.plural || logName; } - if (itemConsumed.quantity > 0){ - spendLog.push(logName + ': ' + itemConsumed.quantity); - } else if (itemConsumed.quantity < 0){ - gainLog.push(logName + ': ' + -itemConsumed.quantity); + if (itemConsumed.quantity.value > 0){ + spendLog.push(logName + ': ' + itemConsumed.quantity.value); + } else if (itemConsumed.quantity.value < 0){ + gainLog.push(logName + ': ' + -itemConsumed.quantity.value); } }); } catch (e){ diff --git a/app/imports/ui/components/propertyToolbar.vue b/app/imports/ui/components/propertyToolbar.vue index c6b474be..32282734 100644 --- a/app/imports/ui/components/propertyToolbar.vue +++ b/app/imports/ui/components/propertyToolbar.vue @@ -102,21 +102,26 @@ /> + + {{ editing ? 'Done' : 'Edit' }} + mdi-check mdi-pencil diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue index 93692c6b..f3dfc0b4 100644 --- a/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue +++ b/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue @@ -41,18 +41,23 @@ This property can't be viewed yet.

- - - + + + +
- mdi-plus - Property + + mdi-plus + + Child Property - Done + Close
diff --git a/app/imports/ui/properties/components/actions/ItemConsumedView.vue b/app/imports/ui/properties/components/actions/ItemConsumedView.vue index 083292e6..3363c5a4 100644 --- a/app/imports/ui/properties/components/actions/ItemConsumedView.vue +++ b/app/imports/ui/properties/components/actions/ItemConsumedView.vue @@ -12,12 +12,11 @@ > + + + + diff --git a/app/imports/ui/properties/forms/DamageForm.vue b/app/imports/ui/properties/forms/DamageForm.vue index 3b3e7c96..b56a913d 100644 --- a/app/imports/ui/properties/forms/DamageForm.vue +++ b/app/imports/ui/properties/forms/DamageForm.vue @@ -19,7 +19,7 @@ cols="12" md="6" > - import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js'; import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js'; -import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue'; export default { - components: { - CalculationErrorList, - }, mixins: [propertyFormMixin], props: { parentTarget: { @@ -90,10 +86,6 @@ export default { self: 'The damage will be applied to the character taking the action', target: 'The damage will be applied to the target of the action', }; - if (this.parentTarget === 'singleTarget'){ - hints.each = hints.target; - hints.every = hints.target; - } return hints[this.model.target]; } }, diff --git a/app/imports/ui/properties/forms/ReferenceForm.vue b/app/imports/ui/properties/forms/ReferenceForm.vue index 95608437..8c580322 100644 --- a/app/imports/ui/properties/forms/ReferenceForm.vue +++ b/app/imports/ui/properties/forms/ReferenceForm.vue @@ -1,9 +1,14 @@ @@ -33,10 +36,12 @@ import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js'; import updateReferenceNode from '/imports/api/library/methods/updateReferenceNode.js'; + import PropertyField from '/imports/ui/properties/viewers/shared/PropertyField.vue'; export default { components: { TreeNodeView, + PropertyField, }, mixins: [propertyFormMixin], data(){return { diff --git a/app/imports/ui/properties/forms/RollForm.vue b/app/imports/ui/properties/forms/RollForm.vue index 27530a93..c0ef80df 100644 --- a/app/imports/ui/properties/forms/RollForm.vue +++ b/app/imports/ui/properties/forms/RollForm.vue @@ -1,23 +1,33 @@ diff --git a/app/imports/ui/properties/viewers/ProficiencyViewer.vue b/app/imports/ui/properties/viewers/ProficiencyViewer.vue index a7c44e1b..0ee43e14 100644 --- a/app/imports/ui/properties/viewers/ProficiencyViewer.vue +++ b/app/imports/ui/properties/viewers/ProficiencyViewer.vue @@ -1,17 +1,25 @@ @@ -27,6 +35,7 @@ export default { computed: { proficiencyText(){ switch (this.model.value){ + case 0.49: return 'Half proficiency bonus rounded down'; case 0.5: return 'Half proficiency bonus'; case 1: return 'Proficient'; case 2: return 'Double proficiency bonus'; diff --git a/app/imports/ui/properties/viewers/ReferenceViewer.vue b/app/imports/ui/properties/viewers/ReferenceViewer.vue index eb70e7a9..9b2c6419 100644 --- a/app/imports/ui/properties/viewers/ReferenceViewer.vue +++ b/app/imports/ui/properties/viewers/ReferenceViewer.vue @@ -1,21 +1,25 @@ diff --git a/app/imports/ui/properties/viewers/RollViewer.vue b/app/imports/ui/properties/viewers/RollViewer.vue index 0cd8e184..d00aeed1 100644 --- a/app/imports/ui/properties/viewers/RollViewer.vue +++ b/app/imports/ui/properties/viewers/RollViewer.vue @@ -1,23 +1,25 @@ diff --git a/app/imports/ui/properties/viewers/SavingThrowViewer.vue b/app/imports/ui/properties/viewers/SavingThrowViewer.vue index 8bd22491..02b4fbcb 100644 --- a/app/imports/ui/properties/viewers/SavingThrowViewer.vue +++ b/app/imports/ui/properties/viewers/SavingThrowViewer.vue @@ -1,14 +1,23 @@ diff --git a/app/imports/ui/properties/viewers/SlotFillerViewer.vue b/app/imports/ui/properties/viewers/SlotFillerViewer.vue index 1da985d0..0834ff2b 100644 --- a/app/imports/ui/properties/viewers/SlotFillerViewer.vue +++ b/app/imports/ui/properties/viewers/SlotFillerViewer.vue @@ -1,37 +1,55 @@ diff --git a/app/imports/ui/properties/viewers/SlotViewer.vue b/app/imports/ui/properties/viewers/SlotViewer.vue index 4e28f038..bdd07cd5 100644 --- a/app/imports/ui/properties/viewers/SlotViewer.vue +++ b/app/imports/ui/properties/viewers/SlotViewer.vue @@ -50,7 +50,7 @@ From 6698d2fd744f05f45d26ebefdb74b2922f337e89 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 18 Nov 2021 16:28:30 +0200 Subject: [PATCH 60/85] Added migration to fix caclulation changes Migrate `.currentValue` to `.value` and `.value` to `.total` --- app/imports/migrations/server/2.0-beta.33-dbv1.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js index 5291e2a1..91b092fd 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -170,7 +170,7 @@ const transformsByPropType = { function getComputedPropertyTransforms(key, toKey){ if (!toKey) toKey = key; return [ - {from: key, to: `${key}.calculation`}, + {from: key, to: `${key}.calculation`, up: calculationUp, down: calculationDown}, {from: `${key}Result`, to: `${toKey}.value`, up: nanToNull}, {from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors}, ]; @@ -179,11 +179,20 @@ function getComputedPropertyTransforms(key, toKey){ function getInlineComputationTransforms(key){ return [ {from: key, to: `${key}.text`}, - {from: `${key}Calculations`, to: `${key}.inlineCalculations`}, + {from: `${key}Calculations`, to: `${key}.inlineCalculations`, up: calculationUp, down: calculationDown}, {from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`}, ]; } +function calculationUp(val){ + if (!val) return val; + return val.replace('.value', '.total').replace('.currentValue', '.value'); +} + +function calculationDown(val){ + return val.replace('.value', '.currentValue').replace('.total', '.value'); +} + function nanToNull(val){ if (Number.isNaN(val)){ return null; From e34f29f95296695250bc42c317779e7d5ae68f30 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 7 Dec 2021 21:05:24 +0200 Subject: [PATCH 61/85] Computations now occupy their own nodes on the dependency graph This mitigates most issues with properties having self-loops, particularly in cases like Strength where the value `strength` is used in the description of Strength --- .../buildComputation/computeInactiveStatus.js | 45 +++-- .../computeToggleDependencies.js | 1 + .../linkCalculationDependencies.js | 15 +- .../buildComputation/linkTypeDependencies.js | 179 ++++++++++++++---- .../parseCalculationFields.js | 9 + .../tests/computeInactiveStatus.testFn.js | 22 +-- .../linkCalculationDependencies.testFn.js | 9 +- .../computeComputation/computeByType.js | 2 + .../computeByType/computeCalculation.js | 6 + .../computeComputation/computeCalculations.js | 13 -- .../tests/computeAttribute.testFn.js | 14 +- .../computation/computeCreatureComputation.js | 18 +- app/imports/api/library/LibraryNodes.js | 9 + app/imports/api/properties/Attributes.js | 3 +- app/imports/api/properties/Buffs.js | 3 +- app/imports/api/properties/SpellLists.js | 6 +- .../migrations/server/2.0-beta.33-dbv1.js | 3 +- 17 files changed, 247 insertions(+), 110 deletions(-) create mode 100644 app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js delete mode 100644 app/imports/api/engine/computation/computeComputation/computeCalculations.js diff --git a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js index b289e1c6..b794a61e 100644 --- a/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js +++ b/app/imports/api/engine/computation/buildComputation/computeInactiveStatus.js @@ -2,32 +2,41 @@ import walkDown from '/imports/api/engine/computation/utility/walkdown.js'; export default function computeInactiveStatus(node){ const prop = node.node; - if (isActive(prop)) return; - // Unequipped items, notes, spells, and actions disable their children, - // but are not disabled themselves - if ( - prop.type !== 'item' && - prop.type !== 'note' && - prop.type !== 'action' && - prop.type !== 'spell' - ){ + if (!isActive(prop)){ + // Mark prop inactive due to self prop.inactive = true; prop.deactivatedBySelf = true; } - // Mark children as inactive due to ancestor - walkDown(node.children, child => { - child.node.inactive = true; - child.node.deactivatedByAncestor = true; - }); + if(!childrenActive(prop)){ + // Mark children as inactive due to ancestor + walkDown(node.children, child => { + child.node.inactive = true; + child.node.deactivatedByAncestor = true; + }); + } } function isActive(prop){ if (prop.disabled) return false; switch (prop.type){ - case 'item': return !!prop.equipped; - case 'spell': return false; - case 'note': return false; - case 'action': return false; + // Unprepared spells are inactive + case 'spell': return !!prop.prepared || !!prop.alwaysPrepared; + default: return true; + } +} + +function childrenActive(prop){ + // Children of disabled properties are always inactive + if (prop.disabled) return false; + switch (prop.type){ + // Only equipped items have active children + case 'item': return !!prop.equipped; + // The children of actions are always inactive + case 'action': return false; + case 'spell': return false; + // The children of notes are always inactive + case 'note': return false; + // Other children are active default: return true; } } diff --git a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js index 6b168c3c..13549e73 100644 --- a/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/computeToggleDependencies.js @@ -11,6 +11,7 @@ export default function computeToggleDependencies(node, dependencyGraph){ ) return; walkDown(node.children, child => { child.node._computationDetails.toggleAncestors.push(prop); + // The child nodes depend on the toggle condition compuation dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 9df70b88..67e4452e 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -4,9 +4,13 @@ import { traverse } from '/imports/parser/resolve.js'; export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { // Store resolved ancestors - let memo = { + const memo = { // ancestors: {} //this gets added if there are resolved ancestors }; + // Add this calculation to the dependency graph + const calcNodeId = `${prop._id}.${calcObj._key}`; + dependencyGraph.addNode(calcNodeId, calcObj); + // Traverse the parsed calculation looking for variable names traverse(calcObj.parseNode, node => { // Skip nodes that aren't symbols or accessors @@ -17,10 +21,15 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop node.name.slice(1), memo, prop, propsById ); if (!ancestorProp) return; - dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); + // Link the ancestor prop as a direct dependency + dependencyGraph.addLink( + calcNodeId, ancestorProp._id, 'ancestorReference' + ); } else { // Link variable name references as variable dependencies - dependencyGraph.addLink(prop._id, node.name, calcObj); + dependencyGraph.addLink( + calcNodeId, node.name, 'variableReference' + ); } }); // Store the resolved ancestors in this calculation's local scope diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index af007302..2d11ae29 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -1,21 +1,111 @@ +import { get } from 'lodash'; + const linkDependenciesByType = { - action: linkResources, + action: linkAction, + adjustment: linkAdjustment, attribute: linkAttribute, + branch: linkBranch, + buff: linkBuff, class: linkVariableName, classLevel: linkClassLevel, constant: linkVariableName, + damage: linkDamage, damageMultiplier: linkDamageMultiplier, - proficiency: linkStats, - effect: linkStats, + effect: linkEffects, + proficiency: linkProficiencies, + roll: linkRoll, + slot: linkSlot, skill: linkSkill, - spell: linkResources, - toggle: linkVariableName, + spell: linkAction, + spellList: linkSpellList, + savingThrow: linkSavingThrow, + toggle: linkToggle, } export default function linkTypeDependencies(dependencyGraph, prop, computation){ linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation); } +function dependOnCalc({dependencyGraph, prop, key}){ + let calc = get(prop, key); + if (!calc) return; + if (calc.type !== '_calculation'){ + console.log(calc); + throw `Expected calculation got ${calc.type}` + } + dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation'); +} + +function linkAction(dependencyGraph, prop, {propsById}){ + // The action depends on its attack roll and uses calculations + dependOnCalc({dependencyGraph, prop, key: 'attackRoll'}); + dependOnCalc({dependencyGraph, prop, key: 'uses'}); + + // Link the resources the action uses + if (!prop.resources) return; + // Link items consumed + prop.resources.itemsConsumed.forEach((itemConsumed, index) => { + if (!itemConsumed.itemId) return; + const item = propsById[itemConsumed.itemId]; + if (!item || item.inactive){ + // Unlink if the item doesn't exist or is inactive + itemConsumed.itemId = undefined; + return; + } + // none of these dependencies are computed, we can use them immediately + itemConsumed.available = item.quantity; + itemConsumed.itemName = item.name; + itemConsumed.itemIcon = item.icon; + itemConsumed.itemColor = item.color; + dependencyGraph.addLink(prop._id, item._id, 'inventory'); + // Link the property to its resource quantity calculation + + dependOnCalc({ + dependencyGraph, + prop, + key: `${prop._id}.resources.itemsConsumed.${index}.quantity`, + }); + }); + // Link attributes consumed + prop.resources.attributesConsumed.forEach((attConsumed, index) => { + if (!attConsumed.variableName) return; + dependencyGraph.addLink(prop._id, attConsumed.variableName, 'resource'); + // Link the property to its resource quantity calculation + dependOnCalc({ + dependencyGraph, + prop, + key: `${prop._id}.resources.attributesConsumed.${index}.quantity`, + }); + }); +} + +function linkAdjustment(dependencyGraph, prop){ + // Adjustment depends on its amount + dependOnCalc({dependencyGraph, prop, key: 'amount'}); +} + +function linkAttribute(dependencyGraph, prop){ + linkVariableName(dependencyGraph, prop); + // Depends on spellSlotLevel + dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'}); + + // Depends on base value + dependOnCalc({dependencyGraph, prop, key: 'baseValue'}); + + // hit dice depend on constitution + if (prop.attributeType === 'hitDice'){ + dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod'); + } +} + +function linkBranch(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'condition'}); +} + +function linkBuff(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'duration'}); +} + function linkClassLevel(dependencyGraph, prop){ // The variableName of the prop depends on the prop if (prop.variableName && prop.level){ @@ -28,6 +118,24 @@ function linkClassLevel(dependencyGraph, prop){ } } +function linkDamage(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'amount'}); +} + +function linkEffects(dependencyGraph, prop){ + // The effect depends on its amount calculation + dependOnCalc({dependencyGraph, prop, key: 'amount'}); + // The stats depend on the effect + prop.stats.forEach(statName => { + if (!statName) return; + dependencyGraph.addLink(statName, prop._id, 'effect'); + }); +} + +function linkRoll(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'roll'}); +} + function linkVariableName(dependencyGraph, prop){ // The variableName of the prop depends on the prop if (prop.variableName){ @@ -35,37 +143,6 @@ function linkVariableName(dependencyGraph, prop){ } } -function linkResources(dependencyGraph, prop, {propsById}){ - if (!prop.resources) return; - prop.resources.itemsConsumed.forEach(itemConsumed => { - if (!itemConsumed.itemId) return; - const item = propsById[itemConsumed.itemId]; - if (!item || item.inactive){ - // Unlink if the item doesn't exist or is inactive - itemConsumed.itemId = undefined; - return; - } - // none of these dependencies are computed, we can use them immediately - itemConsumed.available = item.quantity; - itemConsumed.itemName = item.name; - itemConsumed.itemIcon = item.icon; - itemConsumed.itemColor = item.color; - dependencyGraph.addLink(prop._id, item._id, 'inventory'); - }); - prop.resources.attributesConsumed.forEach(attConsumed => { - if (!attConsumed.variableName) return; - dependencyGraph.addLink(prop._id, attConsumed.variableName, 'resource'); - }); -} - -function linkAttribute(dependencyGraph, prop){ - linkVariableName(dependencyGraph, prop); - // hit dice depend on constitution - if (prop.attributeType === 'hitDice'){ - dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod'); - } -} - function linkDamageMultiplier(dependencyGraph, prop){ prop.damageTypes.forEach(damageType => { // Remove all non-letter characters from the damage name @@ -74,14 +151,18 @@ function linkDamageMultiplier(dependencyGraph, prop){ }); } -function linkStats(dependencyGraph, prop){ - // The stats a prop references depend on that prop - prop.stats.forEach(variableName => { - if (!variableName) return; - dependencyGraph.addLink(variableName, prop._id, prop.type); +function linkProficiencies(dependencyGraph, prop){ + // The stats depend on the proficiency + prop.stats.forEach(statName => { + if (!statName) return; + dependencyGraph.addLink(statName, prop._id, prop.type); }); } +function linkSavingThrow(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'dc'}); +} + function linkSkill(dependencyGraph, prop){ linkVariableName(dependencyGraph, prop); // The prop depends on the variable references as the ability @@ -91,3 +172,19 @@ function linkSkill(dependencyGraph, prop){ // Skills depend on the creature's proficiencyBonus dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); } + +function linkSlot(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'}); + dependOnCalc({dependencyGraph, prop, key: 'slotCondition'}); +} + +function linkSpellList(dependencyGraph, prop){ + dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'}); + dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'}); + dependOnCalc({dependencyGraph, prop, key: 'dc'}); +} + +function linkToggle(dependencyGraph, prop){ + linkVariableName(dependencyGraph, prop); + dependOnCalc({dependencyGraph, prop, key: 'condition'}); +} diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 0daeb792..d49ce9b7 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -26,12 +26,17 @@ function discoverInlineCalculationFields(prop, schemas){ unset(prop, calcKey); return; } + // Has the text, if it matches the existing hash, stop const inlineCalcHash = cyrb53(inlineCalcObj.text); if (inlineCalcHash === inlineCalcObj.hash){ return; } inlineCalcObj.hash = inlineCalcHash; inlineCalcObj.inlineCalculations = []; + // Set the value to the uncomputed string for use in calculations + // It will be re set including the embedded calculation at the end of + // the computation + inlineCalcObj.value = string; let matches = string.matchAll(INLINE_CALCULATION_REGEX); for (let match of matches){ let calculation = match[1]; @@ -63,6 +68,10 @@ function parseAllCalculationFields(prop, schemas){ prop._computationDetails.calculations.push(calcObj); // Store the level to compute down to later calcObj._parseLevel = parseLevel; + // Store the key + calcObj._key = key; + // Set a type + calcObj.type = '_calculation'; // Parse the calculation parseCalculation(calcObj); }); diff --git a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js index ed210c76..c7565156 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/computeInactiveStatus.testFn.js @@ -11,16 +11,16 @@ export default function(){ // Items active('itemUnequippedId', 'Unequipped items should be active'); byAncestor('itemUnequippedChildId', 'Children of unequipped items should be inactive'); - active('itemEquippedId'); - active('itemEquippedChildId'); + active('itemEquippedId', 'Equipped items should be active'); + active('itemEquippedChildId', 'Children of equipped items should be active'); // Spells - active('spellPreparedId'); - active('spellPreparedChildId'); - active('spellAlwaysPreparedId'); - active('spellAlwaysPreparedChildId'); - bySelf('spellUnpreparedId'); - byAncestor('spellUnpreparedChildId'); + active('spellPreparedId', 'Prepared spells should be active'); + byAncestor('spellPreparedChildId', 'Children of prepared spells should be deactivatedByAncestor'); + active('spellAlwaysPreparedId', 'Always prepared spells should be active'); + byAncestor('spellAlwaysPreparedChildId', 'Children of always prepared spells should be deactivatedByAncestor'); + bySelf('spellUnpreparedId', 'Unprepared spells should be deactivatedBySelf'); + byAncestor('spellUnpreparedChildId', 'Children of unprepared spells should be deactivatedByAncestor'); // Notes active('NoteId', 'Notes should be active'); @@ -30,7 +30,7 @@ export default function(){ function assertDeactivatedBySelf(computation, propId, note){ const prop = computation.propsById[propId]; assert.isTrue(prop.deactivatedBySelf, note); - assert.isTrue(prop.inactive, 'The property should be inactive'); + assert.isTrue(prop.inactive, note + '. The property should be inactive'); } function assertDeactivatedByAncestor(computation, propId, note){ @@ -42,8 +42,8 @@ function assertDeactivatedByAncestor(computation, propId, note){ function assertActive(computation, propId, note){ const prop = computation.propsById[propId]; assert.isNotTrue(prop.inactive, note); - assert.isNotTrue(prop.deactivatedBySelf); - assert.isNotTrue(prop.deactivatedBySelf); + assert.isNotTrue(prop.deactivatedBySelf, note); + assert.isNotTrue(prop.deactivatedBySelf, note); } var testProperties = [ diff --git a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js index f432d926..29fd101a 100644 --- a/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js +++ b/app/imports/api/engine/computation/buildComputation/tests/linkCalculationDependencies.testFn.js @@ -6,21 +6,20 @@ export default function(){ const computation = buildComputationFromProps(testProperties); const hasLink = computation.dependencyGraph.hasLink; const prop = (id) => computation.propsById[id]; - assert.isTrue( - !!hasLink('childId', 'spellListId'), + !!hasLink('childId.description.inlineCalculations[0]', 'spellListId'), 'Ancestor references of parent in inline calculations should create dependency' ); assert.isTrue( - !!hasLink('grandchildId', 'spellListId'), + !!hasLink('grandchildId.dc', 'spellListId'), 'References to higher ancestor should create dependency' ); assert.isTrue( - !!hasLink('grandchildId', 'strength'), + !!hasLink('grandchildId.dc', 'strength'), 'Variable references create dependencies' ); assert.isTrue( - !!hasLink('grandchildId', 'wisdom'), + !!hasLink('grandchildId.dc', 'wisdom'), 'Variable references create dependencies even if the attributes don\'t exist' ); assert.equal( diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index e9b7aa23..cba44f28 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -4,9 +4,11 @@ import attribute from './computeByType/computeAttribute.js'; import skill from './computeByType/computeSkill.js'; import slot from './computeByType/computeSlot.js'; import container from './computeByType/computeContainer.js'; +import _calculation from './computeByType/computeCalculation.js'; export default Object.freeze({ _variable, + _calculation, action, attribute, container, diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js new file mode 100644 index 00000000..80dd0746 --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -0,0 +1,6 @@ +import evaluateCalculation from '../../utility/evaluateCalculation.js'; + +export default function computeCalculation(computation, node){ + const calcObj = node.data; + evaluateCalculation(calcObj, computation.scope); +} diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js deleted file mode 100644 index 2346324d..00000000 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ /dev/null @@ -1,13 +0,0 @@ -import embedInlineCalculations from '../utility/embedInlineCalculations.js'; -import evaluateCalculation from '../utility/evaluateCalculation.js'; - -export default function computeCalculations(computation, node){ - if (!node.data) return; - // evaluate all the calculations - node.data._computationDetails?.calculations?.forEach(calcObj => { - evaluateCalculation(calcObj, computation.scope) - }); - node.data._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { - embedInlineCalculations(inlineCalcObj); - }); -} diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js index 49794a02..dfc9fb86 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAttribute.testFn.js @@ -8,13 +8,13 @@ export default function(){ computeCreatureComputation(computation); const prop = id => computation.propsById[id]; const scope = variableName => computation.scope[variableName]; - assert.equal(prop('emptyId').value, 0); - assert.equal(prop('noVariableNameId').value, 8); - assert.equal(prop('strengthId').value, 12); - assert.equal(prop('strengthId').modifier, 1); - assert.equal(scope('strength').modifier, 1); - assert.equal(prop('referencesDexId').value, 4); - assert.equal(prop('hitDiceId').constitutionMod, 5); + assert.equal(prop('emptyId').value, 0, 'calculates empty props to zero'); + assert.equal(prop('noVariableNameId').value, 8, 'Calculates props without a variable name'); + assert.equal(prop('strengthId').value, 12, 'applies base values'); + assert.equal(prop('strengthId').modifier, 1, 'calculates modifiers for basic properties'); + assert.equal(scope('strength').modifier, 1, 'Access properties via variables'); + assert.equal(prop('referencesDexId').value, 4, 'Access variable properties in calculations'); + assert.equal(prop('hitDiceId').constitutionMod, 5, 'Hit dice get constitution modifier'); assert.equal(prop('overriddenDexId').overridden, true, 'override properties with the same variable name'); assert.equal( prop('parseErrorId').baseValue.value, null, diff --git a/app/imports/api/engine/computation/computeCreatureComputation.js b/app/imports/api/engine/computation/computeCreatureComputation.js index d91c8dfc..5eba016d 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.js @@ -1,6 +1,6 @@ -import computeCalculations from '/imports/api/engine/computation/computeComputation/computeCalculations.js'; import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js'; import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js'; +import embedInlineCalculations from './utility/embedInlineCalculations.js'; export default function computeCreatureComputation(computation){ const stack = []; @@ -22,7 +22,7 @@ export default function computeCreatureComputation(computation){ // Mark the object as visited and remove from stack top._visited = true; stack.pop(); - // Compute the top object of the stack + // Compute the top object of the stack compute(computation, top); } else { top._visitedChildren = true; @@ -30,13 +30,14 @@ export default function computeCreatureComputation(computation){ pushDependenciesToStack(top.id, graph, stack); } } + + // Finish the props after the dependency graph has been traversed + computation.props.forEach(finalizeProp); } function compute(computation, node){ // Determine the prop's active status by its toggles computeToggles(computation, node); - computeCalculations(computation, node); - if (node.data) delete node.data._computationDetails; // Compute the property by type computeByType[node.data?.type || '_variable']?.(computation, node); } @@ -46,3 +47,12 @@ function pushDependenciesToStack(nodeId, graph, stack){ stack.push(linkedNode); }, true); } + +function finalizeProp(prop){ + // Embed the inline calculations + prop._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { + embedInlineCalculations(inlineCalcObj); + }); + // Clean up the computation details + delete prop._computationDetails; +} diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index 8b8ec90c..5d04a0e6 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -32,6 +32,15 @@ let LibraryNodeSchema = new SimpleSchema({ type: String, max: STORAGE_LIMITS.tagLength, }, + libraryTags: { + type: Array, + defaultValue: [], + maxCount: STORAGE_LIMITS.tagCount, + }, + 'libraryTags.$': { + type: String, + max: STORAGE_LIMITS.tagLength, + }, icon: { type: storedIconsSchema, optional: true, diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 3c57d982..8bc3a9c8 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -92,7 +92,6 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ // The computed value of the attribute total: { type: SimpleSchema.oneOf(Number, String, Boolean), - defaultValue: 0, optional: true, removeBeforeCompute: true, }, @@ -112,7 +111,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({ // Attributes with proficiency grant it to all skills based on the attribute proficiency: { type: Number, - allowedValues: [0.49, 0.5, 1, 2], + allowedValues: [0, 0.49, 0.5, 1, 2], optional: true, removeBeforeCompute: true, }, diff --git a/app/imports/api/properties/Buffs.js b/app/imports/api/properties/Buffs.js index 6bacabb9..cd964a9d 100644 --- a/app/imports/api/properties/Buffs.js +++ b/app/imports/api/properties/Buffs.js @@ -41,7 +41,6 @@ let ComputedOnlyBuffSchema = createPropertySchema({ type: Number, optional: true, min: 0, - removeBeforeCompute: true, }, appliedBy: { type: Object, @@ -59,7 +58,7 @@ let ComputedOnlyBuffSchema = createPropertySchema({ type: String, max: STORAGE_LIMITS.collectionName, }, -}) +}); const ComputedBuffSchema = new SimpleSchema() .extend(BuffSchema) diff --git a/app/imports/api/properties/SpellLists.js b/app/imports/api/properties/SpellLists.js index a0fe4e59..2ea0405a 100644 --- a/app/imports/api/properties/SpellLists.js +++ b/app/imports/api/properties/SpellLists.js @@ -35,15 +35,15 @@ const ComputedOnlySpellListSchema = new SimpleSchema({ optional: true, }, maxPrepared: { - type: 'fieldToCompute', + type: 'computedOnlyField', optional: true, }, attackRollBonus: { - type: 'fieldToCompute', + type: 'computedOnlyField', optional: true, }, dc: { - type: 'fieldToCompute', + type: 'computedOnlyField', optional: true, }, }); diff --git a/app/imports/migrations/server/2.0-beta.33-dbv1.js b/app/imports/migrations/server/2.0-beta.33-dbv1.js index 91b092fd..53972a54 100644 --- a/app/imports/migrations/server/2.0-beta.33-dbv1.js +++ b/app/imports/migrations/server/2.0-beta.33-dbv1.js @@ -185,11 +185,12 @@ function getInlineComputationTransforms(key){ } function calculationUp(val){ - if (!val) return val; + if (!val || !val.replace) return val; return val.replace('.value', '.total').replace('.currentValue', '.value'); } function calculationDown(val){ + if (!val || !val.replace) return val; return val.replace('.value', '.currentValue').replace('.total', '.value'); } From 6e22e4286f82e4dace1720aaa44fe1805450807a Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 8 Dec 2021 09:23:29 +0200 Subject: [PATCH 62/85] Fixed referencing `variable.description` in a calculation Whether the description's inline calculations are calculated or not is not defined. --- .../computation/buildComputation/parseCalculationFields.js | 7 +++++-- app/imports/parser/parseTree/accessor.js | 7 +++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index d49ce9b7..d8a7cdba 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -26,6 +26,10 @@ function discoverInlineCalculationFields(prop, schemas){ unset(prop, calcKey); return; } + + // Set the value to the uncomputed string for use in calculations + inlineCalcObj.value = string; + // Has the text, if it matches the existing hash, stop const inlineCalcHash = cyrb53(inlineCalcObj.text); if (inlineCalcHash === inlineCalcObj.hash){ @@ -33,10 +37,9 @@ function discoverInlineCalculationFields(prop, schemas){ } inlineCalcObj.hash = inlineCalcHash; inlineCalcObj.inlineCalculations = []; - // Set the value to the uncomputed string for use in calculations + // It will be re set including the embedded calculation at the end of // the computation - inlineCalcObj.value = string; let matches = string.matchAll(INLINE_CALCULATION_REGEX); for (let match of matches){ let calculation = match[1]; diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js index 8c5b3773..855f9044 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.js @@ -16,6 +16,12 @@ const accessor = { value = value[name]; }); let valueType = typeof value; + // If the accessor returns an objet, get the object's value instead + while (valueType === 'object'){ + value = value.value; + valueType = typeof value; + } + // Return a parse node based on the type returned if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){ return { result: constant.create({ @@ -34,6 +40,7 @@ const accessor = { }; } else { context.error(`${node.name} returned an unexpected type`); + context.error(JSON.stringify(value, null, 2)); return { result: accessor.create({ name: node.name, From a58fd8860de7c463ea09d0441796559981c9ae13 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 8 Dec 2021 09:31:02 +0200 Subject: [PATCH 63/85] Fixed descriptions having borders when not appropriate --- app/imports/ui/creature/slots/SlotFillDialog.vue | 1 + .../ui/properties/components/features/FeatureCard.vue | 1 + app/imports/ui/properties/components/persona/NoteCard.vue | 1 + .../ui/properties/viewers/shared/PropertyDescription.vue | 7 ++++++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/imports/ui/creature/slots/SlotFillDialog.vue b/app/imports/ui/creature/slots/SlotFillDialog.vue index d3c86223..6d67cc21 100644 --- a/app/imports/ui/creature/slots/SlotFillDialog.vue +++ b/app/imports/ui/creature/slots/SlotFillDialog.vue @@ -22,6 +22,7 @@ />

diff --git a/app/imports/ui/properties/components/features/FeatureCard.vue b/app/imports/ui/properties/components/features/FeatureCard.vue index 9580db99..0f706b3e 100644 --- a/app/imports/ui/properties/components/features/FeatureCard.vue +++ b/app/imports/ui/properties/components/features/FeatureCard.vue @@ -12,6 +12,7 @@ diff --git a/app/imports/ui/properties/components/persona/NoteCard.vue b/app/imports/ui/properties/components/persona/NoteCard.vue index 0fc91bb8..50ebd270 100644 --- a/app/imports/ui/properties/components/persona/NoteCard.vue +++ b/app/imports/ui/properties/components/persona/NoteCard.vue @@ -12,6 +12,7 @@ diff --git a/app/imports/ui/properties/viewers/shared/PropertyDescription.vue b/app/imports/ui/properties/viewers/shared/PropertyDescription.vue index 2853bbd3..d70969f1 100644 --- a/app/imports/ui/properties/viewers/shared/PropertyDescription.vue +++ b/app/imports/ui/properties/viewers/shared/PropertyDescription.vue @@ -1,6 +1,10 @@ + + diff --git a/app/imports/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/ui/properties/forms/shared/propertyFormIndex.js index 42037893..2e28b01c 100644 --- a/app/imports/ui/properties/forms/shared/propertyFormIndex.js +++ b/app/imports/ui/properties/forms/shared/propertyFormIndex.js @@ -3,6 +3,7 @@ const AdjustmentForm = () => import('/imports/ui/properties/forms/AdjustmentForm const AttackForm = () => import('/imports/ui/properties/forms/AttackForm.vue'); const AttributeForm = () => import('/imports/ui/properties/forms/AttributeForm.vue'); const BuffForm = () => import('/imports/ui/properties/forms/BuffForm.vue'); +const BranchForm = () => import('/imports/ui/properties/forms/BranchForm.vue'); const ClassLevelForm = () => import('/imports/ui/properties/forms/ClassLevelForm.vue'); const ConstantForm = () => import('/imports/ui/properties/forms/ConstantForm.vue'); const ContainerForm = () => import('/imports/ui/properties/forms/ContainerForm.vue'); @@ -30,6 +31,7 @@ export default { attack: AttackForm, attribute: AttributeForm, buff: BuffForm, + branch: BranchForm, constant: ConstantForm, container: ContainerForm, classLevel: ClassLevelForm, diff --git a/app/imports/ui/properties/treeNodeViews/BranchTreeNode.vue b/app/imports/ui/properties/treeNodeViews/BranchTreeNode.vue new file mode 100644 index 00000000..7c0c96e6 --- /dev/null +++ b/app/imports/ui/properties/treeNodeViews/BranchTreeNode.vue @@ -0,0 +1,36 @@ + + + diff --git a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js index 3ce03502..26d52ca0 100644 --- a/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js +++ b/app/imports/ui/properties/treeNodeViews/treeNodeViewIndex.js @@ -1,5 +1,6 @@ import DefaultTreeNode from '/imports/ui/properties/treeNodeViews/DefaultTreeNode.vue'; import AdjustmentTreeNode from '/imports/ui/properties/treeNodeViews/AdjustmentTreeNode.vue'; +import BranchTreeNode from '/imports/ui/properties/treeNodeViews/BranchTreeNode.vue'; import ItemTreeNode from '/imports/ui/properties/treeNodeViews/ItemTreeNode.vue'; import DamageTreeNode from '/imports/ui/properties/treeNodeViews/DamageTreeNode.vue'; import EffectTreeNode from '/imports/ui/properties/treeNodeViews/EffectTreeNode.vue'; @@ -10,6 +11,7 @@ import ReferenceTreeNode from '/imports/ui/properties/treeNodeViews/ReferenceTre export default { default: DefaultTreeNode, adjustment: AdjustmentTreeNode, + branch: BranchTreeNode, classLevel: ClassLevelTreeNode, damage: DamageTreeNode, effect: EffectTreeNode, diff --git a/app/imports/ui/properties/viewers/BranchViewer.vue b/app/imports/ui/properties/viewers/BranchViewer.vue new file mode 100644 index 00000000..bc5c64c2 --- /dev/null +++ b/app/imports/ui/properties/viewers/BranchViewer.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js index 014ad3b1..8845fac6 100644 --- a/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js +++ b/app/imports/ui/properties/viewers/shared/propertyViewerIndex.js @@ -2,6 +2,7 @@ const ActionViewer = () => import ('/imports/ui/properties/viewers/ActionViewer. const AdjustmentViewer = () => import ('/imports/ui/properties/viewers/AdjustmentViewer.vue'); const AttributeViewer = () => import ('/imports/ui/properties/viewers/AttributeViewer.vue'); const BuffViewer = () => import ('/imports/ui/properties/viewers/BuffViewer.vue'); +const BranchViewer = () => import ('/imports/ui/properties/viewers/BranchViewer.vue'); const ContainerViewer = () => import ('/imports/ui/properties/viewers/ContainerViewer.vue'); const ClassLevelViewer = () => import ('/imports/ui/properties/viewers/ClassLevelViewer.vue'); const ConstantViewer = () => import ('/imports/ui/properties/viewers/ConstantViewer.vue'); @@ -28,6 +29,7 @@ export default { adjustment: AdjustmentViewer, attribute: AttributeViewer, buff: BuffViewer, + branch: BranchViewer, container: ContainerViewer, class: SlotViewer, classLevel: ClassLevelViewer, diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js index 30bd9540..347f3eb7 100644 --- a/app/imports/ui/router.js +++ b/app/imports/ui/router.js @@ -269,8 +269,7 @@ RouterFactory.configure(router => { function redirectIfMaintenance(to, from, next){ if (!MAINTENANCE_MODE) return next(); - console.log(to); - if (to?.path === '/admin' || to?.path === '/maintenance') return next(); + if (to?.path === '/admin' || to?.path === '/maintenance' || to?.path === '/sign-in') return next(); Tracker.autorun((computation) => { if (userSubscription.ready()){ computation.stop(); From 3948d20f4678d4f09aaa00056a0de80dbdb39ea1 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 10 Feb 2022 10:37:23 +0200 Subject: [PATCH 81/85] Allowed cross origin requests to REST API --- app/imports/server/config/SimpleRestConfig.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/imports/server/config/SimpleRestConfig.js b/app/imports/server/config/SimpleRestConfig.js index a4d746cf..c633c6a2 100644 --- a/app/imports/server/config/SimpleRestConfig.js +++ b/app/imports/server/config/SimpleRestConfig.js @@ -1,5 +1,18 @@ +import { JsonRoutes } from 'meteor/simple:json-routes'; import { SimpleRest } from 'meteor/simple:rest'; +Meteor.startup(() => { + // + // Enable cross origin requests for all endpoints + JsonRoutes.setResponseHeaders({ + 'Cache-Control': 'no-store', + Pragma: 'no-cache', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, PUT, POST, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With', + }); +}); + SimpleRest.configure({ // No default collection methods get end points collections: [], From 359f18988cb2feed51821d909590ac02ee2a3403 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 10 Feb 2022 19:02:18 +0200 Subject: [PATCH 82/85] Account functionality extended, API authentication implemented - Can now add a second email address to your account and delete one of your email addresses - Reset password now works - Resetting the password of an account without a password set will set one - Email templates overhauled - Login tokens limited to close previously devastating ($800 database bill) security hole - Login with REST API now works - Once logged in, authentication of API calls with token works - Creatures can now be fetched using the API --- app/.meteor/packages | 2 + app/.meteor/versions | 2 + .../writeComputation/writeScope.js | 5 + app/imports/api/sharing/sharingPermissions.js | 2 +- app/imports/api/users/Users.js | 5 +- app/imports/api/users/methods/addEmail.js | 34 ++++ .../users/{ => methods}/deleteMyAccount.js | 2 +- .../users/{ => methods}/linkWithPatreon.js | 0 app/imports/api/users/methods/removeEmail.js | 37 ++++ app/imports/server/config/SimpleRestConfig.js | 18 +- .../server/config/accountsEmailConfig.js | 31 ++++ app/imports/server/config/emailTemplate.js | 167 ++++++++++++++++++ app/imports/server/config/limitLoginTokens.js | 21 +++ .../server/publications/singleCharacter.js | 6 +- .../server/rest/apiPublications/creature.js | 46 +++++ .../server/rest/apiPublications/index.js | 1 + app/imports/server/rest/index.js | 2 + .../middleware/authenticateUserByToken.js | 57 ++++++ app/imports/server/rest/restLogin.js | 85 +++++++++ app/imports/ui/pages/Account.vue | 89 +++++++++- .../ui/pages/EmailVerificationError.vue | 30 ++++ .../ui/pages/EmailVerificationSuccess.vue | 24 +++ app/imports/ui/pages/ResetPassword.vue | 139 +++++++++++++++ app/imports/ui/pages/SignIn.vue | 7 +- app/imports/ui/router.js | 45 +++++ app/imports/ui/user/TierTooLowDialog.vue | 2 +- app/server/main.js | 4 +- 27 files changed, 852 insertions(+), 11 deletions(-) create mode 100644 app/imports/api/users/methods/addEmail.js rename app/imports/api/users/{ => methods}/deleteMyAccount.js (96%) rename app/imports/api/users/{ => methods}/linkWithPatreon.js (100%) create mode 100644 app/imports/api/users/methods/removeEmail.js create mode 100644 app/imports/server/config/emailTemplate.js create mode 100644 app/imports/server/config/limitLoginTokens.js create mode 100644 app/imports/server/rest/apiPublications/creature.js create mode 100644 app/imports/server/rest/apiPublications/index.js create mode 100644 app/imports/server/rest/index.js create mode 100644 app/imports/server/rest/middleware/authenticateUserByToken.js create mode 100644 app/imports/server/rest/restLogin.js create mode 100644 app/imports/ui/pages/EmailVerificationError.vue create mode 100644 app/imports/ui/pages/EmailVerificationSuccess.vue create mode 100644 app/imports/ui/pages/ResetPassword.vue diff --git a/app/.meteor/packages b/app/.meteor/packages index 1df88a4f..8a53403c 100644 --- a/app/.meteor/packages +++ b/app/.meteor/packages @@ -45,3 +45,5 @@ akryum:vue-router2 percolate:migrations meteortesting:mocha ostrio:files +simple:rest-bearer-token-parser +simple:rest-json-error-handler diff --git a/app/.meteor/versions b/app/.meteor/versions index 00189406..e9557bec 100644 --- a/app/.meteor/versions +++ b/app/.meteor/versions @@ -108,6 +108,8 @@ sha@1.0.9 shell-server@0.5.0 simple:json-routes@2.3.1 simple:rest@1.2.1 +simple:rest-bearer-token-parser@1.1.1 +simple:rest-json-error-handler@1.1.1 simple:rest-method-mixin@1.1.0 socket-stream-client@0.4.0 spacebars-compiler@1.3.0 diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index 3cb0027b..3661df4a 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -1,5 +1,10 @@ import Creatures from '/imports/api/creature/creatures/Creatures.js'; export default function writeScope(creatureId, scope){ + // Remove large properties that aren't likely to be accessed + for (const key in scope){ + delete scope[key].parent; + delete scope[key].ancestors; + } Creatures.update(creatureId, {$set: {variables: scope}}); } diff --git a/app/imports/api/sharing/sharingPermissions.js b/app/imports/api/sharing/sharingPermissions.js index 16831209..6f48ed4b 100644 --- a/app/imports/api/sharing/sharingPermissions.js +++ b/app/imports/api/sharing/sharingPermissions.js @@ -4,7 +4,7 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; function assertIdValid(userId){ if (!userId || typeof userId !== 'string'){ throw new Meteor.Error('Permission denied', - 'No user ID given for edit permission check'); + 'No user ID. Are you logged in?'); } } diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index 268b8d99..be3fd3a8 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -1,7 +1,10 @@ import SimpleSchema from 'simpl-schema'; import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import '/imports/api/users/deleteMyAccount.js'; +import '/imports/api/users/methods/deleteMyAccount.js'; +import '/imports/api/users/methods/addEmail.js'; +import '/imports/api/users/methods/removeEmail.js'; + import { some } from 'lodash'; const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || []; diff --git a/app/imports/api/users/methods/addEmail.js b/app/imports/api/users/methods/addEmail.js new file mode 100644 index 00000000..af8e81f6 --- /dev/null +++ b/app/imports/api/users/methods/addEmail.js @@ -0,0 +1,34 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const addEmail = new ValidatedMethod({ + name: 'users.addEmail', + validate: new SimpleSchema({ + email: { + type: String, + regEx: SimpleSchema.RegEx.Email, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({email}){ + const userId = Meteor.userId(); + const user = Meteor.users.findOne(userId); + if (!user) throw new Meteor.Error('No user', + 'You must be logged in to add an email address'); + if (user.emails && user.emails.length >= 2){ + throw new Meteor.Error('Emails full', + 'You may only have up to 2 email addresses per account'); + } + if (Meteor.isServer){ + Accounts.addEmail(userId, email); + Accounts.sendVerificationEmail(userId, email); + } + } +}); + +export default addEmail; diff --git a/app/imports/api/users/deleteMyAccount.js b/app/imports/api/users/methods/deleteMyAccount.js similarity index 96% rename from app/imports/api/users/deleteMyAccount.js rename to app/imports/api/users/methods/deleteMyAccount.js index d887b8c0..f6947bfc 100644 --- a/app/imports/api/users/deleteMyAccount.js +++ b/app/imports/api/users/methods/deleteMyAccount.js @@ -15,7 +15,7 @@ Meteor.users.deleteMyAccount = new ValidatedMethod({ run(){ let userId = Meteor.userId(); if (!userId) throw new Meteor.Error('No user', - 'You must be logged into to delete your account'); + 'You must be logged in to delete your account'); // Delete all creatures let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch(); diff --git a/app/imports/api/users/linkWithPatreon.js b/app/imports/api/users/methods/linkWithPatreon.js similarity index 100% rename from app/imports/api/users/linkWithPatreon.js rename to app/imports/api/users/methods/linkWithPatreon.js diff --git a/app/imports/api/users/methods/removeEmail.js b/app/imports/api/users/methods/removeEmail.js new file mode 100644 index 00000000..86ffb822 --- /dev/null +++ b/app/imports/api/users/methods/removeEmail.js @@ -0,0 +1,37 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; + +const removeEmail = new ValidatedMethod({ + name: 'users.removeEmail', + validate: new SimpleSchema({ + email: { + type: String, + regEx: SimpleSchema.RegEx.Email, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 1, + timeInterval: 5000, + }, + run({email}){ + const userId = Meteor.userId(); + const user = Meteor.users.findOne(userId); + if (!user) throw new Meteor.Error('No user', + 'You must be logged in to remove an email address'); + if (!user.emails){ + throw new Meteor.Error('No email to remove', + 'No email addresses are associated with this account'); + } + if (user.emails.length == 1){ + throw new Meteor.Error('Can\'t remove last email', + 'You may not remove the last email address from your account'); + } + if (Meteor.isServer){ + Accounts.removeEmail(userId, email); + } + } +}); + +export default removeEmail; diff --git a/app/imports/server/config/SimpleRestConfig.js b/app/imports/server/config/SimpleRestConfig.js index c633c6a2..72f63b40 100644 --- a/app/imports/server/config/SimpleRestConfig.js +++ b/app/imports/server/config/SimpleRestConfig.js @@ -1,8 +1,7 @@ -import { JsonRoutes } from 'meteor/simple:json-routes'; +import { JsonRoutes, RestMiddleware } from 'meteor/simple:json-routes'; import { SimpleRest } from 'meteor/simple:rest'; Meteor.startup(() => { - // // Enable cross origin requests for all endpoints JsonRoutes.setResponseHeaders({ 'Cache-Control': 'no-store', @@ -17,3 +16,18 @@ SimpleRest.configure({ // No default collection methods get end points collections: [], }); + +// All errors are handled as JSON +JsonRoutes.ErrorMiddleware.use(RestMiddleware.handleErrorAsJson); + +// Hack to stop simple:rest adding routes automatically unless their URL +// has been explicitly set to 'api/...' +const oldAdd = JsonRoutes.add; +JsonRoutes.add = function(method, path, handler){ + if (path.substring(0,4) !== 'api/'){ + return; + } + oldAdd(method, path, handler); +} + +import '/imports/server/rest/restLogin.js'; diff --git a/app/imports/server/config/accountsEmailConfig.js b/app/imports/server/config/accountsEmailConfig.js index b1526d82..e6853515 100644 --- a/app/imports/server/config/accountsEmailConfig.js +++ b/app/imports/server/config/accountsEmailConfig.js @@ -1,4 +1,35 @@ import { Accounts } from 'meteor/accounts-base' +import emailTemplate from './emailTemplate.js'; Accounts.emailTemplates.from = 'no-reply@dicecloud.com'; Accounts.emailTemplates.siteName = 'DiceCloud'; + +Accounts.emailTemplates.enrollAccount = { + subject: () => 'DiceCloud Invite', + html: (user, url) => emailTemplate({ + heading: 'DiceCloud Invite', + text: 'You have been invited to DiceCloud, click the button below to begin.', + buttonText: 'Get Started', + url: url.replace( '#/', '' ), + }), +}; + +Accounts.emailTemplates.resetPassword = { + subject: () => 'DiceCloud Password Reset', + html: (user, url) => emailTemplate({ + heading: 'Password Reset', + text: 'If you did not request this password reset, please ignore this email.', + buttonText: 'Reset Password', + url: url.replace( '#/', '' ), + }), +}; + +Accounts.emailTemplates.verifyEmail = { + subject: () => 'DiceCloud Email Verification', + html: (user, url) => emailTemplate({ + heading: 'DiceCloud Email Verification', + text: 'Click below to verify your email address', + buttonText: 'Verify Email', + url: url.replace( '#/', '' ), + }), +}; diff --git a/app/imports/server/config/emailTemplate.js b/app/imports/server/config/emailTemplate.js new file mode 100644 index 00000000..80f6d6a4 --- /dev/null +++ b/app/imports/server/config/emailTemplate.js @@ -0,0 +1,167 @@ +export default function emailTemplate({heading, text, buttonText, url}){ + return ` + + + + + + + + + + + + + + + + + + + + +` +} diff --git a/app/imports/server/config/limitLoginTokens.js b/app/imports/server/config/limitLoginTokens.js new file mode 100644 index 00000000..594cb14c --- /dev/null +++ b/app/imports/server/config/limitLoginTokens.js @@ -0,0 +1,21 @@ +const MAX_LOGIN_TOKENS = 20; + +Accounts._insertHashedLoginToken = function(userId, hashedToken, query) { + query = query ? { ...query } : {}; + query._id = userId; + const user = Accounts.users.findOne(query); + let loginTokenLength = user?.services?.resume?.loginTokens?.length; + while (loginTokenLength >= MAX_LOGIN_TOKENS){ + loginTokenLength -=1; + Accounts.users.update(query, { + $pop: { + 'services.resume.loginTokens': -1 + } + }); + } + Accounts.users.update(query, { + $addToSet: { + 'services.resume.loginTokens': hashedToken + } + }); +}; diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js index 69fff4f7..611f1d23 100644 --- a/app/imports/server/publications/singleCharacter.js +++ b/app/imports/server/publications/singleCharacter.js @@ -14,7 +14,11 @@ let schema = new SimpleSchema({ }); Meteor.publish('singleCharacter', function(creatureId){ - schema.validate({ creatureId }); + try { + schema.validate({ creatureId }); + } catch (e){ + this.error(e); + } this.autorun(function (computation){ let userId = this.userId; let creatureCursor diff --git a/app/imports/server/rest/apiPublications/creature.js b/app/imports/server/rest/apiPublications/creature.js new file mode 100644 index 00000000..f805c92b --- /dev/null +++ b/app/imports/server/rest/apiPublications/creature.js @@ -0,0 +1,46 @@ +import SimpleSchema from 'simpl-schema'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; +import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; +import computeCreature from '/imports/api/engine/computeCreature.js'; +import VERSION from '/imports/constants/VERSION.js'; + +Meteor.publish('api-creature', function(creatureId){ + try { + new SimpleSchema({ + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validate({ creatureId }); + } catch (e){ + this.error(e); + return; + } + const userId = this.userId; + const creatureCursor = Creatures.find({ + _id: creatureId, + }); + const creature = creatureCursor.fetch()[0]; + try { + assertViewPermission(creature, userId) + } catch(e){ + this.error(e); + return; + } + if (creature.computeVersion !== VERSION){ + try { + computeCreature(creatureId) + } catch(e){ + console.error(e) + } + } + return [ + creatureCursor, + CreatureProperties.find({ + 'ancestors.id': creatureId, + }), + ]; +}, { + url: 'api/creature/:0' +}); diff --git a/app/imports/server/rest/apiPublications/index.js b/app/imports/server/rest/apiPublications/index.js new file mode 100644 index 00000000..765d7988 --- /dev/null +++ b/app/imports/server/rest/apiPublications/index.js @@ -0,0 +1 @@ +import './creature.js'; diff --git a/app/imports/server/rest/index.js b/app/imports/server/rest/index.js new file mode 100644 index 00000000..b552e02a --- /dev/null +++ b/app/imports/server/rest/index.js @@ -0,0 +1,2 @@ +import './restLogin.js'; +import './apiPublications/index.js'; diff --git a/app/imports/server/rest/middleware/authenticateUserByToken.js b/app/imports/server/rest/middleware/authenticateUserByToken.js new file mode 100644 index 00000000..576f5430 --- /dev/null +++ b/app/imports/server/rest/middleware/authenticateUserByToken.js @@ -0,0 +1,57 @@ +var Fiber = Npm.require('fibers'); +import { RestMiddleware } from 'meteor/simple:json-routes'; + +/** + * SimpleRest middleware for validating a Meteor.user's login token + * + * This middleware must be processed after the request.token has been set to a + * valid login token for a Meteor.user account (from a separate layer of + * middleware). If authentication is successful, the request.userId will be set + * to the ID of the authenticated user. An invalid token will result in a error. + * + * @middleware + */ +const authenticateMeteorUserByToken = + function (req, res, next) { + Fiber(function () { + let userId; + try { + userId = getUserIdFromAuthToken(req.authToken); + } catch (e){ + RestMiddleware.handleErrorAsJson(e, req, res, next); + return; + } + if (userId) { + req.userId = userId; + } + + next(); + }).run(); + }; + +/** + * Retrieves the ID of the Meteor.user that the given auth token belongs to + * + * @param token An unhashed auth token + * @returns {String} The ID of the authenticated Meteor.user, or null if token + * is invalid + */ +function getUserIdFromAuthToken(token) { + if (!token) { + return null; + } + + var user = Meteor.users.findOne({ + 'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(token), + }); + if (user) { + return user._id; + } else { + const error = new Meteor.Error('Permission denied', + 'Invalid authentication token'); + error.statusCode = 403; + throw error; + } +} + +export default authenticateMeteorUserByToken; diff --git a/app/imports/server/rest/restLogin.js b/app/imports/server/rest/restLogin.js new file mode 100644 index 00000000..1099bc37 --- /dev/null +++ b/app/imports/server/rest/restLogin.js @@ -0,0 +1,85 @@ +import { JsonRoutes } from 'meteor/simple:json-routes'; +import authenticateMeteorUserByToken from './middleware/authenticateUserByToken.js'; +/** + * Login with username/email and password: + * POST /api/login + * Body: {"username": "", "password": ""} + * Alternative Body: {"email": "", "password": ""} + * Successful response: + * { + * "id": "", + * "token": "", + * "tokenExpires": "" + * } + * + * Warning: Your token may expire before the given date. + * Since each user has a limited pool of login tokens. If you get a permission + * error, you may need to login again to refresh your token + * + * Once you have your token, you can use it as a standard bearer token header + * in other API endpoints: + * HTTP.post("/methods/return-five-auth", { + * headers: { Authorization: "Bearer " } + * }, callback); +**/ + +JsonRoutes.Middleware.use(JsonRoutes.Middleware.parseBearerToken); +JsonRoutes.Middleware.use(authenticateMeteorUserByToken); + +JsonRoutes.add('options', 'api/login', function (req, res) { + JsonRoutes.sendResult(res); +}); + +JsonRoutes.add('post', 'api/login', function (req, res) { + var options = req.body; + + var user; + if (options.email) { + check(options, { + email: String, + password: String, + }); + user = Accounts.findUserByEmail(options.email); + } else { + check(options, { + username: String, + password: String, + }); + user = Accounts.findUserByUsername(options.username); + } + + if (!user) { + throw new Meteor.Error('not-found', + 'User with that username or email address not found.'); + } + + var result = Accounts._checkPassword(user, options.password); + check(result, { + userId: String, + error: Match.Optional(Meteor.Error), + }); + + if (result.error) { + throw result.error; + } + + var stampedLoginToken = Accounts._generateStampedLoginToken(); + check(stampedLoginToken, { + token: String, + when: Date, + }); + + Accounts._insertLoginToken(result.userId, stampedLoginToken); + + var tokenExpiration = Accounts._tokenExpiration(stampedLoginToken.when); + check(tokenExpiration, Date); + + JsonRoutes.sendResult(res, { + data: { + id: result.userId, + token: stampedLoginToken.token, + tokenExpires: tokenExpiration, + }, + }); + +}); diff --git a/app/imports/ui/pages/Account.vue b/app/imports/ui/pages/Account.vue index 122ae952..bbd25a06 100644 --- a/app/imports/ui/pages/Account.vue +++ b/app/imports/ui/pages/Account.vue @@ -62,10 +62,60 @@ v-for="email in emails" :key="email.address" > + + + mdi-delete + + {{ email.address }} + + + {{ removeEmailError }} + + + + + + mdi-close + + + mdi-send + + + + mdi-plus + + Patreon @@ -167,8 +217,10 @@ import router from '/imports/ui/router.js'; import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js'; import Invites from '/imports/api/users/Invites.js'; - import linkWithPatreon from '/imports/api/users/linkWithPatreon.js' + import linkWithPatreon from '/imports/api/users/methods/linkWithPatreon.js' import { getUserTier } from '/imports/api/users/patreon/tiers.js'; + import addEmail from '/imports/api/users/methods/addEmail.js'; + import removeEmail from '/imports/api/users/methods/removeEmail.js'; export default { meteor: { @@ -216,6 +268,14 @@ linkPatreonError: '', updatePatreonError: '', updatePatreonLoading: false, + // Add email + showEmailInput: false, + addEmailLoading: false, + inputEmail: '', + addEmailError: undefined, + // Remove email + removeEmailLoading: undefined, + removeEmailError: undefined, }}, computed: { entitledCents(){ @@ -233,6 +293,33 @@ elementId: 'username', }); }, + clearEmailInput(){ + this.showEmailInput = false; + this.addEmailError = undefined; + this.inputEmail = ''; + }, + addEmail(){ + this.addEmailLoading = true; + addEmail.call({email: this.inputEmail}, error => { + this.addEmailError = error && error.message; + this.addEmailLoading = false; + if (!error){ + this.showEmailInput = false; + this.inputEmail = ''; + } + }); + }, + removeEmail(address){ + this.removeEmailLoading = address; + removeEmail.call({email: address}, error => { + this.removeEmailError = error && error.message; + this.removeEmailLoading = undefined; + if (!error){ + this.showEmailInput = false; + this.inputEmail = ''; + } + }); + }, signOut(){ Meteor.logout(); router.push('/'); diff --git a/app/imports/ui/pages/EmailVerificationError.vue b/app/imports/ui/pages/EmailVerificationError.vue new file mode 100644 index 00000000..e01d0090 --- /dev/null +++ b/app/imports/ui/pages/EmailVerificationError.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/app/imports/ui/pages/EmailVerificationSuccess.vue b/app/imports/ui/pages/EmailVerificationSuccess.vue new file mode 100644 index 00000000..759e998f --- /dev/null +++ b/app/imports/ui/pages/EmailVerificationSuccess.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/app/imports/ui/pages/ResetPassword.vue b/app/imports/ui/pages/ResetPassword.vue new file mode 100644 index 00000000..f901e3a5 --- /dev/null +++ b/app/imports/ui/pages/ResetPassword.vue @@ -0,0 +1,139 @@ + + + diff --git a/app/imports/ui/pages/SignIn.vue b/app/imports/ui/pages/SignIn.vue index 5bb545cd..87f83356 100644 --- a/app/imports/ui/pages/SignIn.vue +++ b/app/imports/ui/pages/SignIn.vue @@ -33,7 +33,10 @@ required @keyup.enter="submit" /> - + Reset Password

@@ -50,7 +53,7 @@ Register diff --git a/app/imports/ui/router.js b/app/imports/ui/router.js index 347f3eb7..f8fe9442 100644 --- a/app/imports/ui/router.js +++ b/app/imports/ui/router.js @@ -19,6 +19,9 @@ const Feedback = () => import('/imports/ui/pages/Feedback.vue' ); const Account = () => import('/imports/ui/pages/Account.vue' ); const InviteSuccess = () => import('/imports/ui/pages/InviteSuccess.vue' ); const InviteError = () => import('/imports/ui/pages/InviteError.vue' ); +const EmailVerificationSuccess = () => import('/imports/ui/pages/EmailVerificationSuccess.vue' ); +const EmailVerificationError = () => import('/imports/ui/pages/EmailVerificationError.vue' ); +const ResetPassword = () => import('/imports/ui/pages/ResetPassword.vue' ); const NotImplemented = () => import('/imports/ui/pages/NotImplemented.vue'); const PatreonLevelTooLow = () => import('/imports/ui/pages/PatreonLevelTooLow.vue'); const Tabletops = () => import('/imports/ui/pages/Tabletops.vue'); @@ -94,6 +97,17 @@ function claimInvite(to, from, next){ }); } +function verifyEmail(to, from, next){ + const token = to.params.token; + Accounts.verifyEmail(token, error => { + if (error){ + next({name: 'emailVerificationError', params: {error}}); + } else { + next('/email-verification-success') + } + }); +} + RouterFactory.configure(router => { router.addRoutes([{ path: '/', @@ -213,6 +227,9 @@ RouterFactory.configure(router => { },{ path: '/invite/:inviteToken', beforeEnter: claimInvite, + },{ + path: '/verify-email/:token', + beforeEnter: verifyEmail, },{ name: 'inviteError', path: '/invite-error', @@ -233,6 +250,34 @@ RouterFactory.configure(router => { meta: { title: 'Invite Success', }, + },{ + name: 'emailVerificationError', + path: '/email-verification-error', + components: { + default: EmailVerificationError, + }, + props: { + default: true, + }, + meta: { + title: 'Email Verification Error', + }, + },{ + path: '/email-verification-success', + components: { + default: EmailVerificationSuccess, + }, + meta: { + title: 'Email Verification Success', + }, + },{ + path: '/reset-password/:token?', + components: { + default: ResetPassword, + }, + meta: { + title: 'Reset Password', + }, },{ path: '/patreon-level-too-low', components: { diff --git a/app/imports/ui/user/TierTooLowDialog.vue b/app/imports/ui/user/TierTooLowDialog.vue index d1831c81..bd6eeb9a 100644 --- a/app/imports/ui/user/TierTooLowDialog.vue +++ b/app/imports/ui/user/TierTooLowDialog.vue @@ -48,7 +48,7 @@ diff --git a/app/imports/ui/properties/forms/shared/schemaFormMixin.js b/app/imports/ui/properties/forms/shared/schemaFormMixin.js index 2cb8b332..e1d3a349 100644 --- a/app/imports/ui/properties/forms/shared/schemaFormMixin.js +++ b/app/imports/ui/properties/forms/shared/schemaFormMixin.js @@ -57,10 +57,14 @@ const schemaFormMixin = { }, push({path, value, ack}){ let array = get(this.model, path); - if (!array || !array.join){ - throw `${path.join('.')} is ${array}, doesn't have "join"` + if (array === undefined){ + let {object, key} = resolvePath(this.model, path, this.$set); + this.$set(object, key, [value]); + } else if (!array.push){ + throw `${path.join('.')} is ${array}, doesn't have "push"` + } else { + array.push(value); } - array.push(value); if (ack) ack(); }, pull({path, ack}){ diff --git a/app/imports/ui/properties/treeNodeViews/EffectTreeNode.vue b/app/imports/ui/properties/treeNodeViews/EffectTreeNode.vue index d56a48f0..60d22b14 100644 --- a/app/imports/ui/properties/treeNodeViews/EffectTreeNode.vue +++ b/app/imports/ui/properties/treeNodeViews/EffectTreeNode.vue @@ -14,15 +14,21 @@ +
@@ -47,19 +53,32 @@ export default { displayedValue(){ let value = this.resolvedValue; switch(this.model.operation) { - case 'base': return value; - case 'add': return isFinite(value) ? Math.abs(value) : value; + case 'base': return value || 0; + case 'add': return isFinite(value) ? Math.abs(value) : value || 0; case 'mul': return value; case 'min': return value; case 'max': return value; case 'advantage': return; case 'disadvantage': return; - case 'passiveAdd': return isFinite(value) ? Math.abs(value) : value; + case 'passiveAdd': return isFinite(value) ? Math.abs(value) : value || 0; case 'fail': return; case 'conditional': return; default: return undefined; } }, + displayedStats(){ + if (!this.model.stats) return 'Selected stats'; + return this.model.stats.join(', '); + }, + displayedTags(){ + if (!this.model.targetTags) return 'Selected tags'; + const tags = this.model.targetTags.join(', '); + if (!this.model.extraTags) return tags; + const extraTags = this.model.extraTags.map(ex => { + return ` ${ex.operation} ${ex.tags.join(', ')}` + }).join(' '); + return tags + extraTags; + } } } diff --git a/app/imports/ui/properties/viewers/EffectViewer.vue b/app/imports/ui/properties/viewers/EffectViewer.vue index 64159818..dadcde7f 100644 --- a/app/imports/ui/properties/viewers/EffectViewer.vue +++ b/app/imports/ui/properties/viewers/EffectViewer.vue @@ -17,18 +17,54 @@ +
+ + {{ tag }} + +
+ + {{ ex.operation }} + + + {{ extraTag }} + +
+
+
+ + > + + {{ stat }} + + From 378da71f5dee67347b82039815c4e8035ac5a129 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 15 Feb 2022 15:59:41 +0200 Subject: [PATCH 84/85] Effects targeting calculations by tag now work in the engine and actions --- .../api/engine/actions/applyProperty.js | 5 +- .../applyPropertyByType/applyDamage.js | 8 +- .../applyEffectsToCalculationParseNode.js | 24 +++ .../shared/recalculateCalculation.js | 2 + .../engine/computation/CreatureComputation.js | 12 ++ .../buildComputation/linkTypeDependencies.js | 69 ++++++++- .../parseCalculationFields.js | 5 +- .../computeByType/computeCalculation.js | 42 +++++ .../computeByType/computeVariable.js | 2 +- .../aggregate/aggregateEffect.js | 13 ++ .../computeVariableAsAttribute.js | 3 + .../properties/subSchemas/computedField.js | 9 ++ app/imports/parser/parseTree/rollArray.js | 2 +- .../components/attributes/AttributeEffect.vue | 146 +++++++++--------- .../components/effects/InlineEffect.vue | 136 ++++++++++++++++ .../ui/properties/forms/EffectForm.vue | 9 ++ .../properties/forms/shared/ComputedField.vue | 11 +- .../ui/properties/viewers/ActionViewer.vue | 9 +- .../viewers/shared/PropertyField.vue | 45 +++++- 19 files changed, 454 insertions(+), 98 deletions(-) create mode 100644 app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js create mode 100644 app/imports/ui/properties/components/effects/InlineEffect.vue diff --git a/app/imports/api/engine/actions/applyProperty.js b/app/imports/api/engine/actions/applyProperty.js index 23094897..32542390 100644 --- a/app/imports/api/engine/actions/applyProperty.js +++ b/app/imports/api/engine/actions/applyProperty.js @@ -21,6 +21,7 @@ const applyPropertyByType = { toggle, }; -export default function applyProperty(node, ...args){ - return applyPropertyByType[node.node.type]?.(node, ...args); +export default function applyProperty(node, opts, ...rest){ + opts.scope[`#${node.node.type}`] = node.node; + return applyPropertyByType[node.node.type]?.(node, opts, ...rest); } diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js index 87ef98c2..2368b0cb 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyDamage.js @@ -3,6 +3,7 @@ import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js'; import resolve, { Context, toString } from '/imports/parser/resolve.js'; import logErrors from './shared/logErrors.js'; +import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; export default function applyDamage(node, { creature, targets, scope, log @@ -35,11 +36,12 @@ export default function applyDamage(node, { const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage'; // Compile the dice roll and store that string first - const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context); - logValue.push(toString(compiled)); - logErrors(context.errors, log); + // const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context); + // logValue.push(toString(compiled)); + // logErrors(context.errors, log); // roll the dice only and store that string + applyEffectsToCalculationParseNode(prop.amount, log); const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context); logValue.push(toString(rolled)); logErrors(context.errors, log); diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js new file mode 100644 index 00000000..be625eea --- /dev/null +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js @@ -0,0 +1,24 @@ +import operator from '/imports/parser/parseTree/operator.js'; +import { parse } from '/imports/parser/parser.js'; +import logErrors from './logErrors.js'; + +export default function applyEffectsToCalculationParseNode(calcObj, log){ + if (!calcObj.effects) return; + calcObj.effects.forEach(effect => { + if (effect.operation !== 'add') return; + if (!effect.amount) return; + if (effect.amount.value === null) return; + let effectParseNode; + try { + effectParseNode = parse(effect.amount.value.toString()); + calcObj.parseNode = operator.create({ + left: calcObj.parseNode, + right: effectParseNode, + operator: '+', + fn: 'add' + }); + } catch (e){ + logErrors([e], log) + } + }); +} diff --git a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js index 4ac1565a..a10340be 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js +++ b/app/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js @@ -1,9 +1,11 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js'; +import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import logErrors from './logErrors.js'; export default function recalculateCalculation(calc, scope, log, context){ if (!calc?.parseNode) return; calc._parseLevel = 'reduce'; + applyEffectsToCalculationParseNode(calc, log); evaluateCalculation(calc, scope, context); logErrors(calc.errors, log); } diff --git a/app/imports/api/engine/computation/CreatureComputation.js b/app/imports/api/engine/computation/CreatureComputation.js index 416ef7c7..a806ddd6 100644 --- a/app/imports/api/engine/computation/CreatureComputation.js +++ b/app/imports/api/engine/computation/CreatureComputation.js @@ -6,6 +6,7 @@ export default class CreatureComputation { // Set up fields this.originalPropsById = {}; this.propsById = {}; + this.propsWithTag = {}; this.scope = {}; this.props = properties; this.dependencyGraph = createGraph(); @@ -18,6 +19,17 @@ export default class CreatureComputation { // Store by id this.propsById[prop._id] = prop; + // Store sets of ids in each tag + if (prop.tags){ + prop.tags.forEach(tag => { + if (this.propsWithTag[tag]){ + this.propsWithTag[tag].push(prop._id); + } else { + this.propsWithTag[tag] = [prop._id]; + } + }); + } + // Store the prop in the dependency graph this.dependencyGraph.addNode(prop._id, prop); }); diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index 2e1b29ea..0acaf4cc 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, intersection, difference } from 'lodash'; const linkDependenciesByType = { action: linkAction, @@ -127,11 +127,11 @@ function linkEffects(dependencyGraph, prop, computation){ dependOnCalc({dependencyGraph, prop, key: 'amount'}); // The stats depend on the effect if (prop.targetByTags){ - // TODO: - getEffectTagTargets(prop, computation).forEach(targetProp => { + getEffectTagTargets(prop, computation).forEach(targetId => { + const targetProp = computation.propsById[targetId]; const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj){ + if (calcObj && calcObj.calculation){ dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect'); } }); @@ -143,6 +143,67 @@ function linkEffects(dependencyGraph, prop, computation){ } } +// Returns an array of IDs of the properties the effect targets +function getEffectTagTargets(effect, computation){ + const targets = getTargetListFromTags(effect.targetTags, computation); + const notIds = []; + if (effect.extraTags){ + effect.extraTags.forEach(ex => { + if (ex.operation === 'OR'){ + targets.push(...getTargetListFromTags(ex.tags, computation)); + } else if (ex.operation === 'NOT'){ + ex.tags.forEach(tag => { + const idList = computation.propsWithTag[tag]; + if (idList) notIds.push(...computation.propsWithTag[tag]) + }); + } + }); + } + return difference(targets, notIds); +} + +function getTargetListFromTags(tags, computation){ + const targetTagIdLists = []; + if (!tags) return []; + tags.forEach(tag => { + const idList = computation.propsWithTag[tag]; + if (idList) targetTagIdLists.push(idList); + }); + const targets = intersection(...targetTagIdLists); + return targets; +} + +function getDefaultCalculationField(prop){ + switch (prop.type){ + case 'action': return 'attackRoll'; + case 'adjustment': return 'amount'; + case 'attribute': return 'baseValue'; + case 'branch': return 'condition'; + case 'buff': return 'duration'; + case 'class': return null; + case 'classLevel': return null; + case 'constant': return null; + case 'container': return null; + case 'damageMultiplier': return null; + case 'damage': return 'amount'; + case 'effect': return 'amount'; + case 'feature': return null; + case 'folder': return null; + case 'item': return null; + case 'note': return null; + case 'proficiency': return null; + case 'reference': return null; + 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; + case 'toggle': return 'condition'; + } +} + function linkRoll(dependencyGraph, prop){ dependOnCalc({dependencyGraph, prop, key: 'roll'}); } diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index d8a7cdba..a2e19db1 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -29,7 +29,7 @@ function discoverInlineCalculationFields(prop, schemas){ // Set the value to the uncomputed string for use in calculations inlineCalcObj.value = string; - + // Has the text, if it matches the existing hash, stop const inlineCalcHash = cyrb53(inlineCalcObj.text); if (inlineCalcHash === inlineCalcObj.hash){ @@ -57,6 +57,9 @@ function parseAllCalculationFields(prop, schemas){ // Determine the level the calculation should compute down to let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel || 'reduce'; + // Special case of effects, when targeting by tags compile + if (prop.type === 'effect' && prop.targetByTags) parseLevel = 'compile'; + // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, (prop, key) => { diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index 80dd0746..054fc7f9 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -3,4 +3,46 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js'; export default function computeCalculation(computation, node){ const calcObj = node.data; evaluateCalculation(calcObj, computation.scope); + aggregateCalculationEffects(node, computation); +} + +export function aggregateCalculationEffects(node, computation){ + const calcObj = node.data; + delete calcObj.effects; + computation.dependencyGraph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + // Only effect links + if (link.data !== 'effect') return; + // That have effect data + if (!linkedNode.data) return; + // Ignore inactive props + if (linkedNode.data.inactive) return; + + // Collate effects + calcObj.effects = calcObj.effects || []; + calcObj.effects.push({ + _id: linkedNode.data._id, + name: linkedNode.data.name, + operation: linkedNode.data.operation, + amount: linkedNode.data.amount && { + value: linkedNode.data.amount.value, + //parseNode: linkedNode.data.amount.parseNode, + }, + // ancestors: linkedNode.data.ancestors, + }); + }, + true // enumerate only outbound links + ); + if (calcObj.effects && typeof calcObj.value === 'number'){ + calcObj.baseValue = calcObj.value; + calcObj.effects.forEach(effect => { + if ( + effect.operation === 'add' && + effect.amount && typeof effect.amount.value === 'number' + ){ + calcObj.value += effect.amount.value + } + }); + } } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js index 0f9bdab1..2f5f2964 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable.js @@ -28,7 +28,7 @@ function aggregateLinks(computation, node){ // Ignore inactive props if (linkedNode.data.inactive) return; // Apply all the aggregations - let arg = {node, linkedNode, link}; + let arg = {node, linkedNode, link, computation}; aggregate.classLevel(arg); aggregate.damageMultiplier(arg); aggregate.definition(arg); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js index 5d454957..8cf47d36 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -16,10 +16,23 @@ export default function aggregateEffect({node, linkedNode, link}){ conditional: [], rollBonus: [], }; + + // Store a summary of the effect itself + node.data.effects = node.data.effects || []; + node.data.effects.push({ + _id: linkedNode.data._id, + name: linkedNode.data.name, + operation: linkedNode.data.operation, + amount: linkedNode.data.amount && {value: linkedNode.data.amount.value}, + // ancestors: linkedNode.data.ancestors, + }); + // get a shorter reference to the aggregator document const aggregator = node.data.effectAggregator; // Get the result of the effect const result = linkedNode.data.amount?.value; + // Skip aggregating if the result is not resolved completely + if (typeof result === 'string') return; // Aggregate the effect based on its operation switch(linkedNode.data.operation){ case 'base': 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 3a7c2982..21137749 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/computeVariableAsAttribute.js @@ -23,4 +23,7 @@ export default function computeVariableAsAttribute(computation, node, prop){ prop.hide = !node.data.effectAggregator && prop.baseValue === undefined || undefined + + // Store effects + prop.effects = node.data.effects; } diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index e8cb1220..0f0ef37b 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -24,6 +24,15 @@ function computedOnlyField(field){ optional: true, removeBeforeCompute: true, }, + // A list of effects targeting this calculation + [`${field}.effects`]: { + type: Array, + optional: true, + }, + [`${field}.effects.$`]: { + type: Object, + blackbox: true, + }, // A cache of the parse result of the calculation [`${field}.parseNode`]: { type: Object, diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js index ba737cc4..49474acb 100644 --- a/app/imports/parser/parseTree/rollArray.js +++ b/app/imports/parser/parseTree/rollArray.js @@ -16,7 +16,7 @@ const rollArray = { }; }, toString(node){ - return `[${node.values.join(', ')}]`; + return `${node.diceNum || ''}d${node.diceSize} [${node.values.join(', ')}]`; }, reduce(node, scope, context){ const total = node.values.reduce((a, b) => a + b, 0); diff --git a/app/imports/ui/properties/components/attributes/AttributeEffect.vue b/app/imports/ui/properties/components/attributes/AttributeEffect.vue index 623a26b8..687500bc 100644 --- a/app/imports/ui/properties/components/attributes/AttributeEffect.vue +++ b/app/imports/ui/properties/components/attributes/AttributeEffect.vue @@ -27,7 +27,7 @@
{{ displayedText }}
-
+
diff --git a/app/imports/ui/properties/components/effects/InlineEffect.vue b/app/imports/ui/properties/components/effects/InlineEffect.vue new file mode 100644 index 00000000..9ac97e38 --- /dev/null +++ b/app/imports/ui/properties/components/effects/InlineEffect.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/app/imports/ui/properties/forms/EffectForm.vue b/app/imports/ui/properties/forms/EffectForm.vue index 514defad..bed6c6a3 100644 --- a/app/imports/ui/properties/forms/EffectForm.vue +++ b/app/imports/ui/properties/forms/EffectForm.vue @@ -10,6 +10,7 @@ - +
@@ -29,6 +29,15 @@ export default { default: () => ({}), }, }, + computed: { + errorList(){ + if (this.model.parseError){ + return [this.model.parseError, ...this.model.errors]; + } else { + return this.model.errors; + } + } + } } diff --git a/app/imports/ui/properties/viewers/ActionViewer.vue b/app/imports/ui/properties/viewers/ActionViewer.vue index 93a43290..4a8ef728 100644 --- a/app/imports/ui/properties/viewers/ActionViewer.vue +++ b/app/imports/ui/properties/viewers/ActionViewer.vue @@ -29,7 +29,7 @@ large center signed - :value="rollBonus" + :calculation="model.attackRoll" /> 3; }, diff --git a/app/imports/ui/properties/viewers/shared/PropertyField.vue b/app/imports/ui/properties/viewers/shared/PropertyField.vue index 6c2a4d60..6047149a 100644 --- a/app/imports/ui/properties/viewers/shared/PropertyField.vue +++ b/app/imports/ui/properties/viewers/shared/PropertyField.vue @@ -19,7 +19,7 @@ {{ name }}
+
+ + +
@@ -50,14 +74,18 @@ From 352c4d516da6da3d466627ebb851607f9a27ecdd Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 15 Feb 2022 16:19:00 +0200 Subject: [PATCH 85/85] Fixed effect form not showing targetField value when set --- .../ui/properties/forms/EffectForm.vue | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/imports/ui/properties/forms/EffectForm.vue b/app/imports/ui/properties/forms/EffectForm.vue index bed6c6a3..c4fcecdb 100644 --- a/app/imports/ui/properties/forms/EffectForm.vue +++ b/app/imports/ui/properties/forms/EffectForm.vue @@ -153,6 +153,16 @@ name="Advanced" standalone > + + + - - -