Refactored actions, 'cast a spell' task now works

This commit is contained in:
Thaum Rystra
2024-10-28 12:28:36 +02:00
parent 804c5f3aee
commit 8f8c9c28aa
39 changed files with 423 additions and 399 deletions

View File

@@ -11,6 +11,7 @@
outlined
style="font-size: 16px; letter-spacing: normal;"
class="mr-2"
data-id="do-action-button"
:color="model.color || 'primary'"
:loading="doActionLoading"
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
@@ -224,7 +225,12 @@ export default {
},
doAction() {
this.doActionLoading = true;
doAction(this.model, this.$store, this.model._id).catch((e) => {
doAction({
propId: this.model._id,
creatureId: this.model.root.id,
$store: this.$store,
elementId: 'do-action-button',
}).catch((e) => {
console.error(e);
}).finally(() => {
this.doActionLoading = false;

View File

@@ -23,6 +23,7 @@
<script lang="js">
import doAction from '/imports/client/ui/creature/actions/doAction';
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
export default {
components: {
@@ -39,10 +40,22 @@ export default {
},
data(){return {
hovering: false,
loading: false,
}},
methods: {
doAction() {
doAction(this.model, this.$store, `event-btn-${this.model._id}`);
async doAction() {
this.loading = true;
doAction({
propId: this.model._id,
creatureId: this.model.root.id,
$store: this.$store,
elementId: `event-btn-${this.model._id}`,
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}).finally(() => {
this.loading = false;
});
},
}
}

View File

@@ -92,14 +92,19 @@ export default {
},
check() {
this.checkLoading = true;
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: undefined,
abilityVariableName: this.model.variableName,
dc: null,
doAction({
creatureId: this.model.root.id,
$store: this.$store,
elementId: `check-btn-${this.model._id}`,
task: {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: undefined,
abilityVariableName: this.model.variableName,
dc: null,
},
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);

View File

@@ -77,14 +77,19 @@ export default {
signed: numberToSignedString,
check(){
this.checkLoading = true;
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
doAction({
creatureId: this.model.root.id,
$store: this.$store,
elementId: `check-btn-${this.model._id}`,
task: {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
},
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);

View File

@@ -22,6 +22,7 @@
color="accent"
style="width: 100%;"
outlined
data-id="cast-spell-btn"
@click="castSpell"
>
Cast a spell
@@ -32,8 +33,8 @@
<script lang="js">
import SpellSlotListTile from '/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue';
import doAction from '/imports/client/ui/creature/actions/doAction';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import doCastSpell from '/imports/api/engine/action/methods/doCastSpell';
export default {
components: {
@@ -50,38 +51,31 @@ export default {
default: () => [],
},
},
data(){return {
castSpellLoading: false,
}},
methods: {
castSpell() {
this.$store.commit('pushDialogStack', {
component: 'cast-spell-with-slot-dialog',
elementId: 'spell-slot-card',
data: {
creatureId: this.creatureId,
},
callback({ spellId, slotId, advantage, ritual } = {}) {
if (!spellId) return;
doCastSpell.call({
spellId,
slotId,
ritual,
scope: {
'~attackAdvantage': { value: advantage },
},
}, error => {
if (!error) return;
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
});
},
this.castSpellLoading = true;
doAction({
creatureId: this.model.root.id,
propId: this.model._id,
$store: this.$store,
elementId: `spell-slot-card-${this.model._id}`
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}).finally(() => {
this.castSpellLoading = false;
});
},
clickProperty({ _id }) {
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `spell-slot-card-${_id}`,
elementId: 'cast-spell-btn',
data: { _id },
});
},
}
}
</script>../../../../../api/engine/action/methods/doCastSpell
</script>

View File

@@ -1,6 +1,7 @@
<template lang="html">
<v-list-item
:key="model._id"
:data-id="`spell-slot-list-tile-${model._id}`"
class="spell-slot-list-tile"
v-bind="$attrs"
v-on="hasClickListener ? {click} : {}"
@@ -91,16 +92,15 @@ export default {
this.$emit('click', e);
},
disabled(i) {
if (!this.context.editPermission) return true;
// Use these if only the next filled or empty slot can be clicked
// if (this.model.value === i) return false;
// if (this.model.value === i - 1) return false;
// return true
return false;
return !this.context.editPermission;
},
damageProperty({ type, value, ack }) {
const model = this.model;
doAction(model, this.$store, model._id, {
doAction({
creatureId: model.root.id,
$store: this.$store,
elementId: `spell-slot-list-tile-${model._id}`,
task: {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
@@ -110,7 +110,7 @@ export default {
value,
targetProp: model,
}
}).then(() =>{
}}).then(() =>{
ack?.();
}).catch((error) => {
if (ack) {

View File

@@ -95,17 +95,22 @@ export default {
damageProperty({value, type, ack}) {
const model = this.model;
if (type === 'increment') value = -value;
doAction(model, this.$store, model._id, {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: type,
value,
targetProp: model,
doAction({
creatureId: model.root.id,
$store: this.$store,
elementId: this.dataId,
task: {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: type,
value,
targetProp: model,
}
}
}).then(() =>{
}).then(() => {
ack?.();
}).catch((error) => {
if (ack) {
@@ -127,4 +132,4 @@ export default {
.pointer {
cursor: pointer;
}
</style>
</style>

View File

@@ -104,14 +104,19 @@ export default {
},
check() {
this.checkLoading = true;
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
doAction({
creatureId: this.model.root.id,
$store: this.$store,
elementId: `check-btn-${this.model._id}`,
task: {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
},
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
@@ -135,4 +140,4 @@ export default {
.v-icon.theme--light {
color: rgba(0, 0, 0, 0.54) !important;
}
</style>
</style>

View File

@@ -1,414 +0,0 @@
<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-magnify"
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-group
key="slot-list"
v-model="selectedSlotId"
>
<v-list-item
key="cantrip-dummy-slot"
class="spell-slot-list-tile"
:class="{ 'primary--text': selectedSlotId === 'no-slot' }"
value="no-slot"
:disabled="!canCastSpellWithSlot(selectedSpell, 'no-slot')"
@click="selectedSlotId = 'no-slot'"
>
<v-list-item-content>
<v-list-item-title>
Cast without spell slot
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
key="ritual-dummy-slot"
class="spell-slot-list-tile"
:class="{ 'primary--text': selectedSlotId === 'ritual' }"
value="ritual"
:disabled="!canCastSpellWithSlot(selectedSpell, 'ritual')"
@click="selectedSlotId = 'ritual'"
>
<v-list-item-content>
<v-list-item-title>
Cast as ritual
</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 }"
:value="spellSlot._id"
:disabled="!canCastSpellWithSlot(selectedSpell, spellSlot._id, spellSlot)"
hide-cast-button
@click="selectedSlotId = spellSlot._id"
/>
</v-list-item-group>
</template>
<template slot="right">
<div
key="spell-title-right"
class="text-h6 my-3"
>
Spell
</div>
<v-list-item-group
key="slot-list-right"
v-model="selectedSpellId"
>
<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
:model="spell"
:value="spell._id"
:class="{ 'primary--text': selectedSpellId === spell._id }"
:disabled="!canCastSpellWithSlot(spell, selectedSlotId, selectedSlot)"
@show-info="spellDialog(spell._id)"
/>
</template>
</v-list-item-group>
</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="mx-2 px-4"
color="primary"
@click="cast"
>
Cast
</v-btn>
</template>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
import SplitListLayout from '/imports/client/ui/properties/components/attributes/SplitListLayout.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import spellsWithSubheaders from '/imports/client/ui/properties/components/spells/spellsWithSubheaders';
import SpellSlotListTile from '/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue';
import SpellListTile from '/imports/client/ui/properties/components/spells/SpellListTile.vue';
import { find } from 'lodash';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
const slotFilter = {
type: 'attribute',
attributeType: 'spellSlot',
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
'spellSlotLevel.value': { $gte: 1 },
};
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,
selectedSlot: undefined,
selectedSpell: undefined,
searchValue: undefined,
searchError: undefined,
filterMenuOpen: false,
booleanFilters: {
verbal: { name: 'Verbal', enabled: false, value: true },
somatic: { name: 'Somatic', enabled: false, value: true },
material: { name: 'Material', enabled: false, value: true },
concentration: { name: 'Concentration', enabled: false, value: true },
ritual: { name: 'Ritual', enabled: false, value: true },
},
}
},
computed: {
computedSpells() {
return spellsWithSubheaders(this.spells);
},
canCast() {
if (!this.selectedSpell || !this.selectedSlotId) return false;
return this.canCastSpellWithSlot(
this.selectedSpell, this.selectedSlotId, this.selectedSlot
);
},
filtersApplied() {
for (let key in this.booleanFilters) {
if (this.booleanFilters[key].enabled) {
return true;
}
}
return false;
},
},
watch: {
selectedSpellId: {
handler(spellId) {
this.selectedSpell = CreatureProperties.findOne(spellId)
},
immediate: true
},
selectedSpell: {
handler(spell) {
if (!spell) return;
if (this.selectedSlotId && this.canCastSpellWithSlot(
spell, this.selectedSlotId, this.selectedSlot
)) return;
if (
(spell.level === 0 || spell.castWithoutSpellSlots)
) {
this.selectedSlotId = 'no-slot';
} else {
const newSlot = find(
CreatureProperties.find({
'ancestors.id': this.creatureId,
...slotFilter
}, {
sort: { 'spellSlotLevel.value': 1, order: 1 },
}).fetch(),
slot => {
return this.canCastSpellWithSlot(spell, slot._id, slot)
}
);
if (newSlot) {
this.selectedSlotId = newSlot._id;
} else if (spell.ritual) {
this.selectedSlotId = 'ritual';
}
}
},
immediate: true,
},
selectedSlotId: {
handler(slotId) {
this.selectedSlot = CreatureProperties.findOne(slotId);
},
immediate: true
},
selectedSlot: {
handler(slot) {
if (!slot) return;
if (!this.selectedSpell) return;
if (this.selectedSpell.level > slot.spellSlotLevel.value) {
this.selectedSpellId = undefined;
}
},
immediate: true,
},
},
mounted() {
if (this.selectedSpellId) {
this.$vuetify.goTo('.spell.v-list-item--active', { container: '.right' });
}
},
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);
},
canCastSpellWithSlot(spell, slotId, slot) {
if (slot && !slot.value) return false;
if (!spell) return true;
if (!slotId) return true;
if (
spell.castWithoutSpellSlots &&
spell.insufficientResources
) return false;
if (spell.ritual && slotId === 'ritual') return true;
if (!spell.level || spell.castWithoutSpellSlots) {
// Cantrips and no-slot spells
return slotId && slotId === 'no-slot'
} else {
// Levelled spells
return slotId !== 'no-slot' && slot && spell && (
spell.level <= slot.spellSlotLevel.value
);
}
},
cast() {
let selectedSlotId = this.selectedSlotId;
const ritual = selectedSlotId === 'ritual';
if (selectedSlotId === 'no-slot' || selectedSlotId === 'ritual') selectedSlotId = undefined;
this.$store.dispatch('popDialogStack', {
spellId: this.selectedSpellId,
slotId: selectedSlotId,
ritual,
});
}
},
meteor: {
spells() {
let filter = {
...getFilter.descendantsOfRoot(this.creatureId),
removed: { $ne: true },
inactive: { $ne: true },
$or: [
{ prepared: true },
{ alwaysPrepared: true },
],
};
// 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: { left: 1 }
});
},
spellSlots() {
return CreatureProperties.find({
'ancestors.id': this.creatureId,
...slotFilter
}, {
sort: { 'spellSlotLevel.value': 1, order: 1 },
});
},
},
}
</script>
<style lang="css" scoped>
.v-list {
flex-basis: 200px;
}
.v-list.spells {
flex-grow: 1;
}
</style>