From a660ccc458c7dbe6a4f1da4251ccdffffcc3a302 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Thu, 16 Sep 2021 14:31:28 +0200 Subject: [PATCH] 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; }); }