diff --git a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js index a3d65599..10ce0b0a 100644 --- a/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkTypeDependencies.js @@ -159,7 +159,7 @@ function linkEffects(dependencyGraph, prop, computation) { // Otherwise target a field on that property const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation) { + if (calcObj) { dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect'); } } @@ -301,7 +301,7 @@ function linkProficiencies(dependencyGraph, prop, computation) { // Otherwise target a field on that property const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation) { + if (calcObj) { dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); } } @@ -339,7 +339,7 @@ function linkSkill(dependencyGraph, prop, computation) { // other skill isn't supported const key = prop.targetField || getDefaultCalculationField(targetProp); const calcObj = get(targetProp, key); - if (calcObj && calcObj.calculation) { + if (calcObj) { dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency'); } }); diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 73217f80..6ef2b165 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,7 +1,7 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import { prettifyParseError, parse } from '/imports/parser/parser.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { get, unset } from 'lodash'; +import { get, set, unset } from 'lodash'; import errorNode from '/imports/parser/parseTree/error.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; @@ -63,12 +63,21 @@ function parseAllCalculationFields(prop, schemas) { // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, (prop, key) => { - const calcObj = get(prop, key); + let calcObj = get(prop, key); + // Create a calculation object if one doesn't exist, it will get deleted again later if + // it's not used, but if an effect targets a calculated field, we should have one to target + if ( + !calcObj + && subDocsExist(prop, key) + ) { + calcObj = {}; + set(prop, key, calcObj); + } + // Sub document didn't exist, skip this field if (!calcObj) return; - // Delete the whole calculation object if the calculation string isn't set + // Keep a list of empty calculations for potential deletion if they aren't used if (!calcObj.calculation) { - unset(prop, calcKey); - return; + prop._computationDetails.emptyCalculations.push(calcObj); } // Store a reference to all the calculations prop._computationDetails.calculations.push(calcObj); @@ -84,15 +93,31 @@ function parseAllCalculationFields(prop, schemas) { }); } +function subDocsExist(prop, key) { + const path = key.split('.'); + if (path.length < 2) return !!prop; + path.pop(); + const subPath = path.join('.'); + return !!get(prop, subPath); +} + +export function removeEmptyCalculations(prop) { + prop._computationDetails.emptyCalculations.forEach(calcObj => { + if (!calcObj.effects?.length) { + unset(prop, calcObj._key); + } + }); +} + function parseCalculation(calcObj) { - const calcHash = cyrb53(calcObj.calculation); + const calcHash = cyrb53(calcObj.calculation || '0'); // If the cached parse calculation is equal to the calculation, skip if (calcHash === calcObj.hash) { return; } calcObj.hash = calcHash; try { - calcObj.parseNode = parse(calcObj.calculation); + calcObj.parseNode = parse(calcObj.calculation || '0'); calcObj.parseError = null; } catch (e) { let error = { diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index a97f5b0a..e9dbb625 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -75,6 +75,7 @@ export function buildComputationFromProps(properties, creature, variables) { // Add a place to store all the computation details prop._computationDetails = { calculations: [], + emptyCalculations: [], inlineCalculations: [], toggleAncestors: [], }; diff --git a/app/imports/api/engine/computation/computeCreatureComputation.js b/app/imports/api/engine/computation/computeCreatureComputation.js index 7cd4b41c..1ffe58be 100644 --- a/app/imports/api/engine/computation/computeCreatureComputation.js +++ b/app/imports/api/engine/computation/computeCreatureComputation.js @@ -1,9 +1,10 @@ import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js'; import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js'; import embedInlineCalculations from './utility/embedInlineCalculations.js'; +import { removeEmptyCalculations } from './buildComputation/parseCalculationFields.js'; import path from 'ngraph.path'; -export default function computeCreatureComputation(computation){ +export default function computeCreatureComputation(computation) { const stack = []; // Computation scope of {variableName: prop} const graph = computation.dependencyGraph; @@ -20,16 +21,16 @@ export default function computeCreatureComputation(computation){ stack.reverse(); // Depth first traversal of nodes - while (stack.length){ + while (stack.length) { let top = stack[stack.length - 1]; - if (top._visited){ + if (top._visited) { // The object has already been computed, skip stack.pop(); - } else if (top._visitedChildren){ + } else if (top._visitedChildren) { // Mark the object as visited and remove from stack top._visited = true; stack.pop(); - // Compute the top object of the stack + // Compute the top object of the stack compute(computation, top); } else { top._visitedChildren = true; @@ -42,14 +43,14 @@ export default function computeCreatureComputation(computation){ computation.props.forEach(finalizeProp); } -function compute(computation, node){ +function compute(computation, node) { // Determine the prop's active status by its toggles computeToggles(computation, node); // Compute the property by type computeByType[node.data?.type || '_variable']?.(computation, node); } -function pushDependenciesToStack(nodeId, graph, stack, computation){ +function pushDependenciesToStack(nodeId, graph, stack, computation) { graph.forEachLinkedNode(nodeId, linkedNode => { if (linkedNode._visitedChildren && !linkedNode._visited) { // This is a dependency loop, find a path from the node to itself @@ -66,7 +67,7 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){ loop = [linkedNode, ...newLoop]; } }, true); - + if (loop.length) { computation.errors.push({ type: 'dependencyLoop', @@ -80,11 +81,13 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){ }, true); } -function finalizeProp(prop){ +function finalizeProp(prop) { // Embed the inline calculations prop._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => { embedInlineCalculations(inlineCalcObj); }); + // Clean up the calculations that were never used + removeEmptyCalculations(prop); // Clean up the computation details delete prop._computationDetails; }