Moved ui/forms to ui/properties/forms

This commit is contained in:
Stefan Zermatten
2019-08-01 12:07:57 +02:00
parent 9f7d6b8ae7
commit c87a3a3f60
34 changed files with 66 additions and 66 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,9 @@
<template lang="html">
<v-expansion-panel popout expand>
<slot/>
</v-expansion-panel>
</template>
<script>
export default {}
</script>

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

View 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,
};

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