From aaa5d0b63b548cf57104f12f7e37cba7f3c35e38 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Tue, 2 Feb 2021 16:11:59 +0200 Subject: [PATCH] Allowed effects and calculations to target nearest ancestors of `#type` --- app/grammar.js | 0 .../creature/computation/ComputationMemo.js | 14 ++++++++++++-- .../creature/computation/EffectAggregator.js | 6 +++++- .../api/creature/computation/combineStat.js | 6 +++++- .../api/creature/computation/computeEffect.js | 6 +++++- .../computation/computeEndStepProperty.js | 4 ++-- .../computation/computeInlineCalculations.js | 2 +- .../api/creature/computation/computeToggle.js | 2 +- .../computation/evaluateCalculation.js | 19 ++++++++++++++++--- .../computation/findAncestorByType.js | 10 ++++++++++ app/imports/parser/grammar.js | 2 +- app/imports/parser/grammar.ne | 2 +- app/imports/parser/parseTree/AccessorNode.js | 13 ++++++++++--- 13 files changed, 69 insertions(+), 17 deletions(-) create mode 100644 app/grammar.js create mode 100644 app/imports/api/creature/computation/findAncestorByType.js diff --git a/app/grammar.js b/app/grammar.js new file mode 100644 index 00000000..e69de29b diff --git a/app/imports/api/creature/computation/ComputationMemo.js b/app/imports/api/creature/computation/ComputationMemo.js index 94a3e90f..43eb2c46 100644 --- a/app/imports/api/creature/computation/ComputationMemo.js +++ b/app/imports/api/creature/computation/ComputationMemo.js @@ -1,4 +1,5 @@ import { includes, cloneDeep } from 'lodash'; +import findAncestorByType from '/imports/api/creature/computation/findAncestorByType.js'; // The computation memo is an in-memory data structure used only during the // computation process @@ -141,7 +142,7 @@ export default class ComputationMemo { prop = this.registerProperty(prop); let targets = this.getEffectTargets(prop); targets.forEach(target => { - if (target.computationDetails.effects){ + if (target.computationDetails && target.computationDetails.effects){ target.computationDetails.effects.push(prop); } }); @@ -153,7 +154,16 @@ export default class ComputationMemo { let targets = new Set(); if (!prop.stats) return targets; prop.stats.forEach((statName) => { - let target = this.statsByVariableName[statName]; + let target; + if (statName[0] === '#'){ + target = findAncestorByType({ + type: statName.slice(1), + prop, + memo: this + }); + } else { + target = this.statsByVariableName[statName]; + } if (!target) return; targets.add(target); if (isSkillOperation(prop) && isAbility(target)){ diff --git a/app/imports/api/creature/computation/EffectAggregator.js b/app/imports/api/creature/computation/EffectAggregator.js index 4ec98f4c..e24f1ac6 100644 --- a/app/imports/api/creature/computation/EffectAggregator.js +++ b/app/imports/api/creature/computation/EffectAggregator.js @@ -8,7 +8,11 @@ export default class EffectAggregator{ result, context, dependencies - } = evaluateCalculation(stat.baseValueCalculation, memo); + } = evaluateCalculation({ + string: stat.baseValueCalculation, + prop: stat, + memo + }); this.statBaseValue = result.value; stat.dependencies.push(...dependencies); if (context.errors.length){ diff --git a/app/imports/api/creature/computation/combineStat.js b/app/imports/api/creature/computation/combineStat.js index 5588d46e..9449ea65 100644 --- a/app/imports/api/creature/computation/combineStat.js +++ b/app/imports/api/creature/computation/combineStat.js @@ -38,7 +38,11 @@ function combineAttribute(stat, aggregator, memo){ result, context, dependencies - } = evaluateCalculation(stat.spellSlotLevelCalculation, memo); + } = evaluateCalculation({ + string: stat.spellSlotLevelCalculation, + memo, + prop: stat, + }); stat.spellSlotLevelValue = result.value; stat.spellSlotLevelErrors = context.errors; stat.dependencies.push(...dependencies); diff --git a/app/imports/api/creature/computation/computeEffect.js b/app/imports/api/creature/computation/computeEffect.js index 76ae68e2..84ab4427 100644 --- a/app/imports/api/creature/computation/computeEffect.js +++ b/app/imports/api/creature/computation/computeEffect.js @@ -38,7 +38,11 @@ export default function computeEffect(effect, memo){ result, context, dependencies, - } = evaluateCalculation(effect.calculation, memo); + } = evaluateCalculation({ + string: effect.calculation, + prop: effect, + memo + }); effect.result = result.value; effect.dependencies.push(...dependencies); if (context.errors.length){ diff --git a/app/imports/api/creature/computation/computeEndStepProperty.js b/app/imports/api/creature/computation/computeEndStepProperty.js index 3319e9a8..57f4be16 100644 --- a/app/imports/api/creature/computation/computeEndStepProperty.js +++ b/app/imports/api/creature/computation/computeEndStepProperty.js @@ -33,7 +33,7 @@ function computeAction(prop, memo){ result, context, dependencies, - } = evaluateCalculation(prop.uses, memo); + } = evaluateCalculation({ string: prop.uses, prop, memo}); prop.usesResult = result.value; prop.dependencies.push(...dependencies); if (context.errors.length){ @@ -85,7 +85,7 @@ function computePropertyField(prop, memo, fieldName, fn){ result, context, dependencies, - } = evaluateCalculation(prop[fieldName], memo, fn); + } = evaluateCalculation({string: prop[fieldName], prop, memo, fn}); if (result instanceof ConstantNode){ prop[`${fieldName}Result`] = result.value; } else { diff --git a/app/imports/api/creature/computation/computeInlineCalculations.js b/app/imports/api/creature/computation/computeInlineCalculations.js index 879d5da1..17c4e274 100644 --- a/app/imports/api/creature/computation/computeInlineCalculations.js +++ b/app/imports/api/creature/computation/computeInlineCalculations.js @@ -19,7 +19,7 @@ function computeInlineCalcsForField(prop, memo, field){ result, context, dependencies, - } = evaluateCalculation(calculation, memo, 'compile'); + } = evaluateCalculation({string: calculation, prop, memo, fn: 'compile'}); let computation = { calculation, result: result.toString(), diff --git a/app/imports/api/creature/computation/computeToggle.js b/app/imports/api/creature/computation/computeToggle.js index 2783e2b2..507a81af 100644 --- a/app/imports/api/creature/computation/computeToggle.js +++ b/app/imports/api/creature/computation/computeToggle.js @@ -30,7 +30,7 @@ export default function computeToggle(toggle, memo){ result, context, dependencies, - } = evaluateCalculation(toggle.condition, memo); + } = evaluateCalculation({string: toggle.condition, prop: toggle, memo}); toggle.toggleResult = !!result.value; toggle.dependencies.push(...dependencies); if (context.errors.length){ diff --git a/app/imports/api/creature/computation/evaluateCalculation.js b/app/imports/api/creature/computation/evaluateCalculation.js index 58037dc8..1e9e6119 100644 --- a/app/imports/api/creature/computation/evaluateCalculation.js +++ b/app/imports/api/creature/computation/evaluateCalculation.js @@ -3,9 +3,15 @@ import { parse, CompilationContext } from '/imports/parser/parser.js'; import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; +import findAncestorByType from '/imports/api/creature/computation/findAncestorByType.js'; /* Convert a calculation into a constant output and errors*/ -export default function evaluateCalculation(string, memo, fn = 'reduce'){ +export default function evaluateCalculation({ + string, + prop, + memo, + fn = 'reduce', +}){ let dependencies = []; let errors = []; if (!string) return { @@ -38,8 +44,15 @@ export default function evaluateCalculation(string, memo, fn = 'reduce'){ // Ensure all symbol nodes are defined and computed calc.traverse(node => { if (node instanceof SymbolNode || node instanceof AccessorNode){ - let stat = memo.statsByVariableName[node.name]; - if (stat && !stat.computationDetails.computed){ + // References up the tree start with $ + let stat; + if (node.name[0] === '#'){ + stat = findAncestorByType({type: node.name.slice(1), prop, memo}); + memo.statsByVariableName[node.name] = stat; + } else { + stat = memo.statsByVariableName[node.name]; + } + if (stat && stat.computationDetails && !stat.computationDetails.computed){ computeStat(stat, memo); } if (stat) dependencies.push(stat._id || node.name, ...stat.dependencies); diff --git a/app/imports/api/creature/computation/findAncestorByType.js b/app/imports/api/creature/computation/findAncestorByType.js new file mode 100644 index 00000000..f8bafdf4 --- /dev/null +++ b/app/imports/api/creature/computation/findAncestorByType.js @@ -0,0 +1,10 @@ +export default function findAncestorByType({type, prop, memo}){ + if (!prop || !prop.ancestors) return; + let ancestor; + for (let i = prop.ancestors.length - 1; i >= 0; i--){ + ancestor = memo.propsById[prop.ancestors[i].id]; + if (ancestor && ancestor.type === type){ + return ancestor; + } + } +} diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index cd0bf939..4d0b0a78 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -24,7 +24,7 @@ function id(x) { return x[0]; } value: s => s.slice(1, -1), }, name: { - match: /[a-zA-Z_]*[a-ce-zA-Z_][a-zA-Z0-9_]*/, + match: /[a-zA-Z_#]*[a-ce-zA-Z_#][a-zA-Z0-9_#]*/, type: moo.keywords({ 'keywords': ['true', 'false'], }), diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index c75ea517..5f9016be 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -22,7 +22,7 @@ value: s => s.slice(1, -1), }, name: { - match: /[a-zA-Z_]*[a-ce-zA-Z_][a-zA-Z0-9_]*/, + match: /[a-zA-Z_#]*[a-ce-zA-Z_#][a-zA-Z0-9_#]*/, type: moo.keywords({ 'keywords': ['true', 'false'], }), diff --git a/app/imports/parser/parseTree/AccessorNode.js b/app/imports/parser/parseTree/AccessorNode.js index f64eb793..ad2d7cad 100644 --- a/app/imports/parser/parseTree/AccessorNode.js +++ b/app/imports/parser/parseTree/AccessorNode.js @@ -7,7 +7,7 @@ export default class AccessorNode extends ParseNode { this.name = name; this.path = path; } - compile(scope){ + compile(scope, context){ let value = scope && scope[this.name]; // For objects, get their value this.path.forEach(name => { @@ -16,14 +16,21 @@ export default class AccessorNode extends ParseNode { }); let type = typeof value; if (type === 'string' || type === 'number' || type === 'boolean'){ - return new ConstantNode({value, type, previousNodes: [this]}); + return new ConstantNode({value, type}); } else if (type === 'undefined'){ return new AccessorNode({ name: this.name, path: this.path, }); } else { - throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); + if (context) context.storeError({ + type: 'error', + message: `${this.name} returned an unexpected type` + }); + return new AccessorNode({ + name: this.name, + path: this.path, + }); } } reduce(scope, context){