Compare commits

..

8 Commits

Author SHA1 Message Date
Stefan Zermatten
f86152675f Added button to unhide hidden slots 2022-08-16 12:31:37 +02:00
Stefan Zermatten
cbac5264cd Added delete buttons to slot fill card 2022-08-16 11:44:08 +02:00
Stefan Zermatten
34e3325464 Fixed dependency loops created by inactive props
depending on their parent toggles
2022-08-16 11:19:16 +02:00
Stefan Zermatten
79c9e67ce2 Fixed icons being missing from buff-applied props 2022-08-16 10:11:13 +02:00
Stefan Zermatten
4c2aabf90d Fixed character sheet toolbar alignment on mobile 2022-08-16 10:03:07 +02:00
Stefan Zermatten
48331d3806 Fixed added properties being added based on tree
tab selection even when on other tabs
2022-08-16 09:49:34 +02:00
Stefan Zermatten
45f05d0d34 Fixed bug where actions targeting self
weren't applying props to self
2022-08-16 09:26:40 +02:00
Stefan Zermatten
58629c92f4 Added build command to package.json 2022-08-15 16:10:40 +02:00
12 changed files with 283 additions and 126 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -11,8 +11,8 @@ import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'
export default function applyAction(node, actionContext) { export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
let targets = actionContext.targets; if (prop.target === 'self') actionContext.targets = [actionContext.creature];
if (prop.target === 'self') targets = [actionContext.creature]; const targets = actionContext.targets;
// Log the name and summary // Log the name and summary
let content = { name: prop.name }; let content = { name: prop.name };

View File

@@ -10,8 +10,10 @@ export default function computeToggleDependencies(node, dependencyGraph){
prop.enabled prop.enabled
) return; ) return;
walkDown(node.children, child => { walkDown(node.children, child => {
child.node._computationDetails.toggleAncestors.push(prop); // Only for children that aren't inactive
if (child.node.inactive) return;
// The child nodes depend on the toggle condition compuation // The child nodes depend on the toggle condition compuation
child.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
}); });
} }

View File

@@ -89,6 +89,10 @@ export function buildComputationFromProps(properties, creature, variables){
// Walk the property trees computing things that need to be inherited // Walk the property trees computing things that need to be inherited
walkDown(forest, node => { walkDown(forest, node => {
computeInactiveStatus(node); computeInactiveStatus(node);
});
// Inactive status must be complete for the whole tree before toggle deps
// are calculated
walkDown(forest, node => {
computeToggleDependencies(node, dependencyGraph); computeToggleDependencies(node, dependencyGraph);
computeSlotQuantityFilled(node, dependencyGraph); computeSlotQuantityFilled(node, dependencyGraph);
}); });

View File

@@ -51,11 +51,22 @@ function compute(computation, node){
function pushDependenciesToStack(nodeId, graph, stack, computation){ function pushDependenciesToStack(nodeId, graph, stack, computation){
graph.forEachLinkedNode(nodeId, linkedNode => { graph.forEachLinkedNode(nodeId, linkedNode => {
if (linkedNode._visitedChildren && !linkedNode._visited){ if (linkedNode._visitedChildren && !linkedNode._visited) {
const pather = path.nba(graph, { // This is a dependency loop, find a path from the node to itself
oriented: true // and store that path as a dependency loop error
}); const pather = path.nba(graph, { oriented: true });
const loop = pather.find(nodeId, nodeId); let loop = [];
// Pather doesn't like going from a node to iteself, so find all the
// paths going from the next node back to the original node
// and return the shortest one
graph.forEachLinkedNode(nodeId, nextNode => {
const newLoop = pather.find(nextNode.id, nodeId);
if (!newLoop.length) return;
if (!loop.length || newLoop.length < loop.length - 1) {
loop = [linkedNode, ...newLoop];
}
}, true);
if (loop.length) { if (loop.length) {
computation.errors.push({ computation.errors.push({
type: 'dependencyLoop', type: 'dependencyLoop',

View File

@@ -46,7 +46,6 @@ export function getSingleProperty(creatureId, propertyId) {
'removed': {$ne: true}, 'removed': {$ne: true},
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}); });
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return prop; return prop;
@@ -65,7 +64,6 @@ export function getProperties(creatureId) {
'removed': {$ne: true}, 'removed': {$ne: true},
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}).fetch(); }).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props; return props;
@@ -90,7 +88,6 @@ export function getPropertiesOfType(creatureId, propType) {
'type': propType, 'type': propType,
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}).fetch(); }).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`); // console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props; return props;
@@ -103,11 +100,7 @@ export function getCreature(creatureId) {
if (creature) return creature; if (creature) return creature;
} }
// console.time(`Cache miss on Creature: ${creatureId}`); // console.time(`Cache miss on Creature: ${creatureId}`);
const creature = Creatures.findOne(creatureId, { const creature = Creatures.findOne(creatureId);
denormalizedStats: 1,
variables: 1,
dirty: 1,
});
// console.timeEnd(`Cache miss on Creature: ${creatureId}`); // console.timeEnd(`Cache miss on Creature: ${creatureId}`);
return creature; return creature;
} }
@@ -149,6 +142,7 @@ export function getProperyAncestors(creatureId, propertyId) {
// Fetch from database // Fetch from database
return CreatureProperties.find({ return CreatureProperties.find({
_id: { $in: ancestorIds }, _id: { $in: ancestorIds },
removed: {$ne: true},
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
}).fetch(); }).fetch();
@@ -175,6 +169,8 @@ export function getPropertyDecendants(creatureId, propertyId) {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': propertyId, 'ancestors.id': propertyId,
removed: { $ne: true }, removed: { $ne: true },
}, {
sort: { order: 1 },
}).fetch(); }).fetch();
} }
} }
@@ -199,7 +195,6 @@ class LoadedCreature {
removed: { $ne: true }, removed: { $ne: true },
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: { icon: 0 },
}).observeChanges({ }).observeChanges({
added(id, fields) { added(id, fields) {
fields._id = id; fields._id = id;

View File

@@ -28,11 +28,11 @@
</v-btn> </v-btn>
<div <div
class="layout align-center justify-start pr-1" class="layout align-center justify-start pr-1"
style="flex-grow: 0;"
> >
<!--{{node && node.order}}--> <!--{{node && node.order}}-->
<div <div
v-if="isSlot" v-if="isSlot"
class="text-truncate"
> >
<span <span
:class="{ :class="{
@@ -47,15 +47,38 @@
:model="node" :model="node"
/> />
</div> </div>
<tree-node-view <template
v-else v-else
:model="node" >
/> <tree-node-view
:model="node"
/>
<v-spacer />
<v-btn
icon
:disabled="context.editPermission === false"
@click.stop="remove(node)"
>
<v-icon>
mdi-delete
</v-icon>
</v-btn>
</template>
<template v-if="condenseChild"> <template v-if="condenseChild">
<span class="mr-4">:</span> <span class="mr-4">:</span>
<tree-node-view <tree-node-view
:model="children[0].node" :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> </template>
</div> </div>
</div> </div>
@@ -92,98 +115,118 @@
</template> </template>
<script lang="js"> <script lang="js">
/** /**
* TreeNode's are list item views of character properties. Every property which * 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 * 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 * the tree view shows off the full character structure, and where each part of
* character comes from. * character comes from.
**/ **/
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue'; import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue';
import { some } from 'lodash'; 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 { export default {
name: 'BuildTreeNode', name: 'BuildTreeNode',
components: { components: {
TreeNodeView, TreeNodeView,
FillSlotButton, FillSlotButton,
},
inject: {
context: { default: {} }
},
props: {
node: {
type: Object,
required: true,
}, },
props: { children: {
node: { type: Array,
type: Object, default: () => [],
required: true,
},
children: {
type: Array,
default: () => [],
},
},
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;
},
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 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;
},
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> </script>
<style lang="css" scoped> <style lang="css" scoped>

View File

@@ -9,9 +9,9 @@
v-model="fab" v-model="fab"
color="primary" color="primary"
fab fab
small
data-id="insert-creature-property-fab" data-id="insert-creature-property-fab"
class="insert-creature-property-fab" class="insert-creature-property-fab"
small
> >
<transition <transition
name="fab-rotate" name="fab-rotate"
@@ -48,12 +48,13 @@
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.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 // find the parent based on the currently selected property
let el = document.querySelector('.tree-tab .tree-node-title.primary--text'); let el = document.querySelector('.tree-tab .tree-node-title.primary--text');
let selectedComponent = el && el.parentElement.__vue__.$parent; let selectedComponent = el && el.parentElement.__vue__.$parent;
let parentRef, order; let parentRef, order;
if (selectedComponent){ const onTreeTab = $store.getters.tabNameById(creatureId) === 'tree';
if (onTreeTab && selectedComponent){
if (selectedComponent.showExpanded){ if (selectedComponent.showExpanded){
parentRef = { parentRef = {
id: selectedComponent.node._id, id: selectedComponent.node._id,
@@ -156,7 +157,7 @@
let creatureId = this.creatureId; let creatureId = this.creatureId;
let fab = hideFab(); let fab = hideFab();
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId); let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId, this.$store);
let parent; let parent;
try { try {
parent = fetchDocByRef(parentRef); parent = fetchDocByRef(parentRef);

View File

@@ -14,17 +14,19 @@
<v-fade-transition <v-fade-transition
mode="out-in" mode="out-in"
> >
<v-app-bar-title :key="$store.state.pageTitle"> <v-toolbar-title :key="$store.state.pageTitle">
<div> {{ $store.state.pageTitle }}
{{ $store.state.pageTitle }} </v-toolbar-title>
</div>
</v-app-bar-title>
</v-fade-transition> </v-fade-transition>
<v-spacer /> <v-spacer />
<v-fade-transition <v-fade-transition
mode="out-in" mode="out-in"
> >
<div :key="$route.meta.title"> <v-layout
:key="$route.meta.title"
class="flex-shrink-0 flex-grow-0"
justify-end
>
<template v-if="creature"> <template v-if="creature">
<shared-icon :model="creature" /> <shared-icon :model="creature" />
<v-menu <v-menu
@@ -68,7 +70,7 @@
</v-menu> </v-menu>
<v-app-bar-nav-icon @click="toggleRightDrawer" /> <v-app-bar-nav-icon @click="toggleRightDrawer" />
</template> </template>
</div> </v-layout>
</v-fade-transition> </v-fade-transition>
<v-fade-transition <v-fade-transition
slot="extension" slot="extension"

View File

@@ -20,7 +20,51 @@
lg="6" lg="6"
> >
<v-card class="pb-4"> <v-card class="pb-4">
<v-card-title>Slots</v-card-title> <v-card-title style="height: 68px;">
Slots
<v-spacer />
<v-scale-transition>
<v-menu
bottom
left
>
<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 ? 'slots' : 'slot' }}
</v-subheader>
<v-list-item
v-for="slot in hiddenSlots"
:key="slot._id"
@click="unhideSlot(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 <build-tree-node-list
:children="slotBuildTree" :children="slotBuildTree"
class="mx-2" class="mx-2"
@@ -135,6 +179,9 @@ import SlotCardsToFill from '/imports/ui/creature/slots/SlotCardsToFill.vue';
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables'; import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import CharacterErrors from '/imports/ui/creature/character/errors/CharacterErrors.vue'; 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 = []){ function traverse(tree, callback, parents = []){
tree.forEach(node => { tree.forEach(node => {
@@ -179,7 +226,10 @@ export default {
...this.highestLevels, ...this.highestLevels,
...this.classProperties ...this.classProperties
].sort((a, b) => a.order - b.order); ].sort((a, b) => a.order - b.order);
} },
hiddenCount() {
return this.hiddenSlots.length;
},
}, },
meteor: { meteor: {
creature(){ creature(){
@@ -188,6 +238,29 @@ export default {
variables() { variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
}, },
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(){ classProperties(){
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
@@ -313,6 +386,19 @@ export default {
} }
}); });
}, },
getPropertyTitle,
unhideSlot(_id) {
updateCreatureProperty.call({
_id,
path: ['ignored'],
value: false,
}, error => {
if (error){
console.error(error);
snackbar({text: error.reason || error.message || error.toString()});
}
});
},
}, },
}; };
</script> </script>

View File

@@ -1,6 +1,9 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import dialogStackStore from '/imports/ui/dialogStack/dialogStackStore.js'; import dialogStackStore from '/imports/ui/dialogStack/dialogStackStore.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
const tabs = ['stats', 'features', 'inventory', 'spells', 'journal', 'build', 'tree'];
const tabsWithoutSpells = ['stats', 'features', 'inventory', 'journal', 'build', 'tree'];
Vue.use(Vuex); Vue.use(Vuex);
const store = new Vuex.Store({ const store = new Vuex.Store({
@@ -16,13 +19,21 @@ const store = new Vuex.Store({
showDetailsDialog: false, showDetailsDialog: false,
}, },
getters: { getters: {
// ...
tabById: (state) => (id) => { tabById: (state) => (id) => {
if (id in state.characterSheetTabs){ if (id in state.characterSheetTabs){
return state.characterSheetTabs[id]; return state.characterSheetTabs[id];
} else { } else {
return 0; return 0;
} }
},
tabNameById: (state) => (id) => {
const tabNumber = state.characterSheetTabs[id];
const creature = Creatures.findOne(id);
if (creature?.settings?.hideSpellsTab) {
return tabsWithoutSpells[tabNumber];
} else {
return tabs[tabNumber]
}
} }
}, },
mutations: { mutations: {

View File

@@ -1,6 +1,6 @@
{ {
"name": "dicecloud", "name": "dicecloud",
"version": "2.0.33", "version": "2.0.38",
"description": "Unofficial Online Realtime D&D 5e App", "description": "Unofficial Online Realtime D&D 5e App",
"license": "GPL-3.0", "license": "GPL-3.0",
"repository": { "repository": {
@@ -11,7 +11,8 @@
"scripts": { "scripts": {
"run": "meteor", "run": "meteor",
"debug": "meteor --inspect", "debug": "meteor --inspect",
"test": "meteor test --driver-package meteortesting:mocha --port 3001" "test": "meteor test --driver-package meteortesting:mocha --port 3001",
"build": "meteor build ../build --architecture os.linux.x86_64"
}, },
"engines": { "engines": {
"node": "14.0.x", "node": "14.0.x",