Added data and UI for effects targeting calculations by tag

Still need to:
- update engine to compute calculations with effects.
- Add UI for effects applied to each calculation
This commit is contained in:
Stefan Zermatten
2022-02-14 16:26:49 +02:00
parent 359f18988c
commit e0f621cc44
11 changed files with 336 additions and 83 deletions

View File

@@ -11,7 +11,7 @@ accounts-google@1.4.0
email@2.2.0
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.13.0
mongo@1.14.0
session@1.2.0
tracker@1.2.0
logging@1.3.1

View File

@@ -1 +1 @@
METEOR@2.5.3
METEOR@2.6

View File

@@ -1,4 +1,4 @@
accounts-base@2.2.0
accounts-base@2.2.1
accounts-google@1.4.0
accounts-oauth@1.4.0
accounts-password@2.2.0
@@ -10,7 +10,7 @@ akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.8.0
babel-runtime@1.5.0
@@ -25,7 +25,7 @@ callback-hook@1.4.0
check@1.3.1
coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.5
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.5.0
ddp-common@1.4.0
@@ -59,20 +59,20 @@ meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.2.0
mikowals:batch-insert@1.3.0
minifier-css@1.6.0
minifier-js@2.7.3
minimongo@1.7.0
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modules@0.18.0
modules-runtime@0.12.0
mongo@1.13.0
mongo@1.14.4
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@3.9.1
npm-mongo@4.3.1
oauth@2.1.1
oauth2@1.3.1
ordered-dict@1.1.0
@@ -85,7 +85,7 @@ peerlibrary:computed-field@0.10.0
peerlibrary:data-lookup@0.3.0
peerlibrary:extend-publish@0.6.0
peerlibrary:fiber-utils@0.10.0
peerlibrary:reactive-mongo@0.4.0
peerlibrary:reactive-mongo@0.4.1
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0

View File

@@ -122,14 +122,25 @@ function linkDamage(dependencyGraph, prop){
dependOnCalc({dependencyGraph, prop, key: 'amount'});
}
function linkEffects(dependencyGraph, prop){
function linkEffects(dependencyGraph, prop, computation){
// The effect depends on its amount calculation
dependOnCalc({dependencyGraph, prop, key: 'amount'});
// The stats depend on the effect
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, 'effect');
});
if (prop.targetByTags){
// TODO:
getEffectTagTargets(prop, computation).forEach(targetProp => {
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj){
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
}
});
} else {
prop.stats.forEach(statName => {
if (!statName) return;
dependencyGraph.addLink(statName, prop._id, 'effect');
});
}
}
function linkRoll(dependencyGraph, prop){

View File

@@ -7,48 +7,99 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
* that modify their final value or presentation in some way
*/
let EffectSchema = createPropertySchema({
name: {
type: String,
optional: true,
name: {
type: String,
optional: true,
max: STORAGE_LIMITS.name,
},
operation: {
type: String,
defaultValue: 'add',
allowedValues: [
'base',
'add',
'mul',
'min',
'max',
},
operation: {
type: String,
defaultValue: 'add',
allowedValues: [
'base',
'add',
'mul',
'min',
'max',
'set',
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
],
},
amount: {
type: 'fieldToCompute',
optional: true,
},
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
],
},
amount: {
type: 'fieldToCompute',
optional: true,
},
// Conditional benefits store just uncomputed text
text: {
type: String,
optional: true,
max: STORAGE_LIMITS.effectCondition,
},
//which stats the effect is applied to
stats: {
type: Array,
defaultValue: [],
// Which stats the effect is applied to
// Each entry is a variableName targeted by this effect
stats: {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.statsToTarget,
},
'stats.$': {
type: String,
},
'stats.$': {
type: String,
max: STORAGE_LIMITS.variableName,
},
},
// True when targeting by tags instead of stats
targetByTags: {
type: Boolean,
optional: true,
},
// If targeting by tags, the field which will be targeted
targetField: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Which tags the effect is applied to
targetTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'targetTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
extraTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.extraTagsCount,
},
'extraTags.$': {
type: Object,
},
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'extraTags.$.operation': {
type: String,
allowedValues: ['OR', 'NOT'],
defaultValue: 'OR',
},
'extraTags.$.tags': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'extraTags.$.tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
});
const ComputedOnlyEffectSchema = createPropertySchema({
@@ -59,7 +110,7 @@ const ComputedOnlyEffectSchema = createPropertySchema({
});
const ComputedEffectSchema = new SimpleSchema()
.extend(ComputedOnlyEffectSchema)
.extend(EffectSchema);
.extend(ComputedOnlyEffectSchema)
.extend(EffectSchema);
export { EffectSchema, ComputedEffectSchema, ComputedOnlyEffectSchema };

View File

@@ -15,6 +15,10 @@
slot="prepend"
name="prepend"
/>
<slot
slot="prepend-inner"
name="prepend-inner"
/>
</v-select>
</template>

View File

@@ -44,6 +44,7 @@
<v-tabs-items
slot="unwrapped-content"
v-model="tab"
class="fill-height overflow-y-auto"
>
<v-tab-item :disabled="!!forcedType">
<property-selector

View File

@@ -10,7 +10,6 @@
<smart-select
label="Operation"
append-icon="mdi-menu-down"
class="mx-2"
:hint="operationHint"
:error-messages="errors.operation"
:menu-props="{transition: 'slide-y-transition', lazy: true}"
@@ -19,8 +18,8 @@
@change="change('operation', ...arguments)"
>
<v-icon
slot="prepend"
class="icon"
slot="prepend-inner"
class="icon ml-0"
:class="iconClass"
>
{{ displayedIcon }}
@@ -37,18 +36,6 @@
{{ item.item.text }}
</template>
</smart-select>
<smart-combobox
label="Stats"
class="mr-2"
multiple
chips
deletable-chips
hint="Which stats will this effect apply to"
:value="model.stats"
:items="attributeList"
:error-messages="errors.stats"
@change="change('stats', ...arguments)"
/>
<text-field
v-if="model.operation === 'conditional'"
label="Text"
@@ -60,7 +47,6 @@
<computed-field
v-else
label="Value"
class="mr-2"
hint="Number or calculation to determine the value of this effect"
:persistent-hint="needsValue"
:disabled="!needsValue"
@@ -69,15 +55,123 @@
@change="({path, value, ack}) =>
$emit('change', {path: ['amount', ...path], value, ack})"
/>
<v-btn-toggle
mandatory
:value="radioGroup"
class="ma-2 mb-8"
@change="changeTargetByTags"
>
<v-btn value="stats">
Target stats by variable name
</v-btn>
<v-btn value="tags">
Target properties by tag
</v-btn>
</v-btn-toggle>
<smart-combobox
label="Tags"
v-if="!model.targetByTags"
label="Stats"
class="mr-2"
multiple
chips
deletable-chips
:value="model.tags"
:error-messages="errors.tags"
@change="change('tags', ...arguments)"
hint="Which stats will this effect apply to"
persistent-hint
:value="model.stats"
:items="attributeList"
:error-messages="errors.stats"
@change="change('stats', ...arguments)"
/>
<v-layout
v-if="model.targetByTags"
align-center
>
<v-btn
icon
style="margin-top: -30px;"
class="mr-2"
:loading="addExtraTagsLoading"
:disabled="extraTagsFull"
@click="addExtraTags"
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
<smart-combobox
label="Tags Required"
hint="The effect will apply to properties that have all the listed tags"
multiple
chips
deletable-chips
persistent-hint
:value="model.targetTags"
:error-messages="errors.targetTags"
@change="change('targetTags', ...arguments)"
/>
</v-layout>
<v-slide-x-transition
v-if="model.targetByTags"
group
>
<div
v-for="(extras, i) in model.extraTags"
:key="extras._id"
class="target-tags layout align-center justify-space-between"
>
<smart-select
label="Operation"
style="width: 90px; flex-grow: 0;"
:items="['OR', 'NOT']"
:value="extras.operation"
:error-messages="errors.extraTags && errors.extraTags[i]"
@change="change(['extraTags', i, 'operation'], ...arguments)"
/>
<smart-combobox
label="Tags"
:hint="extras.operation === 'OR' ? 'The effect will also target properties that have all of these tags instead' : 'The effect will ignore properties that have any of these tags'"
class="mx-2"
multiple
chips
deletable-chips
persistent-hint
:value="extras.tags"
@change="change(['extraTags', i, 'tags'], ...arguments)"
/>
<v-btn
icon
style="margin-top: -30px;"
@click="$emit('pull', {path: ['extraTags', i]})"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<form-section
name="Advanced"
standalone
>
<smart-combobox
label="Tags"
multiple
chips
deletable-chips
:value="model.tags"
:error-messages="errors.tags"
@change="change('tags', ...arguments)"
/>
<v-expand-transition>
<text-field
v-if="model.targetByTags"
label="Target field"
:value="model.variableName"
hint="Target a specific calculation field on the affected properties"
:error-messages="errors.targetField"
@change="change('targetField', ...arguments)"
/>
</v-expand-transition>
</form-section>
</div>
</template>
@@ -85,13 +179,19 @@
import getEffectIcon from '/imports/ui/utility/getEffectIcon.js';
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import attributeListMixin from '/imports/ui/properties/forms/shared/lists/attributeListMixin.js';
import { EffectSchema } from '/imports/api/properties/Effects.js';
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
const ICON_SPIN_DURATION = 300;
export default {
components: {
FormSection,
},
mixins: [propertyFormMixin, attributeListMixin],
data(){ return {
displayedIcon: 'add',
iconClass: '',
addExtraTagsLoading: false,
operations: [
{value: 'base', text: 'Base Value'},
{value: 'add', text: 'Add'},
@@ -107,6 +207,14 @@
],
}},
computed: {
radioGroup(){
return this.model.targetByTags ? 'tags' : 'stats';
},
extraTagsFull(){
if (!this.model.extraTags) return false;
let maxCount = EffectSchema.get('extraTags', 'maxCount');
return this.model.extraTags.length >= maxCount;
},
needsValue(){
switch(this.model.operation) {
case 'base': return true;
@@ -163,6 +271,25 @@
},
methods: {
getEffectIcon,
changeTargetByTags(value){
if(value === 'stats'){
this.$emit('change', {path: ['targetByTags'], value: undefined});
} else if (value === 'tags'){
this.$emit('change', {path: ['targetByTags'], value: true});
}
},
addExtraTags(){
this.addExtraTagsLoading = true;
this.$emit('push', {
path: ['extraTags'],
value: {
_id: Random.id(),
operation: 'OR',
tags: [],
},
ack: () => this.addExtraTagsLoading = false,
});
},
}
};
</script>

View File

@@ -57,10 +57,14 @@ const schemaFormMixin = {
},
push({path, value, ack}){
let array = get(this.model, path);
if (!array || !array.join){
throw `${path.join('.')} is ${array}, doesn't have "join"`
if (array === undefined){
let {object, key} = resolvePath(this.model, path, this.$set);
this.$set(object, key, [value]);
} else if (!array.push){
throw `${path.join('.')} is ${array}, doesn't have "push"`
} else {
array.push(value);
}
array.push(value);
if (ack) ack();
},
pull({path, ack}){

View File

@@ -14,15 +14,21 @@
<template v-if="model.name">
{{ model.name }}
</template>
<template v-else-if="model.targetByTags">
<span class="mr-1">
{{ displayedValue }}
</span>
<span
class="mr-1"
>{{ displayedTags }}</span>
</template>
<template v-else>
<span class="mr-1">
{{ displayedValue }}
</span>
<span
v-for="stat in model.stats"
:key="stat"
class="mr-1"
>{{ stat }}</span>
>{{ displayedStats }}</span>
</template>
</div>
</div>
@@ -47,19 +53,32 @@ export default {
displayedValue(){
let value = this.resolvedValue;
switch(this.model.operation) {
case 'base': return value;
case 'add': return isFinite(value) ? Math.abs(value) : value;
case 'base': return value || 0;
case 'add': return isFinite(value) ? Math.abs(value) : value || 0;
case 'mul': return value;
case 'min': return value;
case 'max': return value;
case 'advantage': return;
case 'disadvantage': return;
case 'passiveAdd': return isFinite(value) ? Math.abs(value) : value;
case 'passiveAdd': return isFinite(value) ? Math.abs(value) : value || 0;
case 'fail': return;
case 'conditional': return;
default: return undefined;
}
},
displayedStats(){
if (!this.model.stats) return 'Selected stats';
return this.model.stats.join(', ');
},
displayedTags(){
if (!this.model.targetTags) return 'Selected tags';
const tags = this.model.targetTags.join(', ');
if (!this.model.extraTags) return tags;
const extraTags = this.model.extraTags.map(ex => {
return ` ${ex.operation} ${ex.tags.join(', ')}`
}).join(' ');
return tags + extraTags;
}
}
}
</script>

View File

@@ -17,18 +17,54 @@
<property-field
v-if="model.operation !== 'conditional'"
name="Amount"
:value="displayedValue"
:value="displayedValue || ' '"
/>
<property-field
v-if="model.targetByTags"
name="Targeted tags"
>
<div>
<v-chip
v-for="(tag, index) in model.targetTags"
:key="index"
class="ma-1"
>
{{ tag }}
</v-chip>
<div
v-for="ex in model.extraTags"
:key="ex._id"
>
<span class="ma-2">
{{ ex.operation }}
</span>
<v-chip
v-for="(extraTag, index) in ex.tags"
:key="index"
class="ma-1"
>
{{ extraTag }}
</v-chip>
</div>
</div>
</property-field>
<property-field
v-else
name="Stats"
:value="model.stats && model.stats.join(', ')"
mono
/>
>
<v-chip
v-for="(stat, index) in model.stats"
:key="index"
class="ma-1"
>
{{ stat }}
</v-chip>
</property-field>
<property-field
v-if="model.operation === 'conditional'"
name="Text"
:cols="{cols: 12}"
:value="model.text"
:value="model.text || ' '"
/>
</v-row>
</div>