More engine rewrite progress, starting to get messy again
This commit is contained in:
@@ -1,97 +0,0 @@
|
||||
import {computeCreature} from "./recomputeCreature.js";
|
||||
import assert from "assert";
|
||||
|
||||
const makeEffect = function(operation, value){
|
||||
let effect = {computed: false, result: 0, operation}
|
||||
if (_.isFinite(value)){
|
||||
effect.value = +value;
|
||||
} else {
|
||||
effect.calculation = value;
|
||||
}
|
||||
return effect;
|
||||
}
|
||||
|
||||
describe('computeCreature', function () {
|
||||
it('computes an aritrary creature', function () {
|
||||
let char = {
|
||||
atts: {
|
||||
attribute1: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "attribute",
|
||||
attributeType: "ability",
|
||||
result: 0,
|
||||
mod: 0, // The resulting modifier if this is an ability
|
||||
base: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
effects: [
|
||||
makeEffect("base", 10),
|
||||
makeEffect("add", 5),
|
||||
makeEffect("mul", 2),
|
||||
],
|
||||
},
|
||||
attribute2: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "attribute",
|
||||
result: 0,
|
||||
mod: 0, // The resulting modifier if this is an ability
|
||||
base: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
effects: [
|
||||
makeEffect("base", "attribute1"),
|
||||
makeEffect("max", 2),
|
||||
],
|
||||
},
|
||||
},
|
||||
skills: {
|
||||
skill1: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "skill",
|
||||
ability: "attribute1",
|
||||
result: 0,
|
||||
proficiency: 0,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
advantage: 0,
|
||||
disadvantage: 0,
|
||||
passiveAdd: 0,
|
||||
fail: 0,
|
||||
conditional: 0,
|
||||
effects: [],
|
||||
proficiencies: [],
|
||||
},
|
||||
},
|
||||
dms: {
|
||||
dm1: {
|
||||
computed: false,
|
||||
busyComputing: false,
|
||||
type: "damageMultiplier",
|
||||
result: 0,
|
||||
immunityCount: 0,
|
||||
ressistanceCount: 0,
|
||||
vulnerabilityCount: 0,
|
||||
effects: [],
|
||||
}
|
||||
},
|
||||
classes: {
|
||||
Barbarian: {
|
||||
level: 5,
|
||||
},
|
||||
},
|
||||
level: 5,
|
||||
};
|
||||
char = computeCreature(char);
|
||||
console.log(char);
|
||||
assert(true);
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,7 @@ export default function computeToggleDependencies(node, dependencyGraph){
|
||||
prop.enabled
|
||||
) return;
|
||||
walkDown(node.children, child => {
|
||||
child.node._computationDetails.toggleAncestors.push(prop._id);
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
|
||||
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
|
||||
import findAncestorByType from 'imports/api/creature/computation/newEngine/utility/findAncestorByType.js';
|
||||
|
||||
export default function linkCalculationDependencies(dependencyGraph, prop, propsById){
|
||||
export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){
|
||||
prop._computationDetails.calculations.forEach(calcObj => {
|
||||
// Store resolved ancestors
|
||||
let memo = {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
const linkDependenciesByType = {
|
||||
attribute: linkVariableName,
|
||||
action: linkResources,
|
||||
attack: linkResources,
|
||||
attribute: linkAttribute,
|
||||
classLevel: linkVariableName,
|
||||
constant: linkVariableName,
|
||||
damageMultiplier: linkDamageMultiplier,
|
||||
proficiency: linkStats,
|
||||
effect: linkStats,
|
||||
skill: linkSkill,
|
||||
spell: linkResources,
|
||||
}
|
||||
|
||||
export default function linkTypeDependencies(dependencyGraph, prop){
|
||||
@@ -19,9 +22,39 @@ function linkVariableName(dependencyGraph, prop){
|
||||
}
|
||||
}
|
||||
|
||||
function linkResources(dependencyGraph, prop, {propsById}){
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
const item = propsById[itemConsumed.itemId];
|
||||
if (!item.equipped) {
|
||||
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;
|
||||
dependencyGraph.addLink(prop._id, item._id, 'inventory');
|
||||
});
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
if (!attConsumed.variableName) return;
|
||||
dependencyGraph.addLink(prop._id, attConsumed.variableName, 'resource');
|
||||
});
|
||||
}
|
||||
|
||||
function linkAttribute(dependencyGraph, prop){
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// hit dice depend on constitution
|
||||
if (prop.attributeType === 'hitDice'){
|
||||
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
|
||||
}
|
||||
}
|
||||
|
||||
function linkDamageMultiplier(dependencyGraph, prop){
|
||||
prop.damageTypes.forEach(damageType => {
|
||||
dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, 'damageMultiplier');
|
||||
dependencyGraph.addLink(`${damageType}Multiplier`, prop._id, prop.type);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,12 +62,16 @@ 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, 'statChange');
|
||||
dependencyGraph.addLink(variableName, prop._id, prop.type);
|
||||
});
|
||||
}
|
||||
|
||||
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, 'skillAbilityScore');
|
||||
if (prop.ability){
|
||||
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
|
||||
}
|
||||
// Skills depend on the creature's proficiencyBonus
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
}
|
||||
|
||||
@@ -6,23 +6,31 @@ export default function parseCalculationFields(prop, schemas){
|
||||
// For each key in the schema
|
||||
schemas[prop.type]._schemaKeys.forEach( key => {
|
||||
// that ends in '.calculation'
|
||||
if (key.slice(-12) !== '.calculation') return;
|
||||
const calcKey = key.sclice(0, -12);
|
||||
if (key.slice(-12) === '.calculation'){
|
||||
const calcKey = key.sclice(0, -12);
|
||||
|
||||
// Determine the level the calculation should compute down to
|
||||
let parseLevel = schemas[prop.type].getDefinition(calcKey).parseLevel;
|
||||
// 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);
|
||||
});
|
||||
// Or that ends in .inlineCalculations
|
||||
} else if (key.slice(-19) === '.inlineCalculations'){
|
||||
const inlineCalcKey = key.sclice(0, -19);
|
||||
applyFnToKey(prop, inlineCalcKey, inlineCalcObj => {
|
||||
// Store a reference to all the inline calculations
|
||||
prop._computationDetails.inlineCalculations.push(inlineCalcObj);
|
||||
});
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -23,17 +23,16 @@ import computeSlotQuantityFilled from '/imports/api/creature/computation/newEngi
|
||||
* id -> id dependencies for docs that rely on other docs directly
|
||||
* id -> variable deps for docs that rely on a variable's value
|
||||
* variable -> id deps for variables that are impacted by docs
|
||||
* TODO:
|
||||
* Depth first traversal or dependency graph to:
|
||||
* Find loops in the dependency graph
|
||||
* resolve variables in dependency order
|
||||
*/
|
||||
|
||||
/**
|
||||
* Forseen issues: Anything that computes during the build step will not obey
|
||||
* computed toggles
|
||||
*/
|
||||
|
||||
/**
|
||||
* TODO
|
||||
* compute slots spaces left (after computed field of quantityExpected)
|
||||
* compute damage multipliers
|
||||
* compute dependencyGraph variables and properties
|
||||
* compute class levels
|
||||
*/
|
||||
|
||||
export default function buildCreatureComputation(creatureId){
|
||||
@@ -48,6 +47,8 @@ export default function buildCreatureComputation(creatureId){
|
||||
// The graph includes all dependencies even of inactive properties
|
||||
// such that any properties changing without changing their dependencies
|
||||
// can limit the recompute to connected parts of the graph
|
||||
// Each node's data represents a prop or a virtual prop like a variable
|
||||
// Each link's data: {type: String, data: Object, requiresComputation: Boolean}
|
||||
const dependencyGraph = createGraph();
|
||||
|
||||
const computation = {
|
||||
@@ -102,8 +103,8 @@ export default function buildCreatureComputation(creatureId){
|
||||
|
||||
// Graph functions that rely on the props being stored first
|
||||
properties.forEach(prop => {
|
||||
linkTypeDependencies(dependencyGraph, prop, computation.propsById);
|
||||
linkCalculationDependencies(dependencyGraph, prop, computation.propsById);
|
||||
linkTypeDependencies(dependencyGraph, prop, computation);
|
||||
linkCalculationDependencies(dependencyGraph, prop, computation);
|
||||
});
|
||||
|
||||
return computation;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import _variable from './computeByType/computeVariable.js';
|
||||
import action from './computeByType/computeAction.js';
|
||||
import slot from './computeByType/computeSlot.js';
|
||||
|
||||
export default Object.freeze({
|
||||
_variable,
|
||||
action,
|
||||
attack: action,
|
||||
slot,
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import computeResources from './computeAction/computeResources.js';
|
||||
|
||||
export default function computeAction(graph, node, scope){
|
||||
const prop = node.data;
|
||||
if (prop.uses){
|
||||
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
|
||||
}
|
||||
computeResources(graph, node, scope);
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
if (itemConsumed.available < itemConsumed.quantity.value){
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
if (!attConsumed.variableName) return;
|
||||
if (attConsumed.available < attConsumed.quantity.value){
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function computeResources(graph, node, scope){
|
||||
const prop = node.data;
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
if (!attConsumed.variableName) return;
|
||||
const att = scope[attConsumed.variableName];
|
||||
attConsumed.available = att.value;
|
||||
attConsumed.statId = att._id;
|
||||
attConsumed.statName = att.name;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function computSlot(graph, node){
|
||||
const prop = node.data;
|
||||
if (prop.quantityExpected){
|
||||
prop.spaceLeft = prop.quantityExpected - prop.totalFilled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import aggregate from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/aggregate/index.js';
|
||||
import computeVariableAsAttribute from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsAttribute.js';
|
||||
import computeVariableAsSkill from '/imports/api/creature/computation/newEngine/computeComputation/computeVariableAsType/computeVariableAsSkill.js';
|
||||
import computeVariableAsConstant from '/imports/api/creature/computation/newEngine/computeComputation/computeByType/computeVariable/computeVariableAsConstant.js';
|
||||
import computeImplicitVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/computeImplicitVariable.js';
|
||||
|
||||
export default function computeVariable(graph, node, scope){
|
||||
if (!node.data) node.data = {};
|
||||
aggregateLinks(graph, node);
|
||||
combineAggregations(node, scope);
|
||||
if (node.definingProp){
|
||||
// Add the defining variable to the scope
|
||||
scope[node.id] = node.definingProp
|
||||
} else {
|
||||
// Otherwise add an implicit variable to the scope
|
||||
scope[node.id] = computeImplicitVariable(node, scope);
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateLinks(graph, node){
|
||||
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.definition(arg);
|
||||
aggregate.damageMultiplier(arg);
|
||||
aggregate.effect(arg);
|
||||
aggregate.proficiency(arg);
|
||||
},
|
||||
true // enumerate only outbound links
|
||||
);
|
||||
}
|
||||
|
||||
function combineAggregations(node, scope){
|
||||
combineMultiplierAggregator(node);
|
||||
node.overridenProps.forEach(prop => {
|
||||
computeVariableProp(node, prop, scope);
|
||||
});
|
||||
computeVariableProp(node, node.definingProp, scope);
|
||||
}
|
||||
|
||||
function computeVariableProp(node, prop, scope){
|
||||
if (prop.type === 'attribute'){
|
||||
computeVariableAsAttribute(node, prop, scope)
|
||||
} else if (prop.type === 'skill'){
|
||||
computeVariableAsSkill(node, prop, scope)
|
||||
} else if (prop.type === 'constant'){
|
||||
computeVariableAsConstant(node, prop, scope)
|
||||
}
|
||||
}
|
||||
|
||||
function combineMultiplierAggregator(node){
|
||||
// get a reference to the aggregator
|
||||
const aggregator = node.data.multiplierAggregator;
|
||||
|
||||
// Combine
|
||||
let value;
|
||||
if (aggregator.immunityCount){
|
||||
value = 0;
|
||||
} else if (
|
||||
aggregator.ressistanceCount &&
|
||||
!aggregator.vulnerabilityCount
|
||||
){
|
||||
value = 0.5;
|
||||
} else if (
|
||||
!aggregator.ressistanceCount &&
|
||||
aggregator.vulnerabilityCount
|
||||
){
|
||||
value = 2;
|
||||
} else {
|
||||
value = 1;
|
||||
}
|
||||
|
||||
node.data.damageMultiplyValue = value;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export default function aggregateDamageMultipliers({node, linkedNode, link}){
|
||||
if (link.data !== 'damageMultiplier') return;
|
||||
const multiplierValue = linkedNode.data.value;
|
||||
if (multiplierValue === undefined) return;
|
||||
// Store an aggregator, its presence indicates damage multipliers target this
|
||||
// variable
|
||||
if (!node.data.multiplierAggregator) node.data.multiplierAggregator = {
|
||||
immunityCount: 0,
|
||||
resistanceCount: 0,
|
||||
vulnerabilityCount: 0,
|
||||
}
|
||||
// Store a short reference to the aggregator
|
||||
const aggregator = node.data.multiplierAggregator;
|
||||
// Sum the counts of each type of multiplier
|
||||
if (multiplierValue === 0){
|
||||
aggregator.immunityCount += 1;
|
||||
} else if (multiplierValue === 0.5){
|
||||
aggregator.resistanceCount += 1;
|
||||
} else if (multiplierValue === 2){
|
||||
aggregator.vulnerabilityCount += 1;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
|
||||
export default function aggregateDefinitions({node, linkedNode, link}){
|
||||
export default function aggregateDefinition({node, linkedNode, link}){
|
||||
// Look at all definition links
|
||||
if (link.data !== 'definition') return;
|
||||
|
||||
// Store which property is THE defining property and which are overriden
|
||||
const prop = linkedNode.data;
|
||||
// get current defining prop
|
||||
const definingProp = node.data.definingProp;
|
||||
@@ -14,11 +16,18 @@ export default function aggregateDefinitions({node, linkedNode, link}){
|
||||
} else {
|
||||
overrideProp(prop, node);
|
||||
}
|
||||
|
||||
// Aggregate the base value due to the defining properties
|
||||
const propBaseValue = linkedNode.data.baseValue?.value;
|
||||
if (propBaseValue === undefined) return;
|
||||
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
|
||||
node.data.baseValue = propBaseValue;
|
||||
}
|
||||
}
|
||||
|
||||
function overrideProp(prop, node){
|
||||
if (!prop) return;
|
||||
prop.overriden = true;
|
||||
if (!node.data.overridenProps) node.data.overridenProps = [];
|
||||
node.data.overridenProp.push(prop);
|
||||
node.data.overridenProps.push(prop);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
export default function aggregateEffect({node, linkedNode, link}){
|
||||
if (link.data !== 'effect') return;
|
||||
// store the effect aggregator, its presence indicates that the variable is
|
||||
// targeted by effects
|
||||
if (!node.data.effectAggregator) node.data.effectAggregator = {
|
||||
base: undefined,
|
||||
add: 0,
|
||||
mul: 1,
|
||||
min: Number.NEGATIVE_INFINITY,
|
||||
max: Number.POSITIVE_INFINITY,
|
||||
advantage: 0,
|
||||
disadvantage: 0,
|
||||
passiveAdd: undefined,
|
||||
fail: 0,
|
||||
set: undefined,
|
||||
conditional: [],
|
||||
rollBonus: [],
|
||||
};
|
||||
// get a shorter reference to the aggregator document
|
||||
const aggregator = node.data.effectAggregator;
|
||||
// Get the result of the effect
|
||||
const result = linkedNode.data.amount?.value;
|
||||
// Aggregate the effect based on its operation
|
||||
switch(linkedNode.data.operation){
|
||||
case 'base':
|
||||
// Take the largest base value
|
||||
if (Number.isFinite(result)){
|
||||
if(Number.isFinite(aggregator.base)){
|
||||
aggregator.base = Math.max(aggregator.base, result);
|
||||
} else {
|
||||
aggregator.base = result;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'add':
|
||||
// Add all adds together
|
||||
aggregator.add += result || 0;
|
||||
break;
|
||||
case 'mul':
|
||||
// Multiply the muls together
|
||||
aggregator.mul *= result || 1;
|
||||
break;
|
||||
case 'min':
|
||||
// Take the largest min value
|
||||
aggregator.min = result > aggregator.min ? result : aggregator.min;
|
||||
break;
|
||||
case 'max':
|
||||
// Take the smallest max value
|
||||
aggregator.max = result < aggregator.max ? result : aggregator.max;
|
||||
break;
|
||||
case 'set':
|
||||
// Take the highest set value
|
||||
aggregator.set = aggregator.set === undefined || (result > aggregator.set) ?
|
||||
result :
|
||||
aggregator.set;
|
||||
break;
|
||||
case 'advantage':
|
||||
// Sum number of advantages
|
||||
aggregator.advantage++;
|
||||
break;
|
||||
case 'disadvantage':
|
||||
// Sum number of disadvantages
|
||||
aggregator.disadvantage++;
|
||||
break;
|
||||
case 'passiveAdd':
|
||||
// Add all passive adds together
|
||||
aggregator.passiveAdd = (aggregator.passiveAdd || 0) + result;
|
||||
break;
|
||||
case 'fail':
|
||||
// Sum number of fails
|
||||
aggregator.fail++;
|
||||
break;
|
||||
case 'conditional':
|
||||
// Store array of conditionals
|
||||
aggregator.conditional.push(linkedNode.data.text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export default function aggregateProficiency({node, linkedNode, link}){
|
||||
if (
|
||||
link.data !== 'proficiency' &&
|
||||
!(link.data === 'definition' && linkedNode.data.type === 'skill')
|
||||
) return;
|
||||
let proficiency;
|
||||
if (link.data === 'proficiency'){
|
||||
proficiency = linkedNode.data.value || 0;
|
||||
} else if (link.data === 'definition' && linkedNode.data.type === 'skill'){
|
||||
proficiency = linkedNode.data.baseProficiency || 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
// Store the highest proficiency
|
||||
if (
|
||||
node.data.proficiency === undefined ||
|
||||
proficiency > node.data.proficiency
|
||||
){
|
||||
node.data.proficiency = proficiency;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import definition from './aggregateDefinition.js';
|
||||
import damageMultiplier from './aggregateDamageMultiplier.js';
|
||||
import effect from './aggregateEffect.js';
|
||||
import proficiency from './aggregateProficiency.js';
|
||||
|
||||
export default Object.freeze({
|
||||
definition,
|
||||
damageMultiplier,
|
||||
effect,
|
||||
proficiency,
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js';
|
||||
|
||||
/*
|
||||
* Variables with effects, proficiencies, or damage multipliers but no defining
|
||||
* properties are added to the scope as implicit variables
|
||||
*/
|
||||
export default function computeImplicitVariable(node){
|
||||
const prop = {};
|
||||
const result = getAggregatorResult(node);
|
||||
prop.total = result;
|
||||
prop.value = result;
|
||||
prop.proficiency = node.data.proficiency;
|
||||
|
||||
// denormalise the aggregator fields
|
||||
const aggregator = node.data.effectAggregator;
|
||||
if (aggregator.advantage && !aggregator.disadvantage){
|
||||
prop.advantage = 1;
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage){
|
||||
prop.advantage = -1;
|
||||
} else {
|
||||
prop.advantage = 0;
|
||||
}
|
||||
// Passive bonus
|
||||
prop.passiveBonus = aggregator.passiveAdd;
|
||||
// conditional benefits
|
||||
prop.conditionalBenefits = aggregator.conditional;
|
||||
// Roll bonuses
|
||||
prop.rollBonus = aggregator.rollBonus;
|
||||
// Forced to fail
|
||||
prop.fail = aggregator.fail;
|
||||
// Rollbonus
|
||||
prop.rollBonuses = aggregator.rollBonus;
|
||||
|
||||
return prop;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import getAggregatorResult from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable/getAggregatorResult.js';
|
||||
|
||||
export default function computeVariableAsAttribute(node, prop, scope){
|
||||
let result = getAggregatorResult(node);
|
||||
prop.total = result;
|
||||
prop.value = prop.total - (prop.damage || 0);
|
||||
|
||||
// Proficiency
|
||||
prop.proficiency = node.data.proficiency;
|
||||
|
||||
// Ability scores get modifiers
|
||||
if (prop.attributeType === 'ability'){
|
||||
prop.modifier = Math.floor((prop.currentValue - 10) / 2);
|
||||
}
|
||||
|
||||
// Hit dice denormalise constitution modifier
|
||||
if (prop.attributeType === 'hitDice') {
|
||||
prop.constitutionMod = scope['constitution']?.modifier || 0;
|
||||
}
|
||||
|
||||
// Stats that have no effects or base value can be hidden
|
||||
prop.hide = !node.data.effectAggregator &&
|
||||
prop.baseValue === undefined ||
|
||||
undefined
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { parse } from '/imports/parser/parser.js';
|
||||
|
||||
export default function computeVariableAsConstant(node, prop){
|
||||
let string = prop.calculation;
|
||||
if (!string) return;
|
||||
let parseNode;
|
||||
try {
|
||||
parseNode = parse(string);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
prop.value = parseNode;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export default function computeVariableAsSkill(node, prop, scope){
|
||||
// Skills are based on some ability Modifier
|
||||
let ability = scope[prop.ability];
|
||||
prop.abilityMod = ability?.modifier || 0;
|
||||
// TODO: Use this ability's skill effects/profs iff this skill is not a save
|
||||
|
||||
// Proficiency
|
||||
prop.proficiency = node.data.proficiency;
|
||||
|
||||
// Get the character's proficiency bonus to apply
|
||||
let profBonus = scope['proficiencyBonus']?.value || 0;
|
||||
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if(prop.proficiency === 0.49){
|
||||
// Round down proficiency bonus in the special case
|
||||
profBonus = Math.floor(profBonus * 0.5);
|
||||
} else {
|
||||
profBonus = Math.ceil(profBonus * prop.proficiency);
|
||||
}
|
||||
|
||||
// Combine everything to get the final result
|
||||
const statBase = node.data.baseValue;
|
||||
const aggregator = node.data.effectAggregator;
|
||||
|
||||
// If there is no aggregator, determine if the prop can hide, then exit
|
||||
if (!aggregator){
|
||||
prop.hide = statBase === undefined &&
|
||||
prop.proficiency == 0 ||
|
||||
undefined;
|
||||
prop.value = statBase;
|
||||
return;
|
||||
}
|
||||
// Combine aggregator
|
||||
const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0;
|
||||
let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul;
|
||||
if (result < aggregator.min) result = aggregator.min;
|
||||
if (result > aggregator.max) result = aggregator.max;
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (Number.isFinite(result)){
|
||||
result = Math.floor(result);
|
||||
}
|
||||
prop.value = result;
|
||||
// Advantage/disadvantage
|
||||
if (aggregator.advantage && !aggregator.disadvantage){
|
||||
prop.advantage = 1;
|
||||
} else if (aggregator.disadvantage && !aggregator.advantage){
|
||||
prop.advantage = -1;
|
||||
} else {
|
||||
prop.advantage = 0;
|
||||
}
|
||||
// Passive bonus
|
||||
prop.passiveBonus = aggregator.passiveAdd;
|
||||
// conditional benefits
|
||||
prop.conditionalBenefits = aggregator.conditional;
|
||||
// Roll bonuses
|
||||
prop.rollBonus = aggregator.rollBonus;
|
||||
// Forced to fail
|
||||
prop.fail = aggregator.fail;
|
||||
// Rollbonus
|
||||
prop.rollBonuses = aggregator.rollBonus;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import stripFloatingPointOddities from '/imports/api/creature/computation/newEngine/utility/stripFloatingPointOddities.js';
|
||||
|
||||
export default function getAggregatorResult(node){
|
||||
// Work out the base value as the greater of the deining stat value or
|
||||
// the damage multiplier value
|
||||
let statBase = node.data.baseValue;
|
||||
const damageMultiplyValue = node.data.damageMultiplyValue;
|
||||
if (statBase === undefined || damageMultiplyValue > statBase){
|
||||
statBase = damageMultiplyValue;
|
||||
}
|
||||
// get a reference to the aggregator
|
||||
const aggregator = node.data.effectAggregator;
|
||||
|
||||
// Without effects just return the defining base value
|
||||
if (!aggregator) return statBase;
|
||||
|
||||
let base;
|
||||
if (!Number.isFinite(aggregator.base)){
|
||||
base = statBase || 0;
|
||||
} else if (!Number.isFinite(statBase)){
|
||||
base = aggregator.base || 0;
|
||||
} else {
|
||||
base = Math.max(aggregator.base, statBase);
|
||||
}
|
||||
let result = (base + aggregator.add) * aggregator.mul;
|
||||
if (result < aggregator.min) {
|
||||
result = aggregator.min;
|
||||
}
|
||||
if (result > aggregator.max) {
|
||||
result = aggregator.max;
|
||||
}
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (!node.definingProp?.decimal && Number.isFinite(result)){
|
||||
result = Math.floor(result);
|
||||
} else if (Number.isFinite(result)){
|
||||
result = stripFloatingPointOddities(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CompilationContext } from '/imports/parser/parser.js';
|
||||
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
|
||||
|
||||
export default function computeCalculations(node, scope){
|
||||
// evaluate all the calculations
|
||||
node.data._computationDetails?.calculations?.forEach(calcObj => {
|
||||
evaluateCalculation(calcObj, scope)
|
||||
});
|
||||
node.data._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => {
|
||||
embedInlineCalculations(inlineCalcObj);
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function embedInlineCalculations(inlineCalcObj){
|
||||
const string = inlineCalcObj.text;
|
||||
const calculations = inlineCalcObj.inlineCalculations;
|
||||
if (!string || !calculations) return;
|
||||
let index = 0;
|
||||
inlineCalcObj.value = string.replace(INLINE_CALCULATION_REGEX, substring => {
|
||||
let calc = calculations[index++];
|
||||
return (calc && 'value' in calc) ? calc.value : substring;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default function evaluateToggles(node){
|
||||
let prop = node.data;
|
||||
let toggles = prop._computationDetails?.toggleAncestors;
|
||||
if (!toggles) return;
|
||||
toggles.forEach(toggle => {
|
||||
if (prop.inactive || !toggle.condition) return;
|
||||
if (!toggle.condition.value){
|
||||
prop.inactive = true;
|
||||
prop.deactivatedByToggle = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import evaluateCalculation from '/imports/api/creature/computation/newEngine/computeComputation/evaluateCalculation.js';
|
||||
import computeVariable from '/imports/api/creature/computation/newEngine/computeComputation/computeVariable.js';
|
||||
import computeCalculations from '/imports/api/creature/computation/newEngine/computeComputation/computeCalculations.js';
|
||||
import computeToggles from '/imports/api/creature/computation/newEngine/computeComputation/computeToggles.js';
|
||||
import computeByType from '/imports/api/creature/computation/newEngine/computeComputation/computeByType.js';
|
||||
|
||||
export default function computeCreatureComputation(computation){
|
||||
const stack = [];
|
||||
@@ -16,20 +17,16 @@ export default function computeCreatureComputation(computation){
|
||||
while (stack.length){
|
||||
let top = stack[stack.length - 1];
|
||||
if (top.visited){
|
||||
// The object has already
|
||||
// The object has already been computed, skip
|
||||
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
|
||||
// Push dependencies to graph to be computed first
|
||||
pushDependenciesToStack(top.node.id, graph, stack);
|
||||
top.visitedChildren = true;
|
||||
}
|
||||
@@ -37,32 +34,20 @@ export default function computeCreatureComputation(computation){
|
||||
}
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
|
||||
// Determine the prop's active status by its toggles
|
||||
computeToggles(node);
|
||||
computeCalculations(node, scope);
|
||||
// Compute the property by type
|
||||
let typeCompute = propTypeComputations[prop?.type || '_variable'];
|
||||
typeCompute?.(graph, node);
|
||||
computeByType[node.data?.type || '_variable']?.(graph, node, scope);
|
||||
}
|
||||
|
||||
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;
|
||||
// Ignore inventory links, they are already fully computed when they are
|
||||
// created
|
||||
if (link.data === 'inventory' || link.data === 'classLevel') return;
|
||||
stack.push({
|
||||
node: linkedNode,
|
||||
visited: false,
|
||||
|
||||
@@ -78,15 +78,6 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
||||
optional: true,
|
||||
index: 1,
|
||||
},
|
||||
// Denormalised list of all properties or creatures this property depends on
|
||||
dependencies: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
index: 1,
|
||||
},
|
||||
'dependencies.$': {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
|
||||
|
||||
@@ -90,6 +90,11 @@ const ComputedOnlyActionSchema = createPropertySchema({
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
// Uses - usesUsed
|
||||
usesLeft: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedActionSchema = new SimpleSchema()
|
||||
|
||||
@@ -107,6 +107,12 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
},
|
||||
// Attributes with proficiency grant it to all skills based on the attribute
|
||||
proficiency: {
|
||||
type: Number,
|
||||
allowedValues: [0.49, 0.5, 1, 2],
|
||||
optional: true,
|
||||
},
|
||||
// The computed creature constitution modifier for hit dice
|
||||
constitutionMod: {
|
||||
type: Number,
|
||||
|
||||
@@ -34,6 +34,12 @@ let EffectSchema = createPropertySchema({
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// Conditional benefits store just uncomputed text
|
||||
text: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
//which stats the effect is applied to
|
||||
stats: {
|
||||
type: Array,
|
||||
|
||||
@@ -36,6 +36,7 @@ let SavingThrowSchema = createPropertySchema({
|
||||
const ComputedOnlySavingThrowSchema = createPropertySchema({
|
||||
dc: {
|
||||
type: 'computedOnlyField',
|
||||
parseLevel: 'compile',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ let SkillSchema = createPropertySchema({
|
||||
baseProficiency: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
allowedValues: [0.49, 0.5, 1, 2],
|
||||
},
|
||||
// The starting value, before effects
|
||||
baseValue: {
|
||||
@@ -93,7 +94,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
|
||||
// Computed proficiency multiplier
|
||||
proficiency: {
|
||||
type: Number,
|
||||
allowedValues: [0, 0.5, 1, 2],
|
||||
allowedValues: [0, 0.49, 0.5, 1, 2],
|
||||
defaultValue: 0,
|
||||
},
|
||||
// Compiled text of all conditional benefits
|
||||
@@ -103,15 +104,6 @@ let ComputedOnlySkillSchema = createPropertySchema({
|
||||
},
|
||||
'conditionalBenefits.$': {
|
||||
type: String,
|
||||
},
|
||||
// Compiled text of all roll bonuses
|
||||
rollBonuses: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: STORAGE_LIMITS.rollBonusCount,
|
||||
},
|
||||
'rollBonuses.$': {
|
||||
type: String,
|
||||
},
|
||||
// Computed number of things forcing this skill to fail
|
||||
fail: {
|
||||
|
||||
@@ -23,6 +23,10 @@ const AttributeConsumedSchema = createPropertySchema({
|
||||
});
|
||||
|
||||
const ComputedOnlyAttributeConsumedSchema = createPropertySchema({
|
||||
quantity: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
available: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
@@ -37,10 +41,6 @@ const ComputedOnlyAttributeConsumedSchema = createPropertySchema({
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
quantity: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedAttributeConsumedSchema = new SimpleSchema()
|
||||
|
||||
@@ -36,13 +36,6 @@ const ComputedOnlyItemConsumedSchema = new SimpleSchema({
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
// This appears both in the computed and uncomputed schema because it can be
|
||||
// set by both a computation or a form
|
||||
itemId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
itemName: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
|
||||
@@ -9,10 +9,14 @@ export default class SymbolNode extends ParseNode {
|
||||
toString(){
|
||||
return `${this.name}`
|
||||
}
|
||||
compile(scope){
|
||||
compile(scope, context){
|
||||
let value = scope && scope[this.name];
|
||||
let type = typeof value;
|
||||
// For objects, get their value
|
||||
// For parse nodes, compile and return
|
||||
if (value instanceof ParseNode){
|
||||
return value.compile(scope, context);
|
||||
}
|
||||
// For objects, default to their .value
|
||||
if (type === 'object'){
|
||||
value = value.value;
|
||||
type = typeof value;
|
||||
|
||||
Reference in New Issue
Block a user