From 06da15c44a545135f4f6af72c4b8d1fcd466e03a Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Wed, 8 Sep 2021 17:23:00 +0200 Subject: [PATCH] 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';