Compare commits

...

9 Commits

31 changed files with 1026 additions and 653 deletions

View File

@@ -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');

View File

@@ -4,6 +4,7 @@ import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.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 { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
@@ -160,11 +161,25 @@ const insertCreature = new ValidatedMethod({
assertUserHasPaidBenefits(this.userId);
// Create the creature document
let charId = Creatures.insert({
let creatureId = Creatures.insert({
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();
return charId;
return creatureId;
},
});

View 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;

View File

@@ -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
});
}

View File

@@ -7,6 +7,7 @@ export default function writeCreatureVariables(memo, creatureId) {
'name',
'attributeType',
'baseValue',
'spellSlotLevelValue',
'damage',
'decimal',
'reset',

View 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;
}
}

View File

@@ -38,6 +38,10 @@ const ComputedOnlyContainerSchema = new SimpleSchema({
type: Number,
optional: true,
},
contentsValue:{
type: Number,
optional: true,
},
});
const ComputedContainerSchema = new SimpleSchema()

View File

@@ -26,6 +26,10 @@ export default {
value: [String, Number, Date, Array, Object, Boolean],
errorMessages: [String, Array],
disabled: Boolean,
debounce: {
type: Number,
default: undefined,
},
},
watch: {
focused(newFocus){
@@ -113,7 +117,9 @@ export default {
return this.context.editPermission === false || this.disabled;
},
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;
} else {
return 750;

View File

@@ -6,7 +6,7 @@
:error-messages="errors"
:value="safeValue"
:disabled="isDisabled"
box
:box="!regular"
@input="input"
@focus="focused = true"
@blur="focused = false"
@@ -18,5 +18,8 @@
export default {
mixins: [SmartInput],
props: {
regular: Boolean,
},
};
</script>

View File

@@ -35,7 +35,11 @@
class="fill-height"
>
<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>
<stats-tab :creature-id="creatureId" />
@@ -87,10 +91,6 @@
creatureId: {
type: String,
required: true,
},
tabs: {
type: Number,
required: true,
},
},
reactiveProvide: {

View File

@@ -78,11 +78,14 @@
<v-tabs
v-if="creature"
slot="extension"
:value="value"
centered
grow
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>
Stats
@@ -118,12 +121,6 @@ import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
export default {
props: {
value: {
type: Number,
required: true,
},
},
data(){return {
theme,
}},

View File

@@ -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>

View File

@@ -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>

View File

@@ -119,38 +119,18 @@ import Slots from '/imports/ui/creature/slots/Slots.vue';
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
export default {
components: {
ColumnLayout,
NoteCard,
components: {
ColumnLayout,
NoteCard,
Slots,
ToolbarCard,
},
props: {
creatureId: {
},
props: {
creatureId: {
type: String,
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: {
highestClassLevels(){
let highestLevels = {};
@@ -171,48 +151,74 @@ export default {
return highestLevelsList;
},
},
methods: {
showCharacterForm(){
this.$store.commit('pushDialogStack', {
component: 'creature-form-dialog',
elementId: 'creature-summary',
data: {
_id: this.creatureId,
},
});
},
mounted(){
if (this.$store.state.showBuildDialog){
this.$store.commit('setShowBuildDialog', false);
this.showSlotDialog();
}
},
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(){
this.$store.commit('pushDialogStack', {
component: 'experience-insert-dialog',
elementId: 'experience-add-button',
data: {
creatureIds: [this.creatureId],
component: 'experience-insert-dialog',
elementId: 'experience-add-button',
data: {
creatureIds: [this.creatureId],
startAsMilestone: this.creature.variables.milestoneLevels &&
!!this.creature.variables.milestoneLevels.value,
},
});
},
});
},
showExperienceList(){
this.$store.commit('pushDialogStack', {
component: 'experience-list-dialog',
elementId: 'experience-info-button',
data: {
creatureId: this.creatureId,
component: 'experience-list-dialog',
elementId: 'experience-info-button',
data: {
creatureId: this.creatureId,
startAsMilestone: this.creature.variables.milestoneLevels &&
!!this.creature.variables.milestoneLevels.value,
},
});
},
});
},
showSlotDialog(){
this.$store.commit('pushDialogStack', {
component: 'slot-details-dialog',
elementId: 'slot-card',
data: {
creatureId: this.creatureId,
},
});
component: 'slot-details-dialog',
elementId: 'slot-card',
data: {
creatureId: this.creatureId,
},
});
},
},
},
};
</script>

View File

@@ -149,15 +149,18 @@
class="spell-slots"
>
<v-card>
<v-list>
<v-list
two-line
subheader
>
<v-subheader>Spell Slots</v-subheader>
<spell-slot-list-tile
v-for="spellSlot in spellSlots"
:key="spellSlot._id"
v-bind="spellSlot"
:model="spellSlot"
:data-id="spellSlot._id"
@click="clickProperty({_id: spellSlot._id})"
@change="e => incrementChange(spellSlot._id, e)"
@cast="castSpellWithSlot(spellSlot._id)"
/>
</v-list>
</v-card>
@@ -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);
});
},
});
}
},
};

View File

@@ -41,6 +41,7 @@
v-if="!slot.quantityExpected || slot.spaceLeft"
icon
:data-id="`slot-add-button-${slot._id}`"
class="slot-add-button"
style="background-color: inherit;"
@click="fillSlot(slot)"
>
@@ -131,16 +132,25 @@ export default {
],
}
}).map(slot => {
slot.children = CreatureProperties.find({
'parent.id': slot._id,
removed: {$ne: true},
}, {
sort: { order: 1 },
}).fetch();
if (
!this.showHiddenSlots &&
slot.quantityExpected === 0 &&
slot.hideWhenFull
){
slot.children = []
} else {
slot.children = CreatureProperties.find({
'parent.id': slot._id,
removed: {$ne: true},
}, {
sort: { order: 1 },
}).fetch();
}
return slot;
}).filter(slot => !( // Hide full and ignored slots
!this.showHiddenSlots &&
slot.hideWhenFull &&
slot.quantityExpected > 0 &&
slot.totalFilled >= slot.quantityExpected ||
slot.ignored
));

View File

@@ -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,

View File

@@ -1,215 +1,223 @@
<template>
<v-layout class="dialog-stack" align-center justify-center>
<transition name="backdrop-fade">
<div
class="backdrop"
@click="backdropClicked"
v-if="dialogs.length"
></div>
</transition>
<v-layout
class="dialog-stack"
align-center
justify-center
>
<transition name="backdrop-fade">
<div
v-if="dialogs.length"
class="backdrop"
@click="backdropClicked"
/>
</transition>
<transition-group
name="dialog-list"
class="dialog-sizer"
tag="div"
@enter="enter"
@leave="leave"
>
name="dialog-list"
class="dialog-sizer"
tag="div"
@enter="enter"
@leave="leave"
>
<v-card
v-for="(dialog, index) in dialogs"
:key="dialog._id"
:ref="index"
class="dialog"
:data-element-id="dialog.elementId"
:data-index="index"
:ref="index"
:style="getDialogStyle(index)"
:elevation="6"
:data-element-id="dialog.elementId"
:data-index="index"
:style="getDialogStyle(index)"
:elevation="6"
>
<transition name="slide">
<component :is="dialog.component" v-bind="dialog.data" @pop="popDialogStack($event)" class="dialog-component"></component>
</transition>
<transition name="slide">
<component
:is="dialog.component"
v-bind="dialog.data"
class="dialog-component"
@pop="popDialogStack($event)"
/>
</transition>
</v-card>
</transition-group>
</v-layout>
</template>
<script>
import Vue from "vue";
import anime from "animejs";
import "/imports/ui/dialogStack/dialogStackWindowEvents.js";
import mockElement from '/imports/ui/dialogStack/mockElement.js';
import DialogComponentIndex from '/imports/ui/dialogStack/DialogComponentIndex.js';
import '/imports/ui/dialogStack/dialogStackWindowEvents.js';
import mockElement from '/imports/ui/dialogStack/mockElement.js';
import DialogComponentIndex from '/imports/ui/dialogStack/DialogComponentIndex.js';
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 {
components: DialogComponentIndex,
components: DialogComponentIndex,
computed: {
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: {
popDialogStack(result){
this.$store.dispatch("popDialogStack", result);
this.$store.dispatch('popDialogStack', result);
},
backdropClicked(event){
if (event.target === event.currentTarget) this.popDialogStack();
},
getDialogStyle(index){
const length = this.$store.state.dialogStack.dialogs.length;
if (index >= length) return;
const num = length - 1;
const left = (num - index) * -OFFSET;
const top = (num - index) * -OFFSET;
return `left:${left}px; top:${top}px;`;
if (index >= length) return;
const num = length - 1;
const left = (num - index) * -OFFSET;
const top = (num - index) * -OFFSET;
return `left:${left}px; top:${top}px;`;
},
getTopElementByDataId(elementId, offset = 0){
let stackLength = this.$store.state.dialogStack.dialogs.length - offset;
if (stackLength){
let topDialog = this.$refs[stackLength - 1][0];
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';
}
getTopElementByDataId(elementId, offset = 0){
let stackLength = this.$store.state.dialogStack.dialogs.length - offset;
if (stackLength){
let topDialog = this.$refs[stackLength - 1][0];
return topDialog.$el.querySelector(`[data-id='${elementId}']`);
} 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;
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}`);
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>
<style scoped>
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 4;
pointer-events: initial;
}
.backdrop-fade-enter-active, .backdrop-fade-leave-active {
transition: opacity 0.3s;
}
.backdrop-fade-enter, .backdrop-fade-leave-to {
opacity: 0;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.4);
z-index: 4;
pointer-events: initial;
}
.backdrop-fade-enter-active, .backdrop-fade-leave-active {
transition: opacity 0.3s;
}
.backdrop-fade-enter, .backdrop-fade-leave-to {
opacity: 0;
}
.dialog-stack {
position: fixed;
top: 0;
@@ -217,56 +225,56 @@
right: 0;
bottom: 0;
pointer-events: none;
z-index: 4;
z-index: 4;
}
.dialog-sizer {
position: relative;
width: 80%;
width: calc(100% - 64px);
width: 80%;
width: calc(100% - 64px);
max-width: 1000px;
height: 80%;
height: 80%;
height: calc(100% - 64px);
max-height: 800px;
z-index: 5;
flex: initial;
}
/* sm */
@media only screen and (max-width: 960px) and (min-width: 601px){
.dialog-sizer {
width: calc(100% - 32px);
height: calc(100% - 32px);
}
}
/* xs */
@media only screen and (max-width: 600px) {
.dialog-sizer {
width: 100%;
height: 100%;
}
}
.dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component {
opacity: 0;
}
.dialog-list-enter-active .dialog-component {
transition: opacity 0.3s;
}
.dialog-list-leave-active .dialog-component {
transition: opacity 0.3s 0.1s;
}
.dialog-list-enter-active {
transition: all 0.4s, box-shadow 0.1s;
}
.dialog-list-leave-active {
transition: all 0.4s, box-shadow 0.1s 0.3s, opacity 0.1s, pointer-events 0s;
}
/* sm */
@media only screen and (max-width: 960px) and (min-width: 601px){
.dialog-sizer {
width: calc(100% - 32px);
height: calc(100% - 32px);
}
}
/* xs */
@media only screen and (max-width: 600px) {
.dialog-sizer {
width: 100%;
height: 100%;
}
}
.dialog-list-enter .dialog-component, .dialog-list-leave-to .dialog-component {
opacity: 0;
}
.dialog-list-enter-active .dialog-component {
transition: opacity 0.3s;
}
.dialog-list-leave-active .dialog-component {
transition: opacity 0.3s 0.1s;
}
.dialog-list-enter-active {
transition: all 0.4s, box-shadow 0.1s;
}
.dialog-list-leave-active {
transition: all 0.4s, box-shadow 0.1s 0.3s, opacity 0.1s, pointer-events 0s;
}
.dialog {
transform-origin: top left;
transform-origin: top left;
position: absolute;
height: 100%;
width: 100%;
pointer-events: initial;
z-index: 1;
overflow: hidden;
z-index: 1;
overflow: hidden;
}
.dialog > * {
height: 100%;

View File

@@ -48,8 +48,11 @@ export default function mockElement({source, target, offset = {x: 0, y: 0}}){
// Mock the source
target.style.transform = `translate(${deltaLeft}px, ${deltaTop}px) ` +
`scale(${deltaWidth}, ${deltaHeight})`;
target.style.backgroundColor = getComputedStyle(source).backgroundColor;
// Mock the background color unless it's completely transparent
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,
// So we just sample the top left one if we need to
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
);
target.style.setProperty('box-shadow', boxShadow, 'important');
};
}

View File

@@ -10,7 +10,6 @@
<Sidebar />
</v-navigation-drawer>
<router-view
v-model="tabs"
name="toolbar"
/>
<v-toolbar
@@ -50,7 +49,6 @@
style="width: 100%"
>
<router-view
v-model="tabs"
name="toolbarExtension"
/>
</div>
@@ -60,7 +58,7 @@
<v-fade-transition
mode="out-in"
>
<router-view :tabs.sync="tabs" />
<router-view />
</v-fade-transition>
</v-content>
<router-view

View File

@@ -1,14 +1,5 @@
<template>
<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-if="!signedIn"
row

View File

@@ -141,7 +141,12 @@
if (error){
console.error(error);
} 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 {

View File

@@ -2,7 +2,6 @@
<character-sheet
show-menu-button
:creature-id="$route.params.id"
:tabs.sync="activeTab"
/>
</template>
@@ -12,21 +11,5 @@ export default {
components: {
CharacterSheet,
},
props: {
tabs: {
type: Number,
required: true,
},
},
computed: {
activeTab: {
get(){
return this.tabs;
},
set(newTab){
this.$emit('update:tabs', newTab);
},
},
},
}
</script>

View File

@@ -1,86 +1,56 @@
<template lang="html">
<v-list-tile
class="spell-slot-list-tile"
:class="{hover}"
v-on="hasClickListener ? {click} : {}"
>
<v-list-tile-action>
<div
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>
<v-list-tile-content>
<v-list-tile-title>
<div
v-if="model.value > 4"
class="layout row value"
style="align-items: baseline;"
>
<div class="display-1">
{{ currentValue }}
<div
style="font-weight: 500; font-size: 24px"
class="current-value"
>
{{ model.currentValue }}
</div>
<div class="title ml-2 max-value">
/{{ value }}
<div class="ml-2 max-value">
/{{ model.value }}
</div>
</div>
</div>
<div
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)"
<div
v-else
class="layout row align-center slot-bubbles"
>
<v-icon>
<v-icon
v-for="i in model.value"
:key="i"
>
{{
i > currentValue ?
i > model.currentValue ?
'radio_button_unchecked' :
'radio_button_checked'
}}
</v-icon>
</v-btn>
</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 }}
</div>
</v-list-tile-title>
<v-list-tile-sub-title>
{{ model.name }}
</v-list-tile-sub-title>
</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>
</template>
@@ -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})
},
},
};
</script>
@@ -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
}
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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: {

View File

@@ -27,12 +27,22 @@
@change="setPrepared"
/>
<v-icon
v-else
v-else-if="!hideHandle"
style="height: 100%; width: 40px; cursor: move;"
class="handle"
>
drag_indicator
</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>
</template>
@@ -45,6 +55,8 @@ export default {
mixins: [treeNodeViewMixin],
props: {
preparingSpells: Boolean,
hideHandle: Boolean,
showInfoButton: Boolean,
},
computed: {
hasClickListener(){
@@ -82,4 +94,10 @@ export default {
.spell {
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>

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
RegExp.escape = function(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
};

View File

@@ -14,6 +14,8 @@ const store = new Vuex.Store({
drawer: undefined,
rightDrawer: undefined,
pageTitle: undefined,
characterSheetTabs: {},
showBuildDialog: false,
},
mutations: {
toggleDrawer (state) {
@@ -32,6 +34,12 @@ const store = new Vuex.Store({
state.pageTitle = value;
document.title = value;
},
setTabForCharacterSheet(state, {tab, id}){
Vue.set(state.characterSheetTabs, id, tab);
},
setShowBuildDialog(state, value){
state.showBuildDialog = value;
},
},
});