diff --git a/app/imports/api/creature/CreatureProperties.js b/app/imports/api/creature/CreatureProperties.js index 85af5e32..66c8f896 100644 --- a/app/imports/api/creature/CreatureProperties.js +++ b/app/imports/api/creature/CreatureProperties.js @@ -22,6 +22,7 @@ import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import { reorderDocs } from '/imports/api/parenting/order.js'; import '/imports/api/creature/actions/doAction.js'; +import '/imports/api/creature/actions/castSpellWithSlot.js'; import '/imports/api/creature/creatureProperties/manageEquipment.js'; let CreatureProperties = new Mongo.Collection('creatureProperties'); diff --git a/app/imports/api/creature/actions/castSpellWithSlot.js b/app/imports/api/creature/actions/castSpellWithSlot.js new file mode 100644 index 00000000..c8b53864 --- /dev/null +++ b/app/imports/api/creature/actions/castSpellWithSlot.js @@ -0,0 +1,75 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; +import CreatureProperties, { getCreature, damagePropertyWork } from '/imports/api/creature/CreatureProperties.js'; +import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js'; +import { doActionWork } from '/imports/api/creature/actions/doAction.js'; + +const castSpellWithSlot = new ValidatedMethod({ + name: 'creatureProperties.castSpellWithSlot', + validate: new SimpleSchema({ + spellId: SimpleSchema.RegEx.Id, + slotId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + targetId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + optional: true, + }, + }).validator(), + mixins: [RateLimiterMixin], + rateLimit: { + numRequests: 10, + timeInterval: 5000, + }, + run({spellId, slotId, targetId}) { + let spell = CreatureProperties.findOne(spellId); + // Check permissions + let creature = getCreature(spell); + assertEditPermission(creature, this.userId); + let target = undefined; + if (targetId) { + target = getCreature(targetId); + assertEditPermission(target, this.userId); + } + let slotLevel = spell.level || 0; + if (slotLevel !== 0){ + let slot = CreatureProperties.findOne(slotId); + if (!slot){ + throw new Meteor.Error('No slot', + 'Slot not found to cast spell'); + } + if (!slot.currentValue){ + throw new Meteor.Error('No slot', + 'Slot depleted'); + } + if (!(slot.spellSlotLevelValue >= spell.level)){ + throw new Meteor.Error('Slot too small', + 'Slot is not large enough to cast spell'); + } + slotLevel = slot.spellSlotLevelValue; + damagePropertyWork({ + property: slot, + operation: 'increment', + value: 1, + }); + } + doActionWork({ + action: spell, + context: {slotLevel}, + creature, + target, + }); + // Note this only recomputes the top-level creature, not the nearest one + recomputeCreatureByDoc(creature); + if (target){ + recomputeCreatureByDoc(target); + } + }, +}); + +export default castSpellWithSlot; diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/creature/actions/doAction.js index c1bead02..1353631b 100644 --- a/app/imports/api/creature/actions/doAction.js +++ b/app/imports/api/creature/actions/doAction.js @@ -41,8 +41,7 @@ const doAction = new ValidatedMethod({ }, }); -function doActionWork({action, creature, target}){ - let actionContext = {}; +export function doActionWork({action, creature, target, context = {}}){ let decendantForest = nodesToTree({ collection: CreatureProperties, ancestorId: action._id, @@ -53,9 +52,9 @@ function doActionWork({action, creature, target}){ }]; applyProperties({ forest: startingForest, + actionContext: context, creature, target, - actionContext }); } diff --git a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue index 7fcce972..2af4aec4 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/StatsTab.vue @@ -149,15 +149,18 @@ class="spell-slots" > - + Spell Slots @@ -320,6 +323,7 @@ import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue'; import RestButton from '/imports/ui/creature/RestButton.vue'; import getActiveProperties from '/imports/api/creature/getActiveProperties.js'; + import castSpellWithSlot from '/imports/api/creature/actions/castSpellWithSlot.js'; const getProperties = function(creature, filter,){ if (!creature) return; @@ -448,6 +452,22 @@ softRemoveProperty.call({_id}, error => { if (error) console.error(error); }); + }, + castSpellWithSlot(slotId){ + this.$store.commit('pushDialogStack', { + component: 'cast-spell-with-slot-dialog', + elementId: `spell-slot-cast-btn-${slotId}`, + data: { + creatureId: this.creatureId, + slotId, + }, + callback({spellId, slotId} = {}){ + if (!spellId) return; + castSpellWithSlot.call({spellId, slotId}, error => { + if (error) console.error(error); + }); + }, + }); } }, }; diff --git a/app/imports/ui/dialogStack/DialogComponentIndex.js b/app/imports/ui/dialogStack/DialogComponentIndex.js index 42b88566..ac0331c0 100644 --- a/app/imports/ui/dialogStack/DialogComponentIndex.js +++ b/app/imports/ui/dialogStack/DialogComponentIndex.js @@ -1,3 +1,4 @@ +import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue'; import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue'; import CreaturePropertyCreationDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue'; import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue' @@ -19,6 +20,7 @@ import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue'; import UsernameDialog from '/imports/ui/user/UsernameDialog.vue'; export default { + CastSpellWithSlotDialog, CreatureFormDialog, CreaturePropertyCreationDialog, CreaturePropertyDialog, diff --git a/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue b/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue index 2b3ee07f..6955de2f 100644 --- a/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue +++ b/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue @@ -1,86 +1,56 @@ @@ -88,18 +58,13 @@ import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; export default { props: { - _id: String, - name: String, - color: String, - value: Number, - damage: { - type: Number, - default: 0, - }, + model: { + type: Object, + required: true, + }, + dark: Boolean, + hideCastButton: Boolean, }, - data(){ return{ - hover: false, - }}, inject: { context: { default: {} } }, @@ -107,15 +72,15 @@ export default { currentValue(){ return this.value - this.damage; }, + hasClickListener(){ + return this.$listeners && !!this.$listeners.click; + }, }, methods: { signed: numberToSignedString, click(e){ this.$emit('click', e); }, - increment(value){ - this.$emit('change', {type: 'increment', value}) - }, }, }; @@ -124,22 +89,9 @@ export default { .spell-slot-list-tile { background: inherit; } - .spell-slot-list-tile >>> .v-list__tile { - height: 56px; - } .v-list__tile__action { width: 112px; - flex-shrink: 0; - } - .slot-buttons > .v-btn { - margin: 0; - flex-shrink: 1; - } - .buttons { - height: 100%; - } - .buttons > .v-btn { - margin: 0; + flex-shrink: 0; } .spell-slot-list-tile.hover { background: #f5f5f5 !important; @@ -156,4 +108,7 @@ export default { .theme--dark .max-value { color: rgba(255, 255, 255, 0.54); } + .primary--text .v-icon, .primary--text .max-value, .primary--text .current-value, .primary--text .v-list__tile__sub-title { + color: #b71c1c + } diff --git a/app/imports/ui/properties/components/attributes/SplitListLayout.vue b/app/imports/ui/properties/components/attributes/SplitListLayout.vue new file mode 100644 index 00000000..3969bd32 --- /dev/null +++ b/app/imports/ui/properties/components/attributes/SplitListLayout.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/app/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue b/app/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue new file mode 100644 index 00000000..f280b36b --- /dev/null +++ b/app/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/app/imports/ui/properties/components/spells/SpellList.vue b/app/imports/ui/properties/components/spells/SpellList.vue index d49dd4b1..190b6c6b 100644 --- a/app/imports/ui/properties/components/spells/SpellList.vue +++ b/app/imports/ui/properties/components/spells/SpellList.vue @@ -40,24 +40,7 @@ import draggable from 'vuedraggable'; import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue'; import { organizeDoc } from '/imports/api/parenting/organizeMethods.js'; - -function spellsWithSubheaders(spells = []){ - let result = []; - let lastSpell = undefined; - let sortedSpells = [...spells].sort((a, b) => a.level - b.level) - sortedSpells.forEach(spell => { - if (spell.isSubheader) return; - if (!lastSpell || spell.level > lastSpell.level){ - result.push({ - isSubheader: true, - level: spell.level, - }); - } - result.push(spell); - lastSpell = spell; - }); - return result; -} +import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.js'; export default { components: { diff --git a/app/imports/ui/properties/components/spells/SpellListTile.vue b/app/imports/ui/properties/components/spells/SpellListTile.vue index 4046f732..977b9f53 100644 --- a/app/imports/ui/properties/components/spells/SpellListTile.vue +++ b/app/imports/ui/properties/components/spells/SpellListTile.vue @@ -27,7 +27,7 @@ @change="setPrepared" /> @@ -45,6 +45,7 @@ export default { mixins: [treeNodeViewMixin], props: { preparingSpells: Boolean, + hideHandle: Boolean, }, computed: { hasClickListener(){ @@ -82,4 +83,7 @@ export default { .spell { background-color: inherit; } +.primary--text .v-icon, .primary--text .v-list__tile__sub-title { + color: #b71c1c +} diff --git a/app/imports/ui/properties/components/spells/spellsWithSubheaders.js b/app/imports/ui/properties/components/spells/spellsWithSubheaders.js new file mode 100644 index 00000000..87298ff3 --- /dev/null +++ b/app/imports/ui/properties/components/spells/spellsWithSubheaders.js @@ -0,0 +1,17 @@ +export default function spellsWithSubheaders(spells = []){ + let result = []; + let lastSpell = undefined; + let sortedSpells = [...spells].sort((a, b) => a.level - b.level) + sortedSpells.forEach(spell => { + if (spell.isSubheader) return; + if (!lastSpell || spell.level > lastSpell.level){ + result.push({ + isSubheader: true, + level: spell.level, + }); + } + result.push(spell); + lastSpell = spell; + }); + return result; +}