Merge branch 'version-2' into version-2-tabletop
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
@change="(value, ack) => $emit('change', {path: ['avatarPicture'], value, ack})"
|
||||
/>
|
||||
<form-sections>
|
||||
<form-section name="settings">
|
||||
<form-section name="Settings">
|
||||
<v-switch
|
||||
label="Hide redundant stats"
|
||||
:input-value="model.settings.hideUnusedStats"
|
||||
@@ -67,86 +67,205 @@
|
||||
:value="model.settings.discordWebhook"
|
||||
@change="(value, ack) => $emit('change', {path: ['settings','discordWebhook'], value, ack})"
|
||||
/>
|
||||
<!--
|
||||
<v-switch
|
||||
label="Use variant encumbrance"
|
||||
:input-value="model.settings.useVariantEncumbrance"
|
||||
:error-messages="errors.useVariantEncumbrance"
|
||||
@change="value => $emit('change', {path: ['settings','useVariantEncumbrance'], value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Hide spells tab"
|
||||
:input-value="model.settings.hideSpellcasting"
|
||||
:error-messages="errors.hideSpellcasting"
|
||||
@change="value => $emit('change', {path: ['settings','hideSpellcasting'], value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Swap ability scores and modifiers"
|
||||
:input-value="model.settings.swapStatAndModifier"
|
||||
:error-messages="errors.swapStatAndModifier"
|
||||
@change="value => $emit('change', {path: ['settings','swapStatAndModifier'], value})"
|
||||
/>
|
||||
<!--
|
||||
<v-switch
|
||||
label="Use variant encumbrance"
|
||||
:input-value="model.settings.useVariantEncumbrance"
|
||||
:error-messages="errors.useVariantEncumbrance"
|
||||
@change="value => $emit('change', {path: ['settings','useVariantEncumbrance'], value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Hide spells tab"
|
||||
:input-value="model.settings.hideSpellcasting"
|
||||
:error-messages="errors.hideSpellcasting"
|
||||
@change="value => $emit('change', {path: ['settings','hideSpellcasting'], value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Swap ability scores and modifiers"
|
||||
:input-value="model.settings.swapStatAndModifier"
|
||||
:error-messages="errors.swapStatAndModifier"
|
||||
@change="value => $emit('change', {path: ['settings','swapStatAndModifier'], value})"
|
||||
/>
|
||||
-->
|
||||
</form-section>
|
||||
<form-section name="Libraries">
|
||||
<smart-switch
|
||||
label="All user libraries"
|
||||
:value="allUserLibraries"
|
||||
@change="allUserLibrariesChange"
|
||||
/>
|
||||
<library-list
|
||||
selection
|
||||
:disabled="!model.allowedLibraries && !model.allowedLibraryCollections"
|
||||
:libraries-selected="model.allowedLibraries"
|
||||
:library-collections-selected="model.allowedLibraryCollections"
|
||||
:libraries-selected-by-collections="librariesSelectedByCollections"
|
||||
@select-library="selectLibrary"
|
||||
@select-library-collection="selectLibraryCollection"
|
||||
/>
|
||||
<v-progress-linear
|
||||
v-if="libraryWriteLoading"
|
||||
style="margin: 12px -24px -16px -24px; width: calc(100% + 48px);"
|
||||
indeterminate
|
||||
/>
|
||||
<p
|
||||
v-if="libraryWriteError"
|
||||
class="text--error"
|
||||
>
|
||||
{{ libraryWriteError }}
|
||||
</p>
|
||||
</form-section>
|
||||
</form-sections>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import FormSection, {FormSections} from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import { union, without, debounce } from 'lodash';
|
||||
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import LibraryList from '/imports/ui/library/LibraryList.vue';
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
import { changeAllowedLibraries, toggleAllUserLibraries } from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormSection,
|
||||
FormSections,
|
||||
},
|
||||
props: {
|
||||
stored: {
|
||||
type: Boolean,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
attackForm: {
|
||||
type: Boolean,
|
||||
},
|
||||
components: {
|
||||
FormSection,
|
||||
FormSections,
|
||||
LibraryList,
|
||||
},
|
||||
props: {
|
||||
stored: {
|
||||
type: Boolean,
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
attackForm: {
|
||||
type: Boolean,
|
||||
},
|
||||
disabled: Boolean,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
libraryCollections: this.model.allowedLibraryCollections,
|
||||
libraries: this.model.allowedLibraries,
|
||||
libraryWriteLoading: false,
|
||||
libraryWriteError: undefined,
|
||||
dirty: false, // If there are pending changes
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allUserLibraries() {
|
||||
return !this.model.allowedLibraries && !this.model.allowedLibraryCollections
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'model.allowedLibraryCollections': function (newVal) {
|
||||
if (!this.dirty) this.libraryCollections = newVal;
|
||||
},
|
||||
'model.allowedLibraries': function (newVal) {
|
||||
if (!this.dirty) this.libraries = newVal;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.updateAllowedLibraryCollections = debounce(() => {
|
||||
this.libraryWriteLoading = true;
|
||||
this.dirty = false;
|
||||
changeAllowedLibraries.call({
|
||||
_id: this.model._id,
|
||||
allowedLibraryCollections: this.libraryCollections,
|
||||
}, error => {
|
||||
this.libraryWriteLoading = false;
|
||||
this.libraryWriteError = error;
|
||||
});
|
||||
}, 500);
|
||||
|
||||
this.updateAllowedLibraries = debounce(() => {
|
||||
this.libraryWriteLoading = true;
|
||||
this.dirty = false;
|
||||
changeAllowedLibraries.call({
|
||||
_id: this.model._id,
|
||||
allowedLibraries: this.libraries,
|
||||
}, error => {
|
||||
this.libraryWriteLoading = false;
|
||||
this.libraryWriteError = error;
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'libraries': [],
|
||||
},
|
||||
librariesSelectedByCollections() {
|
||||
let ids = [];
|
||||
if (!this.model.allowedLibraryCollections) return ids;
|
||||
LibraryCollections.find({
|
||||
_id: { $in: this.model.allowedLibraryCollections }
|
||||
}).forEach(collection => {
|
||||
ids = union(ids, collection.libraries);
|
||||
});
|
||||
return ids;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
changeShowTreeTab(value){
|
||||
changeShowTreeTab(value) {
|
||||
this.$emit('change', {
|
||||
path: ['settings','showTreeTab'],
|
||||
path: ['settings', 'showTreeTab'],
|
||||
value: !!value
|
||||
});
|
||||
let currentTab = this.$store.getters.tabById(this.model._id);
|
||||
if (!value && currentTab === 5){
|
||||
if (!value && currentTab === 5) {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: this.model._id, tab: 4}
|
||||
{ id: this.model._id, tab: 4 }
|
||||
);
|
||||
}
|
||||
},
|
||||
changeHideSpellsTab(value){
|
||||
changeHideSpellsTab(value) {
|
||||
this.$emit('change', {
|
||||
path: ['settings','hideSpellsTab'],
|
||||
path: ['settings', 'hideSpellsTab'],
|
||||
value: !value
|
||||
});
|
||||
let currentTab = this.$store.getters.tabById(this.model._id);
|
||||
if (!value && currentTab === 3){
|
||||
if (!value && currentTab === 3) {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: this.model._id, tab: 4}
|
||||
{ id: this.model._id, tab: 4 }
|
||||
);
|
||||
}
|
||||
},
|
||||
allUserLibrariesChange(value, ack) {
|
||||
toggleAllUserLibraries.call({
|
||||
_id: this.model._id,
|
||||
value,
|
||||
}, error => ack(error));
|
||||
},
|
||||
selectLibrary(id, val) {
|
||||
if (val) {
|
||||
this.libraries = union(this.libraries, [id]);
|
||||
} else {
|
||||
this.libraries = without(this.libraries, id);
|
||||
}
|
||||
this.dirty = true;
|
||||
this.updateAllowedLibraries();
|
||||
},
|
||||
selectLibraryCollection(id, val) {
|
||||
if (val) {
|
||||
this.libraryCollections = union(this.libraryCollections, [id]);
|
||||
} else {
|
||||
this.libraryCollections = without(this.libraryCollections, id);
|
||||
}
|
||||
this.dirty = true;
|
||||
this.updateAllowedLibraryCollections();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template lang="html">
|
||||
<dialog-base v-if="model" :color="model.color">
|
||||
<dialog-base
|
||||
v-if="model"
|
||||
:color="model.color"
|
||||
>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Character Details
|
||||
@@ -37,23 +40,23 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
||||
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
CreatureForm,
|
||||
components: {
|
||||
DialogBase,
|
||||
CreatureForm,
|
||||
ColorPicker,
|
||||
},
|
||||
props: {
|
||||
_id: {
|
||||
},
|
||||
props: {
|
||||
_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
startInEditTab: Boolean,
|
||||
},
|
||||
meteor: {
|
||||
model(){
|
||||
return Creatures.findOne(this._id);
|
||||
},
|
||||
editPermission(){
|
||||
startInEditTab: Boolean,
|
||||
},
|
||||
meteor: {
|
||||
model() {
|
||||
return Creatures.findOne(this._id);
|
||||
},
|
||||
editPermission() {
|
||||
try {
|
||||
assertEditPermission(this.model, Meteor.userId());
|
||||
return true;
|
||||
@@ -61,12 +64,12 @@ export default {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
change({path, value, ack}){
|
||||
updateCreature.call({_id: this._id, path, value}, (error) =>{
|
||||
if (error){
|
||||
if(ack){
|
||||
},
|
||||
methods: {
|
||||
change({ path, value, ack }) {
|
||||
updateCreature.call({ _id: this._id, path, value }, (error) => {
|
||||
if (error) {
|
||||
if (ack) {
|
||||
ack(error && error.reason || error)
|
||||
} else {
|
||||
console.error(error)
|
||||
@@ -74,11 +77,12 @@ export default {
|
||||
} else if (ack) {
|
||||
ack();
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
295
app/imports/ui/creature/buildTree/BuildTreeNode.vue
Normal file
295
app/imports/ui/creature/buildTree/BuildTreeNode.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template lang="html">
|
||||
<v-sheet
|
||||
class="tree-node"
|
||||
:class="{
|
||||
'empty': !hasChildren,
|
||||
}"
|
||||
:data-id="`build-tree-node-${node._id}`"
|
||||
>
|
||||
<div
|
||||
class="layout align-center justify-start tree-node-title"
|
||||
style="cursor: pointer;"
|
||||
@click.stop="$emit('selected', node._id)"
|
||||
>
|
||||
<v-btn
|
||||
small
|
||||
icon
|
||||
class="expand-button"
|
||||
:class="{
|
||||
'rotate-90': showExpanded,
|
||||
'accent--text': node._descendantCanFill || canFillWithMany
|
||||
}"
|
||||
:disabled="!canExpand"
|
||||
@click.stop="expanded = !expanded"
|
||||
>
|
||||
<v-icon v-if="canExpand">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<div
|
||||
class="layout align-center justify-start pr-1"
|
||||
>
|
||||
<!--{{node && node.order}}-->
|
||||
<div
|
||||
v-if="isSlot"
|
||||
class="text-truncate"
|
||||
>
|
||||
<span
|
||||
:class="{
|
||||
'text--secondary': !canFill,
|
||||
'accent--text': canFill,
|
||||
}"
|
||||
>
|
||||
{{ node.name }}
|
||||
</span>
|
||||
<fill-slot-button
|
||||
v-if="(node.quantityExpected && node.quantityExpected.value === 1) && node.spaceLeft === 1"
|
||||
:model="node"
|
||||
/>
|
||||
</div>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<tree-node-view
|
||||
:model="node"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="node.parent.id === parentSlotId"
|
||||
icon
|
||||
:disabled="context.editPermission === false"
|
||||
@click.stop="remove(node)"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-if="condenseChild">
|
||||
<span class="mr-4">:</span>
|
||||
<tree-node-view
|
||||
:model="children[0].node"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
:disabled="context.editPermission === false"
|
||||
@click.stop="remove(children[0].node)"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-delete
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<v-expand-transition>
|
||||
<div
|
||||
v-show="showExpanded"
|
||||
class="ml-3 expand-area"
|
||||
>
|
||||
<v-fade-transition hide-on-leave>
|
||||
<build-tree-node-list
|
||||
v-if="showExpanded"
|
||||
:children="computedChildren"
|
||||
:parent-slot-id="computedSlotId"
|
||||
@selected="e => $emit('selected', e)"
|
||||
/>
|
||||
<div v-else>
|
||||
<div
|
||||
v-for="i in computedChildren.length"
|
||||
:key="i"
|
||||
class="dummy-node"
|
||||
/>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<div
|
||||
v-if="canFillWithMany"
|
||||
>
|
||||
<fill-slot-button
|
||||
class="ml-5"
|
||||
:model="node"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
/**
|
||||
* TreeNode's are list item views of character properties. Every property which
|
||||
* can belong to the character is shown in the tree view of the character
|
||||
* the tree view shows off the full character structure, and where each part of
|
||||
* character comes from.
|
||||
**/
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue';
|
||||
import { some } from 'lodash';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
|
||||
export default {
|
||||
name: 'BuildTreeNode',
|
||||
components: {
|
||||
TreeNodeView,
|
||||
FillSlotButton,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
props: {
|
||||
node: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
children: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
parentSlotId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
expanded: false,
|
||||
/* expand if there's a slot needing attention:
|
||||
this.node._descendantCanFill || (
|
||||
this.node.type === 'propertySlot' &&
|
||||
this. node.quantityExpected?.value === 0 ||
|
||||
(this.node.quantityExpected?.value > 1 && this.node.spaceLeft > 0)
|
||||
)
|
||||
*/
|
||||
}},
|
||||
computed: {
|
||||
condenseChild(){
|
||||
return this.node.type === 'propertySlot' &&
|
||||
this.children.length === 1 &&
|
||||
this.children[0].node.type !== 'propertySlot' &&
|
||||
this.node.quantityExpected &&
|
||||
this.node.quantityExpected.value === 1;
|
||||
},
|
||||
isSlot(){
|
||||
return this.node.type === 'propertySlot';
|
||||
},
|
||||
canFill(){
|
||||
return !!this.node._canFill;
|
||||
},
|
||||
canFillWithOne(){
|
||||
return this.isSlot &&
|
||||
this.node.quantityExpected &&
|
||||
this.node.quantityExpected.value === 1 &&
|
||||
this.node.spaceLeft === 1
|
||||
},
|
||||
canFillWithMany(){
|
||||
return this.isSlot && (
|
||||
!this.node.quantityExpected ||
|
||||
this.node.quantityExpected.value === 0 ||
|
||||
(this.node.quantityExpected.value > 1 && this.node.spaceLeft > 0)
|
||||
);
|
||||
},
|
||||
hasChildren(){
|
||||
return !!this.children && !!this.computedChildren.length || this.lazy && !this.expanded;
|
||||
},
|
||||
showExpanded(){
|
||||
return this.canExpand && this.expanded;
|
||||
},
|
||||
computedChildren(){
|
||||
if (this.condenseChild){
|
||||
return this.children[0].children;
|
||||
}
|
||||
return this.children;
|
||||
},
|
||||
computedSlotId() {
|
||||
if (this.condenseChild) {
|
||||
if (this.children[0].node.type === 'propertySlot') {
|
||||
return this.children[0].node._id;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
if (this.node.type === 'propertySlot') {
|
||||
return this.node._id;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
},
|
||||
canExpand(){
|
||||
return !!this.computedChildren.length || this.canFillWithMany;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'node._ancestorOfMatchedDocument'(value){
|
||||
this.expanded = !!value ||
|
||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
|
||||
},
|
||||
'selectedNode.ancestors'(value){
|
||||
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
this.$options.components.BuildTreeNodeList = require('./BuildTreeNodeList.vue').default
|
||||
},
|
||||
methods: {
|
||||
remove(model) {
|
||||
const _id = model._id;
|
||||
softRemoveProperty.call({_id});
|
||||
snackbar({
|
||||
text: `Deleted ${getPropertyTitle(model)}`,
|
||||
callbackName: 'undo',
|
||||
callback(){
|
||||
restoreProperty.call({_id});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.rotate-90 {
|
||||
transform: rotate(90deg) translateZ(0);
|
||||
}
|
||||
.expand-area {
|
||||
box-shadow: -2px 0px 0px 0px #808080;
|
||||
margin-left: 0;
|
||||
}
|
||||
.handle {
|
||||
cursor: move;
|
||||
}
|
||||
.empty .drag-area {
|
||||
box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4);
|
||||
}
|
||||
.empty .expand-button {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.found {
|
||||
background: rgba(200, 0, 0, 0.1) !important;
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
background: rgba(251, 0, 0, 0.3);
|
||||
}
|
||||
.v-icon.v-icon--disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
.v-icon {
|
||||
transition: none !important;
|
||||
}
|
||||
.theme--light .tree-node-title:hover {
|
||||
background-color: rgba(0,0,0,.04);
|
||||
}
|
||||
.theme--dark .tree-node-title:hover {
|
||||
background-color: rgba(255,255,255,.04);
|
||||
}
|
||||
.tree-node-title{
|
||||
transition: background ease 0.3s, color ease 0.15s;
|
||||
}
|
||||
.tree-node-title, .dummy-node {
|
||||
height: 40px;
|
||||
}
|
||||
</style>
|
||||
37
app/imports/ui/creature/buildTree/BuildTreeNodeList.vue
Normal file
37
app/imports/ui/creature/buildTree/BuildTreeNodeList.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template lang="html">
|
||||
<div class="build-tree-node-list">
|
||||
<build-tree-node
|
||||
v-for="child in children"
|
||||
:key="child.node._id"
|
||||
:node="child.node"
|
||||
:children="child.children"
|
||||
:parent-slot-id="parentSlotId"
|
||||
@selected="e => $emit('selected', e)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import BuildTreeNode from '/imports/ui/creature/buildTree/BuildTreeNode.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BuildTreeNode,
|
||||
},
|
||||
props: {
|
||||
children: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
parentSlotId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
expanded: false,
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
59
app/imports/ui/creature/buildTree/FillSlotButton.vue
Normal file
59
app/imports/ui/creature/buildTree/FillSlotButton.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template functional>
|
||||
<v-btn
|
||||
v-if="!model.quantityExpected || !model.quantityExpected.value || model.spaceLeft"
|
||||
:icon="!$slots.default"
|
||||
v-bind="$attrs"
|
||||
:data-id="`slot-add-button-${model._id}`"
|
||||
class="slot-add-button accent--text"
|
||||
@click.stop="fillSlot()"
|
||||
>
|
||||
<slot>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</slot>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fillSlot(){
|
||||
let slotId = this.model._id;
|
||||
let creatureId = this.context.creatureId;
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'slot-fill-dialog',
|
||||
elementId: `slot-add-button-${slotId}`,
|
||||
data: {
|
||||
slotId,
|
||||
creatureId,
|
||||
},
|
||||
callback(nodeIds){
|
||||
if (!nodeIds || !nodeIds.length) return;
|
||||
let newPropertyId = insertPropertyFromLibraryNode.call({
|
||||
nodeIds,
|
||||
parentRef: {
|
||||
'id': slotId,
|
||||
'collection': 'creatureProperties',
|
||||
},
|
||||
});
|
||||
return `slot-child-${newPropertyId}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -4,29 +4,28 @@
|
||||
New Character
|
||||
</v-toolbar-title>
|
||||
<v-stepper
|
||||
slot="unwrapped-content"
|
||||
v-model="step"
|
||||
class="no-shadow"
|
||||
flat
|
||||
non-linear
|
||||
>
|
||||
<v-stepper-header class="no-shadow">
|
||||
<v-stepper-header>
|
||||
<v-stepper-step
|
||||
editable
|
||||
:complete="step > 1"
|
||||
step="1"
|
||||
:rules="[() => biographyAlert || true]"
|
||||
>
|
||||
Name
|
||||
Biography
|
||||
<small v-if="biographyAlert">{{ biographyAlert }}</small>
|
||||
</v-stepper-step>
|
||||
<v-divider />
|
||||
<v-stepper-step
|
||||
editable
|
||||
:complete="step > 2"
|
||||
step="2"
|
||||
>
|
||||
Ability Scores
|
||||
</v-stepper-step>
|
||||
<v-divider />
|
||||
<v-stepper-step
|
||||
:complete="step > 3"
|
||||
step="3"
|
||||
>
|
||||
Class
|
||||
Libraries
|
||||
</v-stepper-step>
|
||||
</v-stepper-header>
|
||||
|
||||
@@ -34,195 +33,44 @@
|
||||
<v-stepper-content step="1">
|
||||
<v-text-field
|
||||
v-model="name"
|
||||
outlined
|
||||
label="Name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="gender"
|
||||
label="Gender"
|
||||
class="mt-1"
|
||||
:error="!name"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="alignment"
|
||||
outlined
|
||||
label="Alignment"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="gender"
|
||||
outlined
|
||||
label="Gender"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model.number="startingLevel"
|
||||
outlined
|
||||
label="Level"
|
||||
type="number"
|
||||
height="20"
|
||||
min="0"
|
||||
@keydown.tab="step++"
|
||||
/>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="2">
|
||||
<v-text-field
|
||||
v-model="race"
|
||||
label="Race"
|
||||
<v-switch
|
||||
v-model="allSubscribedLibraries"
|
||||
label="All user libraries"
|
||||
/>
|
||||
<v-layout
|
||||
justify-center
|
||||
align-center
|
||||
>
|
||||
<h3>Point Cost:</h3>
|
||||
<h1
|
||||
class="ml-2"
|
||||
:class="cost > 27 ? 'error--text' : ''"
|
||||
>
|
||||
{{ cost }}
|
||||
</h1>
|
||||
<span class="ml-1">/27</span>
|
||||
</v-layout>
|
||||
<table class="point-buy-table mt-2">
|
||||
<thead>
|
||||
<tr class="font-weight-bold">
|
||||
<td />
|
||||
<td>Base Values</td>
|
||||
<td>Race Bonus</td>
|
||||
<td>Score</td>
|
||||
<td>Modifier</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>Strength</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseStrength"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="strengthBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseStrength + strengthBonus }}</td>
|
||||
<td>{{ mod(baseStrength + strengthBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dexterity</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseDexterity"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="dexterityBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseDexterity + dexterityBonus }}</td>
|
||||
<td>{{ mod(baseDexterity + dexterityBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Constitution</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseConstitution"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="constitutionBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseConstitution + constitutionBonus }}</td>
|
||||
<td>{{ mod(baseConstitution + constitutionBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Intelligence</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseIntelligence"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="intelligenceBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseIntelligence + intelligenceBonus }}</td>
|
||||
<td>{{ mod(baseIntelligence + intelligenceBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wisdom</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseWisdom"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="wisdomBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseWisdom + wisdomBonus }}</td>
|
||||
<td>{{ mod(baseWisdom + wisdomBonus) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Charisma</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="baseCharisma"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
min="8"
|
||||
max="15"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<v-text-field
|
||||
v-model.number="charismaBonus"
|
||||
type="number"
|
||||
height="20"
|
||||
reverse
|
||||
/>
|
||||
</td>
|
||||
<td>{{ baseCharisma + charismaBonus }}</td>
|
||||
<td>{{ mod(baseCharisma + charismaBonus) }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</v-stepper-content>
|
||||
<v-stepper-content step="3">
|
||||
<v-text-field
|
||||
v-model="cls"
|
||||
label="Class"
|
||||
/>
|
||||
<v-select
|
||||
v-model="hitDice"
|
||||
:items="hitDiceItems"
|
||||
label="Hit Dice"
|
||||
<library-list
|
||||
selection
|
||||
:disabled="allSubscribedLibraries"
|
||||
:libraries-selected="librariesSelected"
|
||||
:library-collections-selected="libraryCollectionsSelected"
|
||||
:libraries-selected-by-collections="librariesSelectedByCollections"
|
||||
@select-library="selectLibrary"
|
||||
@select-library-collection="selectLibraryCollection"
|
||||
/>
|
||||
</v-stepper-content>
|
||||
</v-stepper-items>
|
||||
@@ -243,15 +91,16 @@
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="step < 3"
|
||||
v-if="step < 2"
|
||||
color="accent"
|
||||
@click="step++"
|
||||
>
|
||||
Next
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:flat="step < 3"
|
||||
:color="step < 3? '' : 'accent'"
|
||||
:disabled="!!biographyAlert"
|
||||
:text="step < 2"
|
||||
:color="step < 2? '' : 'accent'"
|
||||
@click="submit"
|
||||
>
|
||||
Create
|
||||
@@ -261,109 +110,112 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
const getCost = function(score){
|
||||
const costs = {
|
||||
8: 0,
|
||||
9: 1,
|
||||
10: 2,
|
||||
11: 3,
|
||||
12: 4,
|
||||
13: 5,
|
||||
14: 7,
|
||||
15: 9,
|
||||
};
|
||||
if (costs[score] || costs[score] === 0){
|
||||
return costs[score];
|
||||
} else {
|
||||
return NaN;
|
||||
}
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { defer, union, without } from 'lodash';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import insertCreature from '/imports/api/creature/creatures/methods/insertCreature.js';
|
||||
import LibraryList from '/imports/ui/library/LibraryList.vue';
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
LibraryList,
|
||||
},
|
||||
data(){return {
|
||||
step: 1,
|
||||
name: 'New Character',
|
||||
gender: '',
|
||||
alignment: '',
|
||||
startingLevel: 1,
|
||||
librariesSelected: [],
|
||||
libraryCollectionsSelected: [],
|
||||
librariesSelectedByCollections: [],
|
||||
allSubscribedLibraries: true,
|
||||
}},
|
||||
computed: {
|
||||
biographyAlert() {
|
||||
if (!this.name) return 'Name required';
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'libraries': [],
|
||||
},
|
||||
data(){return {
|
||||
step: 1,
|
||||
name: 'New Character',
|
||||
gender: '',
|
||||
alignment: '',
|
||||
race: 'Race',
|
||||
baseStrength: 10,
|
||||
baseDexterity: 10,
|
||||
baseConstitution: 10,
|
||||
baseIntelligence: 10,
|
||||
baseWisdom: 10,
|
||||
baseCharisma: 10,
|
||||
strengthBonus: 0,
|
||||
dexterityBonus: 0,
|
||||
constitutionBonus: 0,
|
||||
intelligenceBonus: 0,
|
||||
wisdomBonus: 0,
|
||||
charismaBonus: 0,
|
||||
hitDiceItems: ['d6', 'd8', 'd10', 'd12'],
|
||||
hitDice: 'd8',
|
||||
cls: 'Class',
|
||||
}},
|
||||
computed: {
|
||||
cost(){
|
||||
return [
|
||||
this.baseStrength,
|
||||
this.baseDexterity,
|
||||
this.baseConstitution,
|
||||
this.baseIntelligence,
|
||||
this.baseWisdom,
|
||||
this.baseCharisma,
|
||||
].map(getCost)
|
||||
.reduce((memo, score) => memo + score, 0);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
mod(score){
|
||||
let mod = Math.floor((score - 10) / 2);
|
||||
if (mod >= 0) {
|
||||
return `+${mod}`;
|
||||
} else {
|
||||
return `${mod}`;
|
||||
}
|
||||
},
|
||||
submit(){
|
||||
let char = {
|
||||
name: this.name,
|
||||
gender: this.gender,
|
||||
alignment: this.alignment,
|
||||
race: this.race,
|
||||
baseStrength: this.baseStrength,
|
||||
baseDexterity: this.baseDexterity,
|
||||
baseConstitution: this.baseConstitution,
|
||||
baseIntelligence: this.baseIntelligence,
|
||||
baseWisdom: this.baseWisdom,
|
||||
baseCharisma: this.baseCharisma,
|
||||
strengthBonus: this.strengthBonus,
|
||||
dexterityBonus: this.dexterityBonus,
|
||||
constitutionBonus: this.constitutionBonus,
|
||||
intelligenceBonus: this.intelligenceBonus,
|
||||
wisdomBonus: this.wisdomBonus,
|
||||
charismaBonus: this.charismaBonus,
|
||||
hitDice: this.hitDice,
|
||||
cls: this.cls,
|
||||
};
|
||||
this.$emit('pop', char);
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
selectLibrary(libraryId, val) {
|
||||
if (val) {
|
||||
this.librariesSelected = union(this.librariesSelected, [libraryId]);
|
||||
} else {
|
||||
this.librariesSelected = without(this.librariesSelected, libraryId);
|
||||
}
|
||||
},
|
||||
selectLibraryCollection(libraryCollectionId, val) {
|
||||
const collection = LibraryCollections.findOne(libraryCollectionId);
|
||||
if (!collection) return;
|
||||
if (val) {
|
||||
this.libraryCollectionsSelected = union(
|
||||
this.libraryCollectionsSelected,
|
||||
[libraryCollectionId]
|
||||
);
|
||||
this.librariesSelectedByCollections = union(
|
||||
this.librariesSelectedByCollections,
|
||||
collection.libraries
|
||||
);
|
||||
} else {
|
||||
this.libraryCollectionsSelected = without(
|
||||
this.libraryCollectionsSelected,
|
||||
libraryCollectionId,
|
||||
);
|
||||
this.librariesSelectedByCollections = without(
|
||||
this.librariesSelectedByCollections,
|
||||
...collection.libraries
|
||||
);
|
||||
}
|
||||
},
|
||||
submit(){
|
||||
let char = {
|
||||
name: this.name,
|
||||
gender: this.gender,
|
||||
alignment: this.alignment,
|
||||
startingLevel: this.startingLevel,
|
||||
};
|
||||
if (!this.allSubscribedLibraries) {
|
||||
char.allowedLibraries = this.librariesSelected;
|
||||
char.allowedLibraryCollections = this.libraryCollectionsSelected;
|
||||
}
|
||||
insertCreature.call(char, (error, creatureId) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
} else {
|
||||
this.$store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: creatureId, tab: 5}
|
||||
);
|
||||
this.$emit('pop', creatureId);
|
||||
defer(() => {
|
||||
this.$router.push({ name: 'characterSheet', params: {id: creatureId} });
|
||||
});
|
||||
return creatureId;
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-shadow {
|
||||
box-shadow: none;
|
||||
}
|
||||
.point-buy-table {
|
||||
width: 100%;
|
||||
}
|
||||
.point-buy-table td {
|
||||
text-align: center;
|
||||
padding: 0 8px 0 8px;
|
||||
max-width: 50px;
|
||||
}
|
||||
.point-buy-table {
|
||||
width: 100%;
|
||||
}
|
||||
.point-buy-table td {
|
||||
text-align: center;
|
||||
padding: 0 8px 0 8px;
|
||||
max-width: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,43 +37,46 @@ import removeCreature from '/imports/api/creature/creatures/methods/removeCreatu
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
},
|
||||
props: {
|
||||
id: String,
|
||||
},
|
||||
data(){return {
|
||||
inputName: undefined,
|
||||
}},
|
||||
computed: {
|
||||
nameMatch(){
|
||||
if (!this.name) return true;
|
||||
let uppername = this.name.toUpperCase();
|
||||
let upperInputName = this.inputName && this.inputName.toUpperCase();
|
||||
return uppername === upperInputName;
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
name(){
|
||||
let creature = Creatures.findOne(this.id, {fields: {name: 1}});
|
||||
return creature && creature.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
remove(){
|
||||
components: {
|
||||
DialogBase,
|
||||
},
|
||||
props: {
|
||||
id: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
inputName: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
nameMatch() {
|
||||
if (!this.name) return true;
|
||||
let uppername = this.name.toUpperCase();
|
||||
let upperInputName = this.inputName && this.inputName.toUpperCase();
|
||||
return uppername === upperInputName;
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
name() {
|
||||
let creature = Creatures.findOne(this.id, { fields: { name: 1 } });
|
||||
return creature && creature.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
remove() {
|
||||
this.$router.push('/characterList');
|
||||
this.$store.dispatch('popDialogStack');
|
||||
removeCreature.call({charId: this.id}, (error) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
snackbar({text: error.message || error.toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
removeCreature.call({ charId: this.id }, (error) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
snackbar({ text: error.message || error.toString() });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
size="64"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!creature"
|
||||
>
|
||||
<div v-else-if="!creature">
|
||||
<v-layout
|
||||
column
|
||||
align-center
|
||||
@@ -55,9 +53,7 @@
|
||||
<v-tab-item>
|
||||
<inventory-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item
|
||||
v-if="!creature.settings.hideSpellsTab"
|
||||
>
|
||||
<v-tab-item v-if="!creature.settings.hideSpellsTab">
|
||||
<spells-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
@@ -66,9 +62,7 @@
|
||||
<v-tab-item>
|
||||
<build-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item
|
||||
v-if="creature.settings.showTreeTab"
|
||||
>
|
||||
<v-tab-item v-if="creature.settings.showTreeTab">
|
||||
<tree-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
@@ -78,97 +72,108 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
//TODO add a "no character found" screen if shown on a false address
|
||||
// or on a character the user does not have permission to view
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue';
|
||||
import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
|
||||
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
|
||||
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
|
||||
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/JournalTab.vue';
|
||||
import BuildTab from '/imports/ui/creature/character/characterSheetTabs/BuildTab.vue';
|
||||
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
//TODO add a "no character found" screen if shown on a false address
|
||||
// or on a character the user does not have permission to view
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue';
|
||||
import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
|
||||
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
|
||||
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
|
||||
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/JournalTab.vue';
|
||||
import BuildTab from '/imports/ui/creature/character/characterSheetTabs/BuildTab.vue';
|
||||
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatsTab,
|
||||
FeaturesTab,
|
||||
InventoryTab,
|
||||
SpellsTab,
|
||||
CharacterTab,
|
||||
BuildTab,
|
||||
TreeTab,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId', 'editPermission'],
|
||||
export default {
|
||||
components: {
|
||||
StatsTab,
|
||||
FeaturesTab,
|
||||
InventoryTab,
|
||||
SpellsTab,
|
||||
CharacterTab,
|
||||
BuildTab,
|
||||
TreeTab,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
computed: {
|
||||
activeTab: {
|
||||
get(){
|
||||
return this.tabs;
|
||||
},
|
||||
set(newTab){
|
||||
this.$emit('update:tabs', newTab);
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId', 'editPermission'],
|
||||
},
|
||||
computed: {
|
||||
activeTab: {
|
||||
get() {
|
||||
return this.tabs;
|
||||
},
|
||||
set(newTab) {
|
||||
this.$emit('update:tabs', newTab);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'creature.name'(value){
|
||||
this.$store.commit('setPageTitle', value || 'Character Sheet');
|
||||
},
|
||||
watch: {
|
||||
'creature.name'(value) {
|
||||
this.$store.commit('setPageTitle', value || 'Character Sheet');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.commit('setPageTitle', this.creature && this.creature.name || 'Character Sheet');
|
||||
this.nameObserver = Creatures.find({
|
||||
creatureId: this.creatureId,
|
||||
}, {
|
||||
fields: { name: 1 },
|
||||
}).observe({
|
||||
added: ({ name }) =>
|
||||
this.$store.commit('setPageTitle', name || 'Character Sheet'),
|
||||
changed: ({ name }) =>
|
||||
this.$store.commit('setPageTitle', name || 'Character Sheet'),
|
||||
});
|
||||
let that = this;
|
||||
this.logObserver = CreatureLogs.find({
|
||||
creatureId: this.creatureId,
|
||||
}).observe({
|
||||
added({ content }) {
|
||||
if (!that.$subReady.singleCharacter) return;
|
||||
if (that.$store.state.rightDrawer) return;
|
||||
snackbar({ content });
|
||||
},
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.nameObserver.stop();
|
||||
this.logObserver.stop();
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'singleCharacter'() {
|
||||
return [this.creatureId];
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
this.$store.commit('setPageTitle', this.creature && this.creature.name || 'Character Sheet');
|
||||
let that = this;
|
||||
this.logObserver = CreatureLogs.find({
|
||||
creatureId: this.creatureId,
|
||||
}).observe({
|
||||
added({content}){
|
||||
if (!that.$subReady.singleCharacter) return;
|
||||
if (that.$store.state.rightDrawer) return;
|
||||
snackbar({content});
|
||||
},
|
||||
creature() {
|
||||
return Creatures.findOne(this.creatureId, {
|
||||
fields: { variables: 0 }
|
||||
});
|
||||
},
|
||||
beforeDestroy(){
|
||||
this.logObserver.stop();
|
||||
editPermission() {
|
||||
try {
|
||||
assertEditPermission(this.creature, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'singleCharacter'(){
|
||||
return [this.creatureId];
|
||||
},
|
||||
},
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId, {
|
||||
fields: {variables: 0}
|
||||
});
|
||||
},
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.creature, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.character-sheet .v-window-item {
|
||||
min-height: calc(100vh - 96px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.character-sheet .v-window-item {
|
||||
min-height: calc(100vh - 96px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template lang="html">
|
||||
<v-speed-dial
|
||||
v-if="speedDials"
|
||||
v-model="fab"
|
||||
direction="bottom"
|
||||
:style="!speedDials ? 'visibility: hidden;' : ''"
|
||||
>
|
||||
<template #activator>
|
||||
<v-btn
|
||||
v-model="fab"
|
||||
color="primary"
|
||||
fab
|
||||
small
|
||||
data-id="insert-creature-property-fab"
|
||||
class="insert-creature-property-fab"
|
||||
small
|
||||
>
|
||||
<transition
|
||||
name="fab-rotate"
|
||||
@@ -25,7 +25,6 @@
|
||||
</transition>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<labeled-fab
|
||||
v-for="type in speedDials"
|
||||
:key="type"
|
||||
@@ -49,12 +48,13 @@
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
function getParentAndOrderFromSelectedTreeNode(creatureId){
|
||||
function getParentAndOrderFromSelectedTreeNode(creatureId, $store){
|
||||
// find the parent based on the currently selected property
|
||||
let el = document.querySelector('.tree-tab .tree-node-title.primary--text');
|
||||
let selectedComponent = el && el.parentElement.__vue__.$parent;
|
||||
let parentRef, order;
|
||||
if (selectedComponent){
|
||||
const onTreeTab = $store.getters.tabNameById(creatureId) === 'tree';
|
||||
if (onTreeTab && selectedComponent){
|
||||
if (selectedComponent.showExpanded){
|
||||
parentRef = {
|
||||
id: selectedComponent.node._id,
|
||||
@@ -101,7 +101,8 @@
|
||||
'features',
|
||||
'inventory',
|
||||
'spells',
|
||||
'character',
|
||||
'journal',
|
||||
'build',
|
||||
'tree',
|
||||
];
|
||||
|
||||
@@ -134,7 +135,7 @@
|
||||
'features': ['feature'],
|
||||
'inventory': ['item', 'container'],
|
||||
'spells': ['spellList', 'spell'],
|
||||
'character': ['note'],
|
||||
'journal': ['note'],
|
||||
'tree': [null],
|
||||
};},
|
||||
properties(){
|
||||
@@ -156,7 +157,7 @@
|
||||
let creatureId = this.creatureId;
|
||||
let fab = hideFab();
|
||||
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
|
||||
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId, this.$store);
|
||||
let parent;
|
||||
try {
|
||||
parent = fetchDocByRef(parentRef);
|
||||
@@ -170,6 +171,7 @@
|
||||
data: {
|
||||
parentDoc: forcedType ? undefined : parent,
|
||||
forcedType,
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
callback(result){
|
||||
if (!result){
|
||||
|
||||
@@ -11,20 +11,18 @@
|
||||
dense
|
||||
>
|
||||
<v-app-bar-nav-icon @click="toggleDrawer" />
|
||||
<v-fade-transition
|
||||
mode="out-in"
|
||||
>
|
||||
<v-app-bar-title :key="$store.state.pageTitle">
|
||||
<div>
|
||||
{{ $store.state.pageTitle }}
|
||||
</div>
|
||||
</v-app-bar-title>
|
||||
<v-fade-transition mode="out-in">
|
||||
<v-toolbar-title :key="$store.state.pageTitle">
|
||||
{{ $store.state.pageTitle }}
|
||||
</v-toolbar-title>
|
||||
</v-fade-transition>
|
||||
<v-spacer />
|
||||
<v-fade-transition
|
||||
mode="out-in"
|
||||
>
|
||||
<div :key="$route.meta.title">
|
||||
<v-fade-transition mode="out-in">
|
||||
<v-layout
|
||||
:key="$route.meta.title"
|
||||
class="flex-shrink-0 flex-grow-0"
|
||||
justify-end
|
||||
>
|
||||
<template v-if="creature">
|
||||
<shared-icon :model="creature" />
|
||||
<v-menu
|
||||
@@ -68,7 +66,7 @@
|
||||
</v-menu>
|
||||
<v-app-bar-nav-icon @click="toggleRightDrawer" />
|
||||
</template>
|
||||
</div>
|
||||
</v-layout>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition
|
||||
slot="extension"
|
||||
@@ -149,17 +147,17 @@ export default {
|
||||
context: { default: {} }
|
||||
},
|
||||
computed: {
|
||||
creatureId(){
|
||||
creatureId() {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
toolbarColor(){
|
||||
if (this.creature && this.creature.color){
|
||||
toolbarColor() {
|
||||
if (this.creature && this.creature.color) {
|
||||
return this.creature.color;
|
||||
} else {
|
||||
return getThemeColor('secondary');
|
||||
}
|
||||
},
|
||||
isDark(){
|
||||
isDark() {
|
||||
return isDarkColor(this.toolbarColor);
|
||||
},
|
||||
},
|
||||
@@ -168,49 +166,49 @@ export default {
|
||||
'toggleDrawer',
|
||||
'toggleRightDrawer',
|
||||
]),
|
||||
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(){
|
||||
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',
|
||||
@@ -228,10 +226,10 @@ export default {
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature(){
|
||||
creature() {
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
editPermission(){
|
||||
editPermission() {
|
||||
try {
|
||||
assertEditPermission(this.creature, Meteor.userId());
|
||||
return true;
|
||||
@@ -247,9 +245,11 @@ export default {
|
||||
.character-sheet-toolbar .v-tabs__container--grow .v-tabs__div {
|
||||
max-width: 120px !important;
|
||||
}
|
||||
|
||||
.character-sheet-toolbar .v-tabs__bar {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.character-sheet-fab {
|
||||
bottom: -24px;
|
||||
right: 8px;
|
||||
|
||||
189
app/imports/ui/creature/character/CreatureRootDialog.vue
Normal file
189
app/imports/ui/creature/character/CreatureRootDialog.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<template lang="html">
|
||||
<dialog-base>
|
||||
<template #replace-toolbar="{flat}">
|
||||
<property-toolbar
|
||||
:model="creature"
|
||||
:editing="editing"
|
||||
:flat="flat"
|
||||
:embedded="embedded"
|
||||
style="flex-grow: 0;"
|
||||
@toggle-editing="editing = !editing"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="_id">
|
||||
<v-fade-transition
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="editing">
|
||||
<creature-properties-tree
|
||||
style="width: 100%;"
|
||||
class="mb-2"
|
||||
organize
|
||||
:root="{collection: 'creatures', id: _id}"
|
||||
@length="childrenLength = $event"
|
||||
@selected="selectSubProperty"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
outlined
|
||||
color="accent"
|
||||
data-id="insert-creature-property-btn"
|
||||
@click="addProperty"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div v-else>
|
||||
<creature-properties-tree
|
||||
style="width: 100%;"
|
||||
:root="{collection: 'creatures', id: _id}"
|
||||
@length="childrenLength = $event"
|
||||
@selected="selectSubProperty"
|
||||
/>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</template>
|
||||
<div
|
||||
v-if="!embedded"
|
||||
slot="actions"
|
||||
class="layout"
|
||||
>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="accent"
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Close
|
||||
</v-btn>
|
||||
</div>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import PropertyToolbar from '/imports/ui/components/propertyToolbar.vue';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
||||
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
|
||||
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
|
||||
let formIndex = {};
|
||||
for (let key in propertyFormIndex){
|
||||
formIndex[key + 'Form'] = propertyFormIndex[key];
|
||||
}
|
||||
|
||||
let viewerIndex = {};
|
||||
for (let key in propertyViewerIndex){
|
||||
formIndex[key + 'Viewer'] = propertyViewerIndex[key];
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
...formIndex,
|
||||
...viewerIndex,
|
||||
DialogBase,
|
||||
PropertyToolbar,
|
||||
CreaturePropertiesTree,
|
||||
},
|
||||
props: {
|
||||
_id: String,
|
||||
embedded: Boolean, // This dialog is embedded in a page
|
||||
startInEditTab: Boolean,
|
||||
},
|
||||
data(){ return {
|
||||
editing: !!this.startInEditTab,
|
||||
// CurrentId lags behind Id by one tick so that events fired by destroying
|
||||
// forms keyed to the old ID are applied before the new ID overwrites it
|
||||
currentId: undefined,
|
||||
childrenLength: 0,
|
||||
}},
|
||||
meteor: {
|
||||
editPermission(){
|
||||
try {
|
||||
assertEditPermission(this.creature, Meteor.userId());
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
creature(){
|
||||
return Creatures.findOne(this._id);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
_id: {
|
||||
immediate: true,
|
||||
handler(newId){
|
||||
this.$nextTick(() => {
|
||||
this.currentId = newId;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId', 'editPermission'],
|
||||
},
|
||||
methods: {
|
||||
getPropertyName,
|
||||
selectSubProperty(_id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: {
|
||||
_id,
|
||||
startInEditTab: this.editing,
|
||||
},
|
||||
});
|
||||
},
|
||||
addProperty(){
|
||||
let parentPropertyId = this._id;
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'add-creature-property-dialog',
|
||||
elementId: 'insert-creature-property-btn',
|
||||
data: {
|
||||
parentDoc: this.creature,
|
||||
creatureId: this._id,
|
||||
},
|
||||
callback(result){
|
||||
if (!result) return;
|
||||
let parentRef = {
|
||||
id: parentPropertyId,
|
||||
collection: 'creatures',
|
||||
};
|
||||
let order = getHighestOrder({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: parentRef.id,
|
||||
}) + 0.5;
|
||||
if (Array.isArray(result)){
|
||||
let nodeIds = result;
|
||||
let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
|
||||
return `tree-node-${id}`;
|
||||
} else {
|
||||
let creatureProperty = result;
|
||||
// Get order and parent
|
||||
creatureProperty.order = order;
|
||||
// Insert the property
|
||||
let id = insertProperty.call({creatureProperty, parentRef});
|
||||
return `tree-node-${id}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -1,36 +1,119 @@
|
||||
<template lang="html">
|
||||
<div class="build">
|
||||
<column-layout wide-columns>
|
||||
<div>
|
||||
<v-card class="class-details">
|
||||
<v-container fluid>
|
||||
<v-row dense>
|
||||
<v-col cols="12">
|
||||
<character-errors
|
||||
class="mt-4"
|
||||
:creature-id="creatureId"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<slot-cards-to-fill :creature-id="creatureId" />
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="8"
|
||||
lg="6"
|
||||
>
|
||||
<v-card class="pb-4">
|
||||
<v-card-title style="height: 68px;">
|
||||
Slots
|
||||
<v-spacer />
|
||||
<v-scale-transition>
|
||||
<v-menu
|
||||
bottom
|
||||
left
|
||||
transition="slide-y-transition"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-badge
|
||||
v-show="hiddenCount"
|
||||
slot="activator"
|
||||
color="primary"
|
||||
overlap
|
||||
:value="hiddenCount"
|
||||
:content="hiddenCount"
|
||||
>
|
||||
<v-btn
|
||||
icon
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-file-hidden</v-icon>
|
||||
</v-btn>
|
||||
</v-badge>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-subheader>
|
||||
<v-icon class="mr-2">
|
||||
mdi-file-hidden
|
||||
</v-icon>
|
||||
{{ hiddenCount }} hidden {{ hiddenCount > 1 ? 'properties' : 'property' }}
|
||||
</v-subheader>
|
||||
<v-list-item
|
||||
v-for="pointBuy in hiddenPointBuys"
|
||||
:key="pointBuy._id"
|
||||
@click="unhideProp(pointBuy._id)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ getPropertyTitle(pointBuy) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="slot in hiddenSlots"
|
||||
:key="slot._id"
|
||||
@click="unhideProp(slot._id)"
|
||||
>
|
||||
<v-list-item-title>
|
||||
{{ getPropertyTitle(slot) }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-scale-transition>
|
||||
</v-card-title>
|
||||
<build-tree-node-list
|
||||
:children="slotBuildTree"
|
||||
class="mx-2"
|
||||
@selected="_id => propertyClicked({_id, prefix: 'build-tree-node-'})"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
lg="6"
|
||||
>
|
||||
<v-card class="class-details mb-2">
|
||||
<v-card-title
|
||||
v-if="creature.variables.level"
|
||||
v-if="variables.level"
|
||||
class="text-h6"
|
||||
>
|
||||
Level {{ creature.variables.level.value }}
|
||||
Level {{ variables.level.value }}
|
||||
</v-card-title>
|
||||
<v-list two-line>
|
||||
<v-list-item>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title
|
||||
v-if="
|
||||
creature.variables.milestoneLevels &&
|
||||
creature.variables.milestoneLevels.value
|
||||
variables.milestoneLevels &&
|
||||
variables.milestoneLevels.value
|
||||
"
|
||||
>
|
||||
{{ creature.variables.milestoneLevels.value }} Milestone levels
|
||||
{{ variables.milestoneLevels.value }} Milestone levels
|
||||
</v-list-item-title>
|
||||
<v-list-item-title
|
||||
v-if="
|
||||
!(creature.variables.milestoneLevels &&
|
||||
creature.variables.milestoneLevels.value) ||
|
||||
(creature.variables.xp &&
|
||||
creature.variables.xp.value)
|
||||
!(variables.milestoneLevels &&
|
||||
variables.milestoneLevels.value) ||
|
||||
(variables.xp &&
|
||||
variables.xp.value)
|
||||
"
|
||||
>
|
||||
{{
|
||||
creature.variables.xp &&
|
||||
creature.variables.xp.value ||
|
||||
variables.xp &&
|
||||
variables.xp.value ||
|
||||
0
|
||||
}} XP
|
||||
</v-list-item-title>
|
||||
@@ -55,64 +138,71 @@
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
v-for="classLevel in highestClassLevels"
|
||||
:key="classLevel._id"
|
||||
v-for="cls in classes"
|
||||
:key="cls._id"
|
||||
:data-id="`class-${cls._id}`"
|
||||
v-on="cls.type === 'class' ? {click: () => propertyClicked({_id: cls._id, prefix: 'class-'})} : {}"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ classLevel.name }}
|
||||
{{ cls.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-avatar>
|
||||
{{ classLevel.level }}
|
||||
{{ cls.level }}
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-action v-if="cls.type === 'class'">
|
||||
<v-btn
|
||||
outlined
|
||||
color="accent"
|
||||
data-id="level-up-btn"
|
||||
:disabled="cls.slotCondition && cls.slotCondition.hasOwnProperty('value') && !cls.slotCondition.value"
|
||||
@click.stop="levelUpDialog(cls._id)"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
<template v-if="cls.missingLevels && cls.missingLevels.length">
|
||||
Get Missing Levels
|
||||
</template>
|
||||
<template v-else>
|
||||
Level Up
|
||||
</template>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
<div>
|
||||
<toolbar-card
|
||||
data-id="slot-card"
|
||||
@toolbarclick="showSlotDialog"
|
||||
>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
Build
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-toolbar-title>
|
||||
<v-icon
|
||||
small
|
||||
style="width: 16px;"
|
||||
class="mr-1"
|
||||
>
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
</v-toolbar-title>
|
||||
</template>
|
||||
<v-card-text style="background-color: inherit;">
|
||||
<slots :creature-id="creatureId" />
|
||||
</v-card-text>
|
||||
</toolbar-card>
|
||||
</div>
|
||||
</column-layout>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import Slots from '/imports/ui/creature/slots/Slots.vue';
|
||||
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
||||
import CreatureSummary from '/imports/ui/creature/character/CreatureSummary.vue';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import BuildTreeNodeList from '/imports/ui/creature/buildTree/BuildTreeNodeList.vue';
|
||||
import SlotCardsToFill from '/imports/ui/creature/slots/SlotCardsToFill.vue';
|
||||
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import CharacterErrors from '/imports/ui/creature/character/errors/CharacterErrors.vue';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
|
||||
function traverse(tree, callback, parents = []){
|
||||
tree.forEach(node => {
|
||||
callback(node, parents);
|
||||
traverse(node.children, callback, [...parents, node]);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
Slots,
|
||||
ToolbarCard,
|
||||
CreatureSummary,
|
||||
CharacterErrors,
|
||||
BuildTreeNodeList,
|
||||
SlotCardsToFill,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
@@ -121,7 +211,7 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
highestClassLevels(){
|
||||
highestLevels(){
|
||||
let highestLevels = {};
|
||||
let highestLevelsList = [];
|
||||
this.classLevels.forEach(classLevel => {
|
||||
@@ -139,37 +229,136 @@ export default {
|
||||
highestLevelsList.sort((a, b) => a.level - b.level);
|
||||
return highestLevelsList;
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
if (this.$store.state.showDetailsDialog){
|
||||
this.$store.commit('setShowDetailsDialog', false);
|
||||
this.showCharacterForm();
|
||||
}
|
||||
classes() {
|
||||
return [
|
||||
...this.highestLevels,
|
||||
...this.classProperties
|
||||
].sort((a, b) => a.order - b.order);
|
||||
},
|
||||
hiddenCount() {
|
||||
return this.hiddenSlots.length + this.hiddenPointBuys.length;
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId);
|
||||
},
|
||||
classLevels(){
|
||||
variables() {
|
||||
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
|
||||
},
|
||||
hiddenPointBuys() {
|
||||
return CreatureProperties.find({
|
||||
type: 'pointBuy',
|
||||
'ancestors.id': this.creatureId,
|
||||
ignored: true,
|
||||
pointsLeft: {$ne: 0},
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}).fetch();
|
||||
},
|
||||
hiddenSlots(){
|
||||
return CreatureProperties.find({
|
||||
type: 'propertySlot',
|
||||
'ancestors.id': this.creatureId,
|
||||
ignored: true,
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
{'slotCondition.value': {$nin: [false, 0, '']}},
|
||||
{'slotCondition.value': {$exists: false}},
|
||||
]
|
||||
},{
|
||||
$or: [
|
||||
{ 'quantityExpected.value': {$in: [false, 0, '', undefined]} },
|
||||
{ 'quantityExpected.value': {exists: false} },
|
||||
{spaceLeft: {$gt: 0}},
|
||||
]
|
||||
},
|
||||
],
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}).fetch();
|
||||
},
|
||||
classProperties(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'class',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
}).fetch();
|
||||
},
|
||||
classLevels() {
|
||||
const classVariableNames = this.classProperties.map(c => c.variableName)
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'classLevel',
|
||||
variableName: {$nin: classVariableNames},
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
},
|
||||
slotBuildTree(){
|
||||
const slots = CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: {$in: ['propertySlot', 'pointBuy']},
|
||||
$or: [
|
||||
{'slotCondition.value': {$nin: [false, 0, '']}},
|
||||
{'slotCondition.value': {$exists: false}},
|
||||
{'slotCondition': {$exists: false}},
|
||||
],
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
const slotIds = slots.map(s => s._id);
|
||||
const slotChildren = CreatureProperties.find({
|
||||
'parent.id': {$in: slotIds},
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
});
|
||||
const tree = nodeArrayToTree([
|
||||
...slots.fetch(),
|
||||
...slotChildren.fetch()
|
||||
]);
|
||||
traverse(tree, (child, parents) => {
|
||||
const model = child.node;
|
||||
const isSlotWithSpace = model.type === 'propertySlot' && (
|
||||
model.spaceLeft > 0 ||
|
||||
!model.quantityExpected ||
|
||||
model.quantityExpected.value === 0
|
||||
);
|
||||
if(isSlotWithSpace) {
|
||||
model._canFill = true;
|
||||
parents.forEach(node => {
|
||||
node.node._descendantCanFill = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
return tree;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
propertyClicked({_id, prefix}){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${prefix}${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
addExperience(){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'experience-insert-dialog',
|
||||
elementId: 'experience-add-button',
|
||||
data: {
|
||||
creatureIds: [this.creatureId],
|
||||
startAsMilestone: this.creature.variables.milestoneLevels &&
|
||||
!!this.creature.variables.milestoneLevels.value,
|
||||
startAsMilestone: this.variables.milestoneLevels &&
|
||||
!!this.variables.milestoneLevels.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -179,8 +368,8 @@ export default {
|
||||
elementId: 'experience-info-button',
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
startAsMilestone: this.creature.variables.milestoneLevels &&
|
||||
!!this.creature.variables.milestoneLevels.value,
|
||||
startAsMilestone: this.variables.milestoneLevels &&
|
||||
!!this.variables.milestoneLevels.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -193,6 +382,40 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
levelUpDialog(classId){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'level-up-dialog',
|
||||
elementId: 'level-up-btn',
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
classId,
|
||||
},
|
||||
callback(nodeIds){
|
||||
if (!nodeIds || !nodeIds.length) return;
|
||||
let newPropertyId = insertPropertyFromLibraryNode.call({
|
||||
nodeIds,
|
||||
parentRef: {
|
||||
'id': classId,
|
||||
'collection': 'creatureProperties',
|
||||
},
|
||||
});
|
||||
return `tree-node-${newPropertyId}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
getPropertyTitle,
|
||||
unhideProp(_id) {
|
||||
updateCreatureProperty.call({
|
||||
_id,
|
||||
path: ['ignored'],
|
||||
value: false,
|
||||
}, error => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -16,44 +16,45 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
FeatureCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
features(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'feature',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
featureClicked({_id}){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
FeatureCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
features() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'feature',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
featureClicked({ _id }) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -30,13 +30,11 @@
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-list-item-title>
|
||||
<coin-value
|
||||
:value="creature.variables && creature.variables.valueTotal && creature.variables.valueTotal.value|| 0"
|
||||
/>
|
||||
<coin-value :value="variables && variables.valueTotal && variables.valueTotal.value|| 0" />
|
||||
</v-list-item-title>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="creature.variables && creature.variables.itemsAttuned && creature.variables.itemsAttuned.value">
|
||||
<v-list-item v-if="variables && variables.itemsAttuned && variables.itemsAttuned.value">
|
||||
<v-list-item-avatar>
|
||||
<v-icon>$vuetify.icons.spell</v-icon>
|
||||
</v-list-item-avatar>
|
||||
@@ -47,7 +45,7 @@
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-list-item-title>
|
||||
{{ creature.variables.itemsAttuned.value }}
|
||||
{{ variables.itemsAttuned.value }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
@@ -85,9 +83,7 @@
|
||||
v-for="container in containersWithoutAncestorContainers"
|
||||
:key="container._id"
|
||||
>
|
||||
<container-card
|
||||
:model="container"
|
||||
/>
|
||||
<container-card :model="container" />
|
||||
</div>
|
||||
</column-layout>
|
||||
</div>
|
||||
@@ -104,82 +100,90 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/
|
||||
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
|
||||
import CoinValue from '/imports/ui/components/CoinValue.vue';
|
||||
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
||||
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
ContainerCard,
|
||||
components: {
|
||||
ColumnLayout,
|
||||
ContainerCard,
|
||||
ToolbarCard,
|
||||
ItemList,
|
||||
CoinValue,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
organize: false,
|
||||
}},
|
||||
meteor: {
|
||||
containers(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'container',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
},
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId, {fields: {
|
||||
color: 1,
|
||||
variables: 1,
|
||||
}});
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
organize: false,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
containers() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'container',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
});
|
||||
},
|
||||
containersWithoutAncestorContainers(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
$nin: this.containerIds
|
||||
},
|
||||
type: 'container',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
},
|
||||
carriedItems(){
|
||||
creature() {
|
||||
return Creatures.findOne(this.creatureId, {
|
||||
fields: {
|
||||
color: 1,
|
||||
variables: 1,
|
||||
}
|
||||
});
|
||||
},
|
||||
variables() {
|
||||
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
|
||||
},
|
||||
containersWithoutAncestorContainers() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
$nin: this.containerIds
|
||||
},
|
||||
type: 'item',
|
||||
equipped: {$ne: true},
|
||||
removed: {$ne: true},
|
||||
deactivatedByAncestor: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
$eq: this.creatureId,
|
||||
$nin: this.containerIds
|
||||
},
|
||||
type: 'container',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
});
|
||||
},
|
||||
equippedItems(){
|
||||
carriedItems() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
},
|
||||
type: 'item',
|
||||
$eq: this.creatureId,
|
||||
$nin: this.containerIds
|
||||
},
|
||||
type: 'item',
|
||||
equipped: { $ne: true },
|
||||
removed: { $ne: true },
|
||||
deactivatedByAncestor: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
});
|
||||
},
|
||||
equippedItems() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
},
|
||||
type: 'item',
|
||||
equipped: true,
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
});
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
});
|
||||
},
|
||||
equipmentParentRef(){
|
||||
equipmentParentRef() {
|
||||
return getParentRefByTag(
|
||||
this.creatureId, BUILT_IN_TAGS.equipment
|
||||
) || getParentRefByTag(
|
||||
@@ -189,7 +193,7 @@ export default {
|
||||
collection: 'creatures'
|
||||
};
|
||||
},
|
||||
carriedParentRef(){
|
||||
carriedParentRef() {
|
||||
return getParentRefByTag(
|
||||
this.creatureId, BUILT_IN_TAGS.carried
|
||||
) || getParentRefByTag(
|
||||
@@ -199,30 +203,31 @@ export default {
|
||||
collection: 'creatures'
|
||||
};
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
containerIds(){
|
||||
return this.containers.map(container => container._id);
|
||||
},
|
||||
weightCarried(){
|
||||
},
|
||||
computed: {
|
||||
containerIds() {
|
||||
return this.containers.map(container => container._id);
|
||||
},
|
||||
weightCarried() {
|
||||
return stripFloatingPointOddities(
|
||||
this.creature.variables &&
|
||||
this.creature.variables.weightCarried &&
|
||||
this.creature.variables.weightCarried.value || 0
|
||||
this.variables &&
|
||||
this.variables.weightCarried &&
|
||||
this.variables.weightCarried.value || 0
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty(_id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty(_id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
<script lang="js">
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import Slots from '/imports/ui/creature/slots/Slots.vue';
|
||||
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
||||
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
|
||||
import CreatureSummary from '/imports/ui/creature/character/CreatureSummary.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
|
||||
@@ -29,41 +29,43 @@ import SpellListCard from '/imports/ui/properties/components/spells/SpellListCar
|
||||
import SpellList from '/imports/ui/properties/components/spells/SpellList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
components: {
|
||||
ColumnLayout,
|
||||
SpellList,
|
||||
SpellListCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
SpellListCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data(){ return {
|
||||
organize: false,
|
||||
}},
|
||||
meteor: {
|
||||
spellLists(){
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
organize: false,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
spellLists() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'spellList',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
spellsWithoutList(){
|
||||
},
|
||||
spellsWithoutList() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
$nin: this.spellListIds,
|
||||
},
|
||||
type: 'spell',
|
||||
removed: {$ne: true},
|
||||
deactivatedByAncestor: {$ne: true},
|
||||
deactivatedByToggle: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
deactivatedByAncestor: { $ne: true },
|
||||
deactivatedByToggle: { $ne: true },
|
||||
}, {
|
||||
sort: {
|
||||
level: 1,
|
||||
@@ -71,36 +73,37 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
spellListsWithoutAncestorSpellLists(){
|
||||
spellListsWithoutAncestorSpellLists() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
$nin: this.spellListIds,
|
||||
},
|
||||
type: 'spellList',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
spellListIds(){
|
||||
return this.spellLists.map(spellList => spellList._id);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty(_id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `spell-list-tile-${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
spellListIds() {
|
||||
return this.spellLists.map(spellList => spellList._id);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty(_id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `spell-list-tile-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
class="stats-tab ma-2"
|
||||
>
|
||||
<div class="stats-tab ma-2">
|
||||
<health-bar-card-container :creature-id="creatureId" />
|
||||
|
||||
<column-layout>
|
||||
@@ -46,7 +44,7 @@
|
||||
{{ buff.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-list-item-action v-if="!buff.hideRemoveButton">
|
||||
<v-btn
|
||||
icon
|
||||
@click.stop="softRemove(buff._id)"
|
||||
@@ -171,9 +169,7 @@
|
||||
v-if="spellSlots && spellSlots.length || hasSpells"
|
||||
class="spell-slots"
|
||||
>
|
||||
<v-card
|
||||
data-id="spell-slot-card"
|
||||
>
|
||||
<v-card data-id="spell-slot-card">
|
||||
<v-list
|
||||
v-if="spellSlots && spellSlots.length"
|
||||
two-line
|
||||
@@ -250,18 +246,7 @@
|
||||
:model="action"
|
||||
:data-id="action._id"
|
||||
@click="clickProperty({_id: action._id})"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="attack in attacks"
|
||||
:key="attack._id"
|
||||
class="attack"
|
||||
>
|
||||
<action-card
|
||||
attack
|
||||
:model="attack"
|
||||
:data-id="attack._id"
|
||||
@click="clickProperty({_id: attack._id})"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -350,202 +335,207 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
|
||||
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
|
||||
import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue';
|
||||
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
|
||||
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
|
||||
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
|
||||
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
|
||||
import RestButton from '/imports/ui/creature/RestButton.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
||||
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
|
||||
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
|
||||
import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue';
|
||||
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
|
||||
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
|
||||
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
|
||||
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
|
||||
import RestButton from '/imports/ui/creature/RestButton.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
||||
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
const getProperties = function(creature, filter, options = {
|
||||
sort: {order: 1}
|
||||
}){
|
||||
if (!creature) return;
|
||||
if (creature.settings.hideUnusedStats){
|
||||
filter.hide = {$ne: true};
|
||||
}
|
||||
filter['ancestors.id'] = creature._id;
|
||||
filter.removed = {$ne: true};
|
||||
filter.inactive = {$ne: true};
|
||||
filter.overridden = {$ne: true};
|
||||
|
||||
return CreatureProperties.find(filter, options);
|
||||
};
|
||||
|
||||
const getAttributeOfType = function(creature, type){
|
||||
return getProperties(creature, {
|
||||
type: 'attribute',
|
||||
attributeType: type,
|
||||
});
|
||||
};
|
||||
|
||||
const getSkillOfType = function(creature, type){
|
||||
return getProperties(creature, {
|
||||
type: 'skill',
|
||||
skillType: type,
|
||||
});
|
||||
const getProperties = function (creature, filter, options = {
|
||||
sort: { order: 1 }
|
||||
}) {
|
||||
if (!creature) return;
|
||||
if (creature.settings.hideUnusedStats) {
|
||||
filter.hide = { $ne: true };
|
||||
}
|
||||
filter['ancestors.id'] = creature._id;
|
||||
filter.removed = { $ne: true };
|
||||
filter.inactive = { $ne: true };
|
||||
filter.overridden = { $ne: true };
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RestButton,
|
||||
AbilityListTile,
|
||||
AttributeCard,
|
||||
ColumnLayout,
|
||||
DamageMultiplierCard,
|
||||
HealthBarCardContainer,
|
||||
HitDiceListTile,
|
||||
SkillListTile,
|
||||
ResourceCard,
|
||||
SpellSlotListTile,
|
||||
ActionCard,
|
||||
ToggleCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
return CreatureProperties.find(filter, options);
|
||||
};
|
||||
|
||||
const getAttributeOfType = function (creature, type) {
|
||||
return getProperties(creature, {
|
||||
type: 'attribute',
|
||||
attributeType: type,
|
||||
});
|
||||
};
|
||||
|
||||
const getSkillOfType = function (creature, type) {
|
||||
return getProperties(creature, {
|
||||
type: 'skill',
|
||||
skillType: type,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RestButton,
|
||||
AbilityListTile,
|
||||
AttributeCard,
|
||||
ColumnLayout,
|
||||
DamageMultiplierCard,
|
||||
HealthBarCardContainer,
|
||||
HitDiceListTile,
|
||||
SkillListTile,
|
||||
ResourceCard,
|
||||
SpellSlotListTile,
|
||||
ActionCard,
|
||||
ToggleCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
doCheckLoading: false,
|
||||
}},
|
||||
meteor: {
|
||||
creature(){
|
||||
return Creatures.findOne(this.creatureId, {fields: {settings: 1}});
|
||||
},
|
||||
abilities(){
|
||||
return getAttributeOfType(this.creature, 'ability');
|
||||
},
|
||||
stats(){
|
||||
return getAttributeOfType(this.creature, 'stat');
|
||||
},
|
||||
toggles(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'toggle',
|
||||
removed: {$ne: true},
|
||||
deactivatedByAncestor: {$ne: true},
|
||||
showUI: true,
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
},
|
||||
modifiers(){
|
||||
return getAttributeOfType(this.creature, 'modifier');
|
||||
},
|
||||
resources(){
|
||||
return getAttributeOfType(this.creature, 'resource');
|
||||
},
|
||||
spellSlots(){
|
||||
return getAttributeOfType(this.creature, 'spellSlot');
|
||||
},
|
||||
hasSpells(){
|
||||
return getProperties(this.creature, {
|
||||
type: 'spell',
|
||||
}).count();
|
||||
},
|
||||
hitDice(){
|
||||
return getAttributeOfType(this.creature, 'hitDice');
|
||||
},
|
||||
checks(){
|
||||
return getSkillOfType(this.creature, 'check');
|
||||
},
|
||||
savingThrows(){
|
||||
return getSkillOfType(this.creature, 'save');
|
||||
},
|
||||
skills(){
|
||||
return getSkillOfType(this.creature, 'skill');
|
||||
},
|
||||
tools(){
|
||||
return getSkillOfType(this.creature, 'tool');
|
||||
},
|
||||
weapons(){
|
||||
return getSkillOfType(this.creature, 'weapon');
|
||||
},
|
||||
armors(){
|
||||
return getSkillOfType(this.creature, 'armor');
|
||||
},
|
||||
languages(){
|
||||
return getSkillOfType(this.creature, 'language');
|
||||
},
|
||||
actions(){
|
||||
return getProperties(this.creature, {type: 'action'});
|
||||
},
|
||||
appliedBuffs(){
|
||||
return getProperties(this.creature, {type: 'buff'});
|
||||
},
|
||||
multipliers(){
|
||||
return getProperties(this.creature, {
|
||||
type: 'damageMultiplier'
|
||||
}, {
|
||||
sort: {value: 1, order: 1}
|
||||
});
|
||||
},
|
||||
attacks(){
|
||||
let props = getProperties(this.creature, {type: 'attack'})
|
||||
return props && props.map(attack => {
|
||||
attack.children = CreatureProperties.find({
|
||||
'ancestors.id': attack._id,
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
});
|
||||
return attack;
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty({_id}){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
incrementChange(_id, {type, value}){
|
||||
if (type === 'increment'){
|
||||
damageProperty.call({_id, operation: 'increment' ,value: -value});
|
||||
}
|
||||
},
|
||||
softRemove(_id){
|
||||
softRemoveProperty.call({_id}, error => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
},
|
||||
castSpell(){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'cast-spell-with-slot-dialog',
|
||||
elementId: 'spell-slot-card',
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
callback({spellId, slotId} = {}){
|
||||
if (!spellId) return;
|
||||
doCastSpell.call({spellId, slotId}, error => {
|
||||
if (!error) return;
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
creature() {
|
||||
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
|
||||
},
|
||||
abilities() {
|
||||
return getAttributeOfType(this.creature, 'ability');
|
||||
},
|
||||
stats() {
|
||||
return getAttributeOfType(this.creature, 'stat');
|
||||
},
|
||||
toggles() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'toggle',
|
||||
removed: { $ne: true },
|
||||
deactivatedByAncestor: { $ne: true },
|
||||
showUI: true,
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
modifiers() {
|
||||
return getAttributeOfType(this.creature, 'modifier');
|
||||
},
|
||||
resources() {
|
||||
return getAttributeOfType(this.creature, 'resource');
|
||||
},
|
||||
spellSlots() {
|
||||
return getAttributeOfType(this.creature, 'spellSlot');
|
||||
},
|
||||
hasSpells() {
|
||||
const cursor = getProperties(this.creature, {
|
||||
type: 'spell',
|
||||
})
|
||||
return cursor && cursor.count();
|
||||
},
|
||||
hitDice() {
|
||||
return getAttributeOfType(this.creature, 'hitDice');
|
||||
},
|
||||
checks() {
|
||||
return getSkillOfType(this.creature, 'check');
|
||||
},
|
||||
savingThrows() {
|
||||
return getSkillOfType(this.creature, 'save');
|
||||
},
|
||||
skills() {
|
||||
return getSkillOfType(this.creature, 'skill');
|
||||
},
|
||||
tools() {
|
||||
return getSkillOfType(this.creature, 'tool');
|
||||
},
|
||||
weapons() {
|
||||
return getSkillOfType(this.creature, 'weapon');
|
||||
},
|
||||
armors() {
|
||||
return getSkillOfType(this.creature, 'armor');
|
||||
},
|
||||
languages() {
|
||||
return getSkillOfType(this.creature, 'language');
|
||||
},
|
||||
actions() {
|
||||
return getProperties(this.creature, { type: 'action' });
|
||||
},
|
||||
appliedBuffs() {
|
||||
return getProperties(this.creature, { type: 'buff' });
|
||||
},
|
||||
multipliers() {
|
||||
return getProperties(this.creature, {
|
||||
type: 'damageMultiplier'
|
||||
}, {
|
||||
sort: { value: 1, order: 1 }
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty({ _id }) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
clickTreeProperty({ _id }) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
incrementChange(_id, { type, value }) {
|
||||
if (type === 'increment') {
|
||||
damageProperty.call({ _id, operation: 'increment', value: -value });
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
softRemove(_id) {
|
||||
softRemoveProperty.call({ _id }, error => {
|
||||
if (error) console.error(error);
|
||||
});
|
||||
},
|
||||
castSpell() {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'cast-spell-with-slot-dialog',
|
||||
elementId: 'spell-slot-card',
|
||||
data: {
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
callback({ spellId, slotId, advantage, ritual } = {}) {
|
||||
if (!spellId) return;
|
||||
doCastSpell.call({
|
||||
spellId,
|
||||
slotId,
|
||||
ritual,
|
||||
scope: {
|
||||
$attackAdvantage: advantage,
|
||||
},
|
||||
}, error => {
|
||||
if (!error) return;
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
112
app/imports/ui/creature/character/errors/CharacterErrors.vue
Normal file
112
app/imports/ui/creature/character/errors/CharacterErrors.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div v-if="creature && errors && errors.length">
|
||||
<v-btn
|
||||
fab
|
||||
small
|
||||
absolute
|
||||
right
|
||||
color="warning"
|
||||
class="mr-4"
|
||||
style="margin-top: -20px;"
|
||||
@click="expanded = !expanded"
|
||||
>
|
||||
<v-icon
|
||||
v-if="expanded"
|
||||
style="color: rgba(0,0,0,0.8);"
|
||||
>
|
||||
mdi-close
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
style="color: rgba(0,0,0,0.8);"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
<v-slide-y-transition>
|
||||
<div
|
||||
v-if="expanded"
|
||||
class="character-sheet-errors"
|
||||
>
|
||||
<template v-for="(error, index) in errors">
|
||||
<dependency-loop-error
|
||||
v-if="error.type === 'dependencyLoop'"
|
||||
:key="index + 'dependencyLoopError'"
|
||||
:model="error"
|
||||
/>
|
||||
<v-alert
|
||||
v-else
|
||||
:key="index + 'otherError'"
|
||||
border="bottom"
|
||||
colored-border
|
||||
elevation="2"
|
||||
type="error"
|
||||
>
|
||||
{{ error.type }}
|
||||
</v-alert>
|
||||
</template>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import DependencyLoopError from '/imports/ui/creature/character/errors/DependencyLoopError.vue';
|
||||
import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DependencyLoopError,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} },
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
data() { return {
|
||||
expanded: false,
|
||||
}},
|
||||
meteor: {
|
||||
creature() {
|
||||
if (!this.creatureId) return;
|
||||
return Creatures.findOne(this.creatureId, {fields: {computeErrors: 1, settings: 1}});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
errors() {
|
||||
if (!this.creature || !this.creature.computeErrors) return [];
|
||||
return this.creature.computeErrors;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
expanded(value) {
|
||||
if (this.context.editPermission === false) return;
|
||||
updateCreature.call({
|
||||
_id: this.creatureId,
|
||||
path: ['settings', 'hideCalculationErrors'],
|
||||
value: !value || null,
|
||||
}, (error) => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.expanded = !this.creature.settings.hideCalculationErrors;
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
102
app/imports/ui/creature/character/errors/DependencyLoopError.vue
Normal file
102
app/imports/ui/creature/character/errors/DependencyLoopError.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<v-alert
|
||||
border="bottom"
|
||||
colored-border
|
||||
elevation="2"
|
||||
type="warning"
|
||||
class="dependency-loop-error"
|
||||
>
|
||||
<p>
|
||||
The character contains a dependency loop.
|
||||
</p>
|
||||
<p>
|
||||
A set of properties may have been calculated incorrectly, because they form an infinite loop:
|
||||
</p>
|
||||
<div class="d-flex align-center flex-wrap">
|
||||
<template
|
||||
v-for="(prop, index) in loopProperties"
|
||||
>
|
||||
<v-icon
|
||||
v-if="index !== 0"
|
||||
:key="index"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
<a
|
||||
v-if="prop.type"
|
||||
:key="index + 'link'"
|
||||
:data-id="`breadcrumb-${prop._id}`"
|
||||
@click="click(prop._id)"
|
||||
>
|
||||
<tree-node-view
|
||||
:model="prop"
|
||||
class="breadcrumb-tree-node-view"
|
||||
/>
|
||||
</a>
|
||||
<span
|
||||
v-else
|
||||
:key="index + 'variable'"
|
||||
style="font-family: monospace !important;"
|
||||
>
|
||||
{{ prop.name }} {{ prop.path }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import { reverse } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeView,
|
||||
},
|
||||
inject: {
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
loopProperties() {
|
||||
if (!this.model) return;
|
||||
const propAddresses = this.model.details?.nodes || [];
|
||||
const props = propAddresses.map(propAddress => {
|
||||
const [id, ...path] = propAddress.split('.');
|
||||
const prop = CreatureProperties.findOne(id);
|
||||
if (prop) {
|
||||
prop.path = path && path.join('.');
|
||||
if (prop.name && prop.path) prop.name += ` [${prop.path}]`;
|
||||
return prop;
|
||||
} else {
|
||||
return { name: propAddress };
|
||||
}
|
||||
});
|
||||
return reverse(props);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
click(id){
|
||||
// Otherwise open it as a new dialog
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `breadcrumb-${id}`,
|
||||
data: {_id: id},
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,28 +1,29 @@
|
||||
<template lang="html">
|
||||
<v-list-item style="min-height: 60px;">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
<template v-if="!renaming">
|
||||
{{ model.name }}
|
||||
</template>
|
||||
<text-field
|
||||
v-if="renaming"
|
||||
ref="name-input"
|
||||
regular
|
||||
hide-details
|
||||
dense
|
||||
:value="model.name"
|
||||
@change="renameFolder"
|
||||
@click.native.stop=""
|
||||
@input.native.stop=""
|
||||
@keydown.native.stop=""
|
||||
@keyup.native.stop=""
|
||||
/>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<template v-if="!selection && !dense">
|
||||
<v-list-item-action v-if="renaming || open">
|
||||
<v-list-item-content style="min-height: 60px;">
|
||||
<v-list-item-title class="d-flex align-center">
|
||||
<div
|
||||
v-if="!renaming"
|
||||
class="text-truncate text-no-wrap"
|
||||
>
|
||||
{{ model.name }}
|
||||
</div>
|
||||
<text-field
|
||||
v-if="renaming"
|
||||
ref="name-input"
|
||||
regular
|
||||
hide-details
|
||||
dense
|
||||
:value="model.name"
|
||||
@change="renameFolder"
|
||||
@click.native.stop=""
|
||||
@input.native.stop=""
|
||||
@keydown.native.stop=""
|
||||
@keyup.native.stop=""
|
||||
/>
|
||||
<template v-if="!selection && !dense">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
v-if="renaming || open"
|
||||
icon
|
||||
style="flex-grow: 0"
|
||||
@click.stop="renaming = !renaming"
|
||||
@@ -34,18 +35,17 @@
|
||||
mdi-pencil
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
<v-list-item-action v-if="open">
|
||||
<v-btn
|
||||
v-if="open"
|
||||
icon
|
||||
style="flex-grow: 0"
|
||||
@click.stop="removeFolder"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template lang="html">
|
||||
<v-list
|
||||
expand
|
||||
class="creature-folder-list"
|
||||
>
|
||||
<creature-list
|
||||
:creatures="creatures"
|
||||
@@ -66,5 +67,8 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
<style lang="css">
|
||||
.creature-folder-list .v-list-item__icon.v-list-group__header__append-icon {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
:is-selected="selectedCreature === creature._id"
|
||||
v-bind="selection ? {} : {to: creature.url}"
|
||||
:dense="dense"
|
||||
:data-id="dense ? undefined : creature._id"
|
||||
@click="$emit('creature-selected', creature._id)"
|
||||
/>
|
||||
</draggable>
|
||||
|
||||
@@ -171,160 +171,166 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
||||
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
||||
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
||||
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
||||
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
||||
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
||||
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import PropertySelector from '/imports/ui/properties/shared/PropertySelector.vue';
|
||||
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PropertySelector,
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
LibraryNodeExpansionContent,
|
||||
...propertyFormIndex,
|
||||
export default {
|
||||
components: {
|
||||
PropertySelector,
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
LibraryNodeExpansionContent,
|
||||
...propertyFormIndex,
|
||||
},
|
||||
mixins: [schemaFormMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
mixins: [schemaFormMixin],
|
||||
props: {
|
||||
forcedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
suggestedTypes: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
suggestedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
parentDoc: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
forcedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['debounceTime'],
|
||||
suggestedTypes: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
data(){return {
|
||||
selectedNodeIds: [],
|
||||
type: this.forcedType || this.suggestedType,
|
||||
model: {
|
||||
type: this.type,
|
||||
},
|
||||
searchValue: undefined,
|
||||
debounceTime: 0,
|
||||
tab: 0,
|
||||
};},
|
||||
computed: {
|
||||
typeName(){
|
||||
return getPropertyName(this.type) || 'Property';
|
||||
},
|
||||
toolbarColor(){
|
||||
return getThemeColor('secondary');
|
||||
}
|
||||
suggestedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
watch: {
|
||||
type(newType){
|
||||
this.changeType(newType);
|
||||
},
|
||||
parentDoc: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
mounted(){
|
||||
this.changeType(this.type);
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['debounceTime'],
|
||||
},
|
||||
data(){return {
|
||||
selectedNodeIds: [],
|
||||
type: this.forcedType || this.suggestedType,
|
||||
model: {
|
||||
type: this.type,
|
||||
},
|
||||
methods: {
|
||||
|
||||
propertyHelpChanged(value){
|
||||
Meteor.users.setPreference.call({
|
||||
preference: 'hidePropertySelectDialogHelp',
|
||||
value: !value
|
||||
}, error => {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
},
|
||||
searchChanged(val, ack){
|
||||
this._subs.searchLibraryNodes.setData('searchTerm', val);
|
||||
this._subs.searchLibraryNodes.setData('limit', undefined);
|
||||
this.selectedNode = undefined;
|
||||
this.searchValue = val;
|
||||
setTimeout(ack, 200);
|
||||
},
|
||||
loadMore(){
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32);
|
||||
},
|
||||
insert(){
|
||||
if (!this.selectedNodeIds.length) return;
|
||||
this.$store.dispatch('popDialogStack', this.selectedNodeIds);
|
||||
},
|
||||
changeType(type){
|
||||
this._subs.searchLibraryNodes.setData('type', type);
|
||||
if (!type) return;
|
||||
this.tab = 1;
|
||||
this.schema = propertySchemasIndex[type];
|
||||
this.validationContext = this.schema.newContext();
|
||||
let model = this.schema.clean({});
|
||||
model.type = type;
|
||||
this.model = model;
|
||||
},
|
||||
openPropertyDetails(id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
searchValue: undefined,
|
||||
debounceTime: 0,
|
||||
tab: 0,
|
||||
};},
|
||||
computed: {
|
||||
typeName(){
|
||||
return getPropertyName(this.type) || 'Property';
|
||||
},
|
||||
meteor: {
|
||||
'$subscribe':{
|
||||
'searchLibraryNodes': [],
|
||||
'selectedLibraryNodes'(){
|
||||
return [this.selectedNodeIds];
|
||||
},
|
||||
},
|
||||
showPropertyHelp(){
|
||||
let user = Meteor.user();
|
||||
return !(user?.preferences?.hidePropertySelectDialogHelp)
|
||||
},
|
||||
currentLimit(){
|
||||
return this._subs.searchLibraryNodes.data('limit') || 32;
|
||||
},
|
||||
countAll(){
|
||||
return this._subs.searchLibraryNodes.data('countAll');
|
||||
},
|
||||
libraryNodes(){
|
||||
return LibraryNodes.find({
|
||||
_searchResult: true
|
||||
},{
|
||||
sort: {
|
||||
'ancestors.0.id': 1,
|
||||
name: 1,
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
libraryNames(){
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
}
|
||||
toolbarColor(){
|
||||
return getThemeColor('secondary');
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
type(newType){
|
||||
this.changeType(newType);
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
this.changeType(this.type);
|
||||
},
|
||||
methods: {
|
||||
|
||||
propertyHelpChanged(value){
|
||||
Meteor.users.setPreference.call({
|
||||
preference: 'hidePropertySelectDialogHelp',
|
||||
value: !value
|
||||
}, error => {
|
||||
if (!error) return;
|
||||
console.error(error);
|
||||
snackbar({
|
||||
text: error.reason,
|
||||
});
|
||||
});
|
||||
},
|
||||
searchChanged(val, ack){
|
||||
this._subs.searchLibraryNodes.setData('searchTerm', val);
|
||||
this._subs.searchLibraryNodes.setData('limit', undefined);
|
||||
this.selectedNode = undefined;
|
||||
this.searchValue = val;
|
||||
setTimeout(ack, 200);
|
||||
},
|
||||
loadMore(){
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs.searchLibraryNodes.setData('limit', this.currentLimit + 32);
|
||||
},
|
||||
insert(){
|
||||
if (!this.selectedNodeIds.length) return;
|
||||
this.$store.dispatch('popDialogStack', this.selectedNodeIds);
|
||||
},
|
||||
changeType(type){
|
||||
this._subs.searchLibraryNodes.setData('type', type);
|
||||
if (!type) return;
|
||||
this.tab = 1;
|
||||
this.schema = propertySchemasIndex[type];
|
||||
this.validationContext = this.schema.newContext();
|
||||
let model = this.schema.clean({});
|
||||
model.type = type;
|
||||
this.model = model;
|
||||
},
|
||||
openPropertyDetails(id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
'$subscribe':{
|
||||
'searchLibraryNodes'() {
|
||||
return [this.creatureId]
|
||||
},
|
||||
'selectedLibraryNodes'(){
|
||||
return [this.selectedNodeIds];
|
||||
},
|
||||
},
|
||||
showPropertyHelp(){
|
||||
let user = Meteor.user();
|
||||
return !(user?.preferences?.hidePropertySelectDialogHelp)
|
||||
},
|
||||
currentLimit(){
|
||||
return this._subs.searchLibraryNodes.data('limit') || 32;
|
||||
},
|
||||
countAll(){
|
||||
return this._subs.searchLibraryNodes.data('countAll');
|
||||
},
|
||||
libraryNodes(){
|
||||
return LibraryNodes.find({
|
||||
_searchResult: true
|
||||
},{
|
||||
sort: {
|
||||
'ancestors.0.id': 1,
|
||||
name: 1,
|
||||
order: 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
libraryNames(){
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
@@ -3,9 +3,24 @@
|
||||
class="breadcrumbs layout align-center wrap"
|
||||
:class="{'no-icons': noIcons}"
|
||||
>
|
||||
<span
|
||||
v-if="noLinks"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-account
|
||||
</v-icon>
|
||||
</span>
|
||||
<a
|
||||
v-else
|
||||
data-id="breadcrumb-root"
|
||||
@click="clickRootCreature"
|
||||
>
|
||||
<v-icon color="accent">
|
||||
mdi-account
|
||||
</v-icon>
|
||||
</a>
|
||||
<template v-for="(prop, index) in props">
|
||||
<v-icon
|
||||
v-if="index !== 0"
|
||||
:key="index"
|
||||
>
|
||||
mdi-chevron-right
|
||||
@@ -49,6 +64,7 @@
|
||||
},
|
||||
noLinks: Boolean,
|
||||
noIcons: Boolean,
|
||||
editing: Boolean,
|
||||
},
|
||||
computed:{
|
||||
props(){
|
||||
@@ -60,7 +76,7 @@
|
||||
},
|
||||
methods: {
|
||||
click(id){
|
||||
let store = this.$store;
|
||||
const store = this.$store;
|
||||
// Check if there is a dialog open for this doc already
|
||||
let dialogFound;
|
||||
let dialogsToPop = 0;
|
||||
@@ -80,10 +96,41 @@
|
||||
store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `breadcrumb-${id}`,
|
||||
data: {_id: id},
|
||||
data: {
|
||||
_id: id,
|
||||
startInEditTab: this.editing,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
clickRootCreature() {
|
||||
const store = this.$store;
|
||||
// Check if there is a dialog open for this doc already
|
||||
let dialogFound;
|
||||
let dialogsToPop = 0;
|
||||
store.state.dialogStack.dialogs.forEach(dialog => {
|
||||
if (dialog.component === 'creature-root-dialog'){
|
||||
dialogFound = true;
|
||||
dialogsToPop = 0;
|
||||
} else {
|
||||
dialogsToPop += 1;
|
||||
}
|
||||
});
|
||||
if (dialogFound){
|
||||
// Pop dialogs until we get to it
|
||||
store.dispatch('popDialogStacks', dialogsToPop);
|
||||
} else {
|
||||
// Otherwise open it as a new dialog
|
||||
store.commit('pushDialogStack', {
|
||||
component: 'creature-root-dialog',
|
||||
elementId: 'breadcrumb-root',
|
||||
data: {
|
||||
_id: this.model.ancestors[0].id,
|
||||
startInEditTab: this.editing,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
:group="group"
|
||||
:organize="organize"
|
||||
:selected-node="selectedNode"
|
||||
:start-expanded="expanded"
|
||||
@selected="e => $emit('selected', e)"
|
||||
@reordered="reordered"
|
||||
@reorganized="reorganized"
|
||||
@@ -12,76 +13,81 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import nodesToTree from '/imports/api/parenting/nodesToTree.js'
|
||||
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
|
||||
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeList,
|
||||
},
|
||||
props: {
|
||||
root: Object,
|
||||
organize: Boolean,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: 'creatureProperties'
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
children(){
|
||||
const children = nodesToTree({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: this.root.id,
|
||||
filter: this.filter,
|
||||
includeFilteredDocAncestors: true,
|
||||
includeFilteredDocDescendants: true,
|
||||
});
|
||||
this.$emit('length', children.length);
|
||||
return children;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reordered({doc, newIndex}){
|
||||
reorderDoc.call({
|
||||
docRef: {
|
||||
id: doc._id,
|
||||
collection: 'creatureProperties',
|
||||
},
|
||||
order: newIndex,
|
||||
});
|
||||
},
|
||||
reorganized({doc, parent, newIndex}){
|
||||
let parentRef;
|
||||
if (parent){
|
||||
parentRef = {
|
||||
id: parent._id,
|
||||
collection: 'creatureProperties',
|
||||
};
|
||||
} else {
|
||||
parentRef = this.root;
|
||||
}
|
||||
organizeDoc.call({
|
||||
docRef: {
|
||||
id: doc._id,
|
||||
collection: 'creatureProperties',
|
||||
},
|
||||
parentRef,
|
||||
order: newIndex,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeList,
|
||||
},
|
||||
props: {
|
||||
root: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
organize: Boolean,
|
||||
selectedNode: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
group: {
|
||||
type: String,
|
||||
default: 'creatureProperties'
|
||||
},
|
||||
expanded: Boolean,
|
||||
},
|
||||
meteor: {
|
||||
children() {
|
||||
const children = nodesToTree({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: this.root.id,
|
||||
filter: this.filter,
|
||||
includeFilteredDocAncestors: true,
|
||||
includeFilteredDocDescendants: true,
|
||||
});
|
||||
this.$emit('length', children.length);
|
||||
return children;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reordered({ doc, newIndex }) {
|
||||
reorderDoc.call({
|
||||
docRef: {
|
||||
id: doc._id,
|
||||
collection: 'creatureProperties',
|
||||
},
|
||||
order: newIndex,
|
||||
});
|
||||
},
|
||||
reorganized({ doc, parent, newIndex }) {
|
||||
let parentRef;
|
||||
if (parent) {
|
||||
parentRef = {
|
||||
id: parent._id,
|
||||
collection: 'creatureProperties',
|
||||
};
|
||||
} else {
|
||||
parentRef = this.root;
|
||||
}
|
||||
organizeDoc.call({
|
||||
docRef: {
|
||||
id: doc._id,
|
||||
collection: 'creatureProperties',
|
||||
},
|
||||
parentRef,
|
||||
order: newIndex,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -20,7 +20,7 @@ import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
export default {
|
||||
components: {
|
||||
SelectablePropertyDialog,
|
||||
CreaturePropertyInsertForm,
|
||||
CreaturePropertyInsertForm,
|
||||
},
|
||||
props: {
|
||||
forcedType: {
|
||||
@@ -28,21 +28,24 @@ export default {
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() { return {
|
||||
type: undefined,
|
||||
};},
|
||||
methods: {
|
||||
getPropertyName,
|
||||
back(){
|
||||
if (this.forcedType){
|
||||
data() {
|
||||
return {
|
||||
type: undefined,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getPropertyName,
|
||||
back() {
|
||||
if (this.forcedType) {
|
||||
this.$store.dispatch('popDialogStack');
|
||||
} else {
|
||||
this.type = undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -14,33 +14,57 @@
|
||||
/>
|
||||
</template>
|
||||
<template v-if="model">
|
||||
<div
|
||||
class="layout mb-4"
|
||||
>
|
||||
<template v-if="!embedded">
|
||||
<breadcrumbs
|
||||
:model="model"
|
||||
:editing="editing"
|
||||
/>
|
||||
</template>
|
||||
<v-spacer />
|
||||
<v-chip disabled>
|
||||
{{ typeName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-fade-transition
|
||||
mode="out-in"
|
||||
>
|
||||
<component
|
||||
:is="model.type + 'Form'"
|
||||
v-if="editing"
|
||||
:key="_id"
|
||||
class="creature-property-form"
|
||||
:model="model"
|
||||
@change="change"
|
||||
@push="push"
|
||||
@pull="pull"
|
||||
/>
|
||||
<div
|
||||
v-else-if="!editing && $options.components[model.type + 'Viewer']"
|
||||
>
|
||||
<div
|
||||
class="layout mb-4"
|
||||
<div v-if="editing">
|
||||
<component
|
||||
:is="model.type + 'Form'"
|
||||
:key="_id"
|
||||
class="creature-property-form"
|
||||
:model="model"
|
||||
@change="change"
|
||||
@push="push"
|
||||
@pull="pull"
|
||||
>
|
||||
<template v-if="!embedded">
|
||||
<breadcrumbs :model="model" />
|
||||
<template #children>
|
||||
<creature-properties-tree
|
||||
style="width: 100%;"
|
||||
class="mb-2"
|
||||
organize
|
||||
:root="{collection: 'creatureProperties', id: model._id}"
|
||||
@length="childrenLength = $event"
|
||||
@selected="selectSubProperty"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
outlined
|
||||
color="accent"
|
||||
data-id="insert-creature-property-btn"
|
||||
@click="addProperty"
|
||||
>
|
||||
<v-icon>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-spacer />
|
||||
<v-chip disabled>
|
||||
{{ typeName }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</component>
|
||||
</div>
|
||||
<div v-else>
|
||||
<component
|
||||
:is="model.type + 'Viewer'"
|
||||
:key="_id"
|
||||
@@ -65,9 +89,6 @@
|
||||
</property-field>
|
||||
</v-row>
|
||||
</div>
|
||||
<p v-else>
|
||||
This property can't be viewed yet.
|
||||
</p>
|
||||
</v-fade-transition>
|
||||
</template>
|
||||
<div
|
||||
@@ -75,17 +96,6 @@
|
||||
slot="actions"
|
||||
class="layout"
|
||||
>
|
||||
<v-btn
|
||||
v-if="!editing && !embedded"
|
||||
text
|
||||
data-id="insert-creature-property-btn"
|
||||
@click="addProperty"
|
||||
>
|
||||
<v-icon left>
|
||||
mdi-plus
|
||||
</v-icon>
|
||||
Child Property
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
@@ -193,7 +203,7 @@ export default {
|
||||
watch: {
|
||||
_id: {
|
||||
immediate: true,
|
||||
handler(newId){
|
||||
handler(newId) {
|
||||
this.$nextTick(() => {
|
||||
this.currentId = newId;
|
||||
});
|
||||
@@ -256,7 +266,10 @@ export default {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: {_id},
|
||||
data: {
|
||||
_id,
|
||||
startInEditTab: this.editing,
|
||||
},
|
||||
});
|
||||
},
|
||||
addProperty(){
|
||||
@@ -266,6 +279,7 @@ export default {
|
||||
elementId: 'insert-creature-property-btn',
|
||||
data: {
|
||||
parentDoc: this.model,
|
||||
creatureId: this.creatureId,
|
||||
},
|
||||
callback(result){
|
||||
if (!result) return;
|
||||
|
||||
@@ -23,18 +23,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue';
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
LibraryAndNode,
|
||||
},
|
||||
data(){return {
|
||||
node: undefined,
|
||||
};},
|
||||
};
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue';
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
LibraryAndNode,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
node: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -34,9 +34,9 @@ import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
DialogBase,
|
||||
ExperienceForm,
|
||||
},
|
||||
},
|
||||
mixins: [schemaFormMixin],
|
||||
provide: {
|
||||
context: {
|
||||
@@ -52,10 +52,10 @@ export default {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
data(){
|
||||
data() {
|
||||
let schema = ExperienceSchema.omit('creatureId');
|
||||
let startingModel = {};
|
||||
if (this.startAsMilestone){
|
||||
if (this.startAsMilestone) {
|
||||
startingModel.levels = 1;
|
||||
}
|
||||
return {
|
||||
@@ -65,14 +65,14 @@ export default {
|
||||
debounceTime: 0,
|
||||
};
|
||||
},
|
||||
methods:{
|
||||
insertExperience(){
|
||||
methods: {
|
||||
insertExperience() {
|
||||
let experience = this.schema.clean(this.model);
|
||||
let id = insertExperience.call({
|
||||
experience,
|
||||
creatureIds: this.creatureIds,
|
||||
}, (error) => {
|
||||
if (error){
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
@@ -83,4 +83,5 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
|
||||
400
app/imports/ui/creature/slots/LevelUpDialog.vue
Normal file
400
app/imports/ui/creature/slots/LevelUpDialog.vue
Normal file
@@ -0,0 +1,400 @@
|
||||
<template lang="html">
|
||||
<dialog-base
|
||||
:color="model.color"
|
||||
dark-body
|
||||
>
|
||||
<template slot="toolbar">
|
||||
<v-toolbar-title>
|
||||
{{ model.name }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-text-field
|
||||
v-model="searchInput"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
regular
|
||||
clearable
|
||||
hide-details
|
||||
class="flex-grow-0"
|
||||
style="flex-basis: 300px;"
|
||||
:loading="searchLoading"
|
||||
@change="searchValue = searchInput || undefined"
|
||||
@click:clear="searchValue = undefined"
|
||||
/>
|
||||
</template>
|
||||
<property-description
|
||||
text
|
||||
:string="model.description"
|
||||
/>
|
||||
<p>
|
||||
<property-tags
|
||||
v-for="(tags, index) in tagsSearched.or"
|
||||
:key="index"
|
||||
:tags="tags"
|
||||
:prefix="index ? 'OR' : undefined"
|
||||
/>
|
||||
<property-tags
|
||||
v-for="(tags, index) in tagsSearched.not"
|
||||
:key="index"
|
||||
:tags="tags"
|
||||
prefix="NOT"
|
||||
/>
|
||||
</p>
|
||||
<v-expansion-panels
|
||||
multiple
|
||||
inset
|
||||
>
|
||||
<template v-for="libraryNode in libraryNodes">
|
||||
<v-expansion-panel
|
||||
v-if="showDisabled || !libraryNode._disabledBySlotFillerCondition"
|
||||
:key="libraryNode._id"
|
||||
:model="libraryNode"
|
||||
:data-id="libraryNode._id"
|
||||
:class="{disabled: isDisabled(libraryNode)}"
|
||||
>
|
||||
<v-expansion-panel-header>
|
||||
<template #default="{ open }">
|
||||
<v-layout
|
||||
align-center
|
||||
class="flex-grow-0 mr-2"
|
||||
>
|
||||
<v-checkbox
|
||||
v-if="libraryNode._disabledByAlreadyAdded"
|
||||
class="my-0 py-0"
|
||||
hide-details
|
||||
:input-value="true"
|
||||
disabled
|
||||
/>
|
||||
<v-checkbox
|
||||
v-else
|
||||
v-model="selectedNodeIds"
|
||||
class="my-0 py-0"
|
||||
hide-details
|
||||
:disabled="isDisabled(libraryNode)"
|
||||
:value="libraryNode._id"
|
||||
@click.stop
|
||||
/>
|
||||
</v-layout>
|
||||
<v-layout column>
|
||||
<v-layout align-center>
|
||||
<tree-node-view :model="libraryNode" />
|
||||
<div
|
||||
v-if="libraryNode._disabledBySlotFillerCondition"
|
||||
class="error--text text-no-wrap text-truncate"
|
||||
>
|
||||
{{ libraryNode.slotFillerCondition }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<div class="text-caption text-no-wrap text-truncate">
|
||||
{{ libraryNames[libraryNode.ancestors[0].id ] }}
|
||||
</div>
|
||||
</v-layout>
|
||||
<div
|
||||
v-if="libraryNode.slotQuantityFilled !== undefined && libraryNode.slotQuantityFilled !== 1"
|
||||
class="text-overline flex-grow-0 text-no-wrap"
|
||||
:class="{
|
||||
'error--text': isDisabled(libraryNode) &&
|
||||
libraryNode._disabledByQuantityFilled
|
||||
}"
|
||||
>
|
||||
{{ libraryNode.slotQuantityFilled }} slots
|
||||
</div>
|
||||
<template v-if="open">
|
||||
<v-btn
|
||||
icon
|
||||
class="flex-grow-0"
|
||||
@click.stop="openPropertyDetails(libraryNode._id)"
|
||||
>
|
||||
<v-icon>mdi-window-restore</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<library-node-expansion-content :model="libraryNode" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
</v-expansion-panels>
|
||||
<v-layout
|
||||
v-if="(!$subReady.classFillers && !searchValue) || currentLimit < countAll"
|
||||
column
|
||||
align-center
|
||||
justify-center
|
||||
class="ma-3"
|
||||
>
|
||||
<v-btn
|
||||
:loading="!$subReady.classFillers"
|
||||
color="accent"
|
||||
@click="loadMore"
|
||||
>
|
||||
Load More
|
||||
</v-btn>
|
||||
</v-layout>
|
||||
<template v-if="!showDisabled && disabledNodeCount">
|
||||
<v-layout
|
||||
column
|
||||
align-center
|
||||
justify-center
|
||||
class="ma-3"
|
||||
>
|
||||
<div>
|
||||
Requirements of {{ disabledNodeCount }} properties were not met
|
||||
</div>
|
||||
<v-btn
|
||||
class="mt-2"
|
||||
elevation="0"
|
||||
color="accent"
|
||||
@click="showDisabled = true"
|
||||
>
|
||||
Show All
|
||||
</v-btn>
|
||||
</v-layout>
|
||||
</template>
|
||||
<template slot="actions">
|
||||
<v-btn
|
||||
text
|
||||
@click="$store.dispatch('popDialogStack')"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
text
|
||||
color="primary"
|
||||
:disabled="!dummySlot && !selectedNodeIds.length"
|
||||
@click="$store.dispatch('popDialogStack', selectedNodeIds)"
|
||||
>
|
||||
<template v-if="model.spaceLeft">
|
||||
{{ totalQuantitySelected }} / {{ model.spaceLeft }}
|
||||
</template>
|
||||
<template v-if="classId">
|
||||
Insert
|
||||
</template>
|
||||
<template v-else>
|
||||
Close Test
|
||||
</template>
|
||||
</v-btn>
|
||||
</template>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
import { prettifyParseError, parse } from '/imports/parser/parser.js';
|
||||
// import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
|
||||
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
||||
import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
PropertyDescription,
|
||||
LibraryNodeExpansionContent,
|
||||
PropertyTags,
|
||||
},
|
||||
props: {
|
||||
classId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
dummySlot: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedNodeIds: [],
|
||||
searchInput: undefined,
|
||||
searchValue: undefined,
|
||||
showDisabled: false,
|
||||
disabledNodeCount: undefined,
|
||||
}
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId'],
|
||||
},
|
||||
computed: {
|
||||
tagsSearched() {
|
||||
let or = [];
|
||||
let not = [];
|
||||
if (this.model.slotTags && this.model.slotTags.length) {
|
||||
or.push(this.model.slotTags);
|
||||
}
|
||||
this.model.extraTags?.forEach(extras => {
|
||||
if (extras.tags?.length) {
|
||||
if (extras.operation === 'OR') {
|
||||
or.push(extras.tags);
|
||||
} else if (extras.operation === 'NOT') {
|
||||
not.push(extras.tags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return { or, not };
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadMore() {
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs['classFillers'].setData('limit', this.currentLimit + 50);
|
||||
},
|
||||
openPropertyDetails(id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
data: {
|
||||
_id: id,
|
||||
},
|
||||
});
|
||||
},
|
||||
isDisabled(node) {
|
||||
return node._disabledBySlotFillerCondition ||
|
||||
node._disabledByAlreadyAdded ||
|
||||
(
|
||||
node._disabledByQuantityFilled &&
|
||||
!this.selectedNodeIds.includes(node._id)
|
||||
)
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'classFillers'() {
|
||||
return [this.classId, this.searchValue || undefined]
|
||||
},
|
||||
},
|
||||
searchLoading() {
|
||||
return !!this.searchValue && !this.$subReady.classFillers;
|
||||
},
|
||||
model() {
|
||||
if (this.classId) {
|
||||
return CreatureProperties.findOne(this.classId);
|
||||
} else if (this.dummySlot) {
|
||||
let model = clone(this.dummySlot)
|
||||
if (!model.quantityExpected) model.quantityExpected = {};
|
||||
model.quantityExpected.value = +model.quantityExpected.calculation;
|
||||
model.spaceLeft = model.quantityExpected.value;
|
||||
return model;
|
||||
}
|
||||
},
|
||||
variables() {
|
||||
if (!this.creatureId) return {};
|
||||
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
|
||||
},
|
||||
currentLimit() {
|
||||
return this._subs['classFillers'].data('limit') || 50;
|
||||
},
|
||||
countAll() {
|
||||
return this._subs['classFillers'].data('countAll');
|
||||
},
|
||||
alreadyAdded() {
|
||||
let added = new Set();
|
||||
if (!this.model.unique) return added;
|
||||
let ancestorId;
|
||||
if (this.model.unique === 'uniqueInSlot') {
|
||||
ancestorId = this.model._id;
|
||||
} else if (this.model.unique === 'uniqueInCreature') {
|
||||
ancestorId = this.creatureId;
|
||||
}
|
||||
CreatureProperties.find({
|
||||
'ancestors.id': ancestorId,
|
||||
libraryNodeId: { $exists: true },
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
fields: { libraryNodeId: 1 },
|
||||
}).forEach(prop => {
|
||||
added.add(prop.libraryNodeId);
|
||||
});
|
||||
return added;
|
||||
},
|
||||
totalQuantitySelected() {
|
||||
let quantitySelected = 0;
|
||||
LibraryNodes.find({
|
||||
_id: { $in: this.selectedNodeIds }
|
||||
}, {
|
||||
fields: { slotQuantityFilled: 1 },
|
||||
}).forEach(node => {
|
||||
if (Number.isFinite(node.slotQuantityFilled)) {
|
||||
quantitySelected += node.slotQuantityFilled;
|
||||
} else {
|
||||
quantitySelected += 1;
|
||||
}
|
||||
});
|
||||
return quantitySelected;
|
||||
},
|
||||
spaceLeft() {
|
||||
if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined;
|
||||
return this.model.spaceLeft - this.totalQuantitySelected;
|
||||
},
|
||||
libraryNames() {
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
},
|
||||
libraryNodes() {
|
||||
let filter = getSlotFillFilter({ slot: this.model });
|
||||
let nodes = LibraryNodes.find(filter, {
|
||||
sort: { name: 1, order: 1 }
|
||||
}).fetch();
|
||||
let disabledNodeCount = 0;
|
||||
// Mark classFillers whose condition isn't met or are too big to fit
|
||||
// the quantity to fill
|
||||
nodes.forEach(node => {
|
||||
if (node.slotFillerCondition) {
|
||||
try {
|
||||
let parseNode = parse(node.slotFillerCondition);
|
||||
const { result: resultNode } = resolve('reduce', parseNode, this.variables);
|
||||
if (resultNode?.parseType === 'constant') {
|
||||
if (!resultNode.value) {
|
||||
node._disabledBySlotFillerCondition = true;
|
||||
disabledNodeCount += 1;
|
||||
}
|
||||
} else {
|
||||
node._disabledBySlotFillerCondition = true;
|
||||
node._conditionError = toString(resultNode);
|
||||
disabledNodeCount += 1;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
let error = prettifyParseError(e);
|
||||
node._disabledBySlotFillerCondition = true;
|
||||
node._conditionError = error;
|
||||
disabledNodeCount += 1;
|
||||
}
|
||||
}
|
||||
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
|
||||
if (
|
||||
quantityToFill > this.spaceLeft
|
||||
) {
|
||||
node._disabledByQuantityFilled = true;
|
||||
}
|
||||
if (this.alreadyAdded.has(node._id)) {
|
||||
node._disabledByAlreadyAdded = true;
|
||||
}
|
||||
});
|
||||
this.disabledNodeCount = disabledNodeCount;
|
||||
return nodes;
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
76
app/imports/ui/creature/slots/SlotCard.vue
Normal file
76
app/imports/ui/creature/slots/SlotCard.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<v-card
|
||||
v-if="model"
|
||||
v-bind="$attrs"
|
||||
:data-id="`slot-card-${model._id}`"
|
||||
:style="`border: solid 1px ${accentColor};`"
|
||||
hover
|
||||
class="slot-card d-flex flex-column"
|
||||
@mouseover="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<card-highlight
|
||||
:active="hover"
|
||||
/>
|
||||
<v-card-title>
|
||||
{{ model.name }}
|
||||
</v-card-title>
|
||||
<v-card-text v-if="model.description">
|
||||
<property-description
|
||||
text
|
||||
:model="model.description"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-spacer />
|
||||
<v-card-actions>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
icon
|
||||
color="accent"
|
||||
@click.stop="$emit('ignore')"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CardHighlight,
|
||||
PropertyDescription,
|
||||
},
|
||||
inject: {
|
||||
theme: {
|
||||
default: {
|
||||
isDark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){ return {
|
||||
hover: false,
|
||||
}},
|
||||
computed: {
|
||||
accentColor() {
|
||||
if (this.model.color) {
|
||||
return this.model.color
|
||||
} else if (this.theme.isDark){
|
||||
return this.$vuetify.theme.themes.dark.primary;
|
||||
} else {
|
||||
return this.$vuetify.theme.themes.light.primary;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
141
app/imports/ui/creature/slots/SlotCardsToFill.vue
Normal file
141
app/imports/ui/creature/slots/SlotCardsToFill.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<column-layout wide-columns class="slots-to-fill">
|
||||
<v-fade-transition
|
||||
group
|
||||
leave-absolute
|
||||
hide-on-leave
|
||||
>
|
||||
<div
|
||||
v-for="pointBuy in pointBuys"
|
||||
:key="pointBuy._id"
|
||||
style="transition: all 0.3s !important"
|
||||
>
|
||||
<point-buy-card
|
||||
:model="pointBuy"
|
||||
hover
|
||||
@ignore="ignoreProp(pointBuy._id)"
|
||||
@click="editPointBuy(pointBuy._id)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-for="slot in slots"
|
||||
:key="slot._id"
|
||||
style="transition: all 0.3s !important"
|
||||
>
|
||||
<slot-card
|
||||
:model="slot"
|
||||
hover
|
||||
@ignore="ignoreProp(slot._id)"
|
||||
@click="fillSlot(slot._id)"
|
||||
/>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
</column-layout>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import SlotCard from '/imports/ui/creature/slots/SlotCard.vue';
|
||||
import PointBuyCard from '/imports/ui/properties/components/pointBuy/PointBuyCard.vue';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SlotCard,
|
||||
PointBuyCard,
|
||||
ColumnLayout,
|
||||
},
|
||||
inject: {
|
||||
context: { default: {} }
|
||||
},
|
||||
methods: {
|
||||
ignoreProp(_id){
|
||||
updateCreatureProperty.call({
|
||||
_id,
|
||||
path: ['ignored'],
|
||||
value: true
|
||||
}, error => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
}
|
||||
});
|
||||
},
|
||||
fillSlot(slotId){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'slot-fill-dialog',
|
||||
elementId: `slot-card-${slotId}`,
|
||||
data: {
|
||||
slotId,
|
||||
creatureId: this.context.creatureId,
|
||||
},
|
||||
callback(nodeIds){
|
||||
if (!nodeIds || !nodeIds.length) return;
|
||||
insertPropertyFromLibraryNode.call({
|
||||
nodeIds,
|
||||
parentRef: {
|
||||
'id': slotId,
|
||||
'collection': 'creatureProperties',
|
||||
},
|
||||
}, error => {
|
||||
if (error){
|
||||
console.error(error);
|
||||
snackbar({text: error.reason || error.message || error.toString()});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
editPointBuy(_id){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `point-buy-card-${_id}`,
|
||||
data: {
|
||||
_id,
|
||||
startInEditTab: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
slots(){
|
||||
return CreatureProperties.find({
|
||||
type: 'propertySlot',
|
||||
'ancestors.id': this.context.creatureId,
|
||||
ignored: { $ne: true },
|
||||
$and: [
|
||||
{
|
||||
$or: [
|
||||
{'slotCondition.value': {$nin: [false, 0, '']}},
|
||||
{'slotCondition.value': {$exists: false}},
|
||||
]
|
||||
},{
|
||||
$or: [
|
||||
{ 'quantityExpected.value': {$in: [false, 0, '', undefined]} },
|
||||
{ 'quantityExpected.value': {exists: false} },
|
||||
{spaceLeft: {$gt: 0}},
|
||||
]
|
||||
},
|
||||
],
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
});
|
||||
},
|
||||
pointBuys(){
|
||||
return CreatureProperties.find({
|
||||
type: 'pointBuy',
|
||||
'ancestors.id': this.context.creatureId,
|
||||
ignored: { $ne: true },
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<template lang="html">
|
||||
<dialog-base>
|
||||
<v-toolbar-title slot="toolbar">
|
||||
Build
|
||||
</v-toolbar-title>
|
||||
<slots
|
||||
:creature-id="creatureId"
|
||||
show-hidden-slots
|
||||
/>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import Slots from '/imports/ui/creature/slots/Slots.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
Slots,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId'],
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -27,20 +27,18 @@
|
||||
/>
|
||||
<p>
|
||||
{{ slotPropertyTypeName }} with tags:
|
||||
<template v-for="(tags, index) in tagsSearched.or">
|
||||
<property-tags
|
||||
:key="index"
|
||||
:tags="tags"
|
||||
:prefix="index ? 'OR' : undefined"
|
||||
/>
|
||||
</template>
|
||||
<template v-for="(tags, index) in tagsSearched.not">
|
||||
<property-tags
|
||||
:key="index"
|
||||
:tags="tags"
|
||||
prefix="NOT"
|
||||
/>
|
||||
</template>
|
||||
<property-tags
|
||||
v-for="(tags, index) in tagsSearched.or"
|
||||
:key="index + 'tags'"
|
||||
:tags="tags"
|
||||
:prefix="index ? 'OR' : undefined"
|
||||
/>
|
||||
<property-tags
|
||||
v-for="(tags, index) in tagsSearched.not"
|
||||
:key="index + 'not'"
|
||||
:tags="tags"
|
||||
prefix="NOT"
|
||||
/>
|
||||
</p>
|
||||
<v-expansion-panels
|
||||
multiple
|
||||
@@ -182,7 +180,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
@@ -200,13 +198,13 @@ import { clone } from 'lodash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DialogBase,
|
||||
DialogBase,
|
||||
TreeNodeView,
|
||||
PropertyDescription,
|
||||
LibraryNodeExpansionContent,
|
||||
PropertyTags,
|
||||
},
|
||||
props:{
|
||||
},
|
||||
props: {
|
||||
slotId: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
@@ -220,36 +218,38 @@ export default {
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
selectedNodeIds: [],
|
||||
searchInput: undefined,
|
||||
searchValue: undefined,
|
||||
showDisabled: false,
|
||||
disabledNodeCount: undefined,
|
||||
}},
|
||||
data() {
|
||||
return {
|
||||
selectedNodeIds: [],
|
||||
searchInput: undefined,
|
||||
searchValue: undefined,
|
||||
showDisabled: false,
|
||||
disabledNodeCount: undefined,
|
||||
}
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['creatureId'],
|
||||
},
|
||||
computed: {
|
||||
tagsSearched(){
|
||||
tagsSearched() {
|
||||
let or = [];
|
||||
let not = [];
|
||||
if (this.model.slotTags && this.model.slotTags.length){
|
||||
if (this.model.slotTags && this.model.slotTags.length) {
|
||||
or.push(this.model.slotTags);
|
||||
}
|
||||
this.model.extraTags?.forEach(extras => {
|
||||
if (extras.tags?.length){
|
||||
if(extras.operation === 'OR'){
|
||||
if (extras.tags?.length) {
|
||||
if (extras.operation === 'OR') {
|
||||
or.push(extras.tags);
|
||||
} else if (extras.operation === 'NOT'){
|
||||
} else if (extras.operation === 'NOT') {
|
||||
not.push(extras.tags);
|
||||
}
|
||||
}
|
||||
});
|
||||
return {or, not};
|
||||
return { or, not };
|
||||
},
|
||||
slotPropertyTypeName(){
|
||||
slotPropertyTypeName() {
|
||||
if (!this.model) return;
|
||||
if (!this.model.slotType) return 'Property';
|
||||
let propName = getPropertyName(this.model.slotType);
|
||||
@@ -257,11 +257,11 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
loadMore(){
|
||||
loadMore() {
|
||||
if (this.currentLimit >= this.countAll) return;
|
||||
this._subs['slotFillers'].setData('limit', this.currentLimit + 50);
|
||||
},
|
||||
openPropertyDetails(id){
|
||||
openPropertyDetails(id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'library-node-dialog',
|
||||
elementId: id,
|
||||
@@ -270,26 +270,26 @@ export default {
|
||||
},
|
||||
});
|
||||
},
|
||||
isDisabled(node){
|
||||
isDisabled(node) {
|
||||
return node._disabledBySlotFillerCondition ||
|
||||
node._disabledByAlreadyAdded ||
|
||||
(
|
||||
node._disabledByQuantityFilled &&
|
||||
!this.selectedNodeIds.includes(node._id)
|
||||
)
|
||||
(
|
||||
node._disabledByQuantityFilled &&
|
||||
!this.selectedNodeIds.includes(node._id)
|
||||
)
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
'slotFillers'(){
|
||||
'slotFillers'() {
|
||||
return [this.slotId, this.searchValue || undefined]
|
||||
},
|
||||
},
|
||||
searchLoading(){
|
||||
searchLoading() {
|
||||
return !!this.searchValue && !this.$subReady.slotFillers;
|
||||
},
|
||||
model(){
|
||||
if (this.slotId){
|
||||
model() {
|
||||
if (this.slotId) {
|
||||
return CreatureProperties.findOne(this.slotId);
|
||||
} else if (this.dummySlot) {
|
||||
let model = clone(this.dummySlot)
|
||||
@@ -299,44 +299,44 @@ export default {
|
||||
return model;
|
||||
}
|
||||
},
|
||||
creature(){
|
||||
if (!this.creatureId) return {variables: {}};
|
||||
return Creatures.findOne(this.creatureId);
|
||||
variables() {
|
||||
if (!this.creatureId) return {};
|
||||
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
|
||||
},
|
||||
currentLimit(){
|
||||
currentLimit() {
|
||||
return this._subs['slotFillers'].data('limit') || 50;
|
||||
},
|
||||
countAll(){
|
||||
countAll() {
|
||||
return this._subs['slotFillers'].data('countAll');
|
||||
},
|
||||
alreadyAdded(){
|
||||
alreadyAdded() {
|
||||
let added = new Set();
|
||||
if (!this.model.unique) return added;
|
||||
let ancestorId;
|
||||
if (this.model.unique === 'uniqueInSlot'){
|
||||
if (this.model.unique === 'uniqueInSlot') {
|
||||
ancestorId = this.model._id;
|
||||
} else if (this.model.unique === 'uniqueInCreature'){
|
||||
} else if (this.model.unique === 'uniqueInCreature') {
|
||||
ancestorId = this.creatureId;
|
||||
}
|
||||
CreatureProperties.find({
|
||||
'ancestors.id': ancestorId,
|
||||
libraryNodeId: {$exists: true},
|
||||
removed: {$ne: true},
|
||||
libraryNodeId: { $exists: true },
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
fields: {libraryNodeId: 1},
|
||||
fields: { libraryNodeId: 1 },
|
||||
}).forEach(prop => {
|
||||
added.add(prop.libraryNodeId);
|
||||
});
|
||||
return added;
|
||||
},
|
||||
totalQuantitySelected(){
|
||||
totalQuantitySelected() {
|
||||
let quantitySelected = 0;
|
||||
LibraryNodes.find({
|
||||
_id: {$in: this.selectedNodeIds}
|
||||
_id: { $in: this.selectedNodeIds }
|
||||
}, {
|
||||
fields: {slotQuantityFilled: 1},
|
||||
fields: { slotQuantityFilled: 1 },
|
||||
}).forEach(node => {
|
||||
if (Number.isFinite(node.slotQuantityFilled)){
|
||||
if (Number.isFinite(node.slotQuantityFilled)) {
|
||||
quantitySelected += node.slotQuantityFilled;
|
||||
} else {
|
||||
quantitySelected += 1;
|
||||
@@ -344,30 +344,30 @@ export default {
|
||||
});
|
||||
return quantitySelected;
|
||||
},
|
||||
spaceLeft(){
|
||||
spaceLeft() {
|
||||
if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined;
|
||||
return this.model.spaceLeft - this.totalQuantitySelected;
|
||||
},
|
||||
libraryNames(){
|
||||
libraryNames() {
|
||||
let names = {};
|
||||
Libraries.find().forEach(lib => names[lib._id] = lib.name)
|
||||
return names;
|
||||
},
|
||||
libraryNodes(){
|
||||
let filter = getSlotFillFilter({slot: this.model});
|
||||
libraryNodes() {
|
||||
let filter = getSlotFillFilter({ slot: this.model });
|
||||
let nodes = LibraryNodes.find(filter, {
|
||||
sort: {name: 1, order: 1}
|
||||
sort: { name: 1, order: 1 }
|
||||
}).fetch();
|
||||
let disabledNodeCount = 0;
|
||||
// Mark slotFillers whose condition isn't met or are too big to fit
|
||||
// the quantity to fill
|
||||
nodes.forEach(node => {
|
||||
if (node.slotFillerCondition){
|
||||
if (node.slotFillerCondition) {
|
||||
try {
|
||||
let parseNode = parse(node.slotFillerCondition);
|
||||
const {result: resultNode} = resolve('reduce', parseNode, this.creature.variables);
|
||||
if (resultNode?.parseType === 'constant'){
|
||||
if (!resultNode.value){
|
||||
const { result: resultNode } = resolve('reduce', parseNode, this.variables);
|
||||
if (resultNode?.parseType === 'constant') {
|
||||
if (!resultNode.value) {
|
||||
node._disabledBySlotFillerCondition = true;
|
||||
disabledNodeCount += 1;
|
||||
}
|
||||
@@ -376,7 +376,7 @@ export default {
|
||||
node._conditionError = toString(resultNode);
|
||||
disabledNodeCount += 1;
|
||||
}
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
let error = prettifyParseError(e);
|
||||
node._disabledBySlotFillerCondition = true;
|
||||
@@ -387,10 +387,10 @@ export default {
|
||||
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
|
||||
if (
|
||||
quantityToFill > this.spaceLeft
|
||||
){
|
||||
) {
|
||||
node._disabledByQuantityFilled = true;
|
||||
}
|
||||
if (this.alreadyAdded.has(node._id)){
|
||||
if (this.alreadyAdded.has(node._id)) {
|
||||
node._disabledByAlreadyAdded = true;
|
||||
}
|
||||
});
|
||||
@@ -402,7 +402,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<template lang="html">
|
||||
<div class="slots">
|
||||
<div
|
||||
v-for="slot in slots"
|
||||
:key="slot._id"
|
||||
class="slot"
|
||||
>
|
||||
<h3 class="layout align-center">
|
||||
{{ slot.name }}
|
||||
<v-spacer />
|
||||
<span v-if="slot.quantityExpected && slot.quantityExpected.value > 1">
|
||||
{{ slot.totalFilled }} / {{ slot.quantityExpected.value }}
|
||||
</span>
|
||||
</h3>
|
||||
<v-list v-if="slot.children.length">
|
||||
<v-list-item
|
||||
v-for="child in slot.children"
|
||||
:key="child._id"
|
||||
:data-id="`slot-child-${child._id}`"
|
||||
@click="clickSlotChild(child)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<tree-node-view
|
||||
class="slotChild"
|
||||
:model="child"
|
||||
/>
|
||||
</v-list-item-content>
|
||||
<v-list-item-action>
|
||||
<v-btn
|
||||
icon
|
||||
small
|
||||
@click.stop="remove(child)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-btn
|
||||
v-if="!slot.quantityExpected || !slot.quantityExpected.value || slot.spaceLeft"
|
||||
icon
|
||||
:data-id="`slot-add-button-${slot._id}`"
|
||||
class="slot-add-button"
|
||||
style="background-color: inherit;"
|
||||
@click="fillSlot(slot)"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TreeNodeView,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
showHiddenSlots: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickSlotChild({_id}){
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `slot-child-${_id}`,
|
||||
data: {_id},
|
||||
});
|
||||
},
|
||||
fillSlot(slot){
|
||||
let slotId = slot._id;
|
||||
let creatureId = this.creatureId;
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'slot-fill-dialog',
|
||||
elementId: `slot-add-button-${slotId}`,
|
||||
data: {
|
||||
slotId,
|
||||
creatureId,
|
||||
},
|
||||
callback(nodeIds){
|
||||
if (!nodeIds || !nodeIds.length) return;
|
||||
let newPropertyId = insertPropertyFromLibraryNode.call({
|
||||
nodeIds,
|
||||
parentRef: {
|
||||
'id': slotId,
|
||||
'collection': 'creatureProperties',
|
||||
},
|
||||
});
|
||||
return `slot-child-${newPropertyId}`;
|
||||
}
|
||||
});
|
||||
},
|
||||
remove(model){
|
||||
softRemoveProperty.call({_id: model._id});
|
||||
snackbar({
|
||||
text: `Deleted ${getPropertyTitle(model)}`,
|
||||
callbackName: 'undo',
|
||||
callback(){
|
||||
restoreProperty.call({_id: model._id});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
slots(){
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'propertySlot',
|
||||
$or: [
|
||||
{'slotCondition.value': {$nin: [false, 0, '']}},
|
||||
{'slotCondition.value': {$exists: false}},
|
||||
],
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: 1}
|
||||
}).map(slot => {
|
||||
if (
|
||||
!this.showHiddenSlots &&
|
||||
(slot.quantityExpected && slot.quantityExpected.value) === 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 && slot.quantityExpected.value) > 0 &&
|
||||
slot.spaceLeft <= 0 ||
|
||||
slot.ignored
|
||||
)
|
||||
));
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user