Added UI backend that can do computations with context

This commit is contained in:
Thaum Rystra
2020-04-23 14:26:05 +02:00
parent 7416101a34
commit 95bfcd79c9
16 changed files with 294 additions and 40 deletions

View File

@@ -18,7 +18,7 @@ meteorhacks:subs-manager
chuangbo:marked chuangbo:marked
meteor-base@1.4.0 meteor-base@1.4.0
mobile-experience@1.1.0 mobile-experience@1.1.0
mongo@1.9.0 mongo@1.10.0
session@1.2.0 session@1.2.0
jquery@1.11.10 jquery@1.11.10
tracker@1.2.0 tracker@1.2.0
@@ -30,14 +30,14 @@ standard-minifier-js@2.6.0
shell-server@0.5.0 shell-server@0.5.0
seba:minifiers-autoprefixer seba:minifiers-autoprefixer
templates:array templates:array
ecmascript@0.14.2 ecmascript@0.14.3
es5-shim@4.8.0 es5-shim@4.8.0
reactive-dict@1.3.0 reactive-dict@1.3.0
percolate:synced-cron percolate:synced-cron
ongoworks:speakingurl ongoworks:speakingurl
service-configuration@1.0.11 service-configuration@1.0.11
google-config-ui@1.0.1 google-config-ui@1.0.1
dynamic-import@0.5.1 dynamic-import@0.5.2
ddp-rate-limiter@1.0.7 ddp-rate-limiter@1.0.7
rate-limit@1.0.9 rate-limit@1.0.9
meteortesting:mocha meteortesting:mocha

View File

@@ -1 +1 @@
METEOR@1.10.1 METEOR@1.10.2

View File

@@ -70,14 +70,14 @@ meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1 meteortesting:mocha-core@7.0.1
minifier-css@1.5.0 minifier-css@1.5.0
minifier-js@2.6.0 minifier-js@2.6.0
minimongo@1.5.0 minimongo@1.6.0
mobile-experience@1.1.0 mobile-experience@1.1.0
mobile-status-bar@1.1.0 mobile-status-bar@1.1.0
modern-browsers@0.1.5 modern-browsers@0.1.5
modules@0.15.0 modules@0.15.0
modules-runtime@0.12.0 modules-runtime@0.12.0
momentjs:moment@2.24.0 momentjs:moment@2.24.0
mongo@1.9.1 mongo@1.10.0
mongo-decimal@0.1.1 mongo-decimal@0.1.1
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.7 mongo-id@1.0.7
@@ -104,10 +104,10 @@ service-configuration@1.0.11
session@1.2.0 session@1.2.0
sha@1.0.9 sha@1.0.9
shell-server@0.5.0 shell-server@0.5.0
socket-stream-client@0.2.3 socket-stream-client@0.3.0
spacebars@1.0.15 spacebars@1.0.15
spacebars-compiler@1.1.3 spacebars-compiler@1.1.3
srp@1.0.12 srp@1.1.0
standard-minifier-js@2.6.0 standard-minifier-js@2.6.0
static-html@1.2.2 static-html@1.2.2
templates:array@1.0.3 templates:array@1.0.3
@@ -118,6 +118,6 @@ templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2 tmeasday:check-npm-versions@0.3.2
tracker@1.2.0 tracker@1.2.0
underscore@1.0.10 underscore@1.0.10
url@1.2.0 url@1.3.0
webapp@1.9.1 webapp@1.9.1
webapp-hashing@1.0.9 webapp-hashing@1.0.9

View File

@@ -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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return text
}

View File

@@ -19,8 +19,9 @@ function combineAttribute(stat, aggregator){
if (!stat.decimal) result = Math.floor(result); if (!stat.decimal) result = Math.floor(result);
stat.value = result; stat.value = result;
if (stat.attributeType === 'ability') { 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){ function combineSkill(stat, aggregator, memo){
@@ -30,7 +31,7 @@ function combineSkill(stat, aggregator, memo){
if (!ability.computationDetails.computed){ if (!ability.computationDetails.computed){
computeStat(ability, memo); computeStat(ability, memo);
} }
stat.abilityMod = ability.mod; stat.abilityMod = ability.modifier;
} }
// Combine all the child proficiencies // Combine all the child proficiencies
for (let i in stat.proficiencies){ for (let i in stat.proficiencies){

View File

@@ -16,7 +16,7 @@ export default function evaluateCalculation(string, memo){
if (node.isSymbolNode) { if (node.isSymbolNode) {
let val = computedValueOfVariableName(node.name, memo); let val = computedValueOfVariableName(node.name, memo);
if (val === null) return node; if (val === null) return node;
return new math.expression.node.ConstantNode(val); return new math.ConstantNode(val);
} }
else { else {
return node; return node;

View File

@@ -11,7 +11,8 @@ export default function writeCreatureVariables(memo, creatureId) {
'reset', 'reset',
'resetMultiplier', 'resetMultiplier',
'value', 'value',
'mod', 'currentValue',
'modifier',
'ability', 'ability',
'skillType', 'skillType',
'baseProficiency', 'baseProficiency',

View 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>

View File

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

View File

@@ -3,8 +3,8 @@
<v-toolbar <v-toolbar
app app
clipped-left clipped-left
:color="character.color || 'secondary'" :color="creature.color || 'secondary'"
:dark="isDarkColor(character.color || theme.primary)" :dark="isDarkColor(creature.color || theme.primary)"
> >
<v-btn <v-btn
v-if="showMenuButton" v-if="showMenuButton"
@@ -15,12 +15,12 @@
<v-icon>menu</v-icon> <v-icon>menu</v-icon>
</v-btn> </v-btn>
<div class="flex"> <div class="flex">
{{ character.name }} {{ creature.name }}
</div> </div>
<v-btn <v-btn
flat flat
icon icon
@click="recompute(character._id)" @click="recompute(creature._id)"
> >
<v-icon>refresh</v-icon> <v-icon>refresh</v-icon>
</v-btn> </v-btn>
@@ -143,6 +143,10 @@
required: true, required: true,
}, },
}, },
reactiveProvide: {
name: 'computationContext',
include: ['creature'],
},
data(){return { data(){return {
theme, theme,
tab: 0, tab: 0,
@@ -181,7 +185,7 @@
component: 'delete-confirmation-dialog', component: 'delete-confirmation-dialog',
elementId: 'creature-menu', elementId: 'creature-menu',
data: { data: {
name: this.character.name, name: this.creature.name,
typeName: 'Character' typeName: 'Character'
}, },
callback(confirmation){ callback(confirmation){
@@ -204,13 +208,10 @@
return [this.creatureId]; return [this.creatureId];
}, },
}, },
character(){ creature(){
return Creatures.findOne(this.creatureId) || {}; return Creatures.findOne(this.creatureId) || {};
}, },
}, },
provide: {
creature: this.character,
}
} }
</script> </script>

View File

@@ -162,6 +162,14 @@
:data-id="action._id" :data-id="action._id"
@click="clickProperty({_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-list>
</v-card> </v-card>
</div> </div>
@@ -180,6 +188,7 @@
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue'; import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue'; import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
import ActionListTile from '/imports/ui/properties/components/actions/ActionListTile.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){ const getAttributeOfType = function(charId, type){
return CreatureProperties.find({ return CreatureProperties.find({
@@ -215,6 +224,7 @@
ResourceCard, ResourceCard,
SpellSlotListTile, SpellSlotListTile,
ActionListTile, ActionListTile,
AttackListTile,
}, },
props: { props: {
creatureId: { creatureId: {
@@ -300,6 +310,14 @@
sort: {order: 1}, sort: {order: 1},
}); });
}, },
attacks(){
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'attack',
}, {
sort: {order: 1},
});
},
}, },
methods: { methods: {
clickProperty({_id}){ clickProperty({_id}){

View File

@@ -10,7 +10,12 @@
</template> </template>
<script> <script>
import ComputedForCreature from '/imports/ui/components/computation/ComputedForCreature.vue';
export default { export default {
components: {
Computed: ComputedForCreature,
},
props: { props: {
model: { model: {
type: Object, type: Object,

View File

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

View File

@@ -4,6 +4,7 @@ import Vuetify from "vuetify";
import store from "/imports/ui/vuexStore.js"; import store from "/imports/ui/vuexStore.js";
import VueMeteorTracker from 'vue-meteor-tracker'; import VueMeteorTracker from 'vue-meteor-tracker';
import AppLayout from '/imports/ui/layouts/AppLayout.vue'; import AppLayout from '/imports/ui/layouts/AppLayout.vue';
import ReactiveProvide from 'vue-reactive-provide';
import router from "/imports/ui/router.js"; import router from "/imports/ui/router.js";
import { theme } from '/imports/ui/theme.js'; import { theme } from '/imports/ui/theme.js';
import "vuetify/dist/vuetify.min.css"; import "vuetify/dist/vuetify.min.css";
@@ -15,6 +16,9 @@ Vue.use(Vuetify, {
theme, theme,
iconfont: "md", iconfont: "md",
}); });
Vue.use(ReactiveProvide, {
name: 'reactiveProvide', // default value
})
// App start // App start
Meteor.startup(() => { Meteor.startup(() => {

33
app/package-lock.json generated
View File

@@ -1273,18 +1273,18 @@
"integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==" "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw=="
}, },
"mathjs": { "mathjs": {
"version": "5.10.3", "version": "6.6.4",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-5.10.3.tgz", "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-6.6.4.tgz",
"integrity": "sha512-ySjg30BC3dYjQm73ILZtwcWzFJde0VU6otkXW/57IjjuYRa3Qaf0Kb8pydEuBZYtqW2OxreAtsricrAmOj3jIw==", "integrity": "sha512-fvmP89ujJbDAC8ths7FZh7PWdA71dfA5WJVAzJbQhSDCHK1aBk8WRf1XcTw51ERs+sKx9nYBGsRshqmb/oe8Ag==",
"requires": { "requires": {
"complex.js": "2.0.11", "complex.js": "^2.0.11",
"decimal.js": "10.2.0", "decimal.js": "^10.2.0",
"escape-latex": "1.2.0", "escape-latex": "^1.2.0",
"fraction.js": "4.0.12", "fraction.js": "^4.0.12",
"javascript-natural-sort": "0.7.1", "javascript-natural-sort": "^0.7.1",
"seed-random": "2.2.0", "seed-random": "^2.2.0",
"tiny-emitter": "2.1.0", "tiny-emitter": "^2.1.0",
"typed-function": "1.1.0" "typed-function": "^1.1.1"
} }
}, },
"mem": { "mem": {
@@ -2702,9 +2702,9 @@
"dev": true "dev": true
}, },
"typed-function": { "typed-function": {
"version": "1.1.0", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-1.1.0.tgz", "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-1.1.1.tgz",
"integrity": "sha512-TuQzwiT4DDg19beHam3E66oRXhyqlyfgjHB/5fcvsRXbfmWPJfto9B4a0TBdTrQAPGlGmXh/k7iUI+WsObgORA==" "integrity": "sha512-RbN7MaTQBZLJYzDENHPA0nUmWT0Ex80KHItprrgbTPufYhIlTePvCXZxyQK7wgn19FW5bnuaBIKcBb5mRWjB1Q=="
}, },
"underscore": { "underscore": {
"version": "1.10.2", "version": "1.10.2",
@@ -2769,6 +2769,11 @@
"lodash.omit": "^4.5.0" "lodash.omit": "^4.5.0"
} }
}, },
"vue-reactive-provide": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/vue-reactive-provide/-/vue-reactive-provide-0.3.0.tgz",
"integrity": "sha512-hx2JtRPRvne9NY4s1r7ASsCaO8CIby30qwC1kGQRxsrWApO3he+rziGOzTDSfvmr852zWMb11n6qAwHCz6C/vw=="
},
"vue-router": { "vue-router": {
"version": "3.1.6", "version": "3.1.6",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",

View File

@@ -25,7 +25,7 @@
"date-fns": "^1.30.1", "date-fns": "^1.30.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"marked": "^0.8.2", "marked": "^0.8.2",
"mathjs": "^5.10.3", "mathjs": "^6.6.4",
"meteor-node-stubs": "^0.3.3", "meteor-node-stubs": "^0.3.3",
"moo": "^0.5.1", "moo": "^0.5.1",
"nearley": "^2.19.1", "nearley": "^2.19.1",
@@ -35,6 +35,7 @@
"underscore": "^1.10.2", "underscore": "^1.10.2",
"vue": "2.6.10", "vue": "2.6.10",
"vue-meteor-tracker": "^2.0.0-beta.5", "vue-meteor-tracker": "^2.0.0-beta.5",
"vue-reactive-provide": "^0.3.0",
"vue-router": "^3.1.6", "vue-router": "^3.1.6",
"vuedraggable": "^2.23.2", "vuedraggable": "^2.23.2",
"vuetify": "^1.5.24", "vuetify": "^1.5.24",
@@ -61,9 +62,16 @@
], ],
"parser": "vue-eslint-parser", "parser": "vue-eslint-parser",
"rules": { "rules": {
"vue/component-tags-order": ["error", { "vue/component-tags-order": [
"order": ["template", "script", "style"] "error",
}] {
"order": [
"template",
"script",
"style"
]
}
]
} }
} }
], ],