From 6ce7542c4b60e797281cc5340e615653d3601456 Mon Sep 17 00:00:00 2001 From: ThaumRystra <9525416+ThaumRystra@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:12:18 +0200 Subject: [PATCH] Changed aggregation schema of computed fields --- .../api/engine/actions/applyTriggers.js | 2 +- .../computeByType/computeCalculation.js | 127 +++++++++++++++--- .../aggregate/aggregateDefinition.js | 19 ++- .../aggregate/aggregateEffect.js | 21 ++- .../utility/evaluateCalculation.js | 9 +- .../properties/subSchemas/computedField.js | 36 +++-- app/imports/parser/parseTree/accessor.js | 20 ++- 7 files changed, 176 insertions(+), 58 deletions(-) diff --git a/app/imports/api/engine/actions/applyTriggers.js b/app/imports/api/engine/actions/applyTriggers.js index cbaf2108..de9476f6 100644 --- a/app/imports/api/engine/actions/applyTriggers.js +++ b/app/imports/api/engine/actions/applyTriggers.js @@ -39,7 +39,7 @@ export function applyTrigger(trigger, prop, actionContext) { // Prevent triggers from firing if their condition is false if (trigger.condition?.parseNode) { recalculateCalculation(trigger.condition, actionContext); - if (!trigger.condition.value) return; + if (!trigger.condition.value?.value) return; } // Prevent triggers from firing themselves in a loop diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index f695b25d..aae6cdcc 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -1,19 +1,25 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js'; -import { getPropertyName } from '/imports/constants/PROPERTIES.js'; +import call from '/imports/parser/parseTree/call.js'; +import constant from '/imports/parser/parseTree/constant.js'; +import operator from '/imports/parser/parseTree/operator.js'; +import parenthesis from '/imports/parser/parseTree/parenthesis.js'; +import { toString } from '/imports/parser/resolve.js'; export default function computeCalculation(computation, node) { const calcObj = node.data; evaluateCalculation(calcObj, computation.scope); if (calcObj.effects || calcObj.proficiencies) { - calcObj.baseValue = calcObj.value; + calcObj.unaffected = calcObj.value; + calcObj.displayUnaffected = toString(calcObj.unaffected); } aggregateCalculationEffects(node, computation); aggregateCalculationProficiencies(node, computation); + calcObj.displayValue = toString(calcObj.value); } function aggregateCalculationEffects(node, computation) { const calcObj = node.data; - delete calcObj.effects; + delete calcObj.effectIds; computation.dependencyGraph.forEachLinkedNode( node.id, (linkedNode, link) => { @@ -25,29 +31,104 @@ function aggregateCalculationEffects(node, computation) { if (linkedNode.data.inactive) return; // Collate effects - calcObj.effects = calcObj.effects || []; - calcObj.effects.push({ - _id: linkedNode.data._id, - name: linkedNode.data.name, - operation: linkedNode.data.operation, - amount: linkedNode.data.amount && { - value: linkedNode.data.amount.value, - //parseNode: linkedNode.data.amount.parseNode, - }, - // ancestors: linkedNode.data.ancestors, - }); + calcObj.effectIds = calcObj.effectIds || []; + calcObj.effectIds.push(linkedNode.data._id); }, true // enumerate only outbound links ); - if (calcObj.effects && typeof calcObj.value === 'number') { + if (calcObj.effectIds) { + // dictionary of {[operation]: parseNode} + const aggregator = {}; + // Store all effect values calcObj.effects.forEach(effect => { - if ( - effect.operation === 'add' && - effect.amount && typeof effect.amount.value === 'number' - ) { - calcObj.value += effect.amount.value + const op = effect.operation; + switch (op) { + case undefined: + break; + // Conditionals stored as a list of text + case 'conditional': + if (!aggregator[op]) aggregator[op] = []; + aggregator[op].push(effect.text); + break; + // Adv/Dis and Fails just count instances + case 'advantage': + case 'disadvantage': + case 'fail': + if (calcObj[op] === undefined) calcObj[op] = 0; + calcObj[op]++; + break; + // Math functions store value parseNodes + case 'base': + case 'add': + case 'mul': + case 'min': + case 'max': + case 'set': + if (!aggregator[op]) aggregator[op] = []; + aggregator[op].push(effect.amount.value); + break; + // No case for passiveAdd, it doesn't make sense in this context } }); + /** + * Aggregate the effects in a parse tree like so + * x = ( max(...base, unaffectedValue) + sum(...add) ) * mul(...mul) + * min(...min, x) + * max(...max, x) + * set(last(...set))a + */ + // Set + // If we do set, return early, nothing else matters + if (aggregator.set) { + calcObj.value = aggregator.set[aggregator.set.length - 1]; + return; + } + // Base value + if (aggregator.base) { + calcObj.value = call.create({ + functionName: 'max', + args: [calcObj.value, aggregator.base] + }); + } + // Add + aggregator.add?.forEach(node => { + calcObj.value = operator.create({ + left: calcObj.value, + right: node, + operator: '+' + }); + }); + // Multiply + if (aggregator.mul) { + // Wrap the previous node in brackets if it's another operator + if (calcObj.parseType === 'operator') { + calcObj.value = parenthesis.create({ + content: calcObj.value + }); + } + // Append all multiplications + aggregator.mul.forEach(node => { + calcObj.value = operator.create({ + left: calcObj.value, + right: node, + operator: '*' + }); + }); + } + // Min + if (aggregator.min) { + calcObj.value = call.create({ + functionName: 'max', + args: [calcObj.value, aggregator.min] + }); + } + // Max + if (aggregator.max) { + calcObj.value = call.create({ + functionName: 'min', + args: [calcObj.value, aggregator.max] + }); + } } } @@ -110,6 +191,10 @@ function aggregateCalculationProficiencies(node, computation) { prof.overridden = true; } }); - calcObj.value += calcObj.proficiencyBonus; + calcObj.value = operator.create({ + left: calcObj.value, + right: constant.create({ value: calcObj.proficiencyBonus }), + operator: '+' + }); } } diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js index d665b284..9ddd0600 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateDefinition.js @@ -1,5 +1,5 @@ -export default function aggregateDefinition({node, linkedNode, link}){ +export default function aggregateDefinition({ node, linkedNode, link }) { // Look at all definition links if (link.data !== 'definition') return; @@ -24,7 +24,14 @@ export default function aggregateDefinition({node, linkedNode, link}){ } // Aggregate the base value due to the defining properties - let propBaseValue = prop.baseValue?.value; + let propBaseValue = undefined; + const valueNode = prop.baseValue?.value; + if ( + valueNode?.parseType === 'constant' + && valueNode?.valueType === 'number' + ) { + propBaseValue = valueNode.value; + } // Point buy rows use prop.value instead of prop.baseValue if (prop.type === 'pointBuyRow') { propBaseValue = prop.value; @@ -38,7 +45,7 @@ export default function aggregateDefinition({node, linkedNode, link}){ _id: prop.tableId, name: prop.tableName, operation: 'base', - amount: { value: propBaseValue }, + amount: propBaseValue, type: 'pointBuy', }); } else { @@ -46,16 +53,16 @@ export default function aggregateDefinition({node, linkedNode, link}){ _id: prop._id, name: prop.name, operation: 'base', - amount: { value: propBaseValue }, + amount: propBaseValue, type: prop.type, }); } - if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ + if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue) { node.data.baseValue = propBaseValue; } } -function overrideProp(prop, node){ +function overrideProp(prop, node) { if (!prop) return; prop.overridden = true; if (!node.data.overriddenProps) node.data.overriddenProps = []; diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js index 4bbf2e3e..6546f18c 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -22,21 +22,11 @@ export default function aggregateEffect({ node, linkedNode, link }) { // Store a summary of the effect itself node.data.effects = node.data.effects || []; // Store either just - let effectAmount; - if (!linkedNode.data.amount) { - effectAmount = undefined; - } else if (typeof linkedNode.data.amount.value === 'string') { - effectAmount = pick(linkedNode.data.amount, [ - 'calculation', 'parseNode', 'parseError', 'value' - ]); - } else { - effectAmount = pick(linkedNode.data.amount, ['value']); - } node.data.effects.push({ _id: linkedNode.data._id, name: linkedNode.data.name, operation: linkedNode.data.operation, - amount: effectAmount, + amount: linkedNode.data.amount.displayValue, type: linkedNode.data.type, text: linkedNode.data.text, // ancestors: linkedNode.data.ancestors, @@ -45,7 +35,14 @@ export default function aggregateEffect({ node, linkedNode, link }) { // get a shorter reference to the aggregator document const aggregator = node.data.effectAggregator; // Get the result of the effect - let result = linkedNode.data.amount?.value; + let result = undefined; + const valueNode = linkedNode.data.amount?.value; + if ( + valueNode?.parseType === 'constant' + && valueNode?.valueType === 'number' + ) { + result = valueNode.value; + } if (typeof result !== 'number') result = undefined; // Aggregate the effect based on its operation diff --git a/app/imports/api/engine/computation/utility/evaluateCalculation.js b/app/imports/api/engine/computation/utility/evaluateCalculation.js index 6d5f1f82..4323d377 100644 --- a/app/imports/api/engine/computation/utility/evaluateCalculation.js +++ b/app/imports/api/engine/computation/utility/evaluateCalculation.js @@ -6,13 +6,8 @@ export default function evaluateCalculation(calculation, scope, givenContext) { const calculationScope = { ...calculation._localScope, ...scope }; const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext); calculation.errors = context.errors; - if (resultNode?.parseType === 'constant') { - calculation.value = resultNode.value; - } else if (resultNode?.parseType === 'error') { - calculation.value = null; - } else { - calculation.value = toString(resultNode); - } + calculation.value = resultNode; + calculation.displayValue = toString(resultNode); // remove the working fields delete calculation._parseLevel; delete calculation._localScope; diff --git a/app/imports/api/properties/subSchemas/computedField.js b/app/imports/api/properties/subSchemas/computedField.js index 6ff3d20b..48e16760 100644 --- a/app/imports/api/properties/subSchemas/computedField.js +++ b/app/imports/api/properties/subSchemas/computedField.js @@ -25,28 +25,24 @@ function computedOnlyField(field) { optional: true, blackbox: true, }, - /* - // toString(.baseValue) - [`${field}.baseValueString`]: { + // toString(.unaffected) + [`${field}.displayUnaffected`]: { type: SimpleSchema.oneOf(String, Number), optional: true, removeBeforeCompute: true, }, - */ // The compiled parseNode after applying all effects [`${field}.value`]: { type: Object, optional: true, blackbox: true, }, - /* - // toString(.value) - [`${field}.valueString`]: { + // The displayed value of the calculation: toString(.value) + [`${field}.displayValue`]: { type: SimpleSchema.oneOf(String, Number), optional: true, removeBeforeCompute: true, }, - */ // A list of effect Ids targeting this calculation [`${field}.effectIds`]: { type: Array, @@ -89,6 +85,30 @@ function computedOnlyField(field) { [`${field}.errors.$`]: { type: ErrorSchema, }, + // Effect aggregations + [`${field}.advantage`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.disadvantage`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.fail`]: { + type: Number, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.conditional`]: { + type: Array, + optional: true, + removeBeforeCompute: true, + }, + [`${field}.conditional.$`]: { + type: String, + }, } includeParentFields(field, schemaObj); return new SimpleSchema(schemaObj); diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js index 5ca0e325..51fbbcbf 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.js @@ -17,13 +17,20 @@ const accessor = { if (value === undefined) return; value = value[name]; }); - let valueType = Array.isArray(value) ? 'array' : typeof value; + let valueType = getType(value); // If the accessor returns an objet, get the object's value instead while (valueType === 'object') { value = value.value; - valueType = Array.isArray(value) ? 'array' : typeof value; + valueType = getType(value); } - // Return a parse node based on the type returned + // Return a discovered parse node + if (valueType === 'parseNode') { + return { + result: value, + context, + }; + } + // Return a parse node based on the constant type returned if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') { return { result: constant.create({ @@ -83,4 +90,11 @@ const accessor = { } } +function getType(val) { + if (!val) return typeof val; + if (Array.isArray(val)) return 'array'; + if (val.parseType) return 'parseNode'; + return typeof val; +} + export default accessor;