Moved ui/forms to ui/properties/forms
This commit is contained in:
198
app/imports/ui/properties/forms/ActionForm.vue
Normal file
198
app/imports/ui/properties/forms/ActionForm.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<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">
|
||||
<v-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
box
|
||||
:value="model.tags"
|
||||
@change="(value) => $emit('change', {path: ['tags'], value})"
|
||||
/>
|
||||
<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/properties/forms/shared/FormSection.vue';
|
||||
import AdjustmentListForm from '/imports/ui/properties/forms/AdjustmentListForm.vue';
|
||||
import BuffListForm from '/imports/ui/properties/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/properties/forms/AdjustmentForm.vue
Normal file
98
app/imports/ui/properties/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/properties/forms/AdjustmentListForm.vue
Normal file
74
app/imports/ui/properties/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/properties/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/properties/forms/AttributeForm.vue
Normal file
174
app/imports/ui/properties/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.attributeType"
|
||||
:error-messages="errors.attributeType"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['attributeType'], value, ack})"
|
||||
:hint="attributeTypeHints[model.attributeType]"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<form-section name="Advanced" standalone>
|
||||
<div class="layout column align-center">
|
||||
<v-switch
|
||||
label="Allow decimal values"
|
||||
class="no-flex"
|
||||
:input-value="model.decimal"
|
||||
:error-messages="errors.decimal"
|
||||
@change="e => $emit('change', {path: ['decimal'], value: !!e})"
|
||||
/>
|
||||
<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.damage"
|
||||
: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/properties/forms/shared/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/properties/forms/BuffForm.vue
Normal file
113
app/imports/ui/properties/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/properties/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/properties/forms/BuffListForm.vue
Normal file
77
app/imports/ui/properties/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/properties/forms/BuffForm.vue';
|
||||
import {StoredBuffSchema, AppliedBuffSchema} from '/imports/api/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>
|
||||
66
app/imports/ui/properties/forms/ClassLevelForm.vue
Normal file
66
app/imports/ui/properties/forms/ClassLevelForm.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template lang="html">
|
||||
<div class="class-form">
|
||||
<div class="layout column align-center">
|
||||
<text-field
|
||||
label="Level"
|
||||
type="number"
|
||||
class="base-value-field text-xs-center large-format no-flex"
|
||||
:value="model.level"
|
||||
@change="(value, ack) => $emit('change', {path: ['level'], value, ack})"
|
||||
:error-messages="errors.level"
|
||||
: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 class"
|
||||
:error-messages="errors.variableName"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Base Class Variable name"
|
||||
:value="model.baseClass"
|
||||
style="flex-basis: 300px;"
|
||||
@change="(value, ack) => $emit('change', {path: ['baseClass'], value, ack})"
|
||||
hint="This is the name of the class this class level belongs to"
|
||||
:error-messages="errors.baseClass"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
80
app/imports/ui/properties/forms/ContainerForm.vue
Normal file
80
app/imports/ui/properties/forms/ContainerForm.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-form">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div class="layout row wrap">
|
||||
<text-field
|
||||
label="Value"
|
||||
suffix="gp"
|
||||
type="number"
|
||||
min="0"
|
||||
hint="The value of the item in gold pieces, using decimals for values less than 1 gp"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.value"
|
||||
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
|
||||
:error-messages="errors.value"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Weight"
|
||||
suffix="lbs"
|
||||
type="number"
|
||||
min="0"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.weight"
|
||||
@change="(value, ack) => $emit('change', {path: ['weight'], value, ack})"
|
||||
:error-messages="errors.weight"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<text-area
|
||||
label="Description"
|
||||
:value="model.description"
|
||||
:error-messages="errors.description"
|
||||
@change="(value, ack) => $emit('change', {path: ['description'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<form-section name="Advanced" standalone>
|
||||
<v-switch
|
||||
label="Carried"
|
||||
:input-value="model.carried"
|
||||
:error-messages="errors.carried"
|
||||
@change="e => $emit('change', {path: ['carried'], value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Contents are weightless"
|
||||
:input-value="model.contentsWeightless"
|
||||
:error-messages="errors.contentsWeightless"
|
||||
@change="e => $emit('change', {path: ['contentsWeightless'], value})"
|
||||
/>
|
||||
</form-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
133
app/imports/ui/properties/forms/DamageMultiplierForm.vue
Normal file
133
app/imports/ui/properties/forms/DamageMultiplierForm.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-form">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div class="layout row wrap">
|
||||
<smart-select
|
||||
label="Damage Type"
|
||||
style="flex-basis: 300px;"
|
||||
:items="damageTypes"
|
||||
:value="model.damageType"
|
||||
:error-messages="errors.damageType"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['damageType'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<smart-select
|
||||
label="Value"
|
||||
style="flex-basis: 300px;"
|
||||
:items="values"
|
||||
:value="model.value"
|
||||
:error-messages="errors.value"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){return {
|
||||
damageTypes: [
|
||||
{
|
||||
value: "bludgeoning",
|
||||
text: "Bludgeoning",
|
||||
}, {
|
||||
value: "piercing",
|
||||
text: "Piercing",
|
||||
}, {
|
||||
value: "slashing",
|
||||
text: "Slashing",
|
||||
}, {
|
||||
value: "magicalBludgeoning",
|
||||
text: "Magical Bludgeoning",
|
||||
}, {
|
||||
value: "magicalPiercing",
|
||||
text: "Magical Piercing",
|
||||
}, {
|
||||
value: "magicalSlashing",
|
||||
text: "Magical Slashing",
|
||||
}, {
|
||||
value: "acid",
|
||||
text: "Acid",
|
||||
}, {
|
||||
value: "cold",
|
||||
text: "Cold",
|
||||
}, {
|
||||
value: "fire",
|
||||
text: "Fire",
|
||||
}, {
|
||||
value: "force",
|
||||
text: "Force",
|
||||
}, {
|
||||
value: "lightning",
|
||||
text: "Lightning",
|
||||
}, {
|
||||
value: "necrotic",
|
||||
text: "Necrotic",
|
||||
}, {
|
||||
value: "poison",
|
||||
text: "Poison",
|
||||
}, {
|
||||
value: "psychic",
|
||||
text: "Psychic",
|
||||
}, {
|
||||
value: "radiant",
|
||||
text: "Radiant",
|
||||
}, {
|
||||
value: "thunder",
|
||||
text: "Thunder",
|
||||
},
|
||||
],
|
||||
values: [
|
||||
{
|
||||
value: 0,
|
||||
text: "Immunity",
|
||||
}, {
|
||||
value: 0.5,
|
||||
text: "Resistance",
|
||||
}, {
|
||||
value: 2,
|
||||
text: "Vulnerability",
|
||||
},
|
||||
],
|
||||
};},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.no-flex {
|
||||
flex: initial;
|
||||
}
|
||||
.layout.row.wrap {
|
||||
margin-right: -8px;
|
||||
}
|
||||
.layout.row.wrap > *{
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
143
app/imports/ui/properties/forms/EffectForm.vue
Normal file
143
app/imports/ui/properties/forms/EffectForm.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<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"
|
||||
: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/properties/forms/EffectListForm.vue
Normal file
151
app/imports/ui/properties/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/properties/forms/EffectForm.vue';
|
||||
import { EffectSchema } from '/imports/api/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/properties/forms/EffectForm.vue';
|
||||
import { EffectSchema } from '/imports/api/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>
|
||||
74
app/imports/ui/properties/forms/ExperienceForm.vue
Normal file
74
app/imports/ui/properties/forms/ExperienceForm.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template lang="html">
|
||||
<div class="class-form">
|
||||
<div class="layout column align-center">
|
||||
<text-field
|
||||
label="XP"
|
||||
type="number"
|
||||
class="base-value-field text-xs-center large-format no-flex"
|
||||
:value="model.value"
|
||||
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
|
||||
:error-messages="errors.value"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<div class="layout row wrap">
|
||||
<text-field
|
||||
label="Title"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.title"
|
||||
@change="(value, ack) => $emit('change', {path: ['title'], value, ack})"
|
||||
:error-messages="errors.title"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="In-World date"
|
||||
:value="model.worldDate"
|
||||
style="flex-basis: 300px;"
|
||||
@change="(value, ack) => $emit('change', {path: ['worldDate'], value, ack})"
|
||||
hint="The date in-game that the experience occured"
|
||||
:error-messages="errors.worldDate"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<date-picker
|
||||
label="Real date"
|
||||
:value="model.date"
|
||||
style="flex-basis: 300px;"
|
||||
@change="(value, ack) => $emit('change', {path: ['date'], value, ack})"
|
||||
hint="Real life date"
|
||||
:error-messages="errors.date"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
83
app/imports/ui/properties/forms/FeatureForm.vue
Normal file
83
app/imports/ui/properties/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>
|
||||
33
app/imports/ui/properties/forms/FolderForm.vue
Normal file
33
app/imports/ui/properties/forms/FolderForm.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template lang="html">
|
||||
<div class="folder-form">
|
||||
<div class="layout row wrap">
|
||||
<text-field
|
||||
label="Name"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
96
app/imports/ui/properties/forms/ItemForm.vue
Normal file
96
app/imports/ui/properties/forms/ItemForm.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-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-field
|
||||
label="Plural name"
|
||||
:value="model.plural"
|
||||
@change="(value, ack) => $emit('change', {path: ['plural'], value, ack})"
|
||||
:error-messages="errors.plural"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div class="layout row wrap">
|
||||
<text-field
|
||||
label="Value"
|
||||
suffix="gp"
|
||||
type="number"
|
||||
min="0"
|
||||
hint="The value of the item in gold pieces, using decimals for values less than 1 gp"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.value"
|
||||
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
|
||||
:error-messages="errors.value"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Weight"
|
||||
suffix="lbs"
|
||||
type="number"
|
||||
min="0"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.weight"
|
||||
@change="(value, ack) => $emit('change', {path: ['weight'], value, ack})"
|
||||
:error-messages="errors.weight"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<text-field
|
||||
label="Quantity"
|
||||
type="number"
|
||||
min="0"
|
||||
:value="model.quantity"
|
||||
@change="(value, ack) => $emit('change', {path: ['quantity'], value, ack})"
|
||||
:error-messages="errors.quantity"
|
||||
: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"
|
||||
/>
|
||||
<form-section name="Advanced" standalone>
|
||||
<v-switch
|
||||
label="Requires attunement"
|
||||
:input-value="model.requiresAttunement"
|
||||
:error-messages="errors.requiresAttunement"
|
||||
@change="e => $emit('change', {path: ['requiresAttunement'], value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Show increment buttons"
|
||||
:input-value="model.showIncrement"
|
||||
:error-messages="errors.showIncrement"
|
||||
@change="e => $emit('change', {path: ['showIncrement'], value})"
|
||||
/>
|
||||
</form-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
37
app/imports/ui/properties/forms/NoteForm.vue
Normal file
37
app/imports/ui/properties/forms/NoteForm.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<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"
|
||||
/>
|
||||
<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,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
44
app/imports/ui/properties/forms/ProficiencyForm.vue
Normal file
44
app/imports/ui/properties/forms/ProficiencyForm.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template lang="html">
|
||||
<div class="layout row wrap justify-start proficiency-form">
|
||||
<text-field
|
||||
label="Skill"
|
||||
class="mr-2"
|
||||
append-icon="arrow_drop_down"
|
||||
item-text="name"
|
||||
item-value="skill"
|
||||
style="flex-basis: 300px;"
|
||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||
:value="model.skill"
|
||||
:items="undefined"
|
||||
@change="(value, ack) => $emit('change', {path: ['skill'], value, ack})"
|
||||
/>
|
||||
<proficiency-select
|
||||
label="Proficiency"
|
||||
style="flex-basis: 300px;"
|
||||
:value="model.value"
|
||||
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProficiencySelect from '/imports/ui/properties/forms/shared/ProficiencySelect.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProficiencySelect,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
stats: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
156
app/imports/ui/properties/forms/RollForm.vue
Normal file
156
app/imports/ui/properties/forms/RollForm.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template lang="html">
|
||||
<div class="roll-form">
|
||||
<text-field
|
||||
label="Roll"
|
||||
hint="The roll will be calculated using the rolling character's stats"
|
||||
:value="model.roll"
|
||||
@change="(value, ack) => $emit('change', {path: ['roll'], value, ack})"
|
||||
:error-messages="errors.roll"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Target number"
|
||||
hint="The target number or stat to meet or exceed, calculated from the target's stats"
|
||||
:value="model.targetNumber"
|
||||
@change="(value, ack) => $emit('change', {path: ['targetNumber'], value, ack})"
|
||||
:error-messages="errors.targetNumber"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<smart-select
|
||||
label="Type"
|
||||
:items="rollTypes"
|
||||
:value="model.rollType"
|
||||
:error-messages="errors.rollType"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['rollType'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<v-combobox
|
||||
label="Tags"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
box
|
||||
:value="model.tags"
|
||||
@change="(value) => $emit('change', {path: ['tags'], value})"
|
||||
/>
|
||||
<form-sections>
|
||||
<form-section name="Advanced">
|
||||
<v-switch
|
||||
label="Only roll if the parent roll misses"
|
||||
:input-value="model.onMiss"
|
||||
:error-messages="errors.onMiss"
|
||||
@change="e => $emit('change', {path: ['onMiss'], value: !!e})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Swap who wins ties"
|
||||
:input-value="model.invertTies"
|
||||
:error-messages="errors.invertTies"
|
||||
@change="e => $emit('change', {path: ['invertTies'], value: !!e})"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section name="Damage and Adjustments on Hit">
|
||||
<div class="caption">
|
||||
Adjustments can be used to automatically spend resources or deal
|
||||
damage when taking an roll.
|
||||
These apply when a roll suceeds.
|
||||
</div>
|
||||
<adjustment-list-form
|
||||
:model="model.hit.adjustments"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['hit', 'adjustments', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['hit', 'adjustments', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['hit', 'adjustments', ...path], ack})"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section name="Buffs on Hit">
|
||||
<div class="caption">
|
||||
Buffs apply temporary effects to characters when the roll succeeds.
|
||||
</div>
|
||||
<buff-list-form
|
||||
:model="model.hit.buffs"
|
||||
:stored="stored"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['hit', 'buffs', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['hit', 'buffs', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['hit', 'buffs', ...path], ack})"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section name="Damage and Adjustments on Miss">
|
||||
<div class="caption">
|
||||
Adjustments can be used to automatically spend resources or deal
|
||||
damage when taking an roll.
|
||||
These apply when a roll fails.
|
||||
</div>
|
||||
<adjustment-list-form
|
||||
:model="model.miss.adjustments"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['miss', 'adjustments', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['miss', 'adjustments', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['miss', 'adjustments', ...path], ack})"
|
||||
/>
|
||||
</form-section>
|
||||
<form-section name="Buffs on Miss">
|
||||
<div class="caption">
|
||||
Buffs apply temporary effects to characters when the roll fails.
|
||||
</div>
|
||||
<buff-list-form
|
||||
:model="model.miss.buffs"
|
||||
:stored="stored"
|
||||
@change="({path, value, ack}) => $emit('change', {path: ['miss', 'buffs', ...path], value, ack})"
|
||||
@push="({path, value, ack}) => $emit('push', {path: ['miss', 'buffs', ...path], value, ack})"
|
||||
@pull="({path, ack}) => $emit('pull', {path: ['miss', 'buffs', ...path], ack})"
|
||||
/>
|
||||
</form-section>
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormSection, {FormSections} from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import AdjustmentListForm from '/imports/ui/properties/forms/AdjustmentListForm.vue';
|
||||
import BuffListForm from '/imports/ui/properties/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(){return {
|
||||
rollTypes: [
|
||||
{
|
||||
text: 'Roll',
|
||||
value: 'roll',
|
||||
}, {
|
||||
text: 'Saving Throw',
|
||||
value: 'savingThrow',
|
||||
},
|
||||
],
|
||||
};},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.no-flex {
|
||||
flex: initial;
|
||||
}
|
||||
.layout.row.wrap {
|
||||
margin-right: -8px;
|
||||
}
|
||||
.layout.row.wrap > *{
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
114
app/imports/ui/properties/forms/SkillForm.vue
Normal file
114
app/imports/ui/properties/forms/SkillForm.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template lang="html">
|
||||
<div class="skill-form">
|
||||
<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 skill"
|
||||
:error-messages="errors.variableName"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Ability"
|
||||
:value="model.ability"
|
||||
style="flex-basis: 300px;"
|
||||
@change="(value, ack) => $emit('change', {path: ['ability'], value, ack})"
|
||||
hint="Which ability is this skill based off of"
|
||||
:error-messages="errors.ability"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<smart-select
|
||||
label="Type"
|
||||
:items="skillTypes"
|
||||
:value="model.type"
|
||||
:error-messages="errors.type"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', {path: ['type'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<form-section name="Advanced" standalone>
|
||||
<div class="layout row justify-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 skill before effects are applied"
|
||||
:error-messages="errors.baseValue"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<proficiency-select
|
||||
style="flex-basis: 300px;"
|
||||
label="Base Proficiency"
|
||||
clearable
|
||||
:value="model.baseProficiency"
|
||||
:error-messages="errors.baseProficiency"
|
||||
@change="(value, ack) => $emit('change', {path: ['baseProficiency'], value, ack})"
|
||||
/>
|
||||
</div>
|
||||
</form-section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProficiencySelect from '/imports/ui/properties/forms/shared/ProficiencySelect.vue';
|
||||
import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProficiencySelect,
|
||||
FormSection,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
data(){return{
|
||||
skillTypes: [
|
||||
{
|
||||
text: 'Skill',
|
||||
value: 'skill',
|
||||
}, {
|
||||
text: 'Save',
|
||||
value: 'save',
|
||||
}, {
|
||||
text: 'Check',
|
||||
value: 'check',
|
||||
}, {
|
||||
text: 'Tool',
|
||||
value: 'tool',
|
||||
}, {
|
||||
text: 'Weapon',
|
||||
value: 'weapon',
|
||||
}, {
|
||||
text: 'Language',
|
||||
value: 'language',
|
||||
}, {
|
||||
text: 'Utility',
|
||||
value: 'utility',
|
||||
},
|
||||
]
|
||||
};},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
199
app/imports/ui/properties/forms/SpellForm.vue
Normal file
199
app/imports/ui/properties/forms/SpellForm.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-form">
|
||||
<text-field
|
||||
label="Name"
|
||||
:value="model.name"
|
||||
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
|
||||
:error-messages="errors.name"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div class="layout row wrap">
|
||||
<smart-select
|
||||
label="Level"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
:items="spellLevels"
|
||||
:value="model.level"
|
||||
:error-messages="errors.level"
|
||||
@change="(value, ack) => $emit('change', {path: ['level'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<smart-select
|
||||
label="School"
|
||||
class="mx-1"
|
||||
style="flex-basis: 300px;"
|
||||
:items="magicSchools"
|
||||
:value="model.school"
|
||||
:error-messages="errors.school"
|
||||
@change="(value, ack) => $emit('change', {path: ['school'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
<div class="layout row wrap">
|
||||
<v-combobox
|
||||
label="Spell lists"
|
||||
multiple
|
||||
chips
|
||||
deletable-chips
|
||||
box
|
||||
:value="model.spellLists"
|
||||
@change="(value) => $emit('change', {path: ['spellLists'], value})"
|
||||
:error-messages="errors.spellLists"
|
||||
/>
|
||||
<v-switch
|
||||
label="Always prepared"
|
||||
style="width: 200px; flex-grow: 0;"
|
||||
class="ml-2"
|
||||
:input-value="model.alwaysPrepared"
|
||||
:error-messages="errors.alwaysPrepared"
|
||||
@change="e => $emit('change', {path: ['alwaysPrepared'], value: !!e})"
|
||||
/>
|
||||
</div>
|
||||
<text-field
|
||||
label="Casting Time"
|
||||
:value="model.castingTime"
|
||||
@change="(value, ack) => $emit('change', {path: ['castingTime'], value, ack})"
|
||||
:error-messages="errors.castingTime"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Range"
|
||||
:value="model.range"
|
||||
@change="(value, ack) => $emit('change', {path: ['range'], value, ack})"
|
||||
:error-messages="errors.range"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<div class="layout row wrap justify-space-between">
|
||||
<v-checkbox
|
||||
label="Verbal"
|
||||
:value="model.verbal"
|
||||
:error-messages="errors.verbal"
|
||||
@change="(value) => $emit('change', {path: ['verbal'], value})"
|
||||
/>
|
||||
<v-checkbox
|
||||
label="Somatic"
|
||||
:value="model.somatic"
|
||||
:error-messages="errors.somatic"
|
||||
@change="(value) => $emit('change', {path: ['somatic'], value})"
|
||||
/>
|
||||
<v-checkbox
|
||||
label="Concentration"
|
||||
:value="model.concentration"
|
||||
:error-messages="errors.concentration"
|
||||
@change="(value) => $emit('change', {path: ['concentration'], value})"
|
||||
/>
|
||||
<v-checkbox
|
||||
label="Ritual"
|
||||
:value="model.ritual"
|
||||
:error-messages="errors.ritual"
|
||||
@change="(value) => $emit('change', {path: ['ritual'], value})"
|
||||
/>
|
||||
</div>
|
||||
<text-field
|
||||
label="Material"
|
||||
:value="model.material"
|
||||
@change="(value, ack) => $emit('change', {path: ['material'], value, ack})"
|
||||
:error-messages="errors.material"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Duration"
|
||||
:value="model.duration"
|
||||
@change="(value, ack) => $emit('change', {path: ['duration'], value, ack})"
|
||||
:error-messages="errors.duration"
|
||||
: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 {
|
||||
magicSchools: [
|
||||
{
|
||||
text: 'Abjuration',
|
||||
value: 'abjuration',
|
||||
}, {
|
||||
text: 'Conjuration',
|
||||
value: 'conjuration',
|
||||
}, {
|
||||
text: 'Divination',
|
||||
value: 'divination',
|
||||
}, {
|
||||
text: 'Enchantment',
|
||||
value: 'enchantment',
|
||||
}, {
|
||||
text: 'Evocation',
|
||||
value: 'evocation',
|
||||
}, {
|
||||
text: 'Illusion',
|
||||
value: 'illusion',
|
||||
}, {
|
||||
text: 'Necromancy',
|
||||
value: 'necromancy',
|
||||
}, {
|
||||
text: 'Transmutation',
|
||||
value: 'transmutation',
|
||||
},
|
||||
],
|
||||
spellLevels: [
|
||||
{
|
||||
text: 'Cantrip',
|
||||
value: 0,
|
||||
}, {
|
||||
text: 'Level 1',
|
||||
value: 1,
|
||||
}, {
|
||||
text: 'Level 2',
|
||||
value: 2,
|
||||
}, {
|
||||
text: 'Level 3',
|
||||
value: 3,
|
||||
}, {
|
||||
text: 'Level 4',
|
||||
value: 4,
|
||||
}, {
|
||||
text: 'Level 5',
|
||||
value: 5,
|
||||
}, {
|
||||
text: 'Level 6',
|
||||
value: 6,
|
||||
}, {
|
||||
text: 'Level 7',
|
||||
value: 7,
|
||||
}, {
|
||||
text: 'Level 8',
|
||||
value: 8,
|
||||
}, {
|
||||
text: 'Level 9',
|
||||
value: 9,
|
||||
},
|
||||
],
|
||||
};},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.v-input--checkbox {
|
||||
flex-grow: 0;
|
||||
width: 200px;
|
||||
}
|
||||
</style>
|
||||
56
app/imports/ui/properties/forms/SpellListForm.vue
Normal file
56
app/imports/ui/properties/forms/SpellListForm.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template lang="html">
|
||||
<div class="attribute-form">
|
||||
<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>
|
||||
<text-area
|
||||
label="Description"
|
||||
:value="model.description"
|
||||
:error-messages="errors.description"
|
||||
@change="(value, ack) => $emit('change', {path: ['description'], value, ack})"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
<text-field
|
||||
label="Maximum prepared spells"
|
||||
:value="model.maxPrepared"
|
||||
@change="(value, ack) => $emit('change', {path: ['maxPrepared'], value, ack})"
|
||||
hint="How many spells can be prepared"
|
||||
:error-messages="errors.maxPrepared"
|
||||
:debounce-time="debounceTime"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
debounceTime: Number,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
34
app/imports/ui/properties/forms/shared/FormSection.vue
Normal file
34
app/imports/ui/properties/forms/shared/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/properties/forms/shared/FormSections.vue';
|
||||
export default {
|
||||
components: {
|
||||
FormSections,
|
||||
},
|
||||
props: {
|
||||
name: String,
|
||||
standalone: Boolean,
|
||||
},
|
||||
}
|
||||
export { FormSections };
|
||||
</script>
|
||||
9
app/imports/ui/properties/forms/shared/FormSections.vue
Normal file
9
app/imports/ui/properties/forms/shared/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>
|
||||
92
app/imports/ui/properties/forms/shared/ProficiencySelect.vue
Normal file
92
app/imports/ui/properties/forms/shared/ProficiencySelect.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template lang="html">
|
||||
<smart-select
|
||||
append-icon="arrow_drop_down"
|
||||
class="ml-3"
|
||||
v-bind="$attrs"
|
||||
:menu-props="{transition: 'slide-y-transition', lazy: true}"
|
||||
:items="values"
|
||||
:value="value"
|
||||
@change="(value, ack) => $emit('change', value, ack)"
|
||||
>
|
||||
<v-icon
|
||||
class="icon"
|
||||
slot="prepend"
|
||||
:class="iconClass"
|
||||
>{{displayedIcon}}</v-icon>
|
||||
</smart-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ICON_SPIN_DURATION = 300;
|
||||
let proficiencyIcon = function(value){
|
||||
if (value == 0.5){
|
||||
return 'brightness_2';
|
||||
} else if (value == 1) {
|
||||
return 'brightness_1'
|
||||
} else if (value == 2){
|
||||
return 'album'
|
||||
} else {
|
||||
return 'radio_button_unchecked';
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: Number,
|
||||
},
|
||||
data(){ return {
|
||||
displayedIcon: 'radio_button_unchecked',
|
||||
iconClass: '',
|
||||
values: [
|
||||
{value: 1, text: 'Proficient'},
|
||||
{value: 0.5, text: 'Half proficiency bonus'},
|
||||
{value: 2, text: 'Double proficiency bonus'},
|
||||
],
|
||||
}},
|
||||
watch: {
|
||||
'value': {
|
||||
immediate: true,
|
||||
handler(newValue, oldValue, e){
|
||||
let newIcon = proficiencyIcon(newValue);
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
37
app/imports/ui/properties/forms/shared/propertyFormIndex.js
Normal file
37
app/imports/ui/properties/forms/shared/propertyFormIndex.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import ActionForm from '/imports/ui/properties/forms/ActionForm.vue';
|
||||
import AttributeForm from '/imports/ui/properties/forms/AttributeForm.vue';
|
||||
import BuffForm from '/imports/ui/properties/forms/BuffForm.vue';
|
||||
import ContainerForm from '/imports/ui/properties/forms/ContainerForm.vue';
|
||||
import ClassLevelForm from '/imports/ui/properties/forms/ClassLevelForm.vue';
|
||||
import DamageMultiplierForm from '/imports/ui/properties/forms/DamageMultiplierForm.vue';
|
||||
import EffectForm from '/imports/ui/properties/forms/EffectForm.vue';
|
||||
import ExperienceForm from '/imports/ui/properties/forms/ExperienceForm.vue';
|
||||
import FeatureForm from '/imports/ui/properties/forms/FeatureForm.vue';
|
||||
import FolderForm from '/imports/ui/properties/forms/FolderForm.vue';
|
||||
import ItemForm from '/imports/ui/properties/forms/ItemForm.vue';
|
||||
import NoteForm from '/imports/ui/properties/forms/NoteForm.vue';
|
||||
import ProficiencyForm from '/imports/ui/properties/forms/ProficiencyForm.vue';
|
||||
import RollForm from '/imports/ui/properties/forms/RollForm.vue';
|
||||
import SkillForm from '/imports/ui/properties/forms/SkillForm.vue';
|
||||
import SpellListForm from '/imports/ui/properties/forms/SpellListForm.vue';
|
||||
import SpellForm from '/imports/ui/properties/forms/SpellForm.vue';
|
||||
|
||||
export default {
|
||||
action: ActionForm,
|
||||
attribute: AttributeForm,
|
||||
buff: BuffForm,
|
||||
container: ContainerForm,
|
||||
classLevel: ClassLevelForm,
|
||||
damageMultiplier: DamageMultiplierForm,
|
||||
experience:ExperienceForm,
|
||||
effect: EffectForm,
|
||||
feature: FeatureForm,
|
||||
folder: FolderForm,
|
||||
item: ItemForm,
|
||||
note: NoteForm,
|
||||
proficiency: ProficiencyForm,
|
||||
roll: RollForm,
|
||||
skill: SkillForm,
|
||||
spellList: SpellListForm,
|
||||
spell: SpellForm,
|
||||
};
|
||||
59
app/imports/ui/properties/forms/shared/schemaFormMixin.js
Normal file
59
app/imports/ui/properties/forms/shared/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