More engine rewrite progress, starting to get messy again

This commit is contained in:
Stefan Zermatten
2021-09-13 16:12:04 +02:00
parent b877a8b45f
commit 5c84836238
38 changed files with 614 additions and 262 deletions

View File

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

View File

@@ -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');
});
}

View File

@@ -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 = {

View File

@@ -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');
}

View File

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

View File

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

View File

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

View File

@@ -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,
});

View File

@@ -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,
});

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
export default function computSlot(graph, node){
const prop = node.data;
if (prop.quantityExpected){
prop.spaceLeft = prop.quantityExpected - prop.totalFilled;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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);

View File

@@ -90,6 +90,11 @@ const ComputedOnlyActionSchema = createPropertySchema({
type: 'computedOnlyField',
optional: true,
},
// Uses - usesUsed
usesLeft: {
type: Number,
optional: true,
},
});
const ComputedActionSchema = new SimpleSchema()

View File

@@ -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,

View File

@@ -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,

View File

@@ -36,6 +36,7 @@ let SavingThrowSchema = createPropertySchema({
const ComputedOnlySavingThrowSchema = createPropertySchema({
dc: {
type: 'computedOnlyField',
parseLevel: 'compile',
optional: true,
},
});

View File

@@ -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: {

View File

@@ -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()

View File

@@ -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,

View File

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