Referencing a missing variable in an effect now returns zero, not an error
This commit is contained in:
@@ -3,8 +3,14 @@ import evaluateCalculation from '/imports/api/creature/computation/evaluateCalcu
|
|||||||
export default class EffectAggregator{
|
export default class EffectAggregator{
|
||||||
constructor(stat, memo){
|
constructor(stat, memo){
|
||||||
if (stat.baseValueCalculation){
|
if (stat.baseValueCalculation){
|
||||||
this.statBaseValue = evaluateCalculation(stat.baseValueCalculation, memo);
|
let {value, errors} = evaluateCalculation(stat.baseValueCalculation, memo);
|
||||||
this.base = +this.statBaseValue;
|
this.statBaseValue = value;
|
||||||
|
if (errors.length){
|
||||||
|
this.baseValueErrors = errors;
|
||||||
|
} else {
|
||||||
|
delete this.baseValueErrors;
|
||||||
|
}
|
||||||
|
this.base = this.statBaseValue;
|
||||||
} else {
|
} else {
|
||||||
this.base = 0;
|
this.base = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function computeEffect(effect, memo){
|
|||||||
applyToggles(effect, memo);
|
applyToggles(effect, memo);
|
||||||
|
|
||||||
// Determine result of effect calculation
|
// Determine result of effect calculation
|
||||||
|
delete effect.errors;
|
||||||
if (!effect.calculation){
|
if (!effect.calculation){
|
||||||
if(effect.operation === 'add' || effect.operation === 'base'){
|
if(effect.operation === 'add' || effect.operation === 'base'){
|
||||||
effect.result = 0;
|
effect.result = 0;
|
||||||
@@ -31,7 +32,11 @@ export default function computeEffect(effect, memo){
|
|||||||
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
|
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
|
||||||
effect.result = 1;
|
effect.result = 1;
|
||||||
} else {
|
} 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.computed = true;
|
||||||
effect.computationDetails.busyComputing = false;
|
effect.computationDetails.busyComputing = false;
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ export default function computeToggle(toggle, memo){
|
|||||||
} else if (Number.isFinite(+toggle.condition)){
|
} else if (Number.isFinite(+toggle.condition)){
|
||||||
toggle.toggleResult = !!+toggle.condition;
|
toggle.toggleResult = !!+toggle.condition;
|
||||||
} else {
|
} 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.computed = true;
|
||||||
toggle.computationDetails.busyComputing = false;
|
toggle.computationDetails.busyComputing = false;
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import bareSymbolSubtitutor from '/imports/api/creature/computation/utility/bareSymbolSubtitutor.js';
|
|
||||||
import computeStat from '/imports/api/creature/computation/computeStat.js';
|
import computeStat from '/imports/api/creature/computation/computeStat.js';
|
||||||
import math from '/imports/math.js';
|
import math from '/imports/math.js';
|
||||||
|
|
||||||
|
/* Convert a calculation into a constant output and errors*/
|
||||||
export default function evaluateCalculation(string, memo){
|
export default function evaluateCalculation(string, memo){
|
||||||
if (!string) return string;
|
if (!string) return string;
|
||||||
|
let errors = [];
|
||||||
// Parse the string using mathjs
|
// Parse the string using mathjs
|
||||||
let calc;
|
let calc;
|
||||||
try {
|
try {
|
||||||
calc = math.parse(string);
|
calc = math.parse(string);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return string;
|
errors.push({
|
||||||
|
type: 'parsing',
|
||||||
|
message: e.message || e
|
||||||
|
});
|
||||||
|
return {errors, value: string};
|
||||||
}
|
}
|
||||||
// Ensure all symbol nodes are defined and coputed
|
// Ensure all symbol nodes are defined and coputed
|
||||||
calc.traverse(node => {
|
calc.traverse(node => {
|
||||||
@@ -20,12 +25,73 @@ export default function evaluateCalculation(string, memo){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Ensure any bare symbols are value accessors instead
|
// Replace all symbols with their subtitution
|
||||||
let substitutedCalc = calc.transform(bareSymbolSubtitutor(memo.statsByVariableName));
|
let substitutedCalc = calc.transform(
|
||||||
|
symbolSubtitutor(memo.statsByVariableName, errors)
|
||||||
|
);
|
||||||
// Evaluate the expression to a number or return with substitutions
|
// Evaluate the expression to a number or return with substitutions
|
||||||
try {
|
try {
|
||||||
return substitutedCalc.evaluate(memo.statsByVariableName);
|
let value = substitutedCalc.evaluate(memo.statsByVariableName);
|
||||||
|
return {errors, value};
|
||||||
} catch (e){
|
} 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||||
/*
|
/*
|
||||||
* Effects are reason-value attached to skills and abilities
|
* Effects are reason-value attached to skills and abilities
|
||||||
* that modify their final value or presentation in some way
|
* that modify their final value or presentation in some way
|
||||||
@@ -48,7 +48,15 @@ const ComputedOnlyEffectSchema = new SimpleSchema({
|
|||||||
type: SimpleSchema.oneOf(Number, String, Boolean),
|
type: SimpleSchema.oneOf(Number, String, Boolean),
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
})
|
// The errors encountered while computing the result
|
||||||
|
errors: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'errors.$':{
|
||||||
|
type: ErrorSchema,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ComputedEffectSchema = new SimpleSchema()
|
const ComputedEffectSchema = new SimpleSchema()
|
||||||
.extend(ComputedOnlyEffectSchema)
|
.extend(ComputedOnlyEffectSchema)
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ const ComputedOnlyToggleSchema = new SimpleSchema({
|
|||||||
type: SimpleSchema.oneOf(Number, String, Boolean),
|
type: SimpleSchema.oneOf(Number, String, Boolean),
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
// The errors encountered while computing the result
|
||||||
|
errors: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'errors.$':{
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedToggleSchema = new SimpleSchema()
|
const ComputedToggleSchema = new SimpleSchema()
|
||||||
|
|||||||
14
app/imports/api/properties/subSchemas/ErrorSchema.js
Normal file
14
app/imports/api/properties/subSchemas/ErrorSchema.js
Normal file
@@ -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;
|
||||||
@@ -6,48 +6,35 @@
|
|||||||
:error-messages="errors.name"
|
:error-messages="errors.name"
|
||||||
@change="change('name', ...arguments)"
|
@change="change('name', ...arguments)"
|
||||||
/>
|
/>
|
||||||
<div class="layout row wrap justify-start">
|
<smart-select
|
||||||
<smart-select
|
label="Operation"
|
||||||
label="Operation"
|
append-icon="arrow_drop_down"
|
||||||
append-icon="arrow_drop_down"
|
class="mx-2"
|
||||||
class="mx-2"
|
:error-messages="errors.operation"
|
||||||
:error-messages="errors.operation"
|
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
:items="operations"
|
||||||
:items="operations"
|
:value="model.operation"
|
||||||
:value="model.operation"
|
@change="change('operation', ...arguments)"
|
||||||
@change="change('operation', ...arguments)"
|
>
|
||||||
|
<v-icon
|
||||||
|
slot="prepend"
|
||||||
|
class="icon"
|
||||||
|
:class="iconClass"
|
||||||
|
>
|
||||||
|
{{ displayedIcon }}
|
||||||
|
</v-icon>
|
||||||
|
<template
|
||||||
|
slot="item"
|
||||||
|
slot-scope="item"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
slot="prepend"
|
class="icon mr-2"
|
||||||
class="icon"
|
|
||||||
:class="iconClass"
|
|
||||||
>
|
>
|
||||||
{{ displayedIcon }}
|
{{ getEffectIcon(item.item.value, 1) }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
<template
|
{{ item.item.text }}
|
||||||
slot="item"
|
</template>
|
||||||
slot-scope="item"
|
</smart-select>
|
||||||
>
|
|
||||||
<v-icon
|
|
||||||
class="icon mr-2"
|
|
||||||
>
|
|
||||||
{{ getEffectIcon(item.item.value, 1) }}
|
|
||||||
</v-icon>
|
|
||||||
{{ item.item.text }}
|
|
||||||
</template>
|
|
||||||
</smart-select>
|
|
||||||
|
|
||||||
<text-field
|
|
||||||
label="Value"
|
|
||||||
class="mr-2"
|
|
||||||
:persistent-hint="needsValue"
|
|
||||||
:value="needsValue ? (model.calculation) : ' '"
|
|
||||||
:disabled="!needsValue"
|
|
||||||
:error-messages="errors.calculation"
|
|
||||||
:hint="!isFinite(model.calculation) && model.result ? model.result + '' : '' "
|
|
||||||
@change="change('calculation', ...arguments)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<smart-combobox
|
<smart-combobox
|
||||||
label="Stats"
|
label="Stats"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
@@ -59,6 +46,36 @@
|
|||||||
:error-messages="errors.stats"
|
:error-messages="errors.stats"
|
||||||
@change="change('stats', ...arguments)"
|
@change="change('stats', ...arguments)"
|
||||||
/>
|
/>
|
||||||
|
<text-field
|
||||||
|
label="Value"
|
||||||
|
class="mr-2"
|
||||||
|
:persistent-hint="needsValue"
|
||||||
|
:value="needsValue ? (model.calculation) : ' '"
|
||||||
|
:disabled="!needsValue"
|
||||||
|
:error-messages="errors.calculation"
|
||||||
|
:hint="!isFinite(model.calculation) && model.result ? model.result + '' : '' "
|
||||||
|
@change="change('calculation', ...arguments)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="model.errors && model.errors.length"
|
||||||
|
two-line
|
||||||
|
>
|
||||||
|
<v-slide-x-transition
|
||||||
|
group
|
||||||
|
leave-absolute
|
||||||
|
>
|
||||||
|
<v-alert
|
||||||
|
v-for="error in model.errors"
|
||||||
|
:key="error.message"
|
||||||
|
:value="true"
|
||||||
|
:icon="errorIcon(error.type)"
|
||||||
|
:color="errorColor(error.type)"
|
||||||
|
outline
|
||||||
|
>
|
||||||
|
{{ error.message }}
|
||||||
|
</v-alert>
|
||||||
|
</v-slide-x-transition>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -128,6 +145,24 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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,
|
getEffectIcon,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user