Reorganized forms into their own folder
This commit is contained in:
189
app/imports/ui/forms/ActionForm.vue
Normal file
189
app/imports/ui/forms/ActionForm.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template lang="html">
|
||||
<div class="action-form">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<smart-select
|
||||
label="Type"
|
||||
:items="actionTypes"
|
||||
:value="model.type"
|
||||
:error-messages="errors.type"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['type'], value, ack})"
|
||||
:hint="actionTypeHints[model.type]"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<form-sections>
|
||||
<form-section name="Advanced">
|
||||
<smart-select
|
||||
label="Target"
|
||||
style="flex-basis: 300px;"
|
||||
:items="targetOptions"
|
||||
:value="model.target"
|
||||
:error-messages="errors.target"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['target'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div class="layout row wrap">
|
||||
<text-field
|
||||
label="Uses"
|
||||
hint="How many times this action can be used before needing to be reset"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.uses"
|
||||
@change="(value, ack) => $emit('change', {path: ['uses'], value, ack})"
|
||||
:error-messages="errors.uses"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Uses used"
|
||||
type="number"
|
||||
hint="How many times this action has already been used"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.usesUsed"
|
||||
@change="(value, ack) => $emit('change', {path: ['usesUsed'], value, ack})"
|
||||
:error-messages="errors.uses"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<smart-select
|
||||
label="Reset"
|
||||
clearable
|
||||
style="flex-basis: 300px;"
|
||||
:items="resetOptions"
|
||||
:value="model.reset"
|
||||
:error-messages="errors.reset"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['reset'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section name="Damage and Adjustments">
|
||||
<div class="caption">
|
||||
Adjustments can be used to automatically spend resources or deal
|
||||
damage when taking an action.
|
||||
They apply damage to an attribute each time the action is taken.
|
||||
</div>
|
||||
<adjustment-list-form
|
||||
:model="model.adjustments"
|
||||
:parent-target="model.target"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['adjustments', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['adjustments', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['adjustments', ...path], ack})"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section name="Buffs">
|
||||
<div class="caption">
|
||||
Buffs apply temporary effects to characters when taking an action.
|
||||
</div>
|
||||
<buff-list-form
|
||||
:model="model.buffs"
|
||||
:parent-target="model.target"
|
||||
:stored="stored"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['buffs', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['buffs', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['buffs', ...path], ack})"
|
||||
/>
|
||||
</form-section>
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection, {FormSections} from '/imports/ui/forms/components/FormSection.vue';
|
||||
import AdjustmentListForm from 'imports/ui/forms/AdjustmentListForm.vue';
|
||||
import BuffListForm from 'imports/ui/forms/BuffListForm.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
FormSections,
|
||||
AdjustmentListForm,
|
||||
BuffListForm,
|
||||
},
|
||||
props: {
|
||||
stored: {
|
||||
type: Boolean,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){
|
||||
let data = {
|
||||
actionTypes: [
|
||||
{
|
||||
text: 'Action',
|
||||
value: 'action',
|
||||
}, {
|
||||
text: 'Bonus action',
|
||||
value: 'bonus',
|
||||
}, {
|
||||
text: 'Attack action',
|
||||
value: 'attack',
|
||||
help: 'Attack actions replace a single attack when you choose to use your Action to attack',
|
||||
}, {
|
||||
text: 'Reaction',
|
||||
value: 'reaction',
|
||||
}, {
|
||||
text: 'Free action',
|
||||
value: 'free',
|
||||
help: 'You can take one free action on your turn without using an action or bonus action'
|
||||
}, {
|
||||
text: 'Long action',
|
||||
value: 'long',
|
||||
help: 'Long actions take longer than one turn to complete'
|
||||
},
|
||||
],
|
||||
targetOptions: [
|
||||
{
|
||||
text: 'Self',
|
||||
value: 'self',
|
||||
}, {
|
||||
text: 'Single target',
|
||||
value: 'singleTarget',
|
||||
}, {
|
||||
text: 'Multiple targets',
|
||||
value: 'multipleTargets',
|
||||
},
|
||||
],
|
||||
resetOptions: [
|
||||
{
|
||||
text: 'Short rest',
|
||||
value: 'shortRest',
|
||||
}, {
|
||||
text: 'Long rest',
|
||||
value: 'longRest',
|
||||
}
|
||||
],
|
||||
};
|
||||
data.actionTypeHints = {};
|
||||
data.actionTypes.forEach(type => {
|
||||
data.actionTypeHints[type.value] = type.help;
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.no-flex {
|
||||
flex: initial;
|
||||
}
|
||||
.layout.row.wrap {
|
||||
margin-right: -8px;
|
||||
}
|
||||
.layout.row.wrap > *{
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
98
app/imports/ui/forms/AdjustmentForm.vue
Normal file
98
app/imports/ui/forms/AdjustmentForm.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<div class="layout row">
|
||||
<text-field
|
||||
label="Attribute"
|
||||
hint="The attribute this adjustment will apply to"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.stat"
|
||||
@change="(value, ack) => $emit('change', {path: ['stat'], value, ack})"
|
||||
:error-messages="errors.stat"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Damage"
|
||||
hint="The amount of damage to apply to the selected stat, can be a calculation or roll"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.damage"
|
||||
@change="(value, ack) => $emit('change', {path: ['damage'], value, ack})"
|
||||
:error-messages="errors.damage"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<smart-select
|
||||
v-if="parentTarget !== 'self'"
|
||||
label="Target"
|
||||
:hint="targetOptionHint"
|
||||
:items="targetOptions"
|
||||
:value="model.target"
|
||||
:error-messages="errors.target"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['target'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
parentTarget: {
|
||||
type: String,
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
computed: {
|
||||
targetOptions(){
|
||||
if (this.parentTarget === 'singleTarget') {
|
||||
return [
|
||||
{
|
||||
text: 'Self',
|
||||
value: 'self',
|
||||
}, {
|
||||
text: 'Target',
|
||||
value: 'every',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
text: 'Self',
|
||||
value: 'self',
|
||||
}, {
|
||||
text: 'Roll once for each target',
|
||||
value: 'each',
|
||||
}, {
|
||||
text: 'Roll once and apply to every target',
|
||||
value: 'every',
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
targetOptionHint(){
|
||||
let hints = {
|
||||
self: 'The damage will be applied to the character\'s own attribute when taking the action',
|
||||
target: 'The damage will be applied to the target of the action',
|
||||
each: 'The damage will be rolled separately for each of the targets of the action',
|
||||
every: 'The damage will be rolled once and applied to each of the targets of the action',
|
||||
};
|
||||
if (this.parentTarget === 'singleTarget'){
|
||||
hints.each = hints.target;
|
||||
hints.every = hints.target;
|
||||
}
|
||||
return hints[this.model.target];
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
74
app/imports/ui/forms/AdjustmentListForm.vue
Normal file
74
app/imports/ui/forms/AdjustmentListForm.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<v-slide-x-transition group>
|
||||
<div
|
||||
v-for="(adjustment, i) in model"
|
||||
:key="adjustment._id || i"
|
||||
>
|
||||
<v-divider v-if="i !== 0"/>
|
||||
<adjustment-form
|
||||
class="mt-4"
|
||||
:model="adjustment"
|
||||
:parent-target="parentTarget"
|
||||
@change="({path, value, ack}) => $emit('change', {path: [i, ...path], value, ack})"
|
||||
/>
|
||||
<div>
|
||||
<v-btn outline icon large class="ma-3" @click="$emit('pull', {path: [i]})">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-slide-x-transition>
|
||||
<div class="layout row justify-end">
|
||||
<v-btn
|
||||
:loading="addAdjustmentLoading"
|
||||
:disabled="addAdjustmentLoading"
|
||||
outline
|
||||
@click="addAdjustment"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Add Adjustment
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AdjustmentForm from 'imports/ui/forms/AdjustmentForm.vue';
|
||||
import AdjustmentSchema from '/imports/api/creature/subSchemas/AdjustmentSchema.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AdjustmentForm,
|
||||
},
|
||||
data(){return {
|
||||
addAdjustmentLoading: false,
|
||||
}},
|
||||
methods: {
|
||||
acknowledgeAddAdjustment(){
|
||||
this.addAdjustmentLoading = false;
|
||||
},
|
||||
addAdjustment(){
|
||||
this.addAdjustmentLoading = true;
|
||||
this.$emit('push', {
|
||||
path: [],
|
||||
value: AdjustmentSchema.clean({}),
|
||||
ack: this.acknowledgeAddAdjustment,
|
||||
});
|
||||
},
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
parentTarget: {
|
||||
type: String,
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
174
app/imports/ui/forms/AttributeForm.vue
Normal file
174
app/imports/ui/forms/AttributeForm.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-form">
|
||||
<div class="layout column align-center">
|
||||
<text-field
|
||||
label="Base Value"
|
||||
type="number"
|
||||
class="base-value-field text-xs-center large-format no-flex"
|
||||
:value="model.baseValue"
|
||||
@change="(value, ack) => $emit('change', {path: ['baseValue'], value, ack})"
|
||||
hint="This is the value of the attribute before effects are applied"
|
||||
:error-messages="errors.baseValue"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<div class="layout row wrap">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Variable name"
|
||||
:value="model.variableName"
|
||||
style="flex-basis: 300px;"
|
||||
@change="(value, ack) => $emit('change', {path: ['variableName'], value, ack})"
|
||||
hint="Use this name in formulae to reference this attribute"
|
||||
:error-messages="errors.variableName"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<smart-select
|
||||
label="Type"
|
||||
:items="attributeTypes"
|
||||
:value="model.type"
|
||||
:error-messages="errors.type"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['type'], value, ack})"
|
||||
:hint="attributeTypeHints[model.type]"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<form-section name="Advanced" standalone>
|
||||
<div class="layout column align-center">
|
||||
<v-switch
|
||||
label="Allow decimal values"
|
||||
class="no-flex"
|
||||
:value="model.decimal"
|
||||
:error-messages="errors.decimal"
|
||||
@change="e => $emit('change', $emit('change', {path: ['decimal'], value: !!e, ack}))"
|
||||
/>
|
||||
<div class="layout row justify-center" style="align-self: stretch;">
|
||||
<text-field
|
||||
label="Damage"
|
||||
type="number"
|
||||
class="damage-field text-xs-center"
|
||||
style="max-width: 300px;"
|
||||
hint="The attribute's final value is reduced by this amount"
|
||||
:value="model.damage"
|
||||
@change="(value, ack) => $emit('change', {path: ['damage'], value, ack})"
|
||||
:error-messages="errors.adjustment"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout row wrap">
|
||||
<smart-select
|
||||
label="Reset"
|
||||
clearable
|
||||
style="flex-basis: 300px;"
|
||||
:items="resetOptions"
|
||||
:value="model.reset"
|
||||
:error-messages="errors.reset"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['reset'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Reset Multiplier"
|
||||
type="number"
|
||||
style="flex-basis: 400px;"
|
||||
:value="model.resetMultiplier"
|
||||
:error-messages="errors.resetMultiplier"
|
||||
@change="(value, ack) => $emit('change', {path: ['resetMultiplier'], value, ack})"
|
||||
hint="Some attributes, like hit dice, only reset by half their total on a long rest"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</form-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection from '/imports/ui/forms/components/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){
|
||||
let data = {
|
||||
attributeTypes: [
|
||||
{
|
||||
text: 'Ability score',
|
||||
value: 'ability',
|
||||
help: 'Ability scores are your primary attributes, like Strength and Intelligence',
|
||||
}, {
|
||||
text: 'Stat',
|
||||
value: 'stat',
|
||||
help: 'Stats are attributes with a numerical value like speed or carrying capacity',
|
||||
}, {
|
||||
text: 'Modifier',
|
||||
value: 'modifier',
|
||||
help: 'Modifiers are attributes that are added to rolls, like proficiency bonus',
|
||||
}, {
|
||||
text: 'Hit dice',
|
||||
value: 'hitDice',
|
||||
}, {
|
||||
text: 'Health bar',
|
||||
value: 'healthBar',
|
||||
}, {
|
||||
text: 'Resource',
|
||||
value: 'resource',
|
||||
help: 'Resources are attributes that are spent to fuel actions, like sorcery points or ki'
|
||||
}, {
|
||||
text: 'Spell slot',
|
||||
value: 'spellSlot',
|
||||
}, {
|
||||
text: 'Utility',
|
||||
value: 'utility',
|
||||
help: 'Utility attributes aren\'t displayed on your character sheet, but can be referenced or used in calculations',
|
||||
},
|
||||
],
|
||||
resetOptions: [
|
||||
{
|
||||
text: 'Short rest',
|
||||
value: 'shortRest',
|
||||
}, {
|
||||
text: 'Long rest',
|
||||
value: 'longRest',
|
||||
}
|
||||
],
|
||||
};
|
||||
data.attributeTypeHints = {};
|
||||
data.attributeTypes.forEach(type => {
|
||||
data.attributeTypeHints[type.value] = type.help;
|
||||
});
|
||||
return data;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.no-flex {
|
||||
flex: initial;
|
||||
}
|
||||
.layout.row.wrap {
|
||||
margin-right: -8px;
|
||||
}
|
||||
.layout.row.wrap > *{
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
113
app/imports/ui/forms/BuffForm.vue
Normal file
113
app/imports/ui/forms/BuffForm.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template lang="html">
|
||||
<div class="buff-form">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-area
|
||||
label="Description"
|
||||
:value="model.description"
|
||||
@change="(value, ack) => $emit('change', {path: ['description'], value, ack})"
|
||||
:error-messages="errors.description"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Duration"
|
||||
hint="How long the buff lasts"
|
||||
:value="model.duration"
|
||||
@change="(value, ack) => $emit('change', {path: ['duration'], value, ack})"
|
||||
:error-messages="errors.duration"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div v-if="stored">
|
||||
<smart-select
|
||||
v-if="parentTarget !== 'self'"
|
||||
label="Target"
|
||||
:hint="targetOptionHint"
|
||||
:items="targetOptions"
|
||||
:value="model.target"
|
||||
:error-messages="errors.target"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['target'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<effect-list-form
|
||||
:model="model.effects"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['effects', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['effects', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['effects', ...path], ack})"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EffectListForm from '/imports/ui/forms/EffectListForm.vue';
|
||||
export default {
|
||||
props: {
|
||||
stored: Boolean,
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
parentTarget: {
|
||||
type: String,
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
components: {
|
||||
EffectListForm,
|
||||
},
|
||||
computed: {
|
||||
targetOptions(){
|
||||
if (this.parentTarget === 'singleTarget') {
|
||||
return [
|
||||
{
|
||||
text: 'Self',
|
||||
value: 'self',
|
||||
}, {
|
||||
text: 'Target',
|
||||
value: 'every',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
text: 'Self',
|
||||
value: 'self',
|
||||
}, {
|
||||
text: 'Roll once for each target',
|
||||
value: 'each',
|
||||
}, {
|
||||
text: 'Roll once and apply to every target',
|
||||
value: 'every',
|
||||
},
|
||||
];
|
||||
}
|
||||
},
|
||||
targetOptionHint(){
|
||||
let hints = {
|
||||
self: 'The buff will be applied to the character taking the action',
|
||||
target: 'The buff will be applied to the target of the action',
|
||||
each: 'The buff will be rolled separately for each of the targets of the action',
|
||||
every: 'The buff will be rolled once and applied to each of the targets of the action',
|
||||
};
|
||||
if (this.parentTarget === 'singleTarget'){
|
||||
hints.each = hints.target;
|
||||
hints.every = hints.target;
|
||||
}
|
||||
return hints[this.model.target];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
77
app/imports/ui/forms/BuffListForm.vue
Normal file
77
app/imports/ui/forms/BuffListForm.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<v-slide-x-transition group>
|
||||
<div
|
||||
v-for="(buff, i) in model"
|
||||
:key="buff._id || i"
|
||||
>
|
||||
<v-divider v-if="i !== 0"/>
|
||||
<div class="layout row align-center">
|
||||
<div style="flex-grow: 1;">
|
||||
<buff-form
|
||||
class="mt-4"
|
||||
:model="buff"
|
||||
:parent-target="parentTarget"
|
||||
:stored="stored"
|
||||
@change="({path, value, ack}) => $emit('change', {path: [i, ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: [i, ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: [i, ...path], ack})"
|
||||
/>
|
||||
</div>
|
||||
<v-btn outline icon large class="ma-3" @click="$emit('pull', {path: [i]})">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-slide-x-transition>
|
||||
<div class="layout row justify-end">
|
||||
<v-btn
|
||||
:loading="addBuffLoading"
|
||||
:disabled="addBuffLoading"
|
||||
outline
|
||||
@click="addBuff"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Add Buff
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BuffForm from 'imports/ui/forms/BuffForm.vue';
|
||||
import {StoredBuffSchema, AppliedBuffSchema} from '/imports/api/creature/properties/Buffs.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BuffForm,
|
||||
},
|
||||
props: {
|
||||
stored: Boolean,
|
||||
model: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
parentTarget: {
|
||||
type: String,
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){return {
|
||||
addBuffLoading: false,
|
||||
}},
|
||||
methods: {
|
||||
acknowledgeAddBuff(){
|
||||
this.addBuffLoading = false;
|
||||
},
|
||||
addBuff(){
|
||||
this.addBuffLoading = true;
|
||||
let schema = this.stored ? StoredBuffSchema : AppliedBuffSchema;
|
||||
this.$emit('push', {path: [], value: schema.clean({}), ack: this.acknowledgeAddBuff});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
147
app/imports/ui/forms/EffectForm.vue
Normal file
147
app/imports/ui/forms/EffectForm.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template lang="html">
|
||||
<div class="layout row wrap justify-start effect-form">
|
||||
<smart-select
|
||||
label="Operation"
|
||||
append-icon="arrow_drop_down"
|
||||
class="mx-2"
|
||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||
:items="operations"
|
||||
:value="model.operation"
|
||||
@change="(value, ack) => $emit('change', {path: ['operation'], value, ack})"
|
||||
>
|
||||
<v-icon
|
||||
class="icon"
|
||||
slot="prepend"
|
||||
:class="iconClass"
|
||||
>{{displayedIcon}}</v-icon>
|
||||
<template slot="item" slot-scope="item">
|
||||
<v-icon
|
||||
class="icon mr-2"
|
||||
>{{getEffectIcon(item.item.value, 1)}}</v-icon>
|
||||
{{item.item.text}}
|
||||
</template>
|
||||
</smart-select>
|
||||
|
||||
<text-field
|
||||
label="Value"
|
||||
class="mr-2"
|
||||
:persistent-hint="needsValue"
|
||||
:value="needsValue ? (model.calculation) : ' '"
|
||||
:disabled="!needsValue"
|
||||
:hint="!isFinite(model.calculation) && model.result ? model.result + '' : '' "
|
||||
@change="(value, ack) => $emit('change', {path: ['calculation'], value, ack})"
|
||||
/>
|
||||
|
||||
<text-field
|
||||
label="Stat"
|
||||
class="mr-2"
|
||||
append-icon="arrow_drop_down"
|
||||
item-text="name"
|
||||
item-value="variableName"
|
||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||
:value="model.stat"
|
||||
:items="stats"
|
||||
@change="(value, ack) => $emit('change', {path: ['stat'], value, ack})"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import getEffectIcon from '/imports/ui/utility/getEffectIcon.js';
|
||||
|
||||
const ICON_SPIN_DURATION = 300;
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stats: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
displayedIcon: 'add',
|
||||
iconClass: '',
|
||||
operations: [
|
||||
{value: 'base', text: 'Base Value'},
|
||||
{value: 'add', text: 'Add'},
|
||||
{value: 'mul', text: 'Multiply'},
|
||||
{value: 'min', text: 'Minimum'},
|
||||
{value: 'max', text: 'Maximum'},
|
||||
{value: 'advantage', text: 'Advantage'},
|
||||
{value: 'disadvantage', text: 'Disadvantage'},
|
||||
{value: 'passiveAdd', text: 'Passive Bonus'},
|
||||
{value: 'fail', text: 'Fail'},
|
||||
{value: 'conditional', text: 'Conditional Benefit'},
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
needsValue(){
|
||||
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 true;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getEffectIcon,
|
||||
},
|
||||
watch: {
|
||||
'model.operation': {
|
||||
immediate: true,
|
||||
handler(newValue, oldValue, e){
|
||||
let newIcon = getEffectIcon(newValue, 1);
|
||||
if (!oldValue){
|
||||
// Skip animation
|
||||
this.displayedIcon = newIcon;
|
||||
} else {
|
||||
this.iconClass="leaving";
|
||||
setTimeout(() => {
|
||||
this.displayedIcon = newIcon;
|
||||
this.iconClass="arriving";
|
||||
requestAnimationFrame(() => {
|
||||
this.iconClass="";
|
||||
});
|
||||
}, ICON_SPIN_DURATION / 2);
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.theme--light .icon {
|
||||
color: black;
|
||||
}
|
||||
.icon {
|
||||
min-width: 30px;
|
||||
transition: transform 0.15s linear, opacity 0.15s ease;
|
||||
transform-origin: 18px center;
|
||||
margin-left: -12px;
|
||||
}
|
||||
.icon.leaving {
|
||||
transform: translateY(-24px);
|
||||
opacity: 0;
|
||||
}
|
||||
.icon.arriving {
|
||||
transform: translateY(24px);
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
}
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
.effect-form > div {
|
||||
flex-basis: 220px;
|
||||
}
|
||||
</style>
|
||||
151
app/imports/ui/forms/EffectListForm.vue
Normal file
151
app/imports/ui/forms/EffectListForm.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<v-slide-x-transition group>
|
||||
<div
|
||||
v-for="(effect, i) in model"
|
||||
:key="effect._id || i"
|
||||
>
|
||||
<v-divider v-if="i !== 0"/>
|
||||
<div class="layout row align-center">
|
||||
<div style="flex-grow: 1;">
|
||||
<effect-form
|
||||
class="mt-4"
|
||||
:model="effect"
|
||||
:parent-target="parentTarget"
|
||||
:stored="stored"
|
||||
@change="({path, value, ack}) => $emit('change', {path: [i, ...path], value, ack})"
|
||||
@pull="(ack) => $emit('pull', {path: [i], ack})"
|
||||
/>
|
||||
</div>
|
||||
<v-btn outline icon large class="ma-3" @click="$emit('pull', {path: [i]})">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-slide-x-transition>
|
||||
<div class="layout row justify-end">
|
||||
<v-btn
|
||||
:loading="addEffectLoading"
|
||||
:disabled="addEffectLoading"
|
||||
outline
|
||||
@click="addEffect"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Add Effect
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EffectForm from '/imports/ui/forms/EffectForm.vue';
|
||||
import { EffectSchema } from '/imports/api/creature/properties/Effects.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EffectForm,
|
||||
},
|
||||
props: {
|
||||
stored: Boolean,
|
||||
model: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
parentTarget: {
|
||||
type: String,
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){return {
|
||||
addEffectLoading: false,
|
||||
}},
|
||||
methods: {
|
||||
acknowledgeAddEffect(){
|
||||
this.addEffectLoading = false;
|
||||
},
|
||||
addEffect(){
|
||||
this.addEffectLoading = true;
|
||||
this.$emit('push', {
|
||||
path: [],
|
||||
value: EffectSchema.clean({}),
|
||||
ack: this.acknowledgeAddEffect,
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
<template lang="html">
|
||||
<div>
|
||||
<v-slide-x-transition group>
|
||||
<div
|
||||
v-for="(effect, i) in model"
|
||||
:key="effect._id || i"
|
||||
>
|
||||
<v-divider v-if="i !== 0"/>
|
||||
<div class="layout row align-center">
|
||||
<div style="flex-grow: 1;">
|
||||
<effect-form
|
||||
class="mt-4"
|
||||
:model="effect"
|
||||
@change="({path, value, ack}) => $emit('change', {path: [i, ...path], value, ack})"
|
||||
/>
|
||||
</div>
|
||||
<v-btn outline icon large class="ma-3" @click="$emit('pull', {path: [i]})">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</v-slide-x-transition>
|
||||
<div class="layout row justify-end">
|
||||
<v-btn
|
||||
:loading="addEffectLoading"
|
||||
:disabled="addEffectLoading"
|
||||
outline
|
||||
@click="addEffect"
|
||||
>
|
||||
<v-icon>add</v-icon>
|
||||
Add Effect
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EffectForm from '/imports/ui/forms/EffectForm.vue';
|
||||
import { EffectSchema } from '/imports/api/creature/properties/Effects.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
EffectForm,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Array,
|
||||
default: () => ([]),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){return {
|
||||
addEffectLoading: false,
|
||||
}},
|
||||
methods: {
|
||||
acknowledgeAddEffect(){
|
||||
this.addEffectLoading = false;
|
||||
},
|
||||
addEffect(){
|
||||
this.addEffectLoading = true;
|
||||
this.$emit('push', {
|
||||
path: [],
|
||||
value: EffectSchema.clean({}),
|
||||
ack: this.acknowledgeAddEffect,
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
83
app/imports/ui/forms/FeatureForm.vue
Normal file
83
app/imports/ui/forms/FeatureForm.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template lang="html">
|
||||
<div class="feature-form">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<smart-select
|
||||
label="Enabled"
|
||||
:items="enabledOptions"
|
||||
:value="enabledStatus"
|
||||
:error-messages="errors.enabled || errors.alwaysEnabled"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="changeEnabled"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-area
|
||||
label="Description"
|
||||
:value="model.description"
|
||||
:error-messages="errors.description"
|
||||
@change="(value, ack) => $emit('change', {path: ['description'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){ return{
|
||||
enabledOptions: [
|
||||
{
|
||||
text: 'Always enabled',
|
||||
value: 'always',
|
||||
}, {
|
||||
text: 'Enabled',
|
||||
value: 'enabled',
|
||||
}, {
|
||||
text: 'Disabled',
|
||||
value: 'disabled',
|
||||
}
|
||||
],
|
||||
}},
|
||||
computed: {
|
||||
enabledStatus(){
|
||||
if (!this.model) return;
|
||||
if (this.model.alwaysEnabled) return 'always';
|
||||
if (this.model.enabled) return 'enabled';
|
||||
return 'disabled';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeEnabled(value, ack){
|
||||
let change = ({enabled, alwaysEnabled}) => {
|
||||
this.$emit('change', {path: ['enabled'], value: enabled, ack});
|
||||
this.$emit('change', {path: ['alwaysEnabled'], value: alwaysEnabled, ack});
|
||||
}
|
||||
if (value === 'always'){
|
||||
change({enabled: true, alwaysEnabled: true});
|
||||
} else if (value === 'enabled'){
|
||||
change({enabled: true, alwaysEnabled: false});
|
||||
} else if (value === 'disabled'){
|
||||
change({enabled: false, alwaysEnabled: false});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
34
app/imports/ui/forms/components/FormSection.vue
Normal file
34
app/imports/ui/forms/components/FormSection.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template lang="html">
|
||||
<form-sections v-if="standalone">
|
||||
<v-expansion-panel-content>
|
||||
<div slot="header" class="subheading">
|
||||
{{name}}
|
||||
</div>
|
||||
<v-card-text>
|
||||
<slot/>
|
||||
</v-card-text>
|
||||
</v-expansion-panel-content>
|
||||
</form-sections>
|
||||
<v-expansion-panel-content v-else>
|
||||
<div slot="header" class="subheading">
|
||||
{{name}}
|
||||
</div>
|
||||
<v-card-text>
|
||||
<slot/>
|
||||
</v-card-text>
|
||||
</v-expansion-panel-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSections from '/imports/ui/forms/components/FormSections.vue';
|
||||
export default {
|
||||
components: {
|
||||
FormSections,
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
standalone: Boolean,
|
||||
},
|
||||
}
|
||||
export { FormSections };
|
||||
</script>
|
||||
9
app/imports/ui/forms/components/FormSections.vue
Normal file
9
app/imports/ui/forms/components/FormSections.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template lang="html">
|
||||
<v-expansion-panel popout expand>
|
||||
<slot/>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
13
app/imports/ui/forms/components/propertyFormIndex.js
Normal file
13
app/imports/ui/forms/components/propertyFormIndex.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import ActionForm from 'imports/ui/forms/ActionForm.vue';
|
||||
import AttributeForm from 'imports/ui/forms/AttributeForm.vue';
|
||||
import BuffForm from '/imports/ui/forms/BuffForm.vue';
|
||||
import EffectForm from '/imports/ui/forms/EffectForm.vue';
|
||||
import FeatureForm from '/imports/ui/forms/FeatureForm.vue';
|
||||
|
||||
export default {
|
||||
action: ActionForm,
|
||||
attribute: AttributeForm,
|
||||
buff: BuffForm,
|
||||
effect: EffectForm,
|
||||
feature: FeatureForm,
|
||||
};
|
||||
59
app/imports/ui/forms/components/schemaFormMixin.js
Normal file
59
app/imports/ui/forms/components/schemaFormMixin.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Forms that take in a schema and a model of the current data, manages smart
|
||||
* inputs, and sends update events when valid data model changes must occur
|
||||
*/
|
||||
import { get, toPath } from 'lodash';
|
||||
function resolvePath(model, path){
|
||||
let arrayPath = toPath(path);
|
||||
if (arrayPath.length === 1){
|
||||
return { object: model, key: arrayPath[0] };
|
||||
}
|
||||
let objectPath = arrayPath.slice(0, -1);
|
||||
let key = arrayPath.slice(-1);
|
||||
let object = get(model, objectPath);
|
||||
return {object, key};
|
||||
};
|
||||
|
||||
const schemaFormMixin = {
|
||||
data(){ return {
|
||||
valid: true,
|
||||
};},
|
||||
computed: {
|
||||
errors(){
|
||||
this.valid = true;
|
||||
if (!this.model){
|
||||
throw new Error("this.model must be set");
|
||||
}
|
||||
if (!this.validationContext) return {};
|
||||
let cleanModel = this.validationContext.clean(this.model, {
|
||||
getAutoValues: false,
|
||||
});
|
||||
this.validationContext.validate(cleanModel);
|
||||
let errors = {};
|
||||
this.validationContext.validationErrors().forEach(error => {
|
||||
if (this.valid) this.valid = false;
|
||||
errors[error.name] = this.schema.messageForError(error);
|
||||
});
|
||||
return errors;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// Sets the value at the given path
|
||||
change({path, value, ack}){
|
||||
let {object, key} = resolvePath(this.model, path);
|
||||
this.$set(object, key, value);
|
||||
if (ack) ack();
|
||||
},
|
||||
push({path, value, ack}){
|
||||
get(this.model, path).push(value);
|
||||
if (ack) ack();
|
||||
},
|
||||
pull({path, ack}){
|
||||
let {object, key} = resolvePath(this.model, path);
|
||||
object.splice(key, 1);
|
||||
if (ack) ack();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default schemaFormMixin;
|
||||
Reference in New Issue
Block a user