From b877a8b45f4939e6583f249a983dcca136d60b43 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 10 Sep 2021 19:51:03 +0200 Subject: [PATCH] Computation engine rewrite continues --- app/client/main.js | 2 + .../buildComputation/computeInventory.js | 16 ++-- .../computeSlotQuantityFilled.js | 2 +- .../computeToggleDependencies.js | 2 +- .../linkCalculationDependencies.js | 43 ++++++++--- .../buildComputation/linkTypeDependencies.js | 8 +- .../parseCalculationFields.js | 7 +- .../aggregateProps/aggregateBaseValue.js | 13 ++++ .../aggregateProps/aggregateDefinitions.js | 24 ++++++ .../aggregateProps/index.js | 17 +++++ .../computeComputation/computeVariable.js | 33 +++++++++ .../computeComputation/evaluateCalculation.js | 11 +++ .../newEngine/computeCreatureComputation.js | 74 +++++++++++++++++++ app/imports/api/properties/Toggles.js | 9 +++ app/package-lock.json | 2 +- 15 files changed, 235 insertions(+), 28 deletions(-) create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js create mode 100644 app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js create mode 100644 app/imports/api/creature/computation/newEngine/computeCreatureComputation.js diff --git a/app/client/main.js b/app/client/main.js index b036a39f..d9d770b7 100644 --- a/app/client/main.js +++ b/app/client/main.js @@ -3,3 +3,5 @@ import '/imports/ui/vueSetup.js'; import '/imports/ui/styles/stylesIndex.js'; import '/imports/client/config.js'; import '/imports/client/serviceWorker.js'; + +import 'ngraph.graph'; diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js index 9e96c70e..c97f92d4 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeInventory.js @@ -59,12 +59,12 @@ function handleProp(prop, containerStack, data, dependencyGraph){ // Sum the item-specific data if (prop.type === 'item'){ - dependencyGraph.addLink('itemsAttuned', prop._id); + dependencyGraph.addLink('itemsAttuned', prop._id, 'inventory'); if (prop.attuned) data.itemsAttuned += 1; if (prop.equipped){ - dependencyGraph.addLink('weightEquipment', prop._id); + dependencyGraph.addLink('weightEquipment', prop._id, 'inventory'); data.weightEquipment += weight; - dependencyGraph.addLink('valueEquipment', prop._id); + dependencyGraph.addLink('valueEquipment', prop._id, 'inventory'); data.valueEquipment += value; } } @@ -74,7 +74,7 @@ function handleProp(prop, containerStack, data, dependencyGraph){ if (container){ // The container depends on this prop for its contents data - dependencyGraph.addLink(container._id, prop._id); + dependencyGraph.addLink(container._id, prop._id, 'inventory'); // Add this property's weights and values to the container if (!container.weightless){ container.contentsWeight += weight; @@ -84,14 +84,14 @@ function handleProp(prop, containerStack, data, dependencyGraph){ if (carried) container.carriedValue += carriedValue; } else { // There is no parent container, add weights/value to the character data - dependencyGraph.addLink('weightTotal', prop._id); + dependencyGraph.addLink('weightTotal', prop._id, 'inventory'); data.weightTotal += weight; - dependencyGraph.addLink('valueTotal', prop._id); + dependencyGraph.addLink('valueTotal', prop._id, 'inventory'); data.valueTotal += value; if (carried){ - dependencyGraph.addLink('weightCarried', prop._id); + dependencyGraph.addLink('weightCarried', prop._id, 'inventory'); data.weightCarried += carriedWeight; - dependencyGraph.addLink('valueCarried', prop._id); + dependencyGraph.addLink('valueCarried', prop._id, 'inventory'); data.valueCarried += carriedValue; } } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js index dbaa9fda..7dd1694d 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeSlotQuantityFilled.js @@ -7,7 +7,7 @@ export default function computeSlotQuantityFilled(node, dependencyGraph){ slot.totalFilled = 0; node.children.forEach(child => { let childProp = child.node; - dependencyGraph.addLink(slot._id, childProp._id) + dependencyGraph.addLink(slot._id, childProp._id, 'slotFill') if (childProp.type === 'slotFiller'){ slot.totalFilled += child.slotQuantityFilled; } else { diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js index 882b28c7..665ea71e 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/computeToggleDependencies.js @@ -11,6 +11,6 @@ export default function computeToggleDependencies(node, dependencyGraph){ ) return; walkDown(node.children, child => { child.node._computationDetails.toggleAncestors.push(prop._id); - dependencyGraph.addLink(child.node._id, prop._id, prop.condition); + dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); }); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js index 075fdb2e..06cf1a3d 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkCalculationDependencies.js @@ -4,21 +4,40 @@ import findAncestorByType from 'imports/api/creature/computation/newEngine/utili export default function linkCalculationDependencies(dependencyGraph, prop, propsById){ prop._computationDetails.calculations.forEach(calcObj => { + // Store resolved ancestors + let memo = { + // ancestors: {} //this gets added if there are resolved ancestors + }; // Traverse the parsed calculation looking for variable names calcObj._parsedCalculation.travese(node => { - if (node instanceof SymbolNode || node instanceof AccessorNode){ - // Link ancestor references as direct property dependencies - if (node.name[0] === '#'){ - let ancestorProp = findAncestorByType( - prop, node.name.slice(1), propsById - ); - if (!ancestorProp) return; - dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); - } else { - // Link variable name references as variable dependencies - dependencyGraph.addLink(prop._id, node.name, calcObj); - } + // Skip nodes that aren't symbols or accessors + if (!(node instanceof SymbolNode || node instanceof AccessorNode)) return; + // Link ancestor references as direct property dependencies + if (node.name[0] === '#'){ + let ancestorProp = getAncestorProp( + node.name.slice(1), memo, prop, propsById + ); + if (!ancestorProp) return; + dependencyGraph.addLink(prop._id, ancestorProp._id, calcObj); + } else { + // Link variable name references as variable dependencies + dependencyGraph.addLink(prop._id, node.name, calcObj); } }); + // Store the resolved ancestors in this calculation's local scope + if (memo.ancestors) { + calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors}; + } }); } + +function getAncestorProp(type, memo, prop, propsById){ + if (memo.ancestors && memo.ancestors['#' + type]){ + return memo.ancestors['#' + type]; + } else { + var ancestorProp = findAncestorByType( prop, type, propsById ); + if (!memo.ancestors) memo.ancestors = {}; + memo.ancestors['#' + type] = ancestorProp; + return ancestorProp; + } +} diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js index 38e640ea..f2bea38b 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/linkTypeDependencies.js @@ -15,13 +15,13 @@ export default function linkTypeDependencies(dependencyGraph, prop){ function linkVariableName(dependencyGraph, prop){ // The variableName of the prop depends on the prop if (prop.variableName){ - dependencyGraph.addLink(prop.variableName, prop._id); + dependencyGraph.addLink(prop.variableName, prop._id, 'definition'); } } function linkDamageMultiplier(dependencyGraph, prop){ prop.damageTypes.forEach(damageType => { - dependencyGraph.addLink(`${damageType}Multiplier`, prop._id); + dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, 'damageMultiplier'); }); } @@ -29,12 +29,12 @@ function linkStats(dependencyGraph, prop){ // The stats a prop references depend on that prop prop.stats.forEach(variableName => { if (!variableName) return; - dependencyGraph.addLink(variableName, prop._id); + dependencyGraph.addLink(variableName, prop._id, 'statChange'); }); } function linkSkill(dependencyGraph, prop){ linkVariableName(dependencyGraph, prop); // The prop depends on the variable references as the ability - if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability); + if (prop.ability) dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore'); } diff --git a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js index e9584d3d..9952f92b 100644 --- a/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js +++ b/app/imports/api/creature/computation/newEngine/buildComputation/parseCalculationFields.js @@ -9,15 +9,20 @@ export default function parseCalculationFields(prop, schemas){ if (key.slice(-12) !== '.calculation') return; const calcKey = key.sclice(0, -12); + // Determine the level the calculation should compute down to + let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel; + // For all fields matching they keys // supports `keys.$.with.$.arrays` applyFnToKey(prop, calcKey, calcObj => { // Store a reference to all the calculations prop._computationDetails.calculations.push(calcObj); + // Store the level to compute down to later + calcObj._parseLevel = parseLevel; // Parse the calculation parseCalculation(calcObj); }); - + }); } diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js new file mode 100644 index 00000000..96446353 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js @@ -0,0 +1,13 @@ + +/** + * Iterate through all the defining properties and choose the highest + * `baseValue.value` + */ +export default function aggregateBaseValue({node, linkedNode, link}){ + if (link.data !== 'definition') return; + const propBaseValue = linkedNode.data.baseValue?.value; + if (propBaseValue === undefined) return; + if (node.baseValue === undefined || propBaseValue > node.baseValue){ + node.baseValue = propBaseValue; + } +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js new file mode 100644 index 00000000..e21a4d14 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js @@ -0,0 +1,24 @@ + +export default function aggregateDefinitions({node, linkedNode, link}){ + // Look at all definition links + if (link.data !== 'definition') return; + const prop = linkedNode.data; + // get current defining prop + const definingProp = node.data.definingProp; + // Find the last defining prop + if (!definingProp || prop.order > definingProp.order){ + // override the current defining prop + overrideProp(definingProp, node); + // set this prop as the new defining prop + node.data.definingProp = prop; + } else { + overrideProp(prop, node); + } +} + +function overrideProp(prop, node){ + if (!prop) return; + prop.overriden = true; + if (!node.data.overridenProps) node.data.overridenProps = []; + node.data.overridenProp.push(prop); +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js new file mode 100644 index 00000000..e792c5b7 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js @@ -0,0 +1,17 @@ +import definitions from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDefinitions.js'; +import baseValue from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateBaseValue.js'; +import damageMultipliers from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateDamageMultipliers.js'; +import effects from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateEffects.js'; +import proficiencies from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; +import skills from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateProficiencies.js'; +import toggles from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/aggregateToggles.js'; + +export default Object.freeze({ + definitions, + baseValue, + damageMultipliers, + effects, + proficiencies, + skills, + toggles, +}); diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js b/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js new file mode 100644 index 00000000..486fc817 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js @@ -0,0 +1,33 @@ +import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/aggregateProps/index.js'; + +export default function computeVariable(graph, node){ + if (!node.data) node.data = {}; + aggregateLinks(graph, node); +} + +function aggregateLinks(graph, node){ + let definingProp; + let overridenProps = []; + graph.forEachLinkedNode( + node.id, + (linkedNode, link) => { + if (!linkedNode.data) linkedNode.data = {}; + // Ignore inactive props + if (linkedNode.data.inactive) return; + // Apply all the aggregations + let arg = {node, linkedNode, link}; + aggregate.definitions(arg); + aggregate.baseValue(arg); + aggregate.damageMultipliers(arg); + aggregate.effects(arg); + aggregate.proficiencies(arg); + aggregate.skills(arg); + aggregate.toggles(arg); + }, + true // enumerate only outbound links + ); + // store the defining and overriden props on the node + if (!node.data) node.data = {}; + node.data.definingProp = definingProp; + node.data.overridenProps = overridenProps; +} diff --git a/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js b/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js new file mode 100644 index 00000000..81217078 --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js @@ -0,0 +1,11 @@ +import { CompilationContext } from '/imports/parser/parser.js'; + +export default function evaluateCalculation(calculation, scope){ + const context = new CompilationContext(); + const parseNode = calculation._parsedCalculation; + const fn = calculation._parseLevel || 'reduce'; + const calculationScope = {...calculation._localScope, ...scope}; + const result = parseNode[fn](calculationScope, context); + calculation.value = result; + calculation.errors = context.errors; +} diff --git a/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js new file mode 100644 index 00000000..e79620bb --- /dev/null +++ b/app/imports/api/creature/computation/newEngine/computeCreatureComputation.js @@ -0,0 +1,74 @@ +import evaluateCalculation from '/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js'; +import computeVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js'; + +export default function computeCreatureComputation(computation){ + const stack = []; + // dict of computed nodes by id + const scope = {}; + const graph = computation.dependencyGraph; + // Add all nodes to the stack + graph.forEachNode(node => stack.push({ + node, + visited: false, + visitedChildren: false, + })); + // Depth first traversal of nodes + while (stack.length){ + let top = stack[stack.length - 1]; + if (top.visited){ + // The object has already + stack.pop(); + } else if (top.visitedChildren){ + // Compute the top object of the stack + compute(graph, top.node, scope); + // If the node holds a variable, store it in the scope + if (!top.node.data?.type){ + scope[top.node.id] = top.node.data; + } + // Mark the object as visited and remove from stack + top.visited = true; + stack.pop(); + } else { + // Push children to graph + pushDependenciesToStack(top.node.id, graph, stack); + top.visitedChildren = true; + } + } +} + +function compute(graph, node, scope){ + // Get the property + let prop = node.data; + + // evaluate all the calculations + if (prop?._computationDetails?.calculations){ + prop._computationDetails.calculations.forEach(calcObj => { + evaluateCalculation(calcObj, scope) + }); + } + + // Compute the property by type + let typeCompute = propTypeComputations[prop?.type || '_variable']; + typeCompute?.(graph, node); +} + +var propTypeComputations = { + '_variable': computeVariable, +}; + +function pushDependenciesToStack(nodeId, graph, stack){ + graph.forEachLinkedNode( + nodeId, + (linkedNode, link) => { + // Ignore inventory links, they can't cause dependency loops + // and are already fully computed when they are created + if (link.data === 'inventory') return; + stack.push({ + node: linkedNode, + visited: false, + visitedChildren: false, + }); + }, + true // enumerate only outbound links + ); +} diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index ea6d509e..7a76d061 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -8,6 +8,15 @@ const ToggleSchema = createPropertySchema({ optional: true, max: STORAGE_LIMITS.name, }, + variableName: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, + showUI: { + type: Boolean, + optional: true, + }, disabled: { type: Boolean, optional: true, diff --git a/app/package-lock.json b/app/package-lock.json index 8c6f1e4d..19da6540 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2795,7 +2795,7 @@ }, "signal-exit": { "version": "3.0.2", - "resolved": false, + "resolved": "", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simpl-schema": {