Files
DiceCloud/app/imports/api/creature/computation/newEngine/computeCreature.js

175 lines
5.5 KiB
JavaScript

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;
}