Merge branch 'version-2' into version-2-tabletop

This commit is contained in:
Stefan Zermatten
2022-10-22 19:29:31 +02:00
437 changed files with 18762 additions and 8849 deletions

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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