diff --git a/app/imports/api/creature/computation/afterComputation/evaluateString.js b/app/imports/api/creature/computation/afterComputation/evaluateString.js
index 9753d963..e012ff58 100644
--- a/app/imports/api/creature/computation/afterComputation/evaluateString.js
+++ b/app/imports/api/creature/computation/afterComputation/evaluateString.js
@@ -18,7 +18,12 @@ export default function evaluateString(string, scope, fn = 'compile'){
errors.push(e);
return {result: string, errors};
}
-
+ // Parsing failed
+ if (node === null){
+ errors.push('...');
+ return {result: string, errors};
+ }
+ console.log(node);
let context = new CompilationContext();
let result = node[fn](scope, context);
if (result instanceof ConstantNode){
diff --git a/app/imports/api/creature/computation/engine/ComputationMemo.js b/app/imports/api/creature/computation/engine/ComputationMemo.js
index 8bf93765..4fd412a3 100644
--- a/app/imports/api/creature/computation/engine/ComputationMemo.js
+++ b/app/imports/api/creature/computation/engine/ComputationMemo.js
@@ -6,6 +6,7 @@ import findAncestorByType from '/imports/api/creature/computation/engine/findAnc
export default class ComputationMemo {
constructor(props, creature){
this.statsByVariableName = {};
+ this.constantsByVariableName = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
@@ -51,13 +52,15 @@ export default class ComputationMemo {
return true;
}
}).forEach((prop) => {
- // Now add all effects and proficiencies
+ // Now add everything else
if (prop.type === 'effect'){
this.addEffect(prop);
} else if (prop.type === 'proficiency') {
this.addProficiency(prop);
} else if (prop.type === 'classLevel'){
this.addClassLevel(prop);
+ } else if (prop.type === 'constant'){
+ this.addConstant(prop);
} else {
this.addEndStepProp(prop);
}
@@ -72,6 +75,14 @@ export default class ComputationMemo {
}
}
}
+ addConstant(prop){
+ prop = this.registerProperty(prop);
+ if (
+ !this.constantsByVariableName[prop.variableName]
+ ){
+ this.constantsByVariableName[prop.variableName] = prop
+ }
+ }
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
this.propsById[prop._id] = prop;
diff --git a/app/imports/api/creature/computation/engine/evaluateCalculation.js b/app/imports/api/creature/computation/engine/evaluateCalculation.js
index fb49431c..de972e6a 100644
--- a/app/imports/api/creature/computation/engine/evaluateCalculation.js
+++ b/app/imports/api/creature/computation/engine/evaluateCalculation.js
@@ -42,11 +42,75 @@ export default function evaluateCalculation({
dependencies,
};
}
+
+ // Replace constants with their parsed constant
+ let failed = replaceConstants({calc, memo, prop, dependencies, errors})
+ if (failed){
+ return {
+ context: {errors},
+ result: new ConstantNode({value: string, type: 'string'}),
+ dependencies,
+ };
+ }
+
// Ensure all symbol nodes are defined and computed
+ computeSymbols({calc, memo, prop, dependencies})
+
+ // Evaluate
+ let context = new CompilationContext();
+ let result = calc[fn](memo.statsByVariableName, context);
+ return {result, context, dependencies};
+}
+
+// Replace constants in the calc with the right ParseNodes
+function replaceConstants({calc, memo, prop, dependencies, errors}){
+ let constFailed = [];
+ calc.replaceNodes(node => {
+ if (!(node instanceof SymbolNode)) return;
+ let stat, constant;
+ if (node.name[0] !== '#'){
+ stat = memo.statsByVariableName[node.name]
+ constant = memo.constantsByVariableName[node.name];
+ } else if (node.name === '#constant'){
+ constant = findAncestorByType({type: 'constant', prop, memo});
+ }
+ // replace constants that aren't overridden by stats
+ if (constant && !stat){
+ dependencies = union(dependencies, [
+ constant._id,
+ ...constant.dependencies
+ ]);
+ // Fail if the constant has errors
+ if (constant.errors && constant.errors.length){
+ constFailed.push(node.name);
+ return;
+ }
+ let parsedConstantNode;
+ try {
+ parsedConstantNode = parse(constant.calculation);
+ } catch(e){
+ constFailed.push(node.name);
+ return;
+ }
+ if (!parsedConstantNode) constFailed.push(node.name);
+ return parsedConstantNode;
+ }
+ });
+ constFailed.forEach(name => {
+ errors.push({
+ type: 'error',
+ message: `${name} is a constant property with parsing errors`
+ });
+ });
+ return !!constFailed.length;
+}
+
+ // Ensure all symbol nodes are defined and computed
+function computeSymbols({calc, memo, prop, dependencies}){
calc.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
- // References up the tree start with $
let stat;
+ // References up the tree start with #
if (node.name[0] === '#'){
stat = findAncestorByType({type: node.name.slice(1), prop, memo});
memo.statsByVariableName[node.name] = stat;
@@ -64,8 +128,4 @@ export default function evaluateCalculation({
}
}
});
- // Evaluate
- let context = new CompilationContext();
- let result = calc[fn](memo.statsByVariableName, context);
- return {result, context, dependencies};
}
diff --git a/app/imports/api/properties/Constants.js b/app/imports/api/properties/Constants.js
index 3a805547..830ee526 100644
--- a/app/imports/api/properties/Constants.js
+++ b/app/imports/api/properties/Constants.js
@@ -1,6 +1,9 @@
import SimpleSchema from 'simpl-schema';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
-
+import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
+import { parse, CompilationContext } from '/imports/parser/parser.js';
+import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
+import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
/*
* Constants are primitive values that can be used elsewhere in computations
*/
@@ -22,14 +25,59 @@ let ConstantSchema = new SimpleSchema({
type: String,
optional: true,
},
- // The value, or array of values
- result: {
- type: SimpleSchema.oneOf(String, Number, Boolean, Array),
- maxSize: 32,
+ errors: {
+ type: Array,
+ autoValue(){
+ let calc = this.field('calculation');
+ if (!calc.isSet && this.isModifier) {
+ this.unset()
+ return;
+ }
+ let string = calc.value;
+ // Evaluate the calculation with no scope
+ let {result, errors} = parseString(string);
+ // Any errors will result in a failure
+ if (errors.length) return errors;
+ // Ban variables in constants if necessary
+ result && result.traverse(node => {
+ if (node instanceof SymbolNode || node instanceof AccessorNode){
+ errors.push({
+ type: 'error',
+ message: 'Variables can\'t be used to define a constant'
+ });
+ }
+ });
+ return errors;
+ }
+ },
+ 'errors.$':{
+ type: ErrorSchema,
},
- 'result.$': {
- type: SimpleSchema.oneOf(String, Number, Boolean),
- }
});
+function parseString(string, fn = 'compile'){
+ let errors = [];
+ if (!string){
+ return {result: string, errors};
+ }
+
+ // Parse the string using mathjs
+ let node;
+ try {
+ node = parse(string);
+ } catch (e) {
+ let message = e.toString().split('.')[0];
+ errors.push({type: 'error', message});
+ return {result: string, errors};
+ }
+ // Parsing incomplete
+ if (node === null){
+ errors.push({type: 'warning', message: 'Unexpected end of input'});
+ return {result: string, errors};
+ }
+ let context = new CompilationContext();
+ let result = node[fn]({/*empty scope*/}, context);
+ return {result, errors: context.errors}
+}
+
export { ConstantSchema };
diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js
index 273ae54f..5bb0e9d0 100644
--- a/app/imports/api/properties/computedPropertySchemasIndex.js
+++ b/app/imports/api/properties/computedPropertySchemasIndex.js
@@ -5,6 +5,7 @@ import { ComputedAttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedBuffSchema } from '/imports/api/properties/Buffs.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
+import { ConstantSchema } from '/imports/api/properties/Constants.js';
import { ComputedContainerSchema } from '/imports/api/properties/Containers.js';
import { ComputedDamageSchema } from '/imports/api/properties/Damages.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
@@ -30,6 +31,7 @@ const propertySchemasIndex = {
attribute: ComputedAttributeSchema,
buff: ComputedBuffSchema,
classLevel: ClassLevelSchema,
+ constant: ConstantSchema,
damage: ComputedDamageSchema,
damageMultiplier: DamageMultiplierSchema,
effect: ComputedEffectSchema,
diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js
index cc88f2ba..9a3596ab 100644
--- a/app/imports/api/properties/propertySchemasIndex.js
+++ b/app/imports/api/properties/propertySchemasIndex.js
@@ -5,6 +5,7 @@ import { AttackSchema } from '/imports/api/properties/Attacks.js';
import { AttributeSchema } from '/imports/api/properties/Attributes.js';
import { BuffSchema } from '/imports/api/properties/Buffs.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
+import { ConstantSchema } from '/imports/api/properties/Constants.js';
import { DamageSchema } from '/imports/api/properties/Damages.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
import { EffectSchema } from '/imports/api/properties/Effects.js';
@@ -30,6 +31,7 @@ const propertySchemasIndex = {
attribute: AttributeSchema,
buff: BuffSchema,
classLevel: ClassLevelSchema,
+ constant: ConstantSchema,
damage: DamageSchema,
damageMultiplier: DamageMultiplierSchema,
effect: EffectSchema,
diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js
index 18646ac7..ab8e5b0b 100644
--- a/app/imports/constants/PROPERTIES.js
+++ b/app/imports/constants/PROPERTIES.js
@@ -23,6 +23,10 @@ const PROPERTIES = Object.freeze({
icon: '$vuetify.icons.class_level',
name: 'Class level'
},
+ constant: {
+ icon: 'anchor',
+ name: 'Constant'
+ },
container: {
icon: 'work',
name: 'Container'
diff --git a/app/imports/parser/parseTree/ArrayNode.js b/app/imports/parser/parseTree/ArrayNode.js
index 62e0bbd7..2f31fa69 100644
--- a/app/imports/parser/parseTree/ArrayNode.js
+++ b/app/imports/parser/parseTree/ArrayNode.js
@@ -33,4 +33,7 @@ export default class ArrayNode extends ParseNode {
fn(this);
this.values.forEach(value => value.traverse(fn));
}
+ replaceChildren(fn){
+ this.values = this.values.map(node => node.replaceNodes(fn));
+ }
}
diff --git a/app/imports/parser/parseTree/CallNode.js b/app/imports/parser/parseTree/CallNode.js
index a603c6aa..12cab9e4 100644
--- a/app/imports/parser/parseTree/CallNode.js
+++ b/app/imports/parser/parseTree/CallNode.js
@@ -54,6 +54,9 @@ export default class CallNode extends ParseNode {
fn(this);
this.args.forEach(arg => arg.traverse(fn));
}
+ replaceChildren(fn){
+ this.args = this.args.map(arg => arg.replaceNodes(fn));
+ }
}
function castArgsToType({fn, scope, context, args, type}){
diff --git a/app/imports/parser/parseTree/IfNode.js b/app/imports/parser/parseTree/IfNode.js
index 8ffe7831..a94f21c2 100644
--- a/app/imports/parser/parseTree/IfNode.js
+++ b/app/imports/parser/parseTree/IfNode.js
@@ -34,4 +34,9 @@ export default class IfNode extends ParseNode {
this.consequent.traverse(fn);
this.alternative.traverse(fn);
}
+ replaceChildren(fn){
+ this.condition = this.condition.replaceNodes(fn);
+ this.consequent = this.consequent.replaceNodes(fn);
+ this.alternative = this.alternative.replaceNodes(fn);
+ }
}
diff --git a/app/imports/parser/parseTree/IndexNode.js b/app/imports/parser/parseTree/IndexNode.js
index 8f7684f2..850ff088 100644
--- a/app/imports/parser/parseTree/IndexNode.js
+++ b/app/imports/parser/parseTree/IndexNode.js
@@ -29,4 +29,8 @@ export default class IndexNode extends ParseNode {
this.array.traverse(fn);
this.index.traverse(fn);
}
+ replaceChildren(fn){
+ this.array = this.array.replaceNodes(fn);
+ this.index = this.index.replaceNodes(fn);
+ }
}
diff --git a/app/imports/parser/parseTree/NotOperatorNode.js b/app/imports/parser/parseTree/NotOperatorNode.js
index 64568f59..47aca511 100644
--- a/app/imports/parser/parseTree/NotOperatorNode.js
+++ b/app/imports/parser/parseTree/NotOperatorNode.js
@@ -28,4 +28,7 @@ export default class NotOperatorNode extends ParseNode {
fn(this);
this.right.traverse(fn);
}
+ replaceChildren(fn){
+ this.right = this.right.replaceNodes(fn);
+ }
}
diff --git a/app/imports/parser/parseTree/OperatorNode.js b/app/imports/parser/parseTree/OperatorNode.js
index 6fe63428..7bd546de 100644
--- a/app/imports/parser/parseTree/OperatorNode.js
+++ b/app/imports/parser/parseTree/OperatorNode.js
@@ -60,4 +60,8 @@ export default class OperatorNode extends ParseNode {
this.left.traverse(fn);
this.right.traverse(fn);
}
+ replaceChildren(fn){
+ this.left = this.left.replaceNodes(fn);
+ this.right = this.right.replaceNodes(fn);
+ }
}
diff --git a/app/imports/parser/parseTree/ParenthesisNode.js b/app/imports/parser/parseTree/ParenthesisNode.js
index abe600b3..eda6038f 100644
--- a/app/imports/parser/parseTree/ParenthesisNode.js
+++ b/app/imports/parser/parseTree/ParenthesisNode.js
@@ -23,4 +23,7 @@ export default class ParenthesisNode extends ParseNode {
fn(this);
this.content.traverse(fn);
}
+ replaceChildren(fn){
+ this.content = this.content.replaceNodes(fn);
+ }
}
diff --git a/app/imports/parser/parseTree/ParseNode.js b/app/imports/parser/parseTree/ParseNode.js
index 2379081a..740c7b14 100644
--- a/app/imports/parser/parseTree/ParseNode.js
+++ b/app/imports/parser/parseTree/ParseNode.js
@@ -30,4 +30,14 @@ export default class ParseNode {
traverse(fn){
fn(this);
}
+ // replace nodes, only replace nodes if fn returns a value
+ replaceNodes(fn){
+ let newNode = fn(this);
+ if (newNode) {
+ return newNode;
+ } else {
+ if (this.replaceChildren) this.replaceChildren(fn)
+ return this;
+ }
+ }
}
diff --git a/app/imports/parser/parseTree/RollNode.js b/app/imports/parser/parseTree/RollNode.js
index d44bff4c..be84e690 100644
--- a/app/imports/parser/parseTree/RollNode.js
+++ b/app/imports/parser/parseTree/RollNode.js
@@ -64,4 +64,8 @@ export default class RollNode extends ParseNode {
this.left.traverse(fn);
this.right.traverse(fn);
}
+ replaceChildren(fn){
+ this.left = this.left.replaceNodes(fn);
+ this.right = this.right.replaceNodes(fn);
+ }
}
diff --git a/app/imports/parser/parseTree/UnaryOperatorNode.js b/app/imports/parser/parseTree/UnaryOperatorNode.js
index 3f53014c..c8109e6a 100644
--- a/app/imports/parser/parseTree/UnaryOperatorNode.js
+++ b/app/imports/parser/parseTree/UnaryOperatorNode.js
@@ -34,4 +34,7 @@ export default class UnaryOperatorNode extends ParseNode {
fn(this);
this.right.traverse(fn);
}
+ replaceChildren(fn){
+ this.right = this.right.replaceNodes(fn);
+ }
}
diff --git a/app/imports/ui/properties/forms/ConstantForm.vue b/app/imports/ui/properties/forms/ConstantForm.vue
new file mode 100644
index 00000000..ef4fd46b
--- /dev/null
+++ b/app/imports/ui/properties/forms/ConstantForm.vue
@@ -0,0 +1,51 @@
+
+
{{ error.message }}
diff --git a/app/imports/ui/properties/forms/shared/propertyFormIndex.js b/app/imports/ui/properties/forms/shared/propertyFormIndex.js
index 360733a1..7f2c9a8c 100644
--- a/app/imports/ui/properties/forms/shared/propertyFormIndex.js
+++ b/app/imports/ui/properties/forms/shared/propertyFormIndex.js
@@ -4,6 +4,7 @@ import AttackForm from '/imports/ui/properties/forms/AttackForm.vue';
import AttributeForm from '/imports/ui/properties/forms/AttributeForm.vue';
import BuffForm from '/imports/ui/properties/forms/BuffForm.vue';
import ClassLevelForm from '/imports/ui/properties/forms/ClassLevelForm.vue';
+import ConstantForm from '/imports/ui/properties/forms/ConstantForm.vue';
import ContainerForm from '/imports/ui/properties/forms/ContainerForm.vue';
import DamageForm from '/imports/ui/properties/forms/DamageForm.vue';
import DamageMultiplierForm from '/imports/ui/properties/forms/DamageMultiplierForm.vue';
@@ -28,6 +29,7 @@ export default {
attack: AttackForm,
attribute: AttributeForm,
buff: BuffForm,
+ constant: ConstantForm,
container: ContainerForm,
classLevel: ClassLevelForm,
damage: DamageForm,
diff --git a/app/imports/ui/utility/evaluate.js b/app/imports/ui/utility/evaluate.js
deleted file mode 100644
index f93c3a85..00000000
--- a/app/imports/ui/utility/evaluate.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Creatures from '/imports/api/creature/Creatures.js';
-
-// Computations resolve to numbers
-// vars is a dict of variables to substitute
-export function evaluateComputation(string, vars){
- console.warn('Deprecated, evaluate computation should be done by the computation engine')
- if (!string) return string;
- // Replace all the string variables with numbers if possible
- let substitutedString = string.replace(
- /\w*[a-z]\w*/gi,
- sub => vars.hasOwnProperty(sub) ? vars[sub] : sub
- );
-
- // Evaluate the expression to a number or return it as is.
- try {
- return math.eval(substitutedString);
- } catch (e){
- return substitutedString;
- }
-}
-
-// Strings can have computations in bracers like so: {computation}
-// vars is a dict of variables to substitute
-export function evaluateStringWithVariables(string, vars){
- console.warn('Deprecated, evaluateStringWithVariables should be done by the computation engine')
- if (!string) return string;
- // Compute everything inside bracers
- return string.replace(/\{([^\{\}]*)\}/g, function(match, p1){
- return evaluateComputation(p1, vars);
- });
-}
-
-export function evaluateStringForCharId(string, charId){
- console.warn('Deprecated, evaluateStringForCharId should be done by the computation engine')
- let char = Creatures.findOne(charId, {fields: {variables: 1}});
- let vars = char ? char.variables : {};
- return evaluateStringWithVariables(string, vars);
-}