Compare commits
9 Commits
2.0-beta.1
...
2.0-beta.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c780c11e3f | ||
|
|
100c93b5ae | ||
|
|
a4e6dd1d66 | ||
|
|
1b3b6362f7 | ||
|
|
bdf4074e3c | ||
|
|
9492b2d8b8 | ||
|
|
27f1f4e720 | ||
|
|
d63b8c835d | ||
|
|
85f3881935 |
@@ -22,6 +22,7 @@ import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
|||||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||||
|
|
||||||
import '/imports/api/creature/actions/doAction.js';
|
import '/imports/api/creature/actions/doAction.js';
|
||||||
|
import '/imports/api/creature/actions/castSpellWithSlot.js';
|
||||||
import '/imports/api/creature/creatureProperties/manageEquipment.js';
|
import '/imports/api/creature/creatureProperties/manageEquipment.js';
|
||||||
|
|
||||||
let CreatureProperties = new Mongo.Collection('creatureProperties');
|
let CreatureProperties = new Mongo.Collection('creatureProperties');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import SimpleSchema from 'simpl-schema';
|
|||||||
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
|
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
|
||||||
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
|
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
|
||||||
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
|
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
|
||||||
|
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
|
||||||
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
|
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
|
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
|
||||||
|
|
||||||
@@ -160,11 +161,25 @@ const insertCreature = new ValidatedMethod({
|
|||||||
assertUserHasPaidBenefits(this.userId);
|
assertUserHasPaidBenefits(this.userId);
|
||||||
|
|
||||||
// Create the creature document
|
// Create the creature document
|
||||||
let charId = Creatures.insert({
|
let creatureId = Creatures.insert({
|
||||||
owner: this.userId,
|
owner: this.userId,
|
||||||
});
|
});
|
||||||
|
CreatureProperties.insert({
|
||||||
|
slotTags: ['base'],
|
||||||
|
quantityExpected: 1,
|
||||||
|
type: 'propertySlot',
|
||||||
|
name: 'Base',
|
||||||
|
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
|
||||||
|
hideWhenFull: true,
|
||||||
|
parent: {collection: 'creatures', id: creatureId},
|
||||||
|
ancestors: [{collection: 'creatures', id: creatureId}],
|
||||||
|
order: 0,
|
||||||
|
tags: [],
|
||||||
|
spaceLeft: 1,
|
||||||
|
totalFilled: 0,
|
||||||
|
});
|
||||||
this.unblock();
|
this.unblock();
|
||||||
return charId;
|
return creatureId;
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
75
app/imports/api/creature/actions/castSpellWithSlot.js
Normal file
75
app/imports/api/creature/actions/castSpellWithSlot.js
Normal file
@@ -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;
|
||||||
@@ -41,8 +41,7 @@ const doAction = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function doActionWork({action, creature, target}){
|
export function doActionWork({action, creature, target, context = {}}){
|
||||||
let actionContext = {};
|
|
||||||
let decendantForest = nodesToTree({
|
let decendantForest = nodesToTree({
|
||||||
collection: CreatureProperties,
|
collection: CreatureProperties,
|
||||||
ancestorId: action._id,
|
ancestorId: action._id,
|
||||||
@@ -53,9 +52,9 @@ function doActionWork({action, creature, target}){
|
|||||||
}];
|
}];
|
||||||
applyProperties({
|
applyProperties({
|
||||||
forest: startingForest,
|
forest: startingForest,
|
||||||
|
actionContext: context,
|
||||||
creature,
|
creature,
|
||||||
target,
|
target,
|
||||||
actionContext
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export default function writeCreatureVariables(memo, creatureId) {
|
|||||||
'name',
|
'name',
|
||||||
'attributeType',
|
'attributeType',
|
||||||
'baseValue',
|
'baseValue',
|
||||||
|
'spellSlotLevelValue',
|
||||||
'damage',
|
'damage',
|
||||||
'decimal',
|
'decimal',
|
||||||
'reset',
|
'reset',
|
||||||
|
|||||||
88
app/imports/api/creature/denormalise/recomputeInventory.js
Normal file
88
app/imports/api/creature/denormalise/recomputeInventory.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
|
||||||
|
import nodesToTree from '/imports/api/parenting/parenting.js';
|
||||||
|
|
||||||
|
export default function recomputeInventory(creatureId){
|
||||||
|
let inventoryForest = nodesToTree({
|
||||||
|
collection: CreatureProperties,
|
||||||
|
ancestorId: creatureId,
|
||||||
|
filter: {
|
||||||
|
type: {$in: ['container', 'item']},
|
||||||
|
},
|
||||||
|
deactivatedByAncestor: {$ne: true},
|
||||||
|
});
|
||||||
|
return getChildrenInventoryData(inventoryForest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChildrenInventoryData(forest){
|
||||||
|
let data = {
|
||||||
|
weightTotal: 0,
|
||||||
|
weightEquipment: 0,
|
||||||
|
weightCarried: 0,
|
||||||
|
valueTotal: 0,
|
||||||
|
valueEquipment: 0,
|
||||||
|
valueCarried: 0,
|
||||||
|
}
|
||||||
|
forest.forEach(tree => {
|
||||||
|
let treeData = getInventoryData(tree);
|
||||||
|
for (let key in data){
|
||||||
|
data[key] += treeData[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInventoryData(tree){
|
||||||
|
let data = {
|
||||||
|
weightTotal: 0,
|
||||||
|
weightEquipment: 0,
|
||||||
|
weightCarried: 0,
|
||||||
|
valueTotal: 0,
|
||||||
|
valueEquipment: 0,
|
||||||
|
valueCarried: 0,
|
||||||
|
itemsAttuned: 0,
|
||||||
|
}
|
||||||
|
let childData = getChildrenInventoryData(tree.children);
|
||||||
|
let node = tree.node;
|
||||||
|
if (node.type === 'container'){
|
||||||
|
data.weightTotal += node.weight;
|
||||||
|
data.valueTotal += node.value;
|
||||||
|
if (node.carried){
|
||||||
|
data.weightCarried += node.weight;
|
||||||
|
data.valueCarried += node.valueCarried;
|
||||||
|
}
|
||||||
|
storeContentsData(node, childData);
|
||||||
|
} else if (node.type === 'item'){
|
||||||
|
data.weightTotal += node.weight * node.quantity;
|
||||||
|
data.valueTotal += node.value * node.quantity;
|
||||||
|
data.weightCarried += node.weight * node.quantity;
|
||||||
|
data.valueCarried += node.valueCarried * node.quantity;
|
||||||
|
if (node.equipped){
|
||||||
|
data.weightEquipment += node.weight * node.quantity;
|
||||||
|
data.valueEquipment += node.valueCarried * node.quantity;
|
||||||
|
}
|
||||||
|
if (node.attuned){
|
||||||
|
data.itemsAttuned += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key in data){
|
||||||
|
data[key] += childData[key];
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeContentsData(node, childData){
|
||||||
|
let newContentsWeight;
|
||||||
|
if (node.contentsWeightless){
|
||||||
|
newContentsWeight = 0;
|
||||||
|
} else {
|
||||||
|
newContentsWeight = childData.weightCarried
|
||||||
|
}
|
||||||
|
if (node.contentsWeight !== newContentsWeight){
|
||||||
|
node.contentsWeight = newContentsWeight;
|
||||||
|
node.contentsWeightChanged = true;
|
||||||
|
}
|
||||||
|
let newContentsValue = childData.valueCarried;
|
||||||
|
if (node.contentsValue !== newContentsValue){
|
||||||
|
node.contentsValue = newContentsValue;
|
||||||
|
node.contentsValueChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,10 @@ const ComputedOnlyContainerSchema = new SimpleSchema({
|
|||||||
type: Number,
|
type: Number,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
contentsValue:{
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedContainerSchema = new SimpleSchema()
|
const ComputedContainerSchema = new SimpleSchema()
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export default {
|
|||||||
value: [String, Number, Date, Array, Object, Boolean],
|
value: [String, Number, Date, Array, Object, Boolean],
|
||||||
errorMessages: [String, Array],
|
errorMessages: [String, Array],
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
|
debounce: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
focused(newFocus){
|
focused(newFocus){
|
||||||
@@ -113,7 +117,9 @@ export default {
|
|||||||
return this.context.editPermission === false || this.disabled;
|
return this.context.editPermission === false || this.disabled;
|
||||||
},
|
},
|
||||||
debounceTime() {
|
debounceTime() {
|
||||||
if (Number.isFinite(this.context.debounceTime)){
|
if (Number.isFinite(this.debounce)){
|
||||||
|
return this.debounce;
|
||||||
|
} else if (Number.isFinite(this.context.debounceTime)){
|
||||||
return this.context.debounceTime;
|
return this.context.debounceTime;
|
||||||
} else {
|
} else {
|
||||||
return 750;
|
return 750;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
:error-messages="errors"
|
:error-messages="errors"
|
||||||
:value="safeValue"
|
:value="safeValue"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
box
|
:box="!regular"
|
||||||
@input="input"
|
@input="input"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false"
|
@blur="focused = false"
|
||||||
@@ -18,5 +18,8 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [SmartInput],
|
mixins: [SmartInput],
|
||||||
|
props: {
|
||||||
|
regular: Boolean,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -35,7 +35,11 @@
|
|||||||
class="fill-height"
|
class="fill-height"
|
||||||
>
|
>
|
||||||
<v-tabs-items
|
<v-tabs-items
|
||||||
v-model="activeTab"
|
:value="$store.state.characterSheetTabs[$route.params.id]"
|
||||||
|
@change="e => $store.commit(
|
||||||
|
'setTabForCharacterSheet',
|
||||||
|
{id: $route.params.id, tab: e}
|
||||||
|
)"
|
||||||
>
|
>
|
||||||
<v-tab-item>
|
<v-tab-item>
|
||||||
<stats-tab :creature-id="creatureId" />
|
<stats-tab :creature-id="creatureId" />
|
||||||
@@ -87,10 +91,6 @@
|
|||||||
creatureId: {
|
creatureId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
|
||||||
tabs: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
reactiveProvide: {
|
reactiveProvide: {
|
||||||
|
|||||||
@@ -78,11 +78,14 @@
|
|||||||
<v-tabs
|
<v-tabs
|
||||||
v-if="creature"
|
v-if="creature"
|
||||||
slot="extension"
|
slot="extension"
|
||||||
:value="value"
|
|
||||||
centered
|
centered
|
||||||
grow
|
grow
|
||||||
max="100px"
|
max="100px"
|
||||||
@change="e => $emit('input', e)"
|
:value="$store.state.characterSheetTabs[$route.params.id]"
|
||||||
|
@change="e => $store.commit(
|
||||||
|
'setTabForCharacterSheet',
|
||||||
|
{id: $route.params.id, tab: e}
|
||||||
|
)"
|
||||||
>
|
>
|
||||||
<v-tab>
|
<v-tab>
|
||||||
Stats
|
Stats
|
||||||
@@ -118,12 +121,6 @@ import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
|
|||||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data(){return {
|
data(){return {
|
||||||
theme,
|
theme,
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
<template lang="html">
|
|
||||||
<v-tabs
|
|
||||||
v-if="creature"
|
|
||||||
slot="extension"
|
|
||||||
color="secondary"
|
|
||||||
:value="value"
|
|
||||||
centered
|
|
||||||
grow
|
|
||||||
max="100px"
|
|
||||||
@change="e => $emit('input', e)"
|
|
||||||
>
|
|
||||||
<v-tab>
|
|
||||||
Stats
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
|
||||||
Features
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
|
||||||
Inventory
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
|
||||||
Spells
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
|
||||||
Character
|
|
||||||
</v-tab>
|
|
||||||
<v-tab>
|
|
||||||
Tree
|
|
||||||
</v-tab>
|
|
||||||
</v-tabs>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Creatures from '/imports/api/creature/Creatures.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
meteor: {
|
|
||||||
creature(){
|
|
||||||
return Creatures.findOne(this.$route.params.id);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
.v-tabs__container--grow .v-tabs__div {
|
|
||||||
max-width: 120px !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
<template lang="html">
|
|
||||||
<v-toolbar-items v-if="creature">
|
|
||||||
<v-btn
|
|
||||||
v-if="editPermission"
|
|
||||||
flat
|
|
||||||
icon
|
|
||||||
@click="recompute(creature._id)"
|
|
||||||
>
|
|
||||||
<v-icon>refresh</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-menu
|
|
||||||
bottom
|
|
||||||
left
|
|
||||||
transition="slide-y-transition"
|
|
||||||
data-id="creature-menu"
|
|
||||||
>
|
|
||||||
<template #activator="{ on }">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
v-on="on"
|
|
||||||
>
|
|
||||||
<v-icon>more_vert</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list v-if="editPermission">
|
|
||||||
<v-list-tile @click="deleteCharacter">
|
|
||||||
<v-list-tile-title>
|
|
||||||
<v-icon>delete</v-icon> Delete
|
|
||||||
</v-list-tile-title>
|
|
||||||
</v-list-tile>
|
|
||||||
<v-list-tile @click="showCharacterForm">
|
|
||||||
<v-list-tile-title>
|
|
||||||
<v-icon>create</v-icon> Edit details
|
|
||||||
</v-list-tile-title>
|
|
||||||
</v-list-tile>
|
|
||||||
<v-list-tile @click="showShareDialog">
|
|
||||||
<v-list-tile-title>
|
|
||||||
<v-icon>share</v-icon> Sharing
|
|
||||||
</v-list-tile-title>
|
|
||||||
</v-list-tile>
|
|
||||||
</v-list>
|
|
||||||
<v-list v-else>
|
|
||||||
<v-list-tile @click="unshareWithMe">
|
|
||||||
<v-list-tile-title>
|
|
||||||
<v-icon>delete</v-icon> Unshare with me
|
|
||||||
</v-list-tile-title>
|
|
||||||
</v-list-tile>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</v-toolbar-items>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Creatures from '/imports/api/creature/Creatures.js';
|
|
||||||
import removeCreature from '/imports/api/creature/removeCreature.js';
|
|
||||||
import { mapMutations } from 'vuex';
|
|
||||||
import { theme } from '/imports/ui/theme.js';
|
|
||||||
import { recomputeCreature } from '/imports/api/creature/computation/recomputeCreature.js';
|
|
||||||
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
|
|
||||||
import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data(){return {
|
|
||||||
theme,
|
|
||||||
}},
|
|
||||||
computed: {
|
|
||||||
creatureId(){
|
|
||||||
return this.$route.params.id;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapMutations([
|
|
||||||
'toggleDrawer',
|
|
||||||
]),
|
|
||||||
recompute(charId){
|
|
||||||
recomputeCreature.call({charId});
|
|
||||||
},
|
|
||||||
showCharacterForm(){
|
|
||||||
this.$store.commit('pushDialogStack', {
|
|
||||||
component: 'creature-form-dialog',
|
|
||||||
elementId: 'creature-menu',
|
|
||||||
data: {
|
|
||||||
_id: this.creatureId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
showShareDialog(){
|
|
||||||
this.$store.commit('pushDialogStack', {
|
|
||||||
component: 'share-dialog',
|
|
||||||
elementId: 'creature-menu',
|
|
||||||
data: {
|
|
||||||
docRef: {
|
|
||||||
id: this.creatureId,
|
|
||||||
collection: 'creatures',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
deleteCharacter(){
|
|
||||||
let that = this;
|
|
||||||
this.$store.commit('pushDialogStack', {
|
|
||||||
component: 'delete-confirmation-dialog',
|
|
||||||
elementId: 'creature-menu',
|
|
||||||
data: {
|
|
||||||
name: this.creature.name,
|
|
||||||
typeName: 'Character'
|
|
||||||
},
|
|
||||||
callback(confirmation){
|
|
||||||
if(!confirmation) return;
|
|
||||||
removeCreature.call({charId: that.creatureId}, (error) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error);
|
|
||||||
} else {
|
|
||||||
that.$router.push('/characterList');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
unshareWithMe(){
|
|
||||||
updateUserSharePermissions.call({
|
|
||||||
docRef: {
|
|
||||||
collection: 'creatures',
|
|
||||||
id: this.creatureId,
|
|
||||||
},
|
|
||||||
userId: Meteor.userId(),
|
|
||||||
role: 'none',
|
|
||||||
}, (error) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error);
|
|
||||||
} else {
|
|
||||||
this.$router.push('/characterList');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
meteor: {
|
|
||||||
$subscribe: {
|
|
||||||
'singleCharacter'(){
|
|
||||||
return [this.creatureId];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
creature(){
|
|
||||||
return Creatures.findOne(this.creatureId);
|
|
||||||
},
|
|
||||||
editPermission(){
|
|
||||||
try {
|
|
||||||
assertEditPermission(this.creature, Meteor.userId());
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
</style>
|
|
||||||
@@ -119,38 +119,18 @@ import Slots from '/imports/ui/creature/slots/Slots.vue';
|
|||||||
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ColumnLayout,
|
ColumnLayout,
|
||||||
NoteCard,
|
NoteCard,
|
||||||
Slots,
|
Slots,
|
||||||
ToolbarCard,
|
ToolbarCard,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
creatureId: {
|
creatureId: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
meteor: {
|
|
||||||
notes(){
|
|
||||||
return CreatureProperties.find({
|
|
||||||
'ancestors.id': this.creatureId,
|
|
||||||
type: 'note',
|
|
||||||
removed: {$ne: true},
|
|
||||||
}, {
|
|
||||||
sort: {order: 1},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
creature(){
|
|
||||||
return Creatures.findOne(this.creatureId);
|
|
||||||
},
|
|
||||||
classLevels(){
|
|
||||||
return getActiveProperties({
|
|
||||||
ancestorId: this.creatureId,
|
|
||||||
filter: {type: 'classLevel'},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
highestClassLevels(){
|
highestClassLevels(){
|
||||||
let highestLevels = {};
|
let highestLevels = {};
|
||||||
@@ -171,48 +151,74 @@ export default {
|
|||||||
return highestLevelsList;
|
return highestLevelsList;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
mounted(){
|
||||||
showCharacterForm(){
|
if (this.$store.state.showBuildDialog){
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('setShowBuildDialog', false);
|
||||||
component: 'creature-form-dialog',
|
this.showSlotDialog();
|
||||||
elementId: 'creature-summary',
|
}
|
||||||
data: {
|
},
|
||||||
_id: this.creatureId,
|
meteor: {
|
||||||
},
|
notes(){
|
||||||
});
|
return CreatureProperties.find({
|
||||||
},
|
'ancestors.id': this.creatureId,
|
||||||
|
type: 'note',
|
||||||
|
removed: {$ne: true},
|
||||||
|
}, {
|
||||||
|
sort: {order: 1},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
creature(){
|
||||||
|
return Creatures.findOne(this.creatureId);
|
||||||
|
},
|
||||||
|
classLevels(){
|
||||||
|
return getActiveProperties({
|
||||||
|
ancestorId: this.creatureId,
|
||||||
|
filter: {type: 'classLevel'},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showCharacterForm(){
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'creature-form-dialog',
|
||||||
|
elementId: 'creature-summary',
|
||||||
|
data: {
|
||||||
|
_id: this.creatureId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
addExperience(){
|
addExperience(){
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'experience-insert-dialog',
|
component: 'experience-insert-dialog',
|
||||||
elementId: 'experience-add-button',
|
elementId: 'experience-add-button',
|
||||||
data: {
|
data: {
|
||||||
creatureIds: [this.creatureId],
|
creatureIds: [this.creatureId],
|
||||||
startAsMilestone: this.creature.variables.milestoneLevels &&
|
startAsMilestone: this.creature.variables.milestoneLevels &&
|
||||||
!!this.creature.variables.milestoneLevels.value,
|
!!this.creature.variables.milestoneLevels.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
showExperienceList(){
|
showExperienceList(){
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'experience-list-dialog',
|
component: 'experience-list-dialog',
|
||||||
elementId: 'experience-info-button',
|
elementId: 'experience-info-button',
|
||||||
data: {
|
data: {
|
||||||
creatureId: this.creatureId,
|
creatureId: this.creatureId,
|
||||||
startAsMilestone: this.creature.variables.milestoneLevels &&
|
startAsMilestone: this.creature.variables.milestoneLevels &&
|
||||||
!!this.creature.variables.milestoneLevels.value,
|
!!this.creature.variables.milestoneLevels.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
showSlotDialog(){
|
showSlotDialog(){
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'slot-details-dialog',
|
component: 'slot-details-dialog',
|
||||||
elementId: 'slot-card',
|
elementId: 'slot-card',
|
||||||
data: {
|
data: {
|
||||||
creatureId: this.creatureId,
|
creatureId: this.creatureId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -149,15 +149,18 @@
|
|||||||
class="spell-slots"
|
class="spell-slots"
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-list>
|
<v-list
|
||||||
|
two-line
|
||||||
|
subheader
|
||||||
|
>
|
||||||
<v-subheader>Spell Slots</v-subheader>
|
<v-subheader>Spell Slots</v-subheader>
|
||||||
<spell-slot-list-tile
|
<spell-slot-list-tile
|
||||||
v-for="spellSlot in spellSlots"
|
v-for="spellSlot in spellSlots"
|
||||||
:key="spellSlot._id"
|
:key="spellSlot._id"
|
||||||
v-bind="spellSlot"
|
:model="spellSlot"
|
||||||
:data-id="spellSlot._id"
|
:data-id="spellSlot._id"
|
||||||
@click="clickProperty({_id: spellSlot._id})"
|
@click="clickProperty({_id: spellSlot._id})"
|
||||||
@change="e => incrementChange(spellSlot._id, e)"
|
@cast="castSpellWithSlot(spellSlot._id)"
|
||||||
/>
|
/>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -320,6 +323,7 @@
|
|||||||
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
|
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
|
||||||
import RestButton from '/imports/ui/creature/RestButton.vue';
|
import RestButton from '/imports/ui/creature/RestButton.vue';
|
||||||
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
|
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
|
||||||
|
import castSpellWithSlot from '/imports/api/creature/actions/castSpellWithSlot.js';
|
||||||
|
|
||||||
const getProperties = function(creature, filter,){
|
const getProperties = function(creature, filter,){
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
@@ -448,6 +452,22 @@
|
|||||||
softRemoveProperty.call({_id}, error => {
|
softRemoveProperty.call({_id}, error => {
|
||||||
if (error) console.error(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);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
v-if="!slot.quantityExpected || slot.spaceLeft"
|
v-if="!slot.quantityExpected || slot.spaceLeft"
|
||||||
icon
|
icon
|
||||||
:data-id="`slot-add-button-${slot._id}`"
|
:data-id="`slot-add-button-${slot._id}`"
|
||||||
|
class="slot-add-button"
|
||||||
style="background-color: inherit;"
|
style="background-color: inherit;"
|
||||||
@click="fillSlot(slot)"
|
@click="fillSlot(slot)"
|
||||||
>
|
>
|
||||||
@@ -131,16 +132,25 @@ export default {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
}).map(slot => {
|
}).map(slot => {
|
||||||
slot.children = CreatureProperties.find({
|
if (
|
||||||
'parent.id': slot._id,
|
!this.showHiddenSlots &&
|
||||||
removed: {$ne: true},
|
slot.quantityExpected === 0 &&
|
||||||
}, {
|
slot.hideWhenFull
|
||||||
sort: { order: 1 },
|
){
|
||||||
}).fetch();
|
slot.children = []
|
||||||
|
} else {
|
||||||
|
slot.children = CreatureProperties.find({
|
||||||
|
'parent.id': slot._id,
|
||||||
|
removed: {$ne: true},
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 },
|
||||||
|
}).fetch();
|
||||||
|
}
|
||||||
return slot;
|
return slot;
|
||||||
}).filter(slot => !( // Hide full and ignored slots
|
}).filter(slot => !( // Hide full and ignored slots
|
||||||
!this.showHiddenSlots &&
|
!this.showHiddenSlots &&
|
||||||
slot.hideWhenFull &&
|
slot.hideWhenFull &&
|
||||||
|
slot.quantityExpected > 0 &&
|
||||||
slot.totalFilled >= slot.quantityExpected ||
|
slot.totalFilled >= slot.quantityExpected ||
|
||||||
slot.ignored
|
slot.ignored
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
|
||||||
import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue';
|
import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue';
|
||||||
import CreaturePropertyCreationDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue';
|
import CreaturePropertyCreationDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue';
|
||||||
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.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';
|
import UsernameDialog from '/imports/ui/user/UsernameDialog.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
CastSpellWithSlotDialog,
|
||||||
CreatureFormDialog,
|
CreatureFormDialog,
|
||||||
CreaturePropertyCreationDialog,
|
CreaturePropertyCreationDialog,
|
||||||
CreaturePropertyDialog,
|
CreaturePropertyDialog,
|
||||||
|
|||||||
@@ -1,215 +1,223 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-layout class="dialog-stack" align-center justify-center>
|
<v-layout
|
||||||
<transition name="backdrop-fade">
|
class="dialog-stack"
|
||||||
<div
|
align-center
|
||||||
class="backdrop"
|
justify-center
|
||||||
@click="backdropClicked"
|
>
|
||||||
v-if="dialogs.length"
|
<transition name="backdrop-fade">
|
||||||
></div>
|
<div
|
||||||
</transition>
|
v-if="dialogs.length"
|
||||||
|
class="backdrop"
|
||||||
|
@click="backdropClicked"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
<transition-group
|
<transition-group
|
||||||
name="dialog-list"
|
name="dialog-list"
|
||||||
class="dialog-sizer"
|
class="dialog-sizer"
|
||||||
tag="div"
|
tag="div"
|
||||||
@enter="enter"
|
@enter="enter"
|
||||||
@leave="leave"
|
@leave="leave"
|
||||||
>
|
>
|
||||||
<v-card
|
<v-card
|
||||||
v-for="(dialog, index) in dialogs"
|
v-for="(dialog, index) in dialogs"
|
||||||
:key="dialog._id"
|
:key="dialog._id"
|
||||||
|
:ref="index"
|
||||||
class="dialog"
|
class="dialog"
|
||||||
:data-element-id="dialog.elementId"
|
:data-element-id="dialog.elementId"
|
||||||
:data-index="index"
|
:data-index="index"
|
||||||
:ref="index"
|
:style="getDialogStyle(index)"
|
||||||
:style="getDialogStyle(index)"
|
:elevation="6"
|
||||||
:elevation="6"
|
|
||||||
>
|
>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<component :is="dialog.component" v-bind="dialog.data" @pop="popDialogStack($event)" class="dialog-component"></component>
|
<component
|
||||||
</transition>
|
:is="dialog.component"
|
||||||
|
v-bind="dialog.data"
|
||||||
|
class="dialog-component"
|
||||||
|
@pop="popDialogStack($event)"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
</v-card>
|
</v-card>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from "vue";
|
import '/imports/ui/dialogStack/dialogStackWindowEvents.js';
|
||||||
import anime from "animejs";
|
import mockElement from '/imports/ui/dialogStack/mockElement.js';
|
||||||
import "/imports/ui/dialogStack/dialogStackWindowEvents.js";
|
import DialogComponentIndex from '/imports/ui/dialogStack/DialogComponentIndex.js';
|
||||||
import mockElement from '/imports/ui/dialogStack/mockElement.js';
|
|
||||||
import DialogComponentIndex from '/imports/ui/dialogStack/DialogComponentIndex.js';
|
|
||||||
|
|
||||||
const OFFSET = 16;
|
const OFFSET = 16;
|
||||||
const MOCK_DURATION = 400; // Keep in sync with css transition of .dialog
|
const MOCK_DURATION = 400; // Keep in sync with css transition of .dialog
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: DialogComponentIndex,
|
components: DialogComponentIndex,
|
||||||
computed: {
|
computed: {
|
||||||
dialogs(){
|
dialogs(){
|
||||||
return this.$store.state.dialogStack.dialogs;
|
return this.$store.state.dialogStack.dialogs;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
dialogs(newDialogs) {
|
||||||
|
let el = document.documentElement;
|
||||||
|
if (newDialogs.length) {
|
||||||
|
this.top = el.scrollTop;
|
||||||
|
if (el.scrollHeight > el.clientHeight){
|
||||||
|
el.style.position = 'fixed';
|
||||||
|
el.style.top = `${-this.top}px`;
|
||||||
|
el.style.left = 0;
|
||||||
|
el.style.right = 0;
|
||||||
|
el.style.overflowY = 'scroll';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
el.style.position = null;
|
||||||
|
el.style.top = null;
|
||||||
|
el.style.left = null;
|
||||||
|
el.style.right = null;
|
||||||
|
el.style.overflowY = null;
|
||||||
|
el.scrollTop = this.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
popDialogStack(result){
|
popDialogStack(result){
|
||||||
this.$store.dispatch("popDialogStack", result);
|
this.$store.dispatch('popDialogStack', result);
|
||||||
},
|
},
|
||||||
backdropClicked(event){
|
backdropClicked(event){
|
||||||
if (event.target === event.currentTarget) this.popDialogStack();
|
if (event.target === event.currentTarget) this.popDialogStack();
|
||||||
},
|
},
|
||||||
getDialogStyle(index){
|
getDialogStyle(index){
|
||||||
const length = this.$store.state.dialogStack.dialogs.length;
|
const length = this.$store.state.dialogStack.dialogs.length;
|
||||||
if (index >= length) return;
|
if (index >= length) return;
|
||||||
const num = length - 1;
|
const num = length - 1;
|
||||||
const left = (num - index) * -OFFSET;
|
const left = (num - index) * -OFFSET;
|
||||||
const top = (num - index) * -OFFSET;
|
const top = (num - index) * -OFFSET;
|
||||||
return `left:${left}px; top:${top}px;`;
|
return `left:${left}px; top:${top}px;`;
|
||||||
},
|
},
|
||||||
getTopElementByDataId(elementId, offset = 0){
|
getTopElementByDataId(elementId, offset = 0){
|
||||||
let stackLength = this.$store.state.dialogStack.dialogs.length - offset;
|
let stackLength = this.$store.state.dialogStack.dialogs.length - offset;
|
||||||
if (stackLength){
|
if (stackLength){
|
||||||
let topDialog = this.$refs[stackLength - 1][0];
|
let topDialog = this.$refs[stackLength - 1][0];
|
||||||
return topDialog.$el.querySelector(`[data-id='${elementId}']`);
|
return topDialog.$el.querySelector(`[data-id='${elementId}']`);
|
||||||
} else {
|
|
||||||
return document.querySelector(`[data-id='${elementId}']`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enter(target, done){
|
|
||||||
if (!target || !target.attributes['data-element-id']){
|
|
||||||
done();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let elementId = target.attributes['data-element-id'].value;
|
|
||||||
let source = this.getTopElementByDataId(elementId, 1);
|
|
||||||
if (!source){
|
|
||||||
done();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Get the original styles so we can repair them later
|
|
||||||
let originalStyle = {
|
|
||||||
transform: target.style.transform,
|
|
||||||
backgroundColor: target.style.backgroundColor,
|
|
||||||
borderRadius: target.style.borderRadius,
|
|
||||||
transition: target.style.transition,
|
|
||||||
boxShadow: target.style.boxShadow,
|
|
||||||
sourceTransition: source.style.transition,
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide the source
|
|
||||||
source.style.transition = "none";
|
|
||||||
source.style.opacity = "0";
|
|
||||||
this.hiddenElement = source;
|
|
||||||
|
|
||||||
// Instantly mock the source
|
|
||||||
target.style.transition = 'none';
|
|
||||||
mockElement({source, target});
|
|
||||||
|
|
||||||
// on the next animation frame, repair the styles
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
target.style.transform = originalStyle.transform;
|
|
||||||
target.style.backgroundColor = originalStyle.backgroundColor;
|
|
||||||
target.style.borderRadius = originalStyle.borderRadius;
|
|
||||||
target.style.transition = originalStyle.transition;
|
|
||||||
target.style.boxShadow = originalStyle.boxShadow;
|
|
||||||
source.style.transition = originalStyle.sourceTransition;
|
|
||||||
setTimeout(done, MOCK_DURATION);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
leave(target, done){
|
|
||||||
// Give minimongo time to update documents we might need to animate to
|
|
||||||
setTimeout(() => this.doLeave(target, done));
|
|
||||||
},
|
|
||||||
doLeave(target, done){
|
|
||||||
let elementId;
|
|
||||||
let returnElementId = this.$store.state.dialogStack.currentReturnElement;
|
|
||||||
if (returnElementId) {
|
|
||||||
elementId = returnElementId;
|
|
||||||
} else {
|
|
||||||
if (!target || !target.attributes['data-element-id']){
|
|
||||||
done();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
elementId = target.attributes['data-element-id'].value;
|
|
||||||
}
|
|
||||||
let source = this.getTopElementByDataId(elementId);
|
|
||||||
if (!source){
|
|
||||||
console.warn(`Can't find source for ${elementId}`);
|
|
||||||
done();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let index = target.attributes['data-index'].value;
|
|
||||||
if (index != 0){
|
|
||||||
// If we aren't the only dialog, we'll need compensate for offset
|
|
||||||
mockElement({source, target, offset: {x: OFFSET, y: OFFSET}})
|
|
||||||
} else {
|
|
||||||
mockElement({source, target});
|
|
||||||
}
|
|
||||||
// If the source and the hidden Element are different
|
|
||||||
// hide the source and reveal the hidden element
|
|
||||||
let originalSourceTransition = source.style.transition;
|
|
||||||
if (this.hiddenElement !== source){
|
|
||||||
source.style.transition = "none";
|
|
||||||
source.style.opacity = "0";
|
|
||||||
this.hiddenElement.style.opacity = null;
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
source.style.opacity = null;
|
|
||||||
source.style.transition = 'none';
|
|
||||||
target.style.transition = `opacity ${MOCK_DURATION / 4}ms, pointer-events 0s`
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
source.style.transition = originalSourceTransition;
|
|
||||||
target.style.opacity = "0";
|
|
||||||
target.style.pointerEvents = "none";
|
|
||||||
target.style.setProperty('box-shadow', "none", 'important');
|
|
||||||
setTimeout(done, MOCK_DURATION / 4);
|
|
||||||
});
|
|
||||||
}, MOCK_DURATION);
|
|
||||||
},
|
|
||||||
noScroll(e){
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
dialogs(newDialogs) {
|
|
||||||
let el = document.documentElement;
|
|
||||||
if (newDialogs.length) {
|
|
||||||
this.top = el.scrollTop;
|
|
||||||
if (el.scrollHeight > el.clientHeight){
|
|
||||||
el.style.position = 'fixed';
|
|
||||||
el.style.top = `${-this.top}px`;
|
|
||||||
el.style.left = 0;
|
|
||||||
el.style.right = 0;
|
|
||||||
el.style.overflowY = 'scroll';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
el.style.position = null;
|
return document.querySelector(`[data-id='${elementId}']`);
|
||||||
el.style.top = null;
|
|
||||||
el.style.left = null;
|
|
||||||
el.style.right = null;
|
|
||||||
el.style.overflowY = null;
|
|
||||||
el.scrollTop = this.top;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
enter(target, done){
|
||||||
|
if (!target || !target.attributes['data-element-id']){
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let elementId = target.attributes['data-element-id'].value;
|
||||||
|
let source = this.getTopElementByDataId(elementId, 1);
|
||||||
|
if (!source){
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get the original styles so we can repair them later
|
||||||
|
let originalStyle = {
|
||||||
|
transform: target.style.transform,
|
||||||
|
backgroundColor: target.style.backgroundColor,
|
||||||
|
borderRadius: target.style.borderRadius,
|
||||||
|
transition: target.style.transition,
|
||||||
|
boxShadow: target.style.boxShadow,
|
||||||
|
sourceTransition: source.style.transition,
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide the source
|
||||||
|
source.style.transition = 'none';
|
||||||
|
source.style.opacity = '0';
|
||||||
|
this.hiddenElement = source;
|
||||||
|
|
||||||
|
// Instantly mock the source
|
||||||
|
target.style.transition = 'none';
|
||||||
|
mockElement({source, target});
|
||||||
|
|
||||||
|
// on the next animation frame, repair the styles
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
target.style.transform = originalStyle.transform;
|
||||||
|
target.style.backgroundColor = originalStyle.backgroundColor;
|
||||||
|
target.style.borderRadius = originalStyle.borderRadius;
|
||||||
|
target.style.transition = originalStyle.transition;
|
||||||
|
target.style.boxShadow = originalStyle.boxShadow;
|
||||||
|
source.style.transition = originalStyle.sourceTransition;
|
||||||
|
setTimeout(done, MOCK_DURATION);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
leave(target, done){
|
||||||
|
// Give minimongo time to update documents we might need to animate to
|
||||||
|
setTimeout(() => this.doLeave(target, done));
|
||||||
|
},
|
||||||
|
doLeave(target, done){
|
||||||
|
let elementId;
|
||||||
|
let returnElementId = this.$store.state.dialogStack.currentReturnElement;
|
||||||
|
if (returnElementId) {
|
||||||
|
elementId = returnElementId;
|
||||||
|
} else {
|
||||||
|
if (!target || !target.attributes['data-element-id']){
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
elementId = target.attributes['data-element-id'].value;
|
||||||
|
}
|
||||||
|
let source = this.getTopElementByDataId(elementId);
|
||||||
|
if (!source){
|
||||||
|
console.warn(`Can't find source for ${elementId}`);
|
||||||
|
this.hiddenElement.style.opacity = null;
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let index = target.attributes['data-index'].value;
|
||||||
|
if (index != 0){
|
||||||
|
// If we aren't the only dialog, we'll need compensate for offset
|
||||||
|
mockElement({source, target, offset: {x: OFFSET, y: OFFSET}})
|
||||||
|
} else {
|
||||||
|
mockElement({source, target});
|
||||||
|
}
|
||||||
|
// If the source and the hidden Element are different
|
||||||
|
// hide the source and reveal the hidden element
|
||||||
|
let originalSourceTransition = source.style.transition;
|
||||||
|
if (this.hiddenElement !== source){
|
||||||
|
source.style.transition = 'none';
|
||||||
|
source.style.opacity = '0';
|
||||||
|
this.hiddenElement.style.opacity = null;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
source.style.opacity = null;
|
||||||
|
source.style.transition = 'none';
|
||||||
|
target.style.transition = `opacity ${MOCK_DURATION / 4}ms, pointer-events 0s`
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
source.style.transition = originalSourceTransition;
|
||||||
|
target.style.opacity = '0';
|
||||||
|
target.style.pointerEvents = 'none';
|
||||||
|
target.style.setProperty('box-shadow', 'none', 'important');
|
||||||
|
setTimeout(done, MOCK_DURATION / 4);
|
||||||
|
});
|
||||||
|
}, MOCK_DURATION);
|
||||||
|
},
|
||||||
|
noScroll(e){
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.backdrop {
|
.backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
}
|
}
|
||||||
.backdrop-fade-enter-active, .backdrop-fade-leave-active {
|
.backdrop-fade-enter-active, .backdrop-fade-leave-active {
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
.backdrop-fade-enter, .backdrop-fade-leave-to {
|
.backdrop-fade-enter, .backdrop-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.dialog-stack {
|
.dialog-stack {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -217,56 +225,56 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
.dialog-sizer {
|
.dialog-sizer {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
width: calc(100% - 64px);
|
width: calc(100% - 64px);
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
height: calc(100% - 64px);
|
height: calc(100% - 64px);
|
||||||
max-height: 800px;
|
max-height: 800px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
flex: initial;
|
flex: initial;
|
||||||
}
|
}
|
||||||
/* sm */
|
/* sm */
|
||||||
@media only screen and (max-width: 960px) and (min-width: 601px){
|
@media only screen and (max-width: 960px) and (min-width: 601px){
|
||||||
.dialog-sizer {
|
.dialog-sizer {
|
||||||
width: calc(100% - 32px);
|
width: calc(100% - 32px);
|
||||||
height: calc(100% - 32px);
|
height: calc(100% - 32px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* xs */
|
/* xs */
|
||||||
@media only screen and (max-width: 600px) {
|
@media only screen and (max-width: 600px) {
|
||||||
.dialog-sizer {
|
.dialog-sizer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component {
|
.dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.dialog-list-enter-active .dialog-component {
|
.dialog-list-enter-active .dialog-component {
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
.dialog-list-leave-active .dialog-component {
|
.dialog-list-leave-active .dialog-component {
|
||||||
transition: opacity 0.3s 0.1s;
|
transition: opacity 0.3s 0.1s;
|
||||||
}
|
}
|
||||||
.dialog-list-enter-active {
|
.dialog-list-enter-active {
|
||||||
transition: all 0.4s, box-shadow 0.1s;
|
transition: all 0.4s, box-shadow 0.1s;
|
||||||
}
|
}
|
||||||
.dialog-list-leave-active {
|
.dialog-list-leave-active {
|
||||||
transition: all 0.4s, box-shadow 0.1s 0.3s, opacity 0.1s, pointer-events 0s;
|
transition: all 0.4s, box-shadow 0.1s 0.3s, opacity 0.1s, pointer-events 0s;
|
||||||
}
|
}
|
||||||
.dialog {
|
.dialog {
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
pointer-events: initial;
|
pointer-events: initial;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.dialog > * {
|
.dialog > * {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -48,8 +48,11 @@ export default function mockElement({source, target, offset = {x: 0, y: 0}}){
|
|||||||
// Mock the source
|
// Mock the source
|
||||||
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
|
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
|
||||||
`scale(${deltaWidth}, ${deltaHeight})`;
|
`scale(${deltaWidth}, ${deltaHeight})`;
|
||||||
|
// Mock the background color unless it's completely transparent
|
||||||
target.style.backgroundColor = getComputedStyle(source).backgroundColor;
|
let backgroundColor = getComputedStyle(source).backgroundColor
|
||||||
|
if (backgroundColor !== 'rgba(0, 0, 0, 0)'){
|
||||||
|
target.style.backgroundColor = backgroundColor;
|
||||||
|
}
|
||||||
// Edge might not combine all border radii into a single value,
|
// Edge might not combine all border radii into a single value,
|
||||||
// So we just sample the top left one if we need to
|
// So we just sample the top left one if we need to
|
||||||
let oldRadius = getComputedStyle(source).borderRadius ||
|
let oldRadius = getComputedStyle(source).borderRadius ||
|
||||||
@@ -60,4 +63,4 @@ export default function mockElement({source, target, offset = {x: 0, y: 0}}){
|
|||||||
getComputedStyle(source).boxShadow, deltaWidth, deltaHeight
|
getComputedStyle(source).boxShadow, deltaWidth, deltaHeight
|
||||||
);
|
);
|
||||||
target.style.setProperty('box-shadow', boxShadow, 'important');
|
target.style.setProperty('box-shadow', boxShadow, 'important');
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<router-view
|
<router-view
|
||||||
v-model="tabs"
|
|
||||||
name="toolbar"
|
name="toolbar"
|
||||||
/>
|
/>
|
||||||
<v-toolbar
|
<v-toolbar
|
||||||
@@ -50,7 +49,6 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<router-view
|
<router-view
|
||||||
v-model="tabs"
|
|
||||||
name="toolbarExtension"
|
name="toolbarExtension"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +58,7 @@
|
|||||||
<v-fade-transition
|
<v-fade-transition
|
||||||
mode="out-in"
|
mode="out-in"
|
||||||
>
|
>
|
||||||
<router-view :tabs.sync="tabs" />
|
<router-view />
|
||||||
</v-fade-transition>
|
</v-fade-transition>
|
||||||
</v-content>
|
</v-content>
|
||||||
<router-view
|
<router-view
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<v-alert
|
|
||||||
icon="priority_high"
|
|
||||||
type="error"
|
|
||||||
dismissible
|
|
||||||
:value="true"
|
|
||||||
>
|
|
||||||
This version of DiceCloud is in beta. Some data stored here may be destroyed by
|
|
||||||
future updates.
|
|
||||||
</v-alert>
|
|
||||||
<v-layout
|
<v-layout
|
||||||
v-if="!signedIn"
|
v-if="!signedIn"
|
||||||
row
|
row
|
||||||
|
|||||||
@@ -141,7 +141,12 @@
|
|||||||
if (error){
|
if (error){
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({ path: `/character/${result}`})
|
this.$store.commit(
|
||||||
|
'setTabForCharacterSheet',
|
||||||
|
{id: result, tab: 4}
|
||||||
|
);
|
||||||
|
this.$store.commit('setShowBuildDialog', true);
|
||||||
|
this.$router.push({ path: `/character/${result}`});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
<character-sheet
|
<character-sheet
|
||||||
show-menu-button
|
show-menu-button
|
||||||
:creature-id="$route.params.id"
|
:creature-id="$route.params.id"
|
||||||
:tabs.sync="activeTab"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -12,21 +11,5 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
CharacterSheet,
|
CharacterSheet,
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
tabs: {
|
|
||||||
type: Number,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
activeTab: {
|
|
||||||
get(){
|
|
||||||
return this.tabs;
|
|
||||||
},
|
|
||||||
set(newTab){
|
|
||||||
this.$emit('update:tabs', newTab);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,86 +1,56 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-tile
|
<v-list-tile
|
||||||
class="spell-slot-list-tile"
|
class="spell-slot-list-tile"
|
||||||
:class="{hover}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<v-list-tile-action>
|
<v-list-tile-content>
|
||||||
<div
|
<v-list-tile-title>
|
||||||
v-if="value > 4"
|
|
||||||
class="layout row align-center"
|
|
||||||
>
|
|
||||||
<div class="buttons layout column justify-center">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
small
|
|
||||||
:disabled="
|
|
||||||
currentValue >= value ||
|
|
||||||
context.editPermission === false
|
|
||||||
"
|
|
||||||
@click="increment(1)"
|
|
||||||
>
|
|
||||||
<v-icon>arrow_drop_up</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
small
|
|
||||||
:disabled="
|
|
||||||
currentValue <= 0 ||
|
|
||||||
context.editPermission === false
|
|
||||||
"
|
|
||||||
@click="increment(-1)"
|
|
||||||
>
|
|
||||||
<v-icon>arrow_drop_down</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
|
v-if="model.value > 4"
|
||||||
class="layout row value"
|
class="layout row value"
|
||||||
style="align-items: baseline;"
|
style="align-items: baseline;"
|
||||||
>
|
>
|
||||||
<div class="display-1">
|
<div
|
||||||
{{ currentValue }}
|
style="font-weight: 500; font-size: 24px"
|
||||||
|
class="current-value"
|
||||||
|
>
|
||||||
|
{{ model.currentValue }}
|
||||||
</div>
|
</div>
|
||||||
<div class="title ml-2 max-value">
|
<div class="ml-2 max-value">
|
||||||
/{{ value }}
|
/{{ model.value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div
|
||||||
|
v-else
|
||||||
<div
|
class="layout row align-center slot-bubbles"
|
||||||
v-else
|
|
||||||
class="layout row align-center justify-end slot-buttons"
|
|
||||||
>
|
|
||||||
<v-btn
|
|
||||||
v-for="i in value"
|
|
||||||
:key="i"
|
|
||||||
icon
|
|
||||||
small
|
|
||||||
:disabled="
|
|
||||||
!(i === currentValue || i === currentValue + 1) ||
|
|
||||||
context.editPermission === false
|
|
||||||
"
|
|
||||||
@click="increment(i > currentValue ? 1 : -1)"
|
|
||||||
>
|
>
|
||||||
<v-icon>
|
<v-icon
|
||||||
|
v-for="i in model.value"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
{{
|
{{
|
||||||
i > currentValue ?
|
i > model.currentValue ?
|
||||||
'radio_button_unchecked' :
|
'radio_button_unchecked' :
|
||||||
'radio_button_checked'
|
'radio_button_checked'
|
||||||
}}
|
}}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</v-btn>
|
</div>
|
||||||
</div>
|
|
||||||
</v-list-tile-action>
|
|
||||||
|
|
||||||
<v-list-tile-content
|
|
||||||
class="content ml-2"
|
|
||||||
@click="click"
|
|
||||||
@mouseover="hover = true"
|
|
||||||
@mouseleave="hover = false"
|
|
||||||
>
|
|
||||||
<v-list-tile-title>
|
|
||||||
{{ name }}
|
|
||||||
</v-list-tile-title>
|
</v-list-tile-title>
|
||||||
|
<v-list-tile-sub-title>
|
||||||
|
{{ model.name }}
|
||||||
|
</v-list-tile-sub-title>
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
|
<v-list-tile-avatar v-if="!hideCastButton">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
flat
|
||||||
|
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-tile-avatar>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -88,18 +58,13 @@
|
|||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
_id: String,
|
model: {
|
||||||
name: String,
|
type: Object,
|
||||||
color: String,
|
required: true,
|
||||||
value: Number,
|
},
|
||||||
damage: {
|
dark: Boolean,
|
||||||
type: Number,
|
hideCastButton: Boolean,
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data(){ return{
|
|
||||||
hover: false,
|
|
||||||
}},
|
|
||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
},
|
},
|
||||||
@@ -107,15 +72,15 @@ export default {
|
|||||||
currentValue(){
|
currentValue(){
|
||||||
return this.value - this.damage;
|
return this.value - this.damage;
|
||||||
},
|
},
|
||||||
|
hasClickListener(){
|
||||||
|
return this.$listeners && !!this.$listeners.click;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
signed: numberToSignedString,
|
signed: numberToSignedString,
|
||||||
click(e){
|
click(e){
|
||||||
this.$emit('click', e);
|
this.$emit('click', e);
|
||||||
},
|
},
|
||||||
increment(value){
|
|
||||||
this.$emit('change', {type: 'increment', value})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -124,22 +89,9 @@ export default {
|
|||||||
.spell-slot-list-tile {
|
.spell-slot-list-tile {
|
||||||
background: inherit;
|
background: inherit;
|
||||||
}
|
}
|
||||||
.spell-slot-list-tile >>> .v-list__tile {
|
|
||||||
height: 56px;
|
|
||||||
}
|
|
||||||
.v-list__tile__action {
|
.v-list__tile__action {
|
||||||
width: 112px;
|
width: 112px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
|
||||||
.slot-buttons > .v-btn {
|
|
||||||
margin: 0;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.buttons > .v-btn {
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
.spell-slot-list-tile.hover {
|
.spell-slot-list-tile.hover {
|
||||||
background: #f5f5f5 !important;
|
background: #f5f5f5 !important;
|
||||||
@@ -156,4 +108,7 @@ export default {
|
|||||||
.theme--dark .max-value {
|
.theme--dark .max-value {
|
||||||
color: rgba(255, 255, 255, 0.54);
|
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
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<div
|
||||||
|
v-if="$vuetify.breakpoint.smAndUp"
|
||||||
|
class="layout row split"
|
||||||
|
>
|
||||||
|
<v-list
|
||||||
|
class="left"
|
||||||
|
subheader
|
||||||
|
two-line
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<v-slide-x-transition
|
||||||
|
group
|
||||||
|
leave-absolute
|
||||||
|
>
|
||||||
|
<slot name="left" />
|
||||||
|
</v-slide-x-transition>
|
||||||
|
</v-list>
|
||||||
|
<v-divider
|
||||||
|
class="mx-3"
|
||||||
|
vertical
|
||||||
|
/>
|
||||||
|
<v-list
|
||||||
|
class="right"
|
||||||
|
subheader
|
||||||
|
two-line
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<v-slide-x-transition
|
||||||
|
group
|
||||||
|
leave-absolute
|
||||||
|
>
|
||||||
|
<slot name="right" />
|
||||||
|
</v-slide-x-transition>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
<v-list
|
||||||
|
v-else
|
||||||
|
class="small"
|
||||||
|
subheader
|
||||||
|
two-line
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<v-slide-x-transition
|
||||||
|
group
|
||||||
|
leave-absolute
|
||||||
|
>
|
||||||
|
<slot name="left" />
|
||||||
|
<slot name="right" />
|
||||||
|
</v-slide-x-transition>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.split{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.left, .right {
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
flex-basis: 250px;
|
||||||
|
}
|
||||||
|
.right, .small {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
<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="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
|
||||||
|
flat
|
||||||
|
:class="{'primary--text': filtersApplied}"
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
<v-icon>filter_list</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-tile
|
||||||
|
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-tile>
|
||||||
|
<div class="layout row">
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
@click="clearBooleanFilters"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
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="title my-3"
|
||||||
|
>
|
||||||
|
Slot
|
||||||
|
</div>
|
||||||
|
<v-list-tile
|
||||||
|
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-tile-content>
|
||||||
|
<v-list-tile-title class="title">
|
||||||
|
Cantrip
|
||||||
|
</v-list-tile-title>
|
||||||
|
</v-list-tile-content>
|
||||||
|
</v-list-tile>
|
||||||
|
<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="title 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
|
||||||
|
flat
|
||||||
|
@click="$store.dispatch('popDialogStack')"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
flat
|
||||||
|
:disabled="!canCast"
|
||||||
|
class="primary--text"
|
||||||
|
@click="$store.dispatch('popDialogStack', {
|
||||||
|
spellId: selectedSpellId,
|
||||||
|
slotId: selectedSlotId,
|
||||||
|
})"
|
||||||
|
>
|
||||||
|
Cast
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</dialog-base>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
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.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},
|
||||||
|
prepared: 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>
|
||||||
@@ -40,24 +40,7 @@
|
|||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
||||||
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
|
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||||
|
import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
@@ -27,12 +27,22 @@
|
|||||||
@change="setPrepared"
|
@change="setPrepared"
|
||||||
/>
|
/>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-else-if="!hideHandle"
|
||||||
style="height: 100%; width: 40px; cursor: move;"
|
style="height: 100%; width: 40px; cursor: move;"
|
||||||
class="handle"
|
class="handle"
|
||||||
>
|
>
|
||||||
drag_indicator
|
drag_indicator
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
<v-btn
|
||||||
|
v-else-if="showInfoButton"
|
||||||
|
icon
|
||||||
|
flat
|
||||||
|
class="info-icon"
|
||||||
|
:data-id="`spell-info-btn-${model._id}`"
|
||||||
|
@click.stop="$emit('show-info')"
|
||||||
|
>
|
||||||
|
<v-icon>info</v-icon>
|
||||||
|
</v-btn>
|
||||||
</v-list-tile-action>
|
</v-list-tile-action>
|
||||||
</v-list-tile>
|
</v-list-tile>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,6 +55,8 @@ export default {
|
|||||||
mixins: [treeNodeViewMixin],
|
mixins: [treeNodeViewMixin],
|
||||||
props: {
|
props: {
|
||||||
preparingSpells: Boolean,
|
preparingSpells: Boolean,
|
||||||
|
hideHandle: Boolean,
|
||||||
|
showInfoButton: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
@@ -82,4 +94,10 @@ export default {
|
|||||||
.spell {
|
.spell {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
|
.primary--text .v-icon, .primary--text .v-list__tile__sub-title {
|
||||||
|
color: #b71c1c
|
||||||
|
}
|
||||||
|
.theme--light.info-icon{
|
||||||
|
color: rgba(0,0,0,.54) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
3
app/imports/ui/utility/escapeRegex.js
Normal file
3
app/imports/ui/utility/escapeRegex.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
RegExp.escape = function(s) {
|
||||||
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
};
|
||||||
@@ -14,6 +14,8 @@ const store = new Vuex.Store({
|
|||||||
drawer: undefined,
|
drawer: undefined,
|
||||||
rightDrawer: undefined,
|
rightDrawer: undefined,
|
||||||
pageTitle: undefined,
|
pageTitle: undefined,
|
||||||
|
characterSheetTabs: {},
|
||||||
|
showBuildDialog: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
toggleDrawer (state) {
|
toggleDrawer (state) {
|
||||||
@@ -32,6 +34,12 @@ const store = new Vuex.Store({
|
|||||||
state.pageTitle = value;
|
state.pageTitle = value;
|
||||||
document.title = value;
|
document.title = value;
|
||||||
},
|
},
|
||||||
|
setTabForCharacterSheet(state, {tab, id}){
|
||||||
|
Vue.set(state.characterSheetTabs, id, tab);
|
||||||
|
},
|
||||||
|
setShowBuildDialog(state, value){
|
||||||
|
state.showBuildDialog = value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user