Added constants to the UI and Computation Engine

This commit is contained in:
Stefan Zermatten
2021-02-11 13:03:31 +02:00
parent 25fd5c18e8
commit 3313ed0297
21 changed files with 243 additions and 54 deletions

View File

@@ -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){

View File

@@ -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;

View File

@@ -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};
}

View File

@@ -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 };

View File

@@ -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,

View File

@@ -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,

View File

@@ -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'

View File

@@ -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));
}
}

View File

@@ -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}){

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -28,4 +28,7 @@ export default class NotOperatorNode extends ParseNode {
fn(this);
this.right.traverse(fn);
}
replaceChildren(fn){
this.right = this.right.replaceNodes(fn);
}
}

View File

@@ -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);
}
}

View File

@@ -23,4 +23,7 @@ export default class ParenthesisNode extends ParseNode {
fn(this);
this.content.traverse(fn);
}
replaceChildren(fn){
this.content = this.content.replaceNodes(fn);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -34,4 +34,7 @@ export default class UnaryOperatorNode extends ParseNode {
fn(this);
this.right.traverse(fn);
}
replaceChildren(fn){
this.right = this.right.replaceNodes(fn);
}
}

View File

@@ -0,0 +1,51 @@
<template lang="html">
<div>
<div class="layout row wrap">
<text-field
label="Name"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
<text-field
label="Variable name"
:value="model.variableName"
style="flex-basis: 300px;"
hint="Use this name in formulae to reference this attribute"
:error-messages="errors.variableName"
@change="change('variableName', ...arguments)"
/>
</div>
<text-field
label="Value"
:value="model.calculation"
:error-messages="errors.calculation"
@change="change('calculation', ...arguments)"
/>
<calculation-error-list :errors="clientErrors" />
</div>
</template>
<script>
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
import { ConstantSchema } from '/imports/api/properties/Constants.js';
export default {
components: {
CalculationErrorList,
},
mixins: [propertyFormMixin],
computed: {
// We can't rely on autoValue running in every form, so recalculate errors
clientErrors(){
let validationContext = ConstantSchema.newContext();
let cleanModel = validationContext.clean(this.model);
return cleanModel.errors;
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -15,7 +15,7 @@
:color="errorColor(error.type)"
outline
>
{{ error.message }}
<pre>{{ error.message }}</pre>
</v-alert>
</v-slide-x-transition>
</div>

View File

@@ -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,

View File

@@ -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);
}