320 lines
8.4 KiB
Vue
320 lines
8.4 KiB
Vue
<template lang="html">
|
|
<dialog-base>
|
|
<template slot="toolbar">
|
|
<v-toolbar-title>
|
|
Cast a Spell
|
|
</v-toolbar-title>
|
|
<v-spacer />
|
|
<text-field
|
|
ref="focusFirst"
|
|
label="Name"
|
|
prepend-inner-icon="mdi-search"
|
|
regular
|
|
hide-details
|
|
:value="searchValue"
|
|
:error-messages="searchError"
|
|
:debounce="200"
|
|
@change="searchChanged"
|
|
/>
|
|
<v-menu
|
|
v-model="filterMenuOpen"
|
|
left
|
|
:close-on-content-click="false"
|
|
>
|
|
<template #activator="{ on }">
|
|
<v-btn
|
|
icon
|
|
:class="{'primary--text': filtersApplied}"
|
|
v-on="on"
|
|
>
|
|
<v-icon>mdi-filter</v-icon>
|
|
</v-btn>
|
|
</template>
|
|
<v-list>
|
|
<v-list-item
|
|
v-for="filter in booleanFilters"
|
|
:key="filter.name"
|
|
style="height: 52px;"
|
|
>
|
|
<v-checkbox
|
|
v-model="filter.enabled"
|
|
style="flex-grow: 0; margin-right: 8px;"
|
|
/>
|
|
<v-switch
|
|
v-model="filter.value"
|
|
:disabled="!filter.enabled"
|
|
:label="filter.name"
|
|
/>
|
|
</v-list-item>
|
|
<div class="layout">
|
|
<v-btn
|
|
text
|
|
@click="clearBooleanFilters"
|
|
>
|
|
Clear
|
|
</v-btn>
|
|
<v-spacer />
|
|
<v-btn
|
|
text
|
|
class="primary--text"
|
|
@click="filterMenuOpen = false"
|
|
>
|
|
Done
|
|
</v-btn>
|
|
</div>
|
|
</v-list>
|
|
</v-menu>
|
|
</template>
|
|
<split-list-layout>
|
|
<template slot="left">
|
|
<div
|
|
key="slot-title"
|
|
class="text-h6 my-3"
|
|
>
|
|
Slot
|
|
</div>
|
|
<v-list-item
|
|
v-if="!(selectedSpell && selectedSpell.level > 0)"
|
|
key="cantrip-dummy-slot"
|
|
class="spell-slot-list-tile"
|
|
:class="{ 'primary--text': selectedSlotId === undefined}"
|
|
@click="selectedSlotId = undefined"
|
|
>
|
|
<v-list-item-content>
|
|
<v-list-item-title class="text-h6">
|
|
Cantrip
|
|
</v-list-item-title>
|
|
</v-list-item-content>
|
|
</v-list-item>
|
|
<spell-slot-list-tile
|
|
v-for="spellSlot in spellSlots"
|
|
:key="spellSlot._id"
|
|
:model="spellSlot"
|
|
:class="{ 'primary--text': selectedSlotId === spellSlot._id }"
|
|
hide-cast-button
|
|
@click="selectedSlotId = spellSlot._id"
|
|
/>
|
|
</template>
|
|
<template slot="right">
|
|
<div
|
|
key="spell-title"
|
|
class="text-h6 my-3"
|
|
>
|
|
Spell
|
|
</div>
|
|
<template v-for="spell in computedSpells">
|
|
<v-subheader
|
|
v-if="spell.isSubheader"
|
|
:key="`${spell.level}-header`"
|
|
class="item"
|
|
>
|
|
{{ spell.level === 0 ? 'Cantrips' : `Level ${spell.level}` }}
|
|
</v-subheader>
|
|
<spell-list-tile
|
|
v-else
|
|
:key="spell._id"
|
|
hide-handle
|
|
show-info-button
|
|
:class="{ 'primary--text': selectedSpellId === spell._id}"
|
|
:model="spell"
|
|
@click="selectedSpellId = spell._id"
|
|
@show-info="spellDialog(spell._id)"
|
|
/>
|
|
</template>
|
|
</template>
|
|
</split-list-layout>
|
|
<template slot="actions">
|
|
<v-spacer />
|
|
<v-btn
|
|
text
|
|
@click="$store.dispatch('popDialogStack')"
|
|
>
|
|
Cancel
|
|
</v-btn>
|
|
<v-btn
|
|
text
|
|
:disabled="!canCast"
|
|
class="primary--text"
|
|
@click="$store.dispatch('popDialogStack', {
|
|
spellId: selectedSpellId,
|
|
slotId: selectedSlotId,
|
|
})"
|
|
>
|
|
Cast
|
|
</v-btn>
|
|
</template>
|
|
</dialog-base>
|
|
</template>
|
|
|
|
<script lang="js">
|
|
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
|
import SplitListLayout from '/imports/ui/properties/components/attributes/SplitListLayout.vue';
|
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
|
import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.js';
|
|
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
|
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
|
|
|
export default {
|
|
components: {
|
|
DialogBase,
|
|
SplitListLayout,
|
|
SpellSlotListTile,
|
|
SpellListTile,
|
|
},
|
|
props: {
|
|
creatureId: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
slotId: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
spellId: {
|
|
type: String,
|
|
default: undefined,
|
|
},
|
|
},
|
|
data(){ return {
|
|
searchString: undefined,
|
|
selectedSlotId: this.slotId,
|
|
selectedSpellId: this.spellId,
|
|
searchValue: undefined,
|
|
searchError: undefined,
|
|
filterMenuOpen: false,
|
|
booleanFilters: {
|
|
verbal: {name: 'Verbal', enabled: false, value: false},
|
|
somatic: {name: 'Somatic', enabled: false, value: false},
|
|
material: {name: 'Material', enabled: false, value: false},
|
|
concentration: {name: 'Concentration', enabled: false, value: false},
|
|
ritual: {name: 'Ritual', enabled: false, value: false},
|
|
},
|
|
}},
|
|
computed: {
|
|
computedSpells(){
|
|
return spellsWithSubheaders(this.spells);
|
|
},
|
|
canCast(){
|
|
let spell = this.selectedSpell;
|
|
let slot = this.selectedSlot;
|
|
if (!spell) return false;
|
|
if (spell.level === 0){
|
|
return this.selectedSlotId === undefined;
|
|
} else if (!slot) {
|
|
return false
|
|
} else {
|
|
return slot.spellSlotLevelValue >= spell.level;
|
|
}
|
|
},
|
|
filtersApplied(){
|
|
for (let key in this.booleanFilters){
|
|
if (this.booleanFilters[key].enabled){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
},
|
|
watch: {
|
|
selectedSpell(spell){
|
|
if (!spell) return;
|
|
if(spell.level === 0){
|
|
this.selectedSlotId = undefined;
|
|
}
|
|
},
|
|
selectedSlot(slot){
|
|
if (!slot) return;
|
|
if (!this.selectedSpell) return;
|
|
if(slot.spellSlotLevelValue > 0 && this.selectedSpell.level === 0){
|
|
this.selectedSpellId = undefined;
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
clearBooleanFilters(){
|
|
for (let key in this.booleanFilters){
|
|
this.booleanFilters[key].enabled = false;
|
|
}
|
|
},
|
|
spellDialog(_id){
|
|
this.$store.commit('pushDialogStack', {
|
|
component: 'creature-property-dialog',
|
|
elementId: `spell-info-btn-${_id}`,
|
|
data: {_id},
|
|
});
|
|
},
|
|
searchChanged(val, ack){
|
|
this.searchValue = val;
|
|
setTimeout(ack, 200);
|
|
},
|
|
},
|
|
meteor: {
|
|
spells(){
|
|
let slotLevel = this.selectedSlot && this.selectedSlot.spellSlotLevelValue || 0;
|
|
let filter = {
|
|
'ancestors.id': this.creatureId,
|
|
removed: {$ne: true},
|
|
inactive: {$ne: true},
|
|
$or: [
|
|
{prepared: true},
|
|
{alwaysPrepared: true},
|
|
],
|
|
level: {$lte: slotLevel},
|
|
};
|
|
// Apply the filters from the filter menu
|
|
for (let key in this.booleanFilters){
|
|
if (this.booleanFilters[key].enabled){
|
|
let value = this.booleanFilters[key].value;
|
|
if (key === 'material'){
|
|
filter[key] = {$exists: this.booleanFilters[key].value};
|
|
} else {
|
|
filter[key] = value ? true: {$ne: true};
|
|
}
|
|
}
|
|
}
|
|
// Apply the search string to the name field
|
|
if (this.searchValue){
|
|
filter.name = {
|
|
$regex: this.searchValue.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
|
$options: 'i'
|
|
};
|
|
}
|
|
return CreatureProperties.find(filter, {
|
|
sort: {order: 1}
|
|
});
|
|
},
|
|
spellSlots(){
|
|
let filter = {
|
|
'ancestors.id': this.creatureId,
|
|
type: 'attribute',
|
|
attributeType: 'spellSlot',
|
|
removed: {$ne: true},
|
|
inactive: {$ne: true},
|
|
currentValue: {$gte: 1},
|
|
};
|
|
if (this.selectedSpell){
|
|
filter.spellSlotLevelValue = {$gte: this.selectedSpell.level};
|
|
}
|
|
return CreatureProperties.find(filter, {
|
|
sort: {order: 1},
|
|
});
|
|
},
|
|
selectedSlot(){
|
|
return CreatureProperties.findOne(this.selectedSlotId);
|
|
},
|
|
selectedSpell(){
|
|
return CreatureProperties.findOne(this.selectedSpellId);
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="css" scoped>
|
|
.v-list {
|
|
flex-basis: 200px;
|
|
}
|
|
.v-list.spells {
|
|
flex-grow: 1;
|
|
}
|
|
</style>
|