Overhauled spell casting UX
This commit is contained in:
@@ -87,7 +87,7 @@ const doAction = new ValidatedMethod({
|
|||||||
export default doAction;
|
export default doAction;
|
||||||
|
|
||||||
export function doActionWork({
|
export function doActionWork({
|
||||||
creature, targets, properties, ancestors, method, methodScope = {}
|
creature, targets, properties, ancestors, method, methodScope = {}, log
|
||||||
}){
|
}){
|
||||||
// get the docs
|
// get the docs
|
||||||
const ancestorScope = getAncestorScope(ancestors);
|
const ancestorScope = getAncestorScope(ancestors);
|
||||||
@@ -97,7 +97,7 @@ export function doActionWork({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the log
|
// Create the log
|
||||||
let log = CreatureLogSchema.clean({
|
if (!log) log = CreatureLogSchema.clean({
|
||||||
creatureId: creature._id,
|
creatureId: creature._id,
|
||||||
creatureName: creature.name,
|
creatureName: creature.name,
|
||||||
});
|
});
|
||||||
|
|||||||
142
app/imports/api/engine/actions/doCastSpell.js
Normal file
142
app/imports/api/engine/actions/doCastSpell.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
|
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
|
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
|
||||||
|
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||||
|
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
|
|
||||||
|
const doAction = new ValidatedMethod({
|
||||||
|
name: 'creatureProperties.doCastSpell',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
spellId: SimpleSchema.RegEx.Id,
|
||||||
|
slotId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
targetIds: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: 20,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'targetIds.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 10,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({spellId, slotId, targetIds = [], scope = {}}) {
|
||||||
|
let spell = CreatureProperties.findOne(spellId);
|
||||||
|
// Check permissions
|
||||||
|
let creature = getRootCreatureAncestor(spell);
|
||||||
|
|
||||||
|
assertEditPermission(creature, this.userId);
|
||||||
|
|
||||||
|
// Get all the targets and make sure we can edit them
|
||||||
|
let targets = [];
|
||||||
|
targetIds.forEach(targetId => {
|
||||||
|
let target = Creatures.findOne(targetId);
|
||||||
|
assertEditPermission(target, this.userId);
|
||||||
|
targets.push(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all the action's ancestor creatureProperties
|
||||||
|
const ancestorIds = [];
|
||||||
|
spell.ancestors.forEach(ref => {
|
||||||
|
if (ref.collection === 'creatureProperties') {
|
||||||
|
ancestorIds.push(ref.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get cursor of ancestors
|
||||||
|
const ancestors = CreatureProperties.find({
|
||||||
|
_id: {$in: ancestorIds},
|
||||||
|
}, {
|
||||||
|
sort: {order: 1},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get cursor of the properties
|
||||||
|
const properties = CreatureProperties.find({
|
||||||
|
$or: [{_id: spell._id}, {'ancestors.id': spell._id}],
|
||||||
|
removed: {$ne: true},
|
||||||
|
}, {
|
||||||
|
sort: {order: 1},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spend the appropriate slot
|
||||||
|
let slotLevel = spell.level || 0;
|
||||||
|
let slot;
|
||||||
|
if (slotId && !spell.castWithoutSpellSlots){
|
||||||
|
slot = CreatureProperties.findOne(slotId);
|
||||||
|
if (!slot){
|
||||||
|
throw new Meteor.Error('No slot',
|
||||||
|
'Slot not found to cast spell');
|
||||||
|
}
|
||||||
|
if (!slot.value){
|
||||||
|
throw new Meteor.Error('No slot',
|
||||||
|
'Slot depleted');
|
||||||
|
}
|
||||||
|
if (slot.attributeType !== 'spellSlot'){
|
||||||
|
throw new Meteor.Error('Not a slot',
|
||||||
|
'The given property is not a valid spell slot');
|
||||||
|
}
|
||||||
|
if (!slot.spellSlotLevel?.value){
|
||||||
|
throw new Meteor.Error('No slot level',
|
||||||
|
'Slot does not have a spell slot level');
|
||||||
|
}
|
||||||
|
if (slot.spellSlotLevel.value < spell.level){
|
||||||
|
throw new Meteor.Error('Slot too small',
|
||||||
|
'Slot is not large enough to cast spell');
|
||||||
|
}
|
||||||
|
slotLevel = slot.spellSlotLevel.value;
|
||||||
|
damagePropertyWork({
|
||||||
|
property: slot,
|
||||||
|
operation: 'increment',
|
||||||
|
value: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope['slotLevel'] = slotLevel;
|
||||||
|
|
||||||
|
// Post the slot level spent to the log
|
||||||
|
const log = CreatureLogSchema.clean({
|
||||||
|
creatureId: creature._id,
|
||||||
|
creatureName: creature.name,
|
||||||
|
});
|
||||||
|
if (slot?.spellSlotLevel?.value){
|
||||||
|
log.content.push({
|
||||||
|
name: `Casting using a level ${slotLevel} spell slot`
|
||||||
|
});
|
||||||
|
} else if (slotLevel) {
|
||||||
|
log.content.push({
|
||||||
|
name: `Casting at level ${slotLevel}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the action
|
||||||
|
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope, log});
|
||||||
|
|
||||||
|
// Recompute all involved creatures
|
||||||
|
computeCreature(creature._id);
|
||||||
|
targets.forEach(target => {
|
||||||
|
computeCreature(target._id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default doAction;
|
||||||
2
app/imports/api/engine/actions/index.js
Normal file
2
app/imports/api/engine/actions/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import './doCastSpell.js';
|
||||||
|
import './doCheck.js';
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import doAction from '../doAction.js';
|
|
||||||
|
|
||||||
const commitAction = new ValidatedMethod({
|
|
||||||
name: 'creatureProperties.doAction',
|
|
||||||
validate: new SimpleSchema({
|
|
||||||
actionId: SimpleSchema.RegEx.Id,
|
|
||||||
targetIds: {
|
|
||||||
type: Array,
|
|
||||||
defaultValue: [],
|
|
||||||
maxCount: 20,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
'targetIds.$': {
|
|
||||||
type: String,
|
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
|
||||||
},
|
|
||||||
}).validator(),
|
|
||||||
mixins: [RateLimiterMixin],
|
|
||||||
rateLimit: {
|
|
||||||
numRequests: 10,
|
|
||||||
timeInterval: 5000,
|
|
||||||
},
|
|
||||||
run({actionId, targetIds = []}) {
|
|
||||||
let action = CreatureProperties.findOne(actionId);
|
|
||||||
// Check permissions
|
|
||||||
let creature = getRootCreatureAncestor(action);
|
|
||||||
|
|
||||||
assertEditPermission(creature, this.userId);
|
|
||||||
let targets = [];
|
|
||||||
targetIds.forEach(targetId => {
|
|
||||||
let target = Creatures.findOne(targetId);
|
|
||||||
assertEditPermission(target, this.userId);
|
|
||||||
targets.push(target);
|
|
||||||
});
|
|
||||||
doAction({action, creature, targets, method: this});
|
|
||||||
|
|
||||||
// recompute creatures
|
|
||||||
computeCreature(creature._id);
|
|
||||||
|
|
||||||
targets.forEach(target => {
|
|
||||||
computeCreature(target._id);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default commitAction;
|
|
||||||
@@ -5,11 +5,21 @@ import writeScope from './computation/writeComputation/writeScope.js';
|
|||||||
import writeErrors from './computation/writeComputation/writeErrors.js';
|
import writeErrors from './computation/writeComputation/writeErrors.js';
|
||||||
|
|
||||||
export default function computeCreature(creatureId){
|
export default function computeCreature(creatureId){
|
||||||
|
if (Meteor.isClient) return;
|
||||||
const computation = buildCreatureComputation(creatureId);
|
const computation = buildCreatureComputation(creatureId);
|
||||||
computeCreatureComputation(computation);
|
try {
|
||||||
writeAlteredProperties(computation);
|
computeCreatureComputation(computation);
|
||||||
writeScope(creatureId, computation.scope);
|
writeAlteredProperties(computation);
|
||||||
writeErrors(creatureId, computation.errors);
|
writeScope(creatureId, computation.scope);
|
||||||
|
writeErrors(creatureId, computation.errors);
|
||||||
|
} catch (e){
|
||||||
|
computation.errors.push({
|
||||||
|
type: 'crash',
|
||||||
|
details: e.reason,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
writeErrors(creatureId, [...computation.errors]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now just recompute the whole creature, TODO only recompute a single
|
// For now just recompute the whole creature, TODO only recompute a single
|
||||||
|
|||||||
@@ -7,10 +7,15 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
:outlined="!!label"
|
||||||
|
:icon="!label"
|
||||||
|
:min-width="label && 108"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-format-paint</v-icon>
|
{{ label }}
|
||||||
|
<v-icon :right="!!label">
|
||||||
|
mdi-format-paint
|
||||||
|
</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-card class="overflow-hidden">
|
<v-card class="overflow-hidden">
|
||||||
@@ -122,6 +127,10 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data(){ return {
|
data(){ return {
|
||||||
colors: [
|
colors: [
|
||||||
|
|||||||
@@ -8,21 +8,22 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<div class="layout align-center">
|
<div class="layout align-center">
|
||||||
<v-label>{{ label }}</v-label>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
large
|
outlined
|
||||||
icon
|
:min-width="108"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
|
{{ label }}
|
||||||
<svg-icon
|
<svg-icon
|
||||||
v-if="safeValue && safeValue.shape"
|
v-if="safeValue && safeValue.shape"
|
||||||
large
|
right
|
||||||
|
class="ml-2"
|
||||||
:shape="safeValue.shape"
|
:shape="safeValue.shape"
|
||||||
/>
|
/>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-else
|
||||||
large
|
right
|
||||||
>
|
>
|
||||||
mdi-select-search
|
mdi-select-search
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
|||||||
@@ -162,11 +162,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="spellSlots && spellSlots.length"
|
v-if="spellSlots && spellSlots.length || hasSpells"
|
||||||
class="spell-slots"
|
class="spell-slots"
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card
|
||||||
|
data-id="spell-slot-card"
|
||||||
|
>
|
||||||
<v-list
|
<v-list
|
||||||
|
v-if="spellSlots && spellSlots.length"
|
||||||
two-line
|
two-line
|
||||||
subheader
|
subheader
|
||||||
>
|
>
|
||||||
@@ -180,6 +183,19 @@
|
|||||||
@cast="castSpellWithSlot(spellSlot._id)"
|
@cast="castSpellWithSlot(spellSlot._id)"
|
||||||
/>
|
/>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
<div
|
||||||
|
v-if="hasSpells"
|
||||||
|
class="d-flex justify-end"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
color="accent"
|
||||||
|
style="width: 100%;"
|
||||||
|
outlined
|
||||||
|
@click="castSpell"
|
||||||
|
>
|
||||||
|
Cast a spell
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -348,7 +364,8 @@
|
|||||||
import RestButton from '/imports/ui/creature/RestButton.vue';
|
import RestButton from '/imports/ui/creature/RestButton.vue';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
||||||
//import castSpellWithSlot from '/imports/api/creature/actions/castSpellWithSlot.js';
|
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
const getProperties = function(creature, filter){
|
const getProperties = function(creature, filter){
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
@@ -432,6 +449,11 @@
|
|||||||
spellSlots(){
|
spellSlots(){
|
||||||
return getAttributeOfType(this.creature, 'spellSlot');
|
return getAttributeOfType(this.creature, 'spellSlot');
|
||||||
},
|
},
|
||||||
|
hasSpells(){
|
||||||
|
return getProperties(this.creature, {
|
||||||
|
type: 'spell',
|
||||||
|
}).count();
|
||||||
|
},
|
||||||
hitDice(){
|
hitDice(){
|
||||||
return getAttributeOfType(this.creature, 'hitDice');
|
return getAttributeOfType(this.creature, 'hitDice');
|
||||||
},
|
},
|
||||||
@@ -498,18 +520,19 @@
|
|||||||
if (error) console.error(error);
|
if (error) console.error(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
castSpellWithSlot(slotId){
|
castSpell(){
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'cast-spell-with-slot-dialog',
|
component: 'cast-spell-with-slot-dialog',
|
||||||
elementId: `spell-slot-cast-btn-${slotId}`,
|
elementId: 'spell-slot-card',
|
||||||
data: {
|
data: {
|
||||||
creatureId: this.creatureId,
|
creatureId: this.creatureId,
|
||||||
slotId,
|
|
||||||
},
|
},
|
||||||
callback({spellId, slotId} = {}){
|
callback({spellId, slotId} = {}){
|
||||||
if (!spellId) return;
|
if (!spellId) return;
|
||||||
castSpellWithSlot.call({spellId, slotId}, error => {
|
doCastSpell.call({spellId, slotId}, error => {
|
||||||
if (error) console.error(error);
|
if (!error) return;
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
|
:key="model._id"
|
||||||
class="spell-slot-list-tile"
|
class="spell-slot-list-tile"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:disabled="disabled"
|
||||||
v-on="hasClickListener ? {click} : {}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title v-if="Number.isFinite(model.total)">
|
<v-list-item-title
|
||||||
|
v-if="Number.isFinite(model.total)"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="model.total > 4"
|
v-if="model.total > 4"
|
||||||
class="layout value"
|
class="layout value"
|
||||||
@@ -27,6 +32,7 @@
|
|||||||
<v-icon
|
<v-icon
|
||||||
v-for="i in model.total"
|
v-for="i in model.total"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
i > model.value ?
|
i > model.value ?
|
||||||
@@ -38,24 +44,13 @@
|
|||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-list-item-title v-else>
|
<v-list-item-title v-else>
|
||||||
<code>
|
<code>
|
||||||
{{model.total}}
|
{{ model.total }}
|
||||||
</code>
|
</code>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ model.name }}
|
{{ model.name }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
<v-list-item-avatar v-if="!hideCastButton">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
text
|
|
||||||
class="primary--text"
|
|
||||||
:data-id="`spell-slot-cast-btn-${model._id}`"
|
|
||||||
@click.stop="$emit('cast')"
|
|
||||||
>
|
|
||||||
<v-icon>$vuetify.icons.spell</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-avatar>
|
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -69,6 +64,7 @@ export default {
|
|||||||
},
|
},
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
hideCastButton: Boolean,
|
hideCastButton: Boolean,
|
||||||
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
|
|||||||
@@ -73,27 +73,35 @@
|
|||||||
>
|
>
|
||||||
Slot
|
Slot
|
||||||
</div>
|
</div>
|
||||||
<v-list-item
|
<v-list-item-group
|
||||||
v-if="!(selectedSpell && selectedSpell.level > 0)"
|
key="slot-list"
|
||||||
key="cantrip-dummy-slot"
|
v-model="selectedSlotId"
|
||||||
class="spell-slot-list-tile"
|
|
||||||
:class="{ 'primary--text': selectedSlotId === undefined}"
|
|
||||||
@click="selectedSlotId = undefined"
|
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item
|
||||||
<v-list-item-title class="text-h6">
|
key="cantrip-dummy-slot"
|
||||||
Cantrip
|
class="spell-slot-list-tile"
|
||||||
</v-list-item-title>
|
:class="{ 'primary--text': selectedSlotId === 'no-slot' }"
|
||||||
</v-list-item-content>
|
value="no-slot"
|
||||||
</v-list-item>
|
:disabled="!canCastSpellWithSlot(selectedSpell, 'no-slot')"
|
||||||
<spell-slot-list-tile
|
@click="selectedSlotId = 'no-slot'"
|
||||||
v-for="spellSlot in spellSlots"
|
>
|
||||||
:key="spellSlot._id"
|
<v-list-item-content>
|
||||||
:model="spellSlot"
|
<v-list-item-title>
|
||||||
:class="{ 'primary--text': selectedSlotId === spellSlot._id }"
|
Cast without spell slot
|
||||||
hide-cast-button
|
</v-list-item-title>
|
||||||
@click="selectedSlotId = spellSlot._id"
|
</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>
|
||||||
<template slot="right">
|
<template slot="right">
|
||||||
<div
|
<div
|
||||||
@@ -102,25 +110,31 @@
|
|||||||
>
|
>
|
||||||
Spell
|
Spell
|
||||||
</div>
|
</div>
|
||||||
<template v-for="spell in computedSpells">
|
<v-list-item-group
|
||||||
<v-subheader
|
key="slot-list"
|
||||||
v-if="spell.isSubheader"
|
v-model="selectedSpellId"
|
||||||
:key="`${spell.level}-header`"
|
>
|
||||||
class="item"
|
<template v-for="spell in computedSpells">
|
||||||
>
|
<v-subheader
|
||||||
{{ spell.level === 0 ? 'Cantrips' : `Level ${spell.level}` }}
|
v-if="spell.isSubheader"
|
||||||
</v-subheader>
|
:key="`${spell.level}-header`"
|
||||||
<spell-list-tile
|
class="item"
|
||||||
v-else
|
>
|
||||||
:key="spell._id"
|
{{ spell.level === 0 ? 'Cantrips' : `Level ${spell.level}` }}
|
||||||
hide-handle
|
</v-subheader>
|
||||||
show-info-button
|
<spell-list-tile
|
||||||
:class="{ 'primary--text': selectedSpellId === spell._id}"
|
v-else
|
||||||
:model="spell"
|
:key="spell._id"
|
||||||
@click="selectedSpellId = spell._id"
|
hide-handle
|
||||||
@show-info="spellDialog(spell._id)"
|
show-info-button
|
||||||
/>
|
:model="spell"
|
||||||
</template>
|
: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>
|
</template>
|
||||||
</split-list-layout>
|
</split-list-layout>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
@@ -135,10 +149,7 @@
|
|||||||
text
|
text
|
||||||
:disabled="!canCast"
|
:disabled="!canCast"
|
||||||
class="primary--text"
|
class="primary--text"
|
||||||
@click="$store.dispatch('popDialogStack', {
|
@click="cast"
|
||||||
spellId: selectedSpellId,
|
|
||||||
slotId: selectedSlotId,
|
|
||||||
})"
|
|
||||||
>
|
>
|
||||||
Cast
|
Cast
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -153,6 +164,16 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.js';
|
import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.js';
|
||||||
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||||
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
|
const slotFilter = {
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'spellSlot',
|
||||||
|
removed: {$ne: true},
|
||||||
|
inactive: {$ne: true},
|
||||||
|
overridden: {$ne: true},
|
||||||
|
'spellSlotLevel.value': {$gte: 1},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -179,6 +200,8 @@ export default {
|
|||||||
searchString: undefined,
|
searchString: undefined,
|
||||||
selectedSlotId: this.slotId,
|
selectedSlotId: this.slotId,
|
||||||
selectedSpellId: this.spellId,
|
selectedSpellId: this.spellId,
|
||||||
|
selectedSlot: undefined,
|
||||||
|
selectedSpell: undefined,
|
||||||
searchValue: undefined,
|
searchValue: undefined,
|
||||||
searchError: undefined,
|
searchError: undefined,
|
||||||
filterMenuOpen: false,
|
filterMenuOpen: false,
|
||||||
@@ -195,16 +218,10 @@ export default {
|
|||||||
return spellsWithSubheaders(this.spells);
|
return spellsWithSubheaders(this.spells);
|
||||||
},
|
},
|
||||||
canCast(){
|
canCast(){
|
||||||
let spell = this.selectedSpell;
|
if (!this.selectedSpell || !this.selectedSlotId) return false;
|
||||||
let slot = this.selectedSlot;
|
return this.canCastSpellWithSlot(
|
||||||
if (!spell) return false;
|
this.selectedSpell, this.selectedSlotId, this.selectedSlot
|
||||||
if (spell.level === 0){
|
);
|
||||||
return this.selectedSlotId === undefined;
|
|
||||||
} else if (!slot) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return slot.spellSlotLevelValue >= spell.level;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
filtersApplied(){
|
filtersApplied(){
|
||||||
for (let key in this.booleanFilters){
|
for (let key in this.booleanFilters){
|
||||||
@@ -216,19 +233,61 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedSpell(spell){
|
selectedSpellId: {
|
||||||
if (!spell) return;
|
handler(spellId){
|
||||||
if(spell.level === 0){
|
this.selectedSpell = CreatureProperties.findOne(spellId)
|
||||||
this.selectedSlotId = undefined;
|
},
|
||||||
}
|
immediate: true
|
||||||
},
|
},
|
||||||
selectedSlot(slot){
|
selectedSpell: {
|
||||||
if (!slot) return;
|
handler(spell){
|
||||||
if (!this.selectedSpell) return;
|
if (!spell) return;
|
||||||
if(slot.spellSlotLevelValue > 0 && this.selectedSpell.level === 0){
|
if(spell.level === 0 || spell.castWithoutSpellSlots){
|
||||||
this.selectedSpellId = undefined;
|
this.selectedSlotId = 'no-slot';
|
||||||
}
|
} else if (
|
||||||
|
!this.selectedSlotId ||
|
||||||
|
this.selectedSlotId == 'no-slot' ||
|
||||||
|
this.selectedSlot.spellSlotLevel.value < spell.level
|
||||||
|
) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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: {
|
methods: {
|
||||||
clearBooleanFilters(){
|
clearBooleanFilters(){
|
||||||
@@ -247,10 +306,36 @@ export default {
|
|||||||
this.searchValue = val;
|
this.searchValue = val;
|
||||||
setTimeout(ack, 200);
|
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;
|
||||||
|
return (!spell.level || spell.castWithoutSpellSlots) ? (
|
||||||
|
// Cantrips and no-slot spells
|
||||||
|
slotId && slotId === 'no-slot'
|
||||||
|
) : (
|
||||||
|
// Leveled spells
|
||||||
|
slotId !== 'no-slot' &&
|
||||||
|
slot && spell && (
|
||||||
|
spell.level <= slot.spellSlotLevel.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cast(){
|
||||||
|
let selectedSlotId = this.selectedSlotId;
|
||||||
|
if (selectedSlotId === 'no-slot') selectedSlotId = undefined;
|
||||||
|
this.$store.dispatch('popDialogStack', {
|
||||||
|
spellId: this.selectedSpellId,
|
||||||
|
slotId: selectedSlotId,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
spells(){
|
spells(){
|
||||||
let slotLevel = this.selectedSlot && this.selectedSlot.spellSlotLevelValue || 0;
|
|
||||||
let filter = {
|
let filter = {
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
removed: {$ne: true},
|
removed: {$ne: true},
|
||||||
@@ -259,8 +344,8 @@ export default {
|
|||||||
{prepared: true},
|
{prepared: true},
|
||||||
{alwaysPrepared: true},
|
{alwaysPrepared: true},
|
||||||
],
|
],
|
||||||
level: {$lte: slotLevel},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply the filters from the filter menu
|
// Apply the filters from the filter menu
|
||||||
for (let key in this.booleanFilters){
|
for (let key in this.booleanFilters){
|
||||||
if (this.booleanFilters[key].enabled){
|
if (this.booleanFilters[key].enabled){
|
||||||
@@ -284,27 +369,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
spellSlots(){
|
spellSlots(){
|
||||||
let filter = {
|
return CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
type: 'attribute',
|
...slotFilter
|
||||||
attributeType: 'spellSlot',
|
}, {
|
||||||
removed: {$ne: true},
|
sort: {'spellSlotLevel.value': 1, order: 1},
|
||||||
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>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
class="spell"
|
class="spell"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:disabled="disabled"
|
||||||
v-on="hasClickListener ? {click} : {}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<v-list-item-avatar class="spell-avatar">
|
<v-list-item-avatar class="spell-avatar">
|
||||||
@@ -8,6 +10,7 @@
|
|||||||
class="mr-2"
|
class="mr-2"
|
||||||
:model="model"
|
:model="model"
|
||||||
:color="model.color"
|
:color="model.color"
|
||||||
|
:disabled="disabled"
|
||||||
/>
|
/>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
v-else-if="showInfoButton"
|
v-else-if="showInfoButton"
|
||||||
icon
|
icon
|
||||||
class="info-icon"
|
class="info-icon"
|
||||||
|
:disabled="disabled"
|
||||||
:data-id="`spell-info-btn-${model._id}`"
|
:data-id="`spell-info-btn-${model._id}`"
|
||||||
@click.stop="$emit('show-info')"
|
@click.stop="$emit('show-info')"
|
||||||
>
|
>
|
||||||
@@ -53,13 +57,14 @@ import updateCreatureProperty from '/imports/api/creature/creatureProperties/met
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [treeNodeViewMixin],
|
mixins: [treeNodeViewMixin],
|
||||||
|
inject: {
|
||||||
|
context: { default: {} }
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
preparingSpells: Boolean,
|
preparingSpells: Boolean,
|
||||||
hideHandle: Boolean,
|
hideHandle: Boolean,
|
||||||
showInfoButton: Boolean,
|
showInfoButton: Boolean,
|
||||||
},
|
disabled: Boolean,
|
||||||
inject: {
|
|
||||||
context: { default: {} }
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
justify="center"
|
justify="center"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<v-col cols="1">
|
<v-col cols="12">
|
||||||
<icon-color-menu
|
<icon-color-menu
|
||||||
:model="model"
|
:model="model"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="spell-form">
|
<div class="spell-form">
|
||||||
|
<v-row
|
||||||
|
justify="center"
|
||||||
|
class="mb-3"
|
||||||
|
>
|
||||||
|
<v-col cols="12">
|
||||||
|
<icon-color-menu
|
||||||
|
:model="model"
|
||||||
|
:errors="errors"
|
||||||
|
@change="e => $emit('change', e)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
<div class="layout wrap justify-space-between">
|
<div class="layout wrap justify-space-between">
|
||||||
<smart-switch
|
<smart-switch
|
||||||
label="Always prepared"
|
label="Always prepared"
|
||||||
@@ -18,6 +30,15 @@
|
|||||||
:error-messages="errors.prepared"
|
:error-messages="errors.prepared"
|
||||||
@change="change('prepared', ...arguments)"
|
@change="change('prepared', ...arguments)"
|
||||||
/>
|
/>
|
||||||
|
<smart-switch
|
||||||
|
v-show="model.level"
|
||||||
|
label="Cast without spell slots"
|
||||||
|
style="width: 400px; flex-grow: 0;"
|
||||||
|
class="mx-2"
|
||||||
|
:value="model.castWithoutSpellSlots"
|
||||||
|
:error-messages="errors.castWithoutSpellSlots"
|
||||||
|
@change="change('castWithoutSpellSlots', ...arguments)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<text-field
|
<text-field
|
||||||
ref="focusFirst"
|
ref="focusFirst"
|
||||||
@@ -59,6 +80,15 @@
|
|||||||
:error-messages="errors.range"
|
:error-messages="errors.range"
|
||||||
@change="change('range', ...arguments)"
|
@change="change('range', ...arguments)"
|
||||||
/>
|
/>
|
||||||
|
<smart-select
|
||||||
|
label="Target"
|
||||||
|
style="flex-basis: 300px;"
|
||||||
|
:items="targetOptions"
|
||||||
|
:value="model.target"
|
||||||
|
:error-messages="errors.target"
|
||||||
|
:menu-props="{auto: true, lazy: true}"
|
||||||
|
@change="change('target', ...arguments)"
|
||||||
|
/>
|
||||||
<div class="layout wrap justify-space-between">
|
<div class="layout wrap justify-space-between">
|
||||||
<smart-checkbox
|
<smart-checkbox
|
||||||
label="Verbal"
|
label="Verbal"
|
||||||
@@ -105,24 +135,70 @@
|
|||||||
@change="({path, value, ack}) =>
|
@change="({path, value, ack}) =>
|
||||||
$emit('change', {path: ['description', ...path], value, ack})"
|
$emit('change', {path: ['description', ...path], value, ack})"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<smart-combobox
|
|
||||||
label="Tags"
|
|
||||||
multiple
|
|
||||||
chips
|
|
||||||
deletable-chips
|
|
||||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
|
||||||
:value="model.tags"
|
|
||||||
:error-messages="errors.tags"
|
|
||||||
@change="change('tags', ...arguments)"
|
|
||||||
/>
|
|
||||||
<form-sections>
|
<form-sections>
|
||||||
|
<form-section name="Resources">
|
||||||
|
<resources-form
|
||||||
|
:model="model.resources"
|
||||||
|
@change="({path, value, ack}) => $emit('change', {path: ['resources', ...path], value, ack})"
|
||||||
|
@push="({path, value, ack}) => $emit('push', {path: ['resources', ...path], value, ack})"
|
||||||
|
@pull="({path, ack}) => $emit('pull', {path: ['resources', ...path], ack})"
|
||||||
|
/>
|
||||||
|
</form-section>
|
||||||
<form-section
|
<form-section
|
||||||
name="Casting"
|
v-if="model.level && model.castWithoutSpellSlots"
|
||||||
|
name="Limit Uses"
|
||||||
>
|
>
|
||||||
<action-form
|
<v-row dense>
|
||||||
v-bind="$props"
|
<v-col
|
||||||
v-on="$listeners"
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<computed-field
|
||||||
|
label="Uses"
|
||||||
|
hint="How many times this action can be used before needing to be reset"
|
||||||
|
class="mr-2"
|
||||||
|
:model="model.uses"
|
||||||
|
:error-messages="errors.uses"
|
||||||
|
@change="({path, value, ack}) =>
|
||||||
|
$emit('change', {path: ['uses', ...path], value, ack})"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<text-field
|
||||||
|
label="Uses used"
|
||||||
|
type="number"
|
||||||
|
hint="How many times this action has already been used: should be 0 in most cases"
|
||||||
|
style="flex-basis: 300px;"
|
||||||
|
:value="model.usesUsed"
|
||||||
|
:error-messages="errors.uses"
|
||||||
|
@change="change('usesUsed', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<smart-select
|
||||||
|
label="Reset"
|
||||||
|
clearable
|
||||||
|
hint="When number of uses used should be reset to zero"
|
||||||
|
style="flex-basis: 300px;"
|
||||||
|
:items="resetOptions"
|
||||||
|
:value="model.reset"
|
||||||
|
:error-messages="errors.reset"
|
||||||
|
:menu-props="{auto: true, lazy: true}"
|
||||||
|
@change="change('reset', ...arguments)"
|
||||||
|
/>
|
||||||
|
</form-section>
|
||||||
|
<form-section name="Advanced">
|
||||||
|
<smart-combobox
|
||||||
|
label="Tags"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
deletable-chips
|
||||||
|
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||||
|
:value="model.tags"
|
||||||
|
@change="change('tags', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</form-section>
|
</form-section>
|
||||||
</form-sections>
|
</form-sections>
|
||||||
@@ -131,14 +207,16 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||||
import ActionForm from '/imports/ui/properties/forms/ActionForm.vue'
|
|
||||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
|
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
|
||||||
|
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FormSections,
|
FormSections,
|
||||||
FormSection,
|
FormSection,
|
||||||
ActionForm,
|
IconColorMenu,
|
||||||
|
ResourcesForm,
|
||||||
},
|
},
|
||||||
mixins: [propertyFormMixin],
|
mixins: [propertyFormMixin],
|
||||||
data(){return {
|
data(){return {
|
||||||
@@ -202,6 +280,27 @@
|
|||||||
value: 9,
|
value: 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
],
|
||||||
};},
|
};},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,45 +1,23 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-menu offset-y>
|
<div
|
||||||
<template #activator="{ on, attrs }">
|
class="d-flex justify-center flex-wrap"
|
||||||
<v-badge
|
>
|
||||||
icon="mdi-pencil"
|
<div class="mx-1">
|
||||||
overlap
|
<color-picker
|
||||||
>
|
label="Color"
|
||||||
<v-btn
|
:value="model.color"
|
||||||
icon
|
@input="value =>$emit('change', {path: ['color'], value})"
|
||||||
:color="model.color"
|
/>
|
||||||
outlined
|
</div>
|
||||||
v-bind="attrs"
|
<div class="mx-1">
|
||||||
v-on="on"
|
<icon-picker
|
||||||
>
|
label="Icon"
|
||||||
<property-icon
|
:value="model.icon"
|
||||||
:model="model"
|
:error-messages="errors.icon"
|
||||||
:color="model.color"
|
@change="(value, ack) =>$emit('change', {path: ['icon'], value, ack})"
|
||||||
/>
|
/>
|
||||||
</v-btn>
|
</div>
|
||||||
</v-badge>
|
</div>
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item>
|
|
||||||
<v-list-item-title>
|
|
||||||
<icon-picker
|
|
||||||
label="Icon"
|
|
||||||
:value="model.icon"
|
|
||||||
:error-messages="errors.icon"
|
|
||||||
@change="(value, ack) =>$emit('change', {path: ['icon'], value, ack})"
|
|
||||||
/>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item>
|
|
||||||
<v-list-item-title>
|
|
||||||
<color-picker
|
|
||||||
:value="model.color"
|
|
||||||
@input="value =>$emit('change', {path: ['color'], value})"
|
|
||||||
/>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
v-if="model.icon"
|
v-if="model.icon"
|
||||||
:shape="model.icon.shape"
|
:shape="model.icon.shape"
|
||||||
:color="color"
|
:color="color"
|
||||||
|
:class="{disabled}"
|
||||||
/>
|
/>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-else
|
||||||
:color="color"
|
:color="color"
|
||||||
|
:class="{disabled}"
|
||||||
>
|
>
|
||||||
{{ icon }}
|
{{ icon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
@@ -25,6 +27,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
icon(){
|
icon(){
|
||||||
@@ -33,3 +36,9 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.svg-icon.disabled, .v-icon.disabled {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
>
|
>
|
||||||
<property-field
|
<property-field
|
||||||
v-if="context.creatureId"
|
v-if="context.creatureId"
|
||||||
name="Apply action"
|
:name="model.type === 'spell'? 'Cast spell' : 'Apply action'"
|
||||||
center
|
center
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
outlined
|
outlined
|
||||||
style="font-size: 18px;"
|
style="font-size: 18px;"
|
||||||
class="ma-2"
|
class="ma-2"
|
||||||
|
data-id="do-action-button"
|
||||||
:color="model.color || 'primary'"
|
:color="model.color || 'primary'"
|
||||||
icon
|
icon
|
||||||
:loading="doActionLoading"
|
:loading="doActionLoading"
|
||||||
@@ -109,12 +110,13 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
|
||||||
import doAction from '/imports/api/engine/actions/doAction.js';
|
import doAction from '/imports/api/engine/actions/doAction.js';
|
||||||
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
||||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||||
|
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -173,13 +175,33 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doAction(){
|
doAction(){
|
||||||
this.doActionLoading = true;
|
if (this.model.type === 'action'){
|
||||||
doAction.call({actionId: this.model._id}, error => {
|
this.doActionLoading = true;
|
||||||
this.doActionLoading = false;
|
doAction.call({actionId: this.model._id}, error => {
|
||||||
if (error){
|
this.doActionLoading = false;
|
||||||
console.error(error);
|
if (error){
|
||||||
}
|
snackbar({text: error.reason});
|
||||||
});
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (this.model.type === 'spell') {
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'cast-spell-with-slot-dialog',
|
||||||
|
elementId: 'do-action-button',
|
||||||
|
data: {
|
||||||
|
creatureId: this.context.creatureId,
|
||||||
|
spellId: this.model._id,
|
||||||
|
},
|
||||||
|
callback({spellId, slotId} = {}){
|
||||||
|
if (!spellId) return;
|
||||||
|
doCastSpell.call({spellId, slotId}, error => {
|
||||||
|
if (!error) return;
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetUses(){
|
resetUses(){
|
||||||
updateCreatureProperty.call({
|
updateCreatureProperty.call({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import '/imports/server/publications/index.js';
|
|||||||
import '/imports/server/cron/deleteSoftRemovedDocuments.js';
|
import '/imports/server/cron/deleteSoftRemovedDocuments.js';
|
||||||
import '/imports/api/parenting/organizeMethods.js';
|
import '/imports/api/parenting/organizeMethods.js';
|
||||||
import '/imports/api/users/patreon/updatePatreonOnLogin.js';
|
import '/imports/api/users/patreon/updatePatreonOnLogin.js';
|
||||||
import '/imports/api/engine/actions/doCheck.js';
|
import '/imports/api/engine/actions/index.js';
|
||||||
import '/imports/migrations/server/index.js';
|
import '/imports/migrations/server/index.js';
|
||||||
import '/imports/migrations/methods/index.js'
|
import '/imports/migrations/methods/index.js'
|
||||||
import '/imports/constants/MAINTENANCE_MODE.js';
|
import '/imports/constants/MAINTENANCE_MODE.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user