Added constants to the UI and Computation Engine
This commit is contained in:
@@ -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){
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,7 @@ export default class NotOperatorNode extends ParseNode {
|
||||
fn(this);
|
||||
this.right.traverse(fn);
|
||||
}
|
||||
replaceChildren(fn){
|
||||
this.right = this.right.replaceNodes(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,7 @@ export default class ParenthesisNode extends ParseNode {
|
||||
fn(this);
|
||||
this.content.traverse(fn);
|
||||
}
|
||||
replaceChildren(fn){
|
||||
this.content = this.content.replaceNodes(fn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,7 @@ export default class UnaryOperatorNode extends ParseNode {
|
||||
fn(this);
|
||||
this.right.traverse(fn);
|
||||
}
|
||||
replaceChildren(fn){
|
||||
this.right = this.right.replaceNodes(fn);
|
||||
}
|
||||
}
|
||||
|
||||
51
app/imports/ui/properties/forms/ConstantForm.vue
Normal file
51
app/imports/ui/properties/forms/ConstantForm.vue
Normal 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>
|
||||
@@ -15,7 +15,7 @@
|
||||
:color="errorColor(error.type)"
|
||||
outline
|
||||
>
|
||||
{{ error.message }}
|
||||
<pre>{{ error.message }}</pre>
|
||||
</v-alert>
|
||||
</v-slide-x-transition>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user