From 7f2401da8138fd422e9db45d7d3f1cafd4b36981 Mon Sep 17 00:00:00 2001 From: Thaum Rystra Date: Thu, 28 May 2020 19:58:52 +0200 Subject: [PATCH] Referencing a missing variable in an effect now returns zero, not an error --- .../creature/computation/EffectAggregator.js | 10 +- .../api/creature/computation/computeEffect.js | 7 +- .../api/creature/computation/computeToggle.js | 8 +- .../computation/evaluateCalculation.js | 78 +++++++++++- app/imports/api/properties/Effects.js | 12 +- app/imports/api/properties/Toggles.js | 8 ++ .../api/properties/subSchemas/ErrorSchema.js | 14 +++ .../ui/properties/forms/EffectForm.vue | 111 ++++++++++++------ 8 files changed, 198 insertions(+), 50 deletions(-) create mode 100644 app/imports/api/properties/subSchemas/ErrorSchema.js diff --git a/app/imports/api/creature/computation/EffectAggregator.js b/app/imports/api/creature/computation/EffectAggregator.js index 8f93bbfb..1525cfb1 100644 --- a/app/imports/api/creature/computation/EffectAggregator.js +++ b/app/imports/api/creature/computation/EffectAggregator.js @@ -3,8 +3,14 @@ import evaluateCalculation from '/imports/api/creature/computation/evaluateCalcu export default class EffectAggregator{ constructor(stat, memo){ if (stat.baseValueCalculation){ - this.statBaseValue = evaluateCalculation(stat.baseValueCalculation, memo); - this.base = +this.statBaseValue; + let {value, errors} = evaluateCalculation(stat.baseValueCalculation, memo); + this.statBaseValue = value; + if (errors.length){ + this.baseValueErrors = errors; + } else { + delete this.baseValueErrors; + } + this.base = this.statBaseValue; } else { this.base = 0; } diff --git a/app/imports/api/creature/computation/computeEffect.js b/app/imports/api/creature/computation/computeEffect.js index cad59fc6..5645fe1b 100644 --- a/app/imports/api/creature/computation/computeEffect.js +++ b/app/imports/api/creature/computation/computeEffect.js @@ -20,6 +20,7 @@ export default function computeEffect(effect, memo){ applyToggles(effect, memo); // Determine result of effect calculation + delete effect.errors; if (!effect.calculation){ if(effect.operation === 'add' || effect.operation === 'base'){ effect.result = 0; @@ -31,7 +32,11 @@ export default function computeEffect(effect, memo){ } else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){ effect.result = 1; } else { - effect.result = evaluateCalculation(effect.calculation, memo); + let {value, errors} = evaluateCalculation(effect.calculation, memo); + effect.result = value; + if (errors.length){ + effect.errors = errors; + } } effect.computationDetails.computed = true; effect.computationDetails.busyComputing = false; diff --git a/app/imports/api/creature/computation/computeToggle.js b/app/imports/api/creature/computation/computeToggle.js index a8a68ca4..cd0642d3 100644 --- a/app/imports/api/creature/computation/computeToggle.js +++ b/app/imports/api/creature/computation/computeToggle.js @@ -25,7 +25,13 @@ export default function computeToggle(toggle, memo){ } else if (Number.isFinite(+toggle.condition)){ toggle.toggleResult = !!+toggle.condition; } else { - toggle.toggleResult = evaluateCalculation(toggle.condition, memo); + let {value, errors} = evaluateCalculation(toggle.condition, memo); + toggle.toggleResult = value; + if (errors.length){ + toggle.errors = errors; + } else { + delete toggle.errors; + } } toggle.computationDetails.computed = true; toggle.computationDetails.busyComputing = false; diff --git a/app/imports/api/creature/computation/evaluateCalculation.js b/app/imports/api/creature/computation/evaluateCalculation.js index db34d15f..4c077996 100644 --- a/app/imports/api/creature/computation/evaluateCalculation.js +++ b/app/imports/api/creature/computation/evaluateCalculation.js @@ -1,15 +1,20 @@ -import bareSymbolSubtitutor from '/imports/api/creature/computation/utility/bareSymbolSubtitutor.js'; import computeStat from '/imports/api/creature/computation/computeStat.js'; import math from '/imports/math.js'; +/* Convert a calculation into a constant output and errors*/ export default function evaluateCalculation(string, memo){ if (!string) return string; + let errors = []; // Parse the string using mathjs let calc; try { calc = math.parse(string); } catch (e) { - return string; + errors.push({ + type: 'parsing', + message: e.message || e + }); + return {errors, value: string}; } // Ensure all symbol nodes are defined and coputed calc.traverse(node => { @@ -20,12 +25,73 @@ export default function evaluateCalculation(string, memo){ } } }); - // Ensure any bare symbols are value accessors instead - let substitutedCalc = calc.transform(bareSymbolSubtitutor(memo.statsByVariableName)); + // Replace all symbols with their subtitution + let substitutedCalc = calc.transform( + symbolSubtitutor(memo.statsByVariableName, errors) + ); // Evaluate the expression to a number or return with substitutions try { - return substitutedCalc.evaluate(memo.statsByVariableName); + let value = substitutedCalc.evaluate(memo.statsByVariableName); + return {errors, value}; } catch (e){ - return substitutedCalc.toString(); + errors.push({ + type: 'evaluation', + message: e.message || e + }); + let value = substitutedCalc.toString(); + return {errors, value}; + } +} + +// returns a function to replace all symbols with either their resolved value +// or zero, keeping the errors +function symbolSubtitutor(scope, errors){ + return function(node){ + // mark symbol nodes that are children of function nodes to be skipped + if (node.isFunctionNode){ + let fn = node.fn; + if (fn && fn.isSymbolNode){ + fn.skipReplacement = true; + } + return node; + } else if (node.isSymbolNode && node.skipReplacement !== true){ + //bare symbols of name "stat", should search for stat.value + let stat = scope[node.name]; + if (stat){ + if (stat.value === undefined){ + errors.push({ + type: 'subsitution', + message: `${node.name} does not have a value, set to 0` + }); + return new math.ConstantNode(0); + } else { + return new math.ConstantNode(stat.value); + } + } else { + try { + return new math.ConstantNode(node.evaluate(scope)); + } catch (e) { + errors.push({ + type: 'subsitution', + message: `${node.name} not found, set to 0` + }); + return new math.ConstantNode(0); + } + } + } else if (node.isAccessorNode){ + try { + let value = node.evaluate(scope); + if (value === undefined) throw 'Not found'; + return new math.ConstantNode(value); + } catch (e) { + errors.push({ + type: 'subsitution', + message: `${node.toString()} not found, set to 0` + }); + return new math.ConstantNode(0); + } + } else { + return node; + } } } diff --git a/app/imports/api/properties/Effects.js b/app/imports/api/properties/Effects.js index d1ed3adc..7ebc7ab7 100644 --- a/app/imports/api/properties/Effects.js +++ b/app/imports/api/properties/Effects.js @@ -1,5 +1,5 @@ import SimpleSchema from 'simpl-schema'; - +import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; /* * Effects are reason-value attached to skills and abilities * that modify their final value or presentation in some way @@ -48,7 +48,15 @@ const ComputedOnlyEffectSchema = new SimpleSchema({ type: SimpleSchema.oneOf(Number, String, Boolean), optional: true, }, -}) + // The errors encountered while computing the result + errors: { + type: Array, + optional: true, + }, + 'errors.$':{ + type: ErrorSchema, + }, +}); const ComputedEffectSchema = new SimpleSchema() .extend(ComputedOnlyEffectSchema) diff --git a/app/imports/api/properties/Toggles.js b/app/imports/api/properties/Toggles.js index 061f86e3..5f588afc 100644 --- a/app/imports/api/properties/Toggles.js +++ b/app/imports/api/properties/Toggles.js @@ -27,6 +27,14 @@ const ComputedOnlyToggleSchema = new SimpleSchema({ type: SimpleSchema.oneOf(Number, String, Boolean), optional: true, }, + // The errors encountered while computing the result + errors: { + type: Array, + optional: true, + }, + 'errors.$':{ + type: String, + }, }); const ComputedToggleSchema = new SimpleSchema() diff --git a/app/imports/api/properties/subSchemas/ErrorSchema.js b/app/imports/api/properties/subSchemas/ErrorSchema.js new file mode 100644 index 00000000..658f8b53 --- /dev/null +++ b/app/imports/api/properties/subSchemas/ErrorSchema.js @@ -0,0 +1,14 @@ +import SimpleSchema from 'simpl-schema'; + +const ErrorSchema = new SimpleSchema({ + // The roll that determines how much to change the attribute + message: { + type: String, + }, + // Who this adjustment applies to + type: { + type: String, + }, +}); + +export default ErrorSchema; diff --git a/app/imports/ui/properties/forms/EffectForm.vue b/app/imports/ui/properties/forms/EffectForm.vue index 2c18c180..cdf2ec62 100644 --- a/app/imports/ui/properties/forms/EffectForm.vue +++ b/app/imports/ui/properties/forms/EffectForm.vue @@ -6,48 +6,35 @@ :error-messages="errors.name" @change="change('name', ...arguments)" /> -
- + + {{ displayedIcon }} + + + + +
+ + + {{ error.message }} + + +
@@ -128,6 +145,24 @@ }, }, methods: { + errorIcon(type){ + if (type === 'subsitution'){ + return 'info'; + } else if (type === 'evaluation'){ + return 'warning'; + } else { + return 'error' + } + }, + errorColor(type){ + if (type === 'subsitution'){ + return 'info'; + } else if (type === 'evaluation'){ + return 'warning'; + } else { + return 'error' + } + }, getEffectIcon, } };