Added point buy to computation engine

This commit is contained in:
Stefan Zermatten
2022-08-19 14:03:12 +02:00
parent 28307e26c3
commit c6ca8c1fa4
14 changed files with 443 additions and 209 deletions

View File

@@ -98,7 +98,7 @@
Next
</v-btn>
<v-btn
:disabled="biographyAlert"
:disabled="!!biographyAlert"
:text="step < 2"
:color="step < 2? '' : 'accent'"
@click="submit"

View File

@@ -27,9 +27,9 @@
<div class="text-body-1 mb-1">
{{ displayedText }}
</div>
<div v-if="!hideBreadcrumbs && model.ancestors">
<div v-if="!hideBreadcrumbs && ancestors">
<breadcrumbs
:model="model"
:model="{...model, ancestors}"
class="text-caption"
no-links
no-icons
@@ -41,94 +41,101 @@
</template>
<script lang="js">
import getEffectIcon from '/imports/ui/utility/getEffectIcon.js';
import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue';
import { isFinite } from 'lodash';
import getEffectIcon from '/imports/ui/utility/getEffectIcon.js';
import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { isFinite } from 'lodash';
export default {
components: {
Breadcrumbs,
export default {
components: {
Breadcrumbs,
},
props: {
hideBreadcrumbs: Boolean,
model: {
type: Object,
required: true,
},
props: {
hideBreadcrumbs: Boolean,
model: {
type: Object,
required: true,
},
},
computed: {
hasClickListener(){
return this.$listeners && this.$listeners.click
},
computed: {
hasClickListener(){
return this.$listeners && this.$listeners.click
},
displayedText(){
if (this.model.operation === 'conditional'){
return this.model.text || this.model.name || this.operation
} else {
return this.model.name || this.operation
}
},
resolvedValue(){
let amount = this.model.amount;
if (!amount) return;
return amount.value !== undefined ? amount.value : amount.calculation;
},
effectIcon(){
let value = this.resolvedValue;
return getEffectIcon(this.model.operation, value);
},
operation(){
switch(this.model.operation) {
case 'base': return 'Base value';
case 'add': return 'Add';
case 'mul': return 'Multiply';
case 'min': return 'Minimum';
case 'max': return 'Maximum';
case 'advantage': return 'Advantage';
case 'disadvantage': return 'Disadvantage';
case 'passiveAdd': return 'Passive bonus';
case 'fail': return 'Always fail';
case 'conditional': return 'Conditional benefit' ;
default: return '';
}
},
showValue(){
switch(this.model.operation) {
case 'base': return true;
case 'add': return true;
case 'mul': return true;
case 'min': return true;
case 'max': return true;
case 'advantage': return false;
case 'disadvantage': return false;
case 'passiveAdd': return true;
case 'fail': return false;
case 'conditional': return false;
default: return false;
}
},
displayedValue(){
let value = this.resolvedValue;
switch(this.model.operation) {
case 'base': return value;
case 'add': return isFinite(value) ? Math.abs(value) : value;
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 'fail': return;
case 'conditional': return undefined;
default: return undefined;
}
displayedText(){
if (this.model.operation === 'conditional'){
return this.model.text || this.model.name || this.operation
} else {
return this.model.name || this.operation
}
},
methods: {
click(e){
this.$emit('click', e);
},
resolvedValue() {
let amount = this.model.amount;
if (!amount) return;
return amount.value !== undefined ? amount.value : amount.calculation;
},
};
effectIcon(){
let value = this.resolvedValue;
return getEffectIcon(this.model.operation, value);
},
operation(){
switch(this.model.operation) {
case 'base': return 'Base value';
case 'add': return 'Add';
case 'mul': return 'Multiply';
case 'min': return 'Minimum';
case 'max': return 'Maximum';
case 'advantage': return 'Advantage';
case 'disadvantage': return 'Disadvantage';
case 'passiveAdd': return 'Passive bonus';
case 'fail': return 'Always fail';
case 'conditional': return 'Conditional benefit' ;
default: return '';
}
},
showValue(){
switch(this.model.operation) {
case 'base': return true;
case 'add': return true;
case 'mul': return true;
case 'min': return true;
case 'max': return true;
case 'advantage': return false;
case 'disadvantage': return false;
case 'passiveAdd': return true;
case 'fail': return false;
case 'conditional': return false;
default: return false;
}
},
displayedValue(){
let value = this.resolvedValue;
switch(this.model.operation) {
case 'base': return value;
case 'add': return isFinite(value) ? Math.abs(value) : value;
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 'fail': return;
case 'conditional': return undefined;
default: return undefined;
}
}
},
meteor: {
ancestors() {
const prop = CreatureProperties.findOne(this.model._id);
return prop && prop.ancestors || [];
}
},
methods: {
click(e){
this.$emit('click', e);
},
},
};
</script>
<style lang="css" scoped>

View File

@@ -1,86 +1,172 @@
<template lang="html">
<div class="point-buy-form">
<v-row dense>
<text-field
ref="focusFirst"
label="Name"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
<text-field
label="Variable name"
:value="model.variableName"
hint="Use this name in calculations to reference this point buy table"
:error-messages="errors.variableName"
@change="change('variableName', ...arguments)"
/>
<computed-field
label="Min"
hint="The minimum value for each row"
:model="model.min"
:error-messages="errors.min"
@change="change('min', ...arguments)"
/>
<computed-field
label="Max"
hint="The maximum value for each row"
:model="model.max"
:error-messages="errors.max"
@change="change('max', ...arguments)"
/>
<computed-field
label="Cost"
hint="A function of `value` that determines the cost of each row"
:model="model.cost"
:error-messages="errors.cost"
@change="change('cost', ...arguments)"
/>
<computed-field
label="Total"
hint="The total allowed cost of all rows"
:model="model.total"
:error-messages="errors.total"
@change="change('total', ...arguments)"
/>
</v-row>
<v-row
v-for="(row, i) in model.values"
:key="row._id"
dense
>
<text-field
ref="focusFirst"
label="Name"
:value="model.name"
:error-messages="errors.name"
@change="change(['values', i, 'name'], ...arguments)"
/>
<text-field
label="Variable name"
:value="model.variableName"
hint="Use this name to reference this row of the table: tableVariableName.thisVariableName"
:error-messages="errors.variableName"
@change="change(['values', i, 'variableName'], ...arguments)"
/>
<v-btn
icon
@click="$emit('pull', {path: ['values', i]})"
<v-col
cols="12"
md="6"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
<text-field
ref="focusFirst"
label="Table name"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Min"
hint="The minimum value for each row"
:model="model.min"
:error-messages="errors.min"
@change="({path, value, ack}) =>
$emit('change', {path: ['min', ...path], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Max"
hint="The maximum value for each row"
:model="model.max"
:error-messages="errors.max"
@change="({path, value, ack}) =>
$emit('change', {path: ['max', ...path], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Cost"
hint="A function of `value` that determines the cost of each row"
hide-value
:model="model.cost"
:error-messages="errors.cost"
@change="({path, value, ack}) =>
$emit('change', {path: ['cost', ...path], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Total available points"
hint="The total allowed cost of all rows"
:model="model.total"
:error-messages="errors.total"
@change="({path, value, ack}) =>
$emit('change', {path: ['total', ...path], value, ack})"
/>
</v-col>
</v-row>
<v-btn
icon
outlined
:loading="addRowLoading"
:disabled="rowsFull"
@click="addRow"
<v-subheader>
Rows
</v-subheader>
<v-slide-x-transition
group
leave-absolute
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
<v-row
v-for="(row, i) in model.values"
:key="row._id"
dense
>
<v-divider
v-if="i"
style="flex-basis: 100%;"
class="mb-6"
/>
<v-col cols="11">
<v-row dense>
<v-col
cols="12"
md="6"
>
<text-field
ref="focusFirst"
label="Row Name"
:value="row.name"
:error-messages="errors.values && errors.values[i] && errors.values[i].name"
@change="change(['values', i, 'name'], ...arguments)"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<text-field
label="Variable name"
:value="row.variableName"
hint="Use this name in calculations to reference this row of the table"
:error-messages="errors.values && errors.values[i] && errors.values[i].variableName"
@change="change(['values', i, 'variableName'], ...arguments)"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<text-field
label="Value"
type="number"
:min="row.hasOwnProperty('min') ? row.min && row.min.value : model.min && model.min.value"
:max="row.hasOwnProperty('max') ? row.max && row.max.value : model.max && model.max.value"
:value="row.value"
:error-messages="errors.values && errors.values[i] && errors.values[i].value"
@change="(value, ack) => $emit('change', {path: ['values', i, 'value'], value, ack})"
>
<template v-if="row.spent">
Cost: {{ row.spent }}
</template>
</text-field>
</v-col>
</v-row>
</v-col>
<v-col
cols="1"
class="d-flex align-center justify-center"
>
<v-btn
icon
large
@click="$emit('pull', {path: ['values', i]})"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
<v-row
key="addButton"
dense
justify="end"
class="mb-4"
>
<v-col
cols="1"
class="d-flex justify-center"
>
<v-btn
icon
outlined
:loading="addRowLoading"
:disabled="rowsFull"
@click="addRow"
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
</v-col>
</v-row>
</v-slide-x-transition>
<form-section
v-if="$slots.children"
name="Children"

View File

@@ -6,7 +6,7 @@
@change="(value, ack) => $emit('change', {path: ['calculation'], value, ack})"
>
<template
v-if="model.value !== undefined || model.value !== null"
v-if="showValue"
#value
>
{{ model.value }}
@@ -28,8 +28,20 @@ export default {
type: Object,
default: () => ({}),
},
hideValue: {
type: Boolean,
},
},
computed: {
showValue() {
const value = this.model.value;
if (
this.hideValue ||
(value === undefined || value === null) ||
value == this.model.calculation
) return false;
return true;
},
errorList(){
if (this.model.parseError){
return [this.model.parseError, ...this.model.errors];

View File

@@ -107,25 +107,18 @@
</v-row>
<v-row dense>
<property-field
v-if="baseEffects.length || effects.length"
v-if="effects && effects.length"
:cols="{col: 12}"
name="Effects"
>
<v-list style="width: 100%;">
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
<attribute-effect
v-for="effect in effects"
:key="effect._id"
:model="effect"
:data-id="effect._id"
@click="clickEffect(effect._id)"
:hide-breadcrumbs="effect._id === model._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
</v-list>
</property-field>
@@ -137,7 +130,6 @@
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js';
@@ -211,31 +203,8 @@
},
},
meteor: {
baseEffects(){
if (this.context.creatureId && this.model.variableName){
let creatureId = this.context.creatureId;
return CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
variableName: this.model.variableName,
removed: {$ne: true},
inactive: {$ne: true},
}).map( prop => ({
_id: prop._id,
name: 'Attribute base value',
operation: 'base',
amount: prop.baseValue,
stats: [prop.variableName],
ancestors: prop.ancestors,
}) ).filter(effect => effect.amount);
} else {
return [];
}
},
effects() {
return CreatureProperties.find({
_id: { $in: this.model.effects?.map(e => e._id) || [] }
});
return this.model.effects;
},
},
}

View File

@@ -0,0 +1,45 @@
<template lang="html">
<div class="point-buy-viewer">
<v-row dense>
<property-field
v-for="(row, i) in model.values"
:key="row._id"
:name="row.name"
:value="row.value"
/>
</v-row>
</div>
</template>
<script lang="js">
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { timingOptions, eventOptions, actionPropertyTypeOptions } from '/imports/api/properties/Triggers.js';
export default {
mixins: [propertyViewerMixin],
inject: {
context: {
default: {},
},
},
computed: {
slotTypeName(){
if (!this.model.slotType) return;
return getPropertyName(this.model.slotType);
},
timingText(){
if (!this.model.timing) return;
return timingOptions[this.model.timing];
},
actionPropertyText(){
if (!this.model.actionPropertyType) return;
return actionPropertyTypeOptions[this.model.actionPropertyType];
},
eventText(){
if (!this.model.event) return;
return eventOptions[this.model.event];
},
}
}
</script>

View File

@@ -15,6 +15,7 @@ const FeatureViewer = () => import ('/imports/ui/properties/viewers/FeatureViewe
const FolderViewer = () => import ('/imports/ui/properties/viewers/FolderViewer.vue');
const ItemViewer = () => import ('/imports/ui/properties/viewers/ItemViewer.vue');
const NoteViewer = () => import ('/imports/ui/properties/viewers/NoteViewer.vue');
const PointBuyViewer = () => import ('/imports/ui/properties/viewers/PointBuyViewer.vue');
const ProficiencyViewer = () => import ('/imports/ui/properties/viewers/ProficiencyViewer.vue');
const ReferenceViewer = () => import ('/imports/ui/properties/viewers/ReferenceViewer.vue');
const RollViewer = () => import ('/imports/ui/properties/viewers/RollViewer.vue');
@@ -45,6 +46,7 @@ export default {
folder: FolderViewer,
item: ItemViewer,
note: NoteViewer,
pointBuy: PointBuyViewer,
proficiency: ProficiencyViewer,
propertySlot: SlotViewer,
roll: RollViewer,