175 lines
5.5 KiB
JavaScript
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;
|
|
}
|