Added UI backend that can do computations with context
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import * as math from 'mathjs';
|
||||
|
||||
export default function evaluateString(string, scope){
|
||||
let errors = [];
|
||||
if (!string){
|
||||
errors.push('No string provided');
|
||||
return {result: string, errors};
|
||||
}
|
||||
|
||||
if (!scope) errors.push('No scope provided');
|
||||
|
||||
// Parse the string using mathjs
|
||||
let calc;
|
||||
try {
|
||||
calc = math.parse(string);
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
return {result: string, errors};
|
||||
}
|
||||
|
||||
// Replace all bare symbols with symbol.value
|
||||
let transformedCalc = calc.transform(replaceBareSymbolsWithValueAccessor);
|
||||
|
||||
// Evaluate the expression to a number or return with substitutions
|
||||
try {
|
||||
let result = transformedCalc.evaluate(scope);
|
||||
return {result, errors};
|
||||
} catch (e1){
|
||||
errors.push(e1);
|
||||
try {
|
||||
result = simplifyWithAccessors(calc, scope).toHTML();
|
||||
return {result, errors};
|
||||
} catch (e2){
|
||||
errors.push(e2);
|
||||
return {result: calc.toHTML(), errors};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replaceBareSymbolsWithValueAccessor(node, path, parent) {
|
||||
if (node.isSymbolNode && path !== 'object') {
|
||||
const object = new math.SymbolNode(node.name);
|
||||
const address = new math.ConstantNode('value');
|
||||
const index = new math.IndexNode([address]);
|
||||
return new math.AccessorNode(object, index);
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
function simplifyWithAccessors(calc, scope){
|
||||
let noAccessorCalc = calc.transform(substituteAccessors(scope));
|
||||
return math.simplify(noAccessorCalc);
|
||||
}
|
||||
|
||||
// returns a function to replace all accessors with either their resolved value
|
||||
// or a symbol to simplify with
|
||||
function substituteAccessors(scope){
|
||||
return function(node, path, parent){
|
||||
if (node.isAccessorNode){
|
||||
try {
|
||||
return evaluateAccessor(node, scope);
|
||||
} catch (e) {
|
||||
console.log(typeof e);
|
||||
return replaceAccessorWithSymbol(node);
|
||||
}
|
||||
} else {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throws error if symbol is undefined in scope
|
||||
function evaluateAccessor(node, scope){
|
||||
let value = node.evaluate(scope);
|
||||
if (value === undefined){
|
||||
throw 'Undefined symbol'
|
||||
}
|
||||
return new math.ConstantNode(value);
|
||||
}
|
||||
|
||||
function replaceAccessorWithSymbol(node){
|
||||
let symbolNode = new math.SymbolNode(node.toString());
|
||||
return symbolNode;
|
||||
}
|
||||
|
||||
function overrideSymbolNodeHTML(symbolNode){
|
||||
let safeName = escape(symbolNode.name);
|
||||
symbolNode.toHTML = function(){
|
||||
console.log('running custom tohtml function')
|
||||
return `<span class="math-symbol math-substitution-failed">${safeName}</span>`
|
||||
}
|
||||
return symbolNode;
|
||||
}
|
||||
|
||||
// Escape special HTML characters
|
||||
// Copied directly from math.js source to help with overriding toHTML
|
||||
function escape (value) {
|
||||
let text = String(value)
|
||||
text = text.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
||||
return text
|
||||
}
|
||||
@@ -19,8 +19,9 @@ function combineAttribute(stat, aggregator){
|
||||
if (!stat.decimal) result = Math.floor(result);
|
||||
stat.value = result;
|
||||
if (stat.attributeType === 'ability') {
|
||||
stat.mod = Math.floor((result - 10) / 2);
|
||||
stat.modifier = Math.floor((result - 10) / 2);
|
||||
}
|
||||
stat.currentValue = stat.value - (stat.damage || 0);
|
||||
}
|
||||
|
||||
function combineSkill(stat, aggregator, memo){
|
||||
@@ -30,7 +31,7 @@ function combineSkill(stat, aggregator, memo){
|
||||
if (!ability.computationDetails.computed){
|
||||
computeStat(ability, memo);
|
||||
}
|
||||
stat.abilityMod = ability.mod;
|
||||
stat.abilityMod = ability.modifier;
|
||||
}
|
||||
// Combine all the child proficiencies
|
||||
for (let i in stat.proficiencies){
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function evaluateCalculation(string, memo){
|
||||
if (node.isSymbolNode) {
|
||||
let val = computedValueOfVariableName(node.name, memo);
|
||||
if (val === null) return node;
|
||||
return new math.expression.node.ConstantNode(val);
|
||||
return new math.ConstantNode(val);
|
||||
}
|
||||
else {
|
||||
return node;
|
||||
|
||||
@@ -11,7 +11,8 @@ export default function writeCreatureVariables(memo, creatureId) {
|
||||
'reset',
|
||||
'resetMultiplier',
|
||||
'value',
|
||||
'mod',
|
||||
'currentValue',
|
||||
'modifier',
|
||||
'ability',
|
||||
'skillType',
|
||||
'baseProficiency',
|
||||
|
||||
35
app/imports/ui/components/computation/Computed.vue
Normal file
35
app/imports/ui/components/computation/Computed.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template lang="html">
|
||||
<div v-html="computedValue" class="computed" :class="expectNumber && 'symbols-are-errors'"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
scope: {
|
||||
type: Object,
|
||||
},
|
||||
expectNumber: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedValue(){
|
||||
if (!this.value) return;
|
||||
let {result, errors} = evaluateString(this.value, this.scope);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
.computed.symbols-are-errors .math-symbol {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template lang="html">
|
||||
<computed :value="value" :scope="scope"/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Computed from '/imports/ui/components/computation/Computed.vue';
|
||||
|
||||
export default {
|
||||
inject: ['computationContext'],
|
||||
components: {
|
||||
Computed,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
scope(){
|
||||
return this.computationContext.creature && this.computationContext.creature.variables;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,8 +3,8 @@
|
||||
<v-toolbar
|
||||
app
|
||||
clipped-left
|
||||
:color="character.color || 'secondary'"
|
||||
:dark="isDarkColor(character.color || theme.primary)"
|
||||
:color="creature.color || 'secondary'"
|
||||
:dark="isDarkColor(creature.color || theme.primary)"
|
||||
>
|
||||
<v-btn
|
||||
v-if="showMenuButton"
|
||||
@@ -15,12 +15,12 @@
|
||||
<v-icon>menu</v-icon>
|
||||
</v-btn>
|
||||
<div class="flex">
|
||||
{{ character.name }}
|
||||
{{ creature.name }}
|
||||
</div>
|
||||
<v-btn
|
||||
flat
|
||||
icon
|
||||
@click="recompute(character._id)"
|
||||
@click="recompute(creature._id)"
|
||||
>
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
@@ -143,6 +143,10 @@
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'computationContext',
|
||||
include: ['creature'],
|
||||
},
|
||||
data(){return {
|
||||
theme,
|
||||
tab: 0,
|
||||
@@ -181,7 +185,7 @@
|
||||
component: 'delete-confirmation-dialog',
|
||||
elementId: 'creature-menu',
|
||||
data: {
|
||||
name: this.character.name,
|
||||
name: this.creature.name,
|
||||
typeName: 'Character'
|
||||
},
|
||||
callback(confirmation){
|
||||
@@ -204,13 +208,10 @@
|
||||
return [this.creatureId];
|
||||
},
|
||||
},
|
||||
character(){
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId) || {};
|
||||
},
|
||||
},
|
||||
provide: {
|
||||
creature: this.character,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -162,6 +162,14 @@
|
||||
:data-id="action._id"
|
||||
@click="clickProperty({_id: action._id})"
|
||||
/>
|
||||
<v-subheader>Attacks</v-subheader>
|
||||
<attack-list-tile
|
||||
v-for="attack in attacks"
|
||||
:key="attack._id"
|
||||
:model="attack"
|
||||
:data-id="attack._id"
|
||||
@click="clickProperty({_id: attack._id})"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
@@ -180,6 +188,7 @@
|
||||
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
|
||||
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||
import ActionListTile from '/imports/ui/properties/components/actions/ActionListTile.vue';
|
||||
import AttackListTile from '/imports/ui/properties/components/actions/AttackListTile.vue';
|
||||
|
||||
const getAttributeOfType = function(charId, type){
|
||||
return CreatureProperties.find({
|
||||
@@ -215,6 +224,7 @@
|
||||
ResourceCard,
|
||||
SpellSlotListTile,
|
||||
ActionListTile,
|
||||
AttackListTile,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
@@ -300,6 +310,14 @@
|
||||
sort: {order: 1},
|
||||
});
|
||||
},
|
||||
attacks(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'attack',
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty({_id}){
|
||||
|
||||
@@ -10,7 +10,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ComputedForCreature from '/imports/ui/components/computation/ComputedForCreature.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Computed: ComputedForCreature,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<template lang="html">
|
||||
<v-list-tile
|
||||
class="ability-list-tile"
|
||||
v-on="hasClickListener ? {click} : {}"
|
||||
>
|
||||
<v-list-tile-content>
|
||||
<computed :value="model.rollBonus"/>
|
||||
{{ model.name }}
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ComputedForCreature from '/imports/ui/components/computation/ComputedForCreature.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Computed: ComputedForCreature,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasClickListener(){
|
||||
return this.$listeners && this.$listeners.click
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
click(e){
|
||||
this.$emit('click', e);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -4,6 +4,7 @@ import Vuetify from "vuetify";
|
||||
import store from "/imports/ui/vuexStore.js";
|
||||
import VueMeteorTracker from 'vue-meteor-tracker';
|
||||
import AppLayout from '/imports/ui/layouts/AppLayout.vue';
|
||||
import ReactiveProvide from 'vue-reactive-provide';
|
||||
import router from "/imports/ui/router.js";
|
||||
import { theme } from '/imports/ui/theme.js';
|
||||
import "vuetify/dist/vuetify.min.css";
|
||||
@@ -15,6 +16,9 @@ Vue.use(Vuetify, {
|
||||
theme,
|
||||
iconfont: "md",
|
||||
});
|
||||
Vue.use(ReactiveProvide, {
|
||||
name: 'reactiveProvide', // default value
|
||||
})
|
||||
|
||||
// App start
|
||||
Meteor.startup(() => {
|
||||
|
||||
Reference in New Issue
Block a user