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 @@ + + + + + diff --git a/app/imports/ui/properties/forms/shared/CalculationErrorList.vue b/app/imports/ui/properties/forms/shared/CalculationErrorList.vue index 11ba5090..63b649ca 100644 --- a/app/imports/ui/properties/forms/shared/CalculationErrorList.vue +++ b/app/imports/ui/properties/forms/shared/CalculationErrorList.vue @@ -15,7 +15,7 @@ :color="errorColor(error.type)" outline > - {{ error.message }} +
{{ 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); -}