Compare commits

...

27 Commits

Author SHA1 Message Date
Stefan Zermatten
31c2580a9b Bumped version 2023-08-01 11:30:53 +02:00
Stefan Zermatten
e86a1269c9 Merge branch 'develop' 2023-08-01 11:29:59 +02:00
Stefan Zermatten
a262d773c0 Reduced snackbar timeout to 15s 2023-08-01 11:28:39 +02:00
Stefan Zermatten
7ea972d476 Fixed level up backfill selecting too many props
Fixed out of order
2023-08-01 11:28:17 +02:00
Stefan Zermatten
0e5bf39958 Bumped version 2023-07-21 16:18:50 +02:00
Stefan Zermatten
5d8485123e Merge branch 'develop' 2023-07-21 16:17:42 +02:00
Stefan Zermatten
85f13713f2 Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop 2023-07-21 16:14:14 +02:00
Stefan Zermatten
b0afc86ad4 "fixed" column layout again
As yet untested on Safari
2023-07-21 16:12:50 +02:00
Stefan Zermatten
30fabce7f1 Removed variables object from creature docs 2023-07-20 11:13:57 +02:00
Stefan Zermatten
4133a0f78c Fixed proficiency bonus not applying in actions 2023-07-19 19:40:59 +02:00
Stefan Zermatten
2b1a6de1e5 Relaxed rate limiting on duplicating library props 2023-07-19 19:03:36 +02:00
Stefan Zermatten
25e2523d51 Fixed resources in folder increment button loading forever 2023-07-19 18:57:06 +02:00
Stefan Zermatten
7072e9ba97 Fixed searching by tag in slot fillers 2023-07-19 18:49:18 +02:00
Stefan Zermatten
b3ed77964f Hotfix library select turning into links incorrectly 2023-06-28 10:22:38 +02:00
Stefan Zermatten
912fff64a8 Bumped version 2023-06-27 09:33:22 +02:00
Stefan Zermatten
22d51eacab Added second library tree for multi-track drifting 2023-06-26 16:31:23 +02:00
Stefan Zermatten
7562e29fac Increased power of tree searching 2023-06-26 14:45:19 +02:00
Stefan Zermatten
d4cac831e6 Sorted lib browser by sub count 2023-06-24 00:55:22 +02:00
Stefan Zermatten
5112ecd0c7 Fixed migration not counting collection subs 2023-06-24 00:50:34 +02:00
Stefan Zermatten
6c7308ebf8 Fixed library options not showing in create dialog 2023-06-24 00:28:03 +02:00
Stefan Zermatten
c50c512587 Fixed buff $target.var -> ~target.var
to skip crystallization
2023-06-23 23:31:54 +02:00
Stefan Zermatten
93dfbc8a93 Fixed search for library prop not using tags 2023-06-23 23:29:46 +02:00
Stefan Zermatten
2c89323764 Fixed errors with empty quantity ammo resources 2023-06-23 12:21:51 +02:00
Stefan Zermatten
33576e02fa Fixed users failing to create because of a bad hook 2023-06-23 09:55:48 +02:00
Stefan Zermatten
81cfc3919e Improved migrations 2023-06-23 09:42:14 +02:00
Stefan Zermatten
bae621cd47 Hotfix migrations 2023-06-22 15:52:42 +02:00
Stefan Zermatten
70edd7b2c0 Don't batch prop updates in migration to save memory
Should run slower, but within memory constraints
2023-06-22 13:45:55 +02:00
39 changed files with 594 additions and 163 deletions

View File

@@ -60,6 +60,8 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
nodeIds.forEach(nodeId => { nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting // TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order); node = insertPropertyFromNode(nodeId, ancestors, order);
// Increment order, slightly, to keep the nodes inserted in the given id order
order += 0.001;
}); });
// get one of the root inserted docs // get one of the root inserted docs

View File

@@ -151,11 +151,6 @@ let CreatureSchema = new SimpleSchema({
blackbox: true, blackbox: true,
defaultValue: {} defaultValue: {}
}, },
variables: {
type: Object,
blackbox: true,
defaultValue: {}
},
computeErrors: { computeErrors: {
type: Array, type: Array,
optional: true, optional: true,

View File

@@ -225,7 +225,7 @@ function spendResources(prop, actionContext) {
throw 'The prop\'s ammo was not found on the creature'; throw 'The prop\'s ammo was not found on the creature';
} }
if ( if (
!itemConsumed.quantity.value || !itemConsumed?.quantity?.value ||
!isFinite(itemConsumed.quantity.value) !isFinite(itemConsumed.quantity.value)
) return; ) return;
itemQuantityAdjustments.push({ itemQuantityAdjustments.push({
@@ -247,8 +247,9 @@ function spendResources(prop, actionContext) {
} catch (e) { } catch (e) {
actionContext.addLog({ actionContext.addLog({
name: 'Error', name: 'Error',
value: e, value: e.toString(),
}); });
console.error(e);
return true; return true;
} }
// No more errors should be thrown after this line // No more errors should be thrown after this line

View File

@@ -100,7 +100,7 @@ function copyNodeListToTarget(propList, target, oldParent) {
/** /**
* Replaces all variables with their resolved values * Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total` * except variables of the form `~target.thing.total` become `thing.total`
*/ */
function crystalizeVariables({ propList, actionContext }) { function crystalizeVariables({ propList, actionContext }) {
propList.forEach(prop => { propList.forEach(prop => {
@@ -119,8 +119,8 @@ function crystalizeVariables({ propList, actionContext }) {
node.parseType !== 'accessor' && node.parseType !== 'symbol' node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node; ) return node;
// Handle variables // Handle variables
if (node.name === '$target') { if (node.name === '~target') {
// strip $target // strip ~target
if (node.parseType === 'accessor') { if (node.parseType === 'accessor') {
node.name = node.path.shift(); node.name = node.path.shift();
if (!node.path.length) { if (!node.path.length) {
@@ -130,7 +130,7 @@ function crystalizeVariables({ propList, actionContext }) {
// Can't strip symbols // Can't strip symbols
actionContext.addLog({ actionContext.addLog({
name: 'Error', name: 'Error',
value: 'Variable `$target` should not be used without a property: $target.property', value: 'Variable `~target` should not be used without a property: ~target.property',
}); });
} }
return node; return node;

View File

@@ -3,8 +3,7 @@ import { parse } from '/imports/parser/parser.js';
import logErrors from './logErrors.js'; import logErrors from './logErrors.js';
export default function applyEffectsToCalculationParseNode(calcObj, actionContext) { export default function applyEffectsToCalculationParseNode(calcObj, actionContext) {
if (!calcObj.effects) return; calcObj.effects?.forEach(effect => {
calcObj.effects.forEach(effect => {
if (effect.operation !== 'add') return; if (effect.operation !== 'add') return;
if (!effect.amount) return; if (!effect.amount) return;
if (effect.amount.value === null) return; if (effect.amount.value === null) return;
@@ -21,4 +20,27 @@ export default function applyEffectsToCalculationParseNode(calcObj, actionContex
logErrors([e], actionContext) logErrors([e], actionContext)
} }
}); });
// Add the highest proficiency as well
let highestProficiency;
calcObj.proficiencies?.forEach(proficiency => {
if (
proficiency.value > highestProficiency
|| (highestProficiency === undefined && Number.isFinite(proficiency.value))
) {
highestProficiency = proficiency.value;
}
});
if (highestProficiency) {
try {
let profParseNode = parse(highestProficiency.toString());
calcObj.parseNode = operator.create({
left: calcObj.parseNode,
right: profParseNode,
operator: '+',
fn: 'add'
});
} catch (e) {
logErrors([e], actionContext)
}
}
} }

View File

@@ -29,9 +29,9 @@ export default function linkTypeDependencies(dependencyGraph, prop, computation)
function dependOnCalc({ dependencyGraph, prop, key }) { function dependOnCalc({ dependencyGraph, prop, key }) {
let calc = get(prop, key); let calc = get(prop, key);
if (!calc) return; if (!calc?.type) return;
if (calc.type !== '_calculation') { if (calc.type !== '_calculation') {
throw `Expected calculation got ${calc.type}` throw `Failed to dependOnCal for prop: ${prop._id}, key: ${key}. Expected calculation got ${calc.type}`
} }
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation'); dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
} }

View File

@@ -28,8 +28,8 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
rateLimit: { rateLimit: {
numRequests: 1, numRequests: 4,
timeInterval: 5000, timeInterval: 6000,
}, },
run({ _id }) { run({ _id }) {
let libraryNode = LibraryNodes.findOne(_id); let libraryNode = LibraryNodes.findOne(_id);

View File

@@ -1,4 +1,4 @@
import { union, difference, sortBy, findLast } from 'lodash'; import { union, difference, sortBy, findLast, intersection } from 'lodash';
export function nodeArrayToTree(nodes) { export function nodeArrayToTree(nodes) {
// Store a dict and list of all the nodes // Store a dict and list of all the nodes
@@ -83,9 +83,15 @@ export default function nodesToTree({
docs.forEach(doc => { docs.forEach(doc => {
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id)); ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
}); });
// Remove the IDs of docs we have already found // Get all the docs that are also ancestors and mark them
docs.forEach(doc => {
if (ancestorIds.includes(doc._id)) {
doc._ancestorOfMatchedDocument = true;
}
});
// Remove the ancestor IDs of docs we have already found
ancestorIds = difference(ancestorIds, docIds); ancestorIds = difference(ancestorIds, docIds);
// Get the docs from the collection, don't worry about `removed` docs, // Get the ancestor docs from the collection, don't worry about `removed` docs,
// if their descendant was not removed, neither are they // if their descendant was not removed, neither are they
ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => { ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => {
// Mark that the nodes are ancestors of the found nodes // Mark that the nodes are ancestors of the found nodes

View File

@@ -252,7 +252,7 @@ Meteor.users.setPreference = new ValidatedMethod({
}); });
if (Meteor.isServer) { if (Meteor.isServer) {
Accounts.onCreateUser(() => { Accounts.onCreateUser((options, user) => {
if (defaultLibraries?.length) { if (defaultLibraries?.length) {
Libraries.update({ Libraries.update({
_id: { $in: defaultLibraries } _id: { $in: defaultLibraries }
@@ -271,6 +271,7 @@ if (Meteor.isServer) {
multi: true, multi: true,
}, () => {/**/ }); }, () => {/**/ });
} }
return user;
}); });
} }

View File

@@ -1,3 +1,3 @@
export default function escapeRegex(string) { export default function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, ''); return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
} }

View File

@@ -16,42 +16,27 @@ export default {
wideColumns: Boolean, wideColumns: Boolean,
}, },
}; };
/*
Removed to improve chrome layout performance, put it back if there are rendering errors
.column-layout>span>div {
display: table;
table-layout: fixed;
}
*/
</script> </script>
<style lang="css"> <style lang="css">
.column-layout { .column-layout {
column-count: 12; column-count: 12;
column-fill: balance; column-fill: balance;
column-gap: 0; column-gap: 8px;
column-width: 240px; column-width: 240px;
transform: translateZ(0); padding: 8px;
padding: 4px;
} }
.column-layout.wide-columns { .column-layout.wide-columns {
column-count: 12;
column-fill: balance;
column-gap: 0;
column-width: 320px; column-width: 320px;
transform: translateZ(0);
padding: 4px;
} }
.column-layout>div, .column-layout>*,
.column-layout>span>div { .column-layout>span>div {
width: 100%; display: inline-block;
backface-visibility: hidden;
transform: translateX(0);
page-break-inside: avoid;
break-inside: avoid; break-inside: avoid;
padding: 4px; page-break-inside: avoid;
margin-bottom: 8px;
width: 100%;
} }
</style> </style>

View File

@@ -3,6 +3,19 @@
class="layout" class="layout"
style="height: 100%;" style="height: 100%;"
> >
<div
v-if="$slots['left-tree']"
class="layout column justify-start"
:style="computedTreeStyle"
>
<slot
name="left-tree"
/>
</div>
<v-divider
v-if="$slots['left-tree']"
vertical
/>
<div <div
class="layout column justify-start" class="layout column justify-start"
:style="computedTreeStyle" :style="computedTreeStyle"

View File

@@ -90,6 +90,20 @@
<v-icon>mdi-content-duplicate</v-icon> <v-icon>mdi-content-duplicate</v-icon>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
<v-list-item
v-if="$listeners && $listeners['make-reference']"
:disabled="context.editPermission === false"
@click="$emit('make-reference')"
>
<v-list-item-content>
<v-list-item-title>
Create Reference
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-link-plus</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item <v-list-item
v-if="$listeners && $listeners.move" v-if="$listeners && $listeners.move"
:disabled="context.editPermission === false" :disabled="context.editPermission === false"

View File

@@ -52,7 +52,7 @@ export default {
props: { props: {
timeout: { timeout: {
type: Number, type: Number,
default: 6000000, default: 15000,
}, },
pause: { pause: {
type: Number, type: Number,

View File

@@ -1,19 +1,111 @@
<template lang="html"> <template lang="html">
<v-combobox <v-menu
v-model="filterTerms" v-model="menu"
:close-on-content-click="false"
>
<template #activator="{ on, attrs }">
<v-btn
v-bind="attrs"
icon
v-on="on"
>
<v-badge
:content="numFilters"
:value="numFilters"
color="primary"
overlap
>
<v-icon>mdi-magnify</v-icon>
</v-badge>
</v-btn>
</template>
<v-card>
<v-card-title>
Search
</v-card-title>
<v-card-text>
<v-select
v-model="typeFilterInput"
outlined
label="Type"
:items="filterOptions" :items="filterOptions"
prepend-inner-icon="mdi-magnify"
hide-no-data
hide-selected
multiple multiple
clearable clearable
small-chips small-chips
deletable-chips deletable-chips
/> />
<v-slide-x-transition group>
<div
v-for="(fieldFilter, index) in fieldFilters"
:key="index"
class="d-flex"
>
<v-text-field
v-model="fieldFilter.field"
class="text--mono"
label="Field"
outlined
/>
<v-text-field
v-model="fieldFilter.value"
label="Text"
class="ml-2"
outlined
/>
<v-btn
v-if="fieldFilters.length > 1"
icon
@click="fieldFilters.splice(index, 1)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
</v-slide-x-transition>
<div
v-if="fieldFilters.length < 5"
class="d-flex"
>
<v-spacer />
<v-btn
icon
@click="fieldFilters.push({name: '', value: undefined})"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<v-card-actions>
<v-btn
text
@click="
fieldFilters = [{field: 'name', value: undefined}];
typeFilterInput = [];
menu = false;
"
>
<v-icon left>
mdi-close
</v-icon>
Clear
</v-btn>
<v-spacer />
<v-btn
text
color="primary"
@click="menu = false"
>
Find
</v-btn>
</v-card-actions>
</v-card-text>
</v-card>
</v-menu>
</template> </template>
<script lang="js"> <script lang="js">
import PROPERTIES from '/imports/constants/PROPERTIES.js'; import PROPERTIES from '/imports/constants/PROPERTIES.js';
import escapeRegex from '/imports/api/utility/escapeRegex.js';
const filterOptions = []; const filterOptions = [];
for (let key in PROPERTIES) { for (let key in PROPERTIES) {
if (key === 'reference') continue; if (key === 'reference') continue;
@@ -31,39 +123,62 @@ export default {
}, },
}, },
data(){return { data(){return {
filterTerms: [], typeFilterInput: [],
fieldFilters: [{field: 'name', value: undefined}],
filterOptions, filterOptions,
menu: false,
}}, }},
computed: { computed: {
filter() { filter() {
if (!this.filterTerms.length) return; let filter = undefined;
let typeFilters = []; if (this.typeFilterInput?.length) {
let nameFilters = []; filter = filter || {};
this.filterTerms.forEach(filter => { filter.type = {$in: this.typeFilterInput};
if (filter.value){ }
typeFilters.push(filter.value); this.fieldFilters?.forEach(fieldFilter => {
if (!fieldFilter.field || !fieldFilter.value) return;
const search = { $regex: escapeRegex(fieldFilter.value), '$options': 'i' };
filter = filter || {};
if (fieldFilter.field.includes('.')) {
// The user used dot notation, search exactly where they are looking
filter[fieldFilter.field] = search;
} else { } else {
// escape string // No dot notation, search fields and their likely sub-fields
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' ); filter.$and = filter.$and || [];
var reg = new RegExp( '.*' + term + '.*', 'i' ); filter.$and.push({
nameFilters.push(reg) $or: [
{ [fieldFilter.field]: search },
{ [fieldFilter.field + '.calculation']: search },
{ [fieldFilter.field + '.text']: search },
],
});
} }
}); });
let filter = {};
if (typeFilters.length){
filter.type = {$in: typeFilters};
}
if (nameFilters.length){
filter.name = {$in: nameFilters};
}
return filter; return filter;
}, },
extraFields() {
let extraFields = [];
this.fieldFilters?.forEach(fieldFilter => {
if (!fieldFilter.field || !fieldFilter.value) return;
extraFields.push(fieldFilter.field);
});
return extraFields;
},
numFilters() {
let numFilters = 0;
if (this.typeFilterInput?.length) numFilters += 1;
numFilters += this.extraFields.length;
return numFilters;
}
}, },
watch: { watch: {
filter(value){ menu(val) {
this.$emit('input', value); if (!val) {
this.$emit('input', this.filter);
this.$emit('extra-fields-changed', this.extraFields);
} }
} }
},
} }
</script> </script>

View File

@@ -11,9 +11,14 @@
<template slot="tree"> <template slot="tree">
<v-toolbar <v-toolbar
flat flat
dense dark
style="flex-grow: 0;" style="flex-grow: 0;"
> >
<tree-search-input
ref="searchBox"
v-model="filter"
class="mx-4"
/>
<v-spacer /> <v-spacer />
<v-switch <v-switch
v-if="context.editPermission !== false" v-if="context.editPermission !== false"
@@ -23,12 +28,6 @@
:disabled="organizeDisabled" :disabled="organizeDisabled"
style="flex-grow: 0; height: 32px;" style="flex-grow: 0; height: 32px;"
/> />
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
</v-toolbar> </v-toolbar>
<creature-properties-tree <creature-properties-tree
class="pt-2 flex" class="pt-2 flex"

View File

@@ -596,7 +596,6 @@ export default {
margin-top: 4px; margin-top: 4px;
margin-left: -30px; margin-left: -30px;
padding-left: 34px; padding-left: 34px;
z-index: -1;
} }
.number-label .number { .number-label .number {

View File

@@ -81,7 +81,7 @@
v-if="libraryNode._disabledBySlotFillerCondition" v-if="libraryNode._disabledBySlotFillerCondition"
class="error--text text-no-wrap text-truncate" class="error--text text-no-wrap text-truncate"
> >
{{ libraryNode.slotFillerCondition }} {{ libraryNode._conditionError }}
</div> </div>
</v-layout> </v-layout>
<div class="text-caption text-no-wrap text-truncate"> <div class="text-caption text-no-wrap text-truncate">
@@ -192,7 +192,7 @@ import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodeExpansionContent from '/imports/client/ui/library/LibraryNodeExpansionContent.vue'; import LibraryNodeExpansionContent from '/imports/client/ui/library/LibraryNodeExpansionContent.vue';
import PropertyTags from '/imports/client/ui/properties/viewers/shared/PropertyTags.vue'; import PropertyTags from '/imports/client/ui/properties/viewers/shared/PropertyTags.vue';
import { clone, difference } from 'lodash'; import { clone, difference, isEqual } from 'lodash';
export default { export default {
components: { components: {
@@ -250,8 +250,8 @@ export default {
}, },
watch: { watch: {
selectedNodeIds(selectedIds, oldSelectedIds) { selectedNodeIds(selectedIds, oldSelectedIds) {
// Skip if we didn't increase the length by adding a new Id // Skip if we increased the length by adding a new Id, see if we need to backfill levels
if (oldSelectedIds.length >= selectedIds.length) return; if (oldSelectedIds.length < selectedIds.length) {
// Find out which library node was added // Find out which library node was added
const addedId = difference(selectedIds, oldSelectedIds)[0]; const addedId = difference(selectedIds, oldSelectedIds)[0];
if (!addedId) return; if (!addedId) return;
@@ -264,11 +264,25 @@ export default {
!selectedIds.includes(node._id) !selectedIds.includes(node._id)
&& node.level < addedNode.level && node.level < addedNode.level
&& !backFilledLevels.has(node.level) && !backFilledLevels.has(node.level)
&& !this.isDisabled(node)
) { ) {
selectedIds.push(node._id); selectedIds.push(node._id);
backFilledLevels.add(node.level)
} }
}); });
this.selectedNodeIds = selectedIds; this.selectedNodeIds = sortedIds;
}
// Refetch the library nodes to sort them correctly
const sortedIds = LibraryNodes.find({
_id: { $in: selectedIds }
}, {
sort: { level: 1, name: 1, order: 1 }
}).map(node => node._id);
// Only update if the order changed
if (!isEqual(this.selectedNodeIds, sortedIds)) {
this.selectedNodeIds = sortedIds;
}
} }
}, },
methods: { methods: {
@@ -377,7 +391,7 @@ export default {
if (!this.libraryNodeFilter) return []; if (!this.libraryNodeFilter) return [];
if (!this.$subReady.classFillers) return []; if (!this.$subReady.classFillers) return [];
let nodes = LibraryNodes.find(this.libraryNodeFilter, { let nodes = LibraryNodes.find(this.libraryNodeFilter, {
sort: { name: 1, order: 1 } sort: { level: 1, name: 1, order: 1 }
}).fetch(); }).fetch();
let disabledNodeCount = 0; let disabledNodeCount = 0;
// Mark classFillers whose condition isn't met or are too big to fit // Mark classFillers whose condition isn't met or are too big to fit
@@ -390,18 +404,19 @@ export default {
if (resultNode?.parseType === 'constant') { if (resultNode?.parseType === 'constant') {
if (!resultNode.value) { if (!resultNode.value) {
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
node._conditionError = node.slotFillerConditionNote || node.slotFillerCondition;
disabledNodeCount += 1; disabledNodeCount += 1;
} }
} else { } else {
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
node._conditionError = toString(resultNode); node._conditionError = node.slotFillerConditionNote || toString(resultNode);
disabledNodeCount += 1; disabledNodeCount += 1;
} }
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
let error = prettifyParseError(e); let error = prettifyParseError(e);
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
node._conditionError = error; node._conditionError = 'Condition error: ' + error;
disabledNodeCount += 1; disabledNodeCount += 1;
} }
} }

View File

@@ -57,6 +57,7 @@ export default {
hideLibraryTab: true, hideLibraryTab: true,
noBackdropClose: true, noBackdropClose: true,
showLibraryOnlyProps: true, showLibraryOnlyProps: true,
collection: 'libraryNodes',
}, },
callback(libraryNode){ callback(libraryNode){
if (!libraryNode) return; if (!libraryNode) return;

View File

@@ -1,5 +1,12 @@
<template lang="html"> <template lang="html">
<tree-detail-layout> <tree-detail-layout>
<library-second-tree
v-if="showSecondTree"
slot="left-tree"
:selected-node="selectedNode"
@close="showSecondTree = false"
@selected="clickNode"
/>
<div <div
slot="tree" slot="tree"
class="layout column" class="layout column"
@@ -17,23 +24,44 @@
:dark="isToolbarDark" :dark="isToolbarDark"
:light="!isToolbarDark" :light="!isToolbarDark"
> >
<tree-search-input
ref="searchBox"
v-model="filter"
class="mx-4"
@extra-fields-changed="val => extraFields = val"
/>
<v-spacer /> <v-spacer />
<v-fade-transition>
<v-menu v-if="organize && $vuetify.breakpoint.mdAndUp">
<template #activator="{ on, attrs }">
<v-btn
icon
v-bind="attrs"
v-on="on"
>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-card>
<v-card-text>
<v-switch
v-model="showSecondTree"
label="Show second library tree"
/>
</v-card-text>
</v-card>
</v-menu>
</v-fade-transition>
<v-switch <v-switch
v-if="!libraryId || canEditLibrary" v-if="!libraryId || canEditLibrary"
v-model="organize" v-model="organize"
hide-details
label="Organize" label="Organize"
class="mx-3" class="ml-1 mr-3 mt-2"
style="flex-grow: 0; height: 32px;" style="flex-grow: 0; height: 32px;"
/> />
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filter"
class="mx-4"
/>
<insert-library-node-button <insert-library-node-button
v-if="libraryId && canEditLibrary" v-if="libraryId && canEditLibrary"
slot="extension"
style="bottom: -24px" style="bottom: -24px"
fab fab
:library-id="libraryId" :library-id="libraryId"
@@ -49,6 +77,7 @@
:library-id="libraryId" :library-id="libraryId"
:organize-mode="organize" :organize-mode="organize"
:selected-node="selectedNode" :selected-node="selectedNode"
:extra-fields="extraFields"
should-subscribe should-subscribe
:filter="filter" :filter="filter"
@selected="clickNode" @selected="clickNode"
@@ -93,6 +122,7 @@ import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
import TreeSearchInput from '/imports/client/ui/components/tree/TreeSearchInput.vue'; import TreeSearchInput from '/imports/client/ui/components/tree/TreeSearchInput.vue';
import LibrarySecondTree from '/imports/client/ui/library/LibrarySecondTree.vue';
export default { export default {
components: { components: {
@@ -102,6 +132,7 @@ export default {
LibraryContentsContainer, LibraryContentsContainer,
InsertLibraryNodeButton, InsertLibraryNodeButton,
TreeSearchInput, TreeSearchInput,
LibrarySecondTree,
}, },
props: { props: {
selection: Boolean, selection: Boolean,
@@ -114,6 +145,8 @@ export default {
organize: false, organize: false,
selectedNodeId: undefined, selectedNodeId: undefined,
filter: undefined, filter: undefined,
extraFields: [],
showSecondTree: false,
};}, };},
computed: { computed: {
isToolbarDark(){ isToolbarDark(){
@@ -121,7 +154,7 @@ export default {
this.selectedNode && this.selectedNode.color || this.selectedNode && this.selectedNode.color ||
getThemeColor('secondary') getThemeColor('secondary')
); );
} },
}, },
watch:{ watch:{
selectedNode(val){ selectedNode(val){

View File

@@ -5,7 +5,7 @@
:class="isSelected && !disabled && 'primary--text v-list-item--active'" :class="isSelected && !disabled && 'primary--text v-list-item--active'"
> >
<v-list-item-action <v-list-item-action
v-if="selection" v-if="selection && !singleSelect"
> >
<v-checkbox <v-checkbox
:disabled="disabled" :disabled="disabled"
@@ -66,6 +66,7 @@ export default {
}, },
open: Boolean, open: Boolean,
selection: Boolean, selection: Boolean,
singleSelect: Boolean,
dense: Boolean, dense: Boolean,
isSelected: Boolean, isSelected: Boolean,
disabled: Boolean, disabled: Boolean,

View File

@@ -50,6 +50,10 @@ export default {
type: Object, type: Object,
default: undefined, default: undefined,
}, },
extraFields: {
type: Array,
default: undefined,
},
}, },
data() { data() {
return { return {
@@ -75,7 +79,7 @@ export default {
$subscribe: { $subscribe: {
'libraryNodes'() { 'libraryNodes'() {
if (this.slowShouldSubscribe) { if (this.slowShouldSubscribe) {
return [this.libraryId]; return [this.libraryId, this.extraFields];
} else { } else {
return []; return [];
} }

View File

@@ -9,6 +9,7 @@
:model="library" :model="library"
:to="{ name: 'singleLibrary', params: { id: library._id }}" :to="{ name: 'singleLibrary', params: { id: library._id }}"
:selection="selection" :selection="selection"
:single-select="singleSelect"
:is-selected="librariesSelected && librariesSelected.includes(library._id)" :is-selected="librariesSelected && librariesSelected.includes(library._id)"
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)" :selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
:disabled="disabled" :disabled="disabled"
@@ -26,6 +27,7 @@
:open="openCollections[libraryCollection._id]" :open="openCollections[libraryCollection._id]"
:model="libraryCollection" :model="libraryCollection"
:selection="selection" :selection="selection"
:single-select="singleSelect"
:is-selected="libraryCollectionsSelected && libraryCollectionsSelected.includes(libraryCollection._id)" :is-selected="libraryCollectionsSelected && libraryCollectionsSelected.includes(libraryCollection._id)"
:disabled="disabled" :disabled="disabled"
@select="val => $emit('select-library-collection', libraryCollection._id, val)" @select="val => $emit('select-library-collection', libraryCollection._id, val)"
@@ -37,6 +39,7 @@
:model="library" :model="library"
:to="{ name: 'singleLibrary', params: { id: library._id }}" :to="{ name: 'singleLibrary', params: { id: library._id }}"
:selection="selection" :selection="selection"
:single-select="singleSelect"
:is-selected="librariesSelected && librariesSelected.includes(library._id)" :is-selected="librariesSelected && librariesSelected.includes(library._id)"
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)" :selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
:disabled="disabled" :disabled="disabled"
@@ -44,6 +47,14 @@
@select="val => $emit('select-library', library._id, val)" @select="val => $emit('select-library', library._id, val)"
/> />
</v-list-group> </v-list-group>
<v-list-item v-if="!$subReady.libraries">
<v-spacer />
<v-progress-circular
indeterminate
color="primary"
/>
<v-spacer />
</v-list-item>
</v-list> </v-list>
</template> </template>
@@ -62,6 +73,7 @@ export default {
}, },
props: { props: {
selection: Boolean, selection: Boolean,
singleSelect: Boolean,
disabled: Boolean, disabled: Boolean,
librariesSelected: { librariesSelected: {
type: Array, type: Array,
@@ -87,7 +99,7 @@ export default {
libraryCollections(){ libraryCollections(){
const userId = Meteor.userId(); const userId = Meteor.userId();
if (!userId) return; if (!userId) return;
const subCollections = Meteor.user().subscribedLibraryCollections || []; const subCollections = Meteor.user()?.subscribedLibraryCollections || [];
return LibraryCollections.find({ return LibraryCollections.find({
$or: [ $or: [
{ owner: userId }, { owner: userId },

View File

@@ -6,9 +6,10 @@
v-bind="$attrs" v-bind="$attrs"
:class="(isSelected || selectedByCollection) && !disabled && 'primary--text v-list-item--active'" :class="(isSelected || selectedByCollection) && !disabled && 'primary--text v-list-item--active'"
:to="selection ? undefined : to" :to="selection ? undefined : to"
@click="singleSelect && $emit('select')"
> >
<v-list-item-action <v-list-item-action
v-if="selection" v-if="selection && !singleSelect"
> >
<v-checkbox <v-checkbox
:disabled="disabled" :disabled="disabled"
@@ -42,6 +43,7 @@ export default {
required: true, required: true,
}, },
selection: Boolean, selection: Boolean,
singleSelect: Boolean,
isSelected: Boolean, isSelected: Boolean,
selectedByCollection: Boolean, selectedByCollection: Boolean,
disabled: Boolean, disabled: Boolean,

View File

@@ -10,6 +10,7 @@
@move="move" @move="move"
@copy="copy" @copy="copy"
@remove="remove" @remove="remove"
@make-reference="makeReference"
@toggle-editing="editing = !editing" @toggle-editing="editing = !editing"
@color-changed="value => change({path: ['color'], value})" @color-changed="value => change({path: ['color'], value})"
/> />
@@ -206,6 +207,26 @@ export default {
} }
}); });
}, },
makeReference() {
insertNode.call({
libraryNode: {
type: 'reference',
ref: {
collection: 'libraryNodes',
id: this.model._id,
},
order: (this.model.order || 0) + 0.5,
},
parentRef: this.model.parent,
}, (error, docId) => {
if (error) console.error(error);
if (this.embedded){
this.$emit('duplicated', docId);
} else {
this.$store.dispatch('popDialogStack');
}
});
},
selectSubProperty(_id) { selectSubProperty(_id) {
if (this.embedded) { if (this.embedded) {
this.$emit('select-sub-property', _id); this.$emit('select-sub-property', _id);
@@ -324,6 +345,7 @@ export default {
suggestedType, suggestedType,
noBackdropClose: true, noBackdropClose: true,
showLibraryOnlyProps: true, showLibraryOnlyProps: true,
collection: 'libraryNodes',
}, },
callback(result){ callback(result){
if (!result) return; if (!result) return;

View File

@@ -0,0 +1,125 @@
<template lang="html">
<div class="d-flex flex-column fill-height">
<v-fade-transition mode="out-in">
<v-toolbar
v-if="libraryId"
dark
flat
color="secondary"
>
<v-btn
icon
@click="libraryId = undefined"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-toolbar-title
key="library-name"
class="d-flex"
>
<div class="flex-shrink-1">
{{ library && library.name }}
</div>
<v-spacer />
</v-toolbar-title>
<v-btn
v-if="library && ($route.params.id !== library._id)"
icon
@click="libraryId = undefined; $router.push({ name: 'singleLibrary', params: { id: library._id }})"
>
<v-icon>mdi-arrow-right-bold</v-icon>
</v-btn>
</v-toolbar>
<v-toolbar
v-else
dark
flat
color="secondary"
>
<v-toolbar-title
key="no-library"
>
<v-btn
icon
@click="$emit('close')"
>
<v-icon>mdi-close</v-icon>
</v-btn>
Select Library
</v-toolbar-title>
</v-toolbar>
</v-fade-transition>
<v-sheet
class="pa-3 flex-grow-1 flex-shrink-1"
style="overflow: auto;"
>
<v-fade-transition mode="out-in">
<library-list
v-if="!libraryId"
selection
single-select
@select-library="id => libraryId = id"
/>
<library-contents-container
v-else
:library-id="libraryId"
:organize-mode="canEditLibrary"
should-subscribe
:selected-node="selectedNode"
@selected="e => $emit('selected', e)"
/>
</v-fade-transition>
</v-sheet>
</div>
</template>
<script lang="js">
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
import LibraryContentsContainer from '/imports/client/ui/library/LibraryContentsContainer.vue';
import Libraries from '/imports/api/library/Libraries.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
export default {
components: {
LibraryList,
LibraryContentsContainer,
},
props: {
selectedNode: {
type: Object,
default: undefined,
},
},
data() {
return {
libraryId: undefined
};
},
meteor: {
$subscribe: {
'library'(){
if (this.libraryId){
return [this.libraryId]
} else {
return [];
}
},
},
library() {
return Libraries.findOne(this.libraryId);
},
canEditLibrary(){
if (!this.libraryId) return;
try {
assertEditPermission(this.library, Meteor.userId());
return true;
} catch (e){
return false;
}
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -75,7 +75,7 @@
</template> </template>
<script lang="js"> <script lang="js">
import { sortBy } from 'lodash'; import { orderBy } from 'lodash';
import LibraryCollections from '/imports/api/library/LibraryCollections.js'; import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue'; import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
@@ -122,7 +122,7 @@ export default {
}); });
}, },
libraryCards() { libraryCards() {
return sortBy([...this.libraries, ...this.collections], 'name'); return orderBy([...this.libraries, ...this.collections], ['subscriberCount', 'name'], ['desc', 'asc']);
}, },
}, },
methods: { methods: {

View File

@@ -80,6 +80,7 @@
no-child-insert no-child-insert
:model="model" :model="model"
:errors="errors" :errors="errors"
:collection="collection"
@change="change" @change="change"
@push="push" @push="push"
@pull="pull" @pull="pull"
@@ -229,6 +230,10 @@ export default {
type: Array, type: Array,
default: undefined, default: undefined,
}, },
collection: {
type: String,
default: undefined,
},
suggestedType: { suggestedType: {
type: String, type: String,
default: undefined, default: undefined,
@@ -250,7 +255,7 @@ export default {
}, },
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['debounceTime'], include: ['debounceTime', 'isLibraryForm'],
}, },
data(){return { data(){return {
selectedNodeIds: [], selectedNodeIds: [],
@@ -274,6 +279,9 @@ export default {
const propDef = PROPERTIES[this.type]; const propDef = PROPERTIES[this.type];
return propDef && propDef.docsPath; return propDef && propDef.docsPath;
}, },
isLibraryForm() {
return this.collection === 'libraryNodes' || undefined;
},
}, },
watch: { watch: {
type(newType){ type(newType){

View File

@@ -30,7 +30,7 @@
v-else-if="model.attributeType === 'resource'" v-else-if="model.attributeType === 'resource'"
:model="model" :model="model"
@click="$emit('click')" @click="$emit('click')"
@change="({ type, value }) => damageProperty({type, value: -value})" @change="({ type, value, ack }) => damageProperty({type, value: -value, ack})"
@mouseover="hover = true" @mouseover="hover = true"
@mouseleave="hover = false" @mouseleave="hover = false"
/> />
@@ -96,6 +96,9 @@ export default {
_id: this.model._id, _id: this.model._id,
operation: change.type, operation: change.type,
value: change.value value: change.value
}, e => {
console.log(change);
change.ack?.(e);
}); });
}, },
log({_id}) { log({_id}) {

View File

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

View File

@@ -2,7 +2,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { get } from 'lodash'; import { get } from 'lodash';
const dollarSignRegex = /(\W)\$(\w+)/gi; const dollarSignRegex = /(\W|^)\$(\w+)/gi;
export default function migrate1To2(archive) { export default function migrate1To2(archive) {
archive.properties = archive.properties.map(prop => { archive.properties = archive.properties.map(prop => {
@@ -10,6 +10,10 @@ export default function migrate1To2(archive) {
// Migrate slot fillers to folders // Migrate slot fillers to folders
if (prop.type === 'slotFiller') { if (prop.type === 'slotFiller') {
prop.type = 'folder'; prop.type = 'folder';
// If the slot filler has a description, change it to a computed one
if (typeof prop.description == 'string') {
prop.description = { text: prop.description };
}
} }
// Migrate slot filler slot type to folders // Migrate slot filler slot type to folders
if (prop.slotType === 'slotFiller') { if (prop.slotType === 'slotFiller') {

View File

@@ -32,12 +32,15 @@ Migrations.add({
}); });
function migrateCollection(collection, migrateDoc) { function migrateCollection(collection, migrateDoc) {
const bulk = collection.rawCollection().initializeUnorderedBulkOp(); collection.find({}).forEach((doc, index) => {
collection.find({}).forEach(doc => migrateDoc(bulk, doc, collection)); if (index % 1000 === 0) {
bulk.execute(); console.log(`Migrating document #${index}`);
}
migrateDoc(doc, collection)
});
} }
export function migratePropUp(bulk, prop, collection) { export function migratePropUp(prop, collection) {
let update; let update;
if (prop.type === 'slotFiller') { if (prop.type === 'slotFiller') {
update = update || { $set: {} }; update = update || { $set: {} };
@@ -48,6 +51,10 @@ export function migratePropUp(bulk, prop, collection) {
update.$set.slotFillImage = prop.picture; update.$set.slotFillImage = prop.picture;
update.$unset = { picture: 1 }; update.$unset = { picture: 1 };
} }
// If the slot filler has a description, change it to a computed one
if (typeof prop.description == 'string') {
prop.description = { text: prop.description };
}
} }
// Don't look for slot fillers // Don't look for slot fillers
@@ -67,13 +74,19 @@ export function migratePropUp(bulk, prop, collection) {
// Replace dollar sign with tilde in calculated fields // Replace dollar sign with tilde in calculated fields
update = dollarSignToTilde(prop, update); update = dollarSignToTilde(prop, update);
// Add the update to the bulk op // update the document
if (update) { if (update) {
bulk.find({ _id: prop._id }).updateOne(update); try {
collection.update({ _id: prop._id }, update, { bypassCollection2: true }, e => {
if (e) console.warn('Doc Migration failed: ', prop._id, e);
});
} catch (e) {
console.warn('Doc Migration failed: ', prop._id, e);
}
} }
} }
export function migratePropDown(bulk, prop) { export function migratePropDown(prop, collection) {
const update = { const update = {
$unset: { $unset: {
slotFillImage: 1, slotFillImage: 1,
@@ -88,7 +101,15 @@ export function migratePropDown(bulk, prop) {
tags: union(prop.libraryTags, prop.tags) tags: union(prop.libraryTags, prop.tags)
} }
} }
bulk.find({ _id: prop._id }).updateOne(update); if (update) {
try {
collection.update({ _id: prop._id }, update, { bypassCollection2: true }, e => {
if (e) console.warn('Doc Migration failed: ', prop._id, e);
});
} catch (e) {
console.warn('Doc Migration failed: ', prop._id, e);
}
}
} }
function countSubscribers() { function countSubscribers() {
@@ -104,7 +125,7 @@ function countSubscribers() {
}); });
bulkLib.execute(); bulkLib.execute();
const bulkLibCols = Libraries.rawCollection().initializeUnorderedBulkOp(); const bulkLibCols = LibraryCollections.rawCollection().initializeUnorderedBulkOp();
LibraryCollections.find({}, { LibraryCollections.find({}, {
fields: { _id: 1 } fields: { _id: 1 }
}).forEach(col => { }).forEach(col => {
@@ -117,7 +138,7 @@ function countSubscribers() {
bulkLibCols.execute(); bulkLibCols.execute();
} }
const dollarSignRegex = /(\W)\$(\w+)/gi; const dollarSignRegex = /(\W|^)\$(\w+)/gi;
function dollarSignToTilde(prop, update) { function dollarSignToTilde(prop, update) {
computedSchemas[prop.type]?.inlineCalculationFields()?.forEach(calcKey => { computedSchemas[prop.type]?.inlineCalculationFields()?.forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {

View File

@@ -203,22 +203,42 @@ let libraryIdSchema = new SimpleSchema({
}, },
}); });
Meteor.publish('libraryNodes', function (libraryId) { const extraFieldsSchema = new SimpleSchema({
extraFields: {
type: Array,
optional: true,
},
'extraFields.$': {
type: String,
},
});
Meteor.publish('libraryNodes', function (libraryId, extraFields) {
if (!libraryId) return []; if (!libraryId) return [];
try {
libraryIdSchema.validate({ libraryId }); libraryIdSchema.validate({ libraryId });
extraFieldsSchema.validate({ extraFields });
} catch (e) {
return this.error(e);
}
this.autorun(function () { this.autorun(function () {
let userId = this.userId; let userId = this.userId;
let library = Libraries.findOne(libraryId); let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) } try {
catch (e) { assertViewPermission(library, userId)
} catch (e) {
return this.error(e); return this.error(e);
} }
const fields = { ...LIBRARY_NODE_TREE_FIELDS };
extraFields?.forEach(field => {
fields[field] = 1;
});
return [ return [
LibraryNodes.find({ LibraryNodes.find({
'ancestors.id': libraryId, 'ancestors.id': libraryId,
}, { }, {
sort: { order: 1 }, sort: { order: 1 },
fields: LIBRARY_NODE_TREE_FIELDS, fields,
}), }),
]; ];
}); });

View File

@@ -86,7 +86,13 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
let options = undefined; let options = undefined;
if (searchTerm) { if (searchTerm) {
filter.name = { $regex: escapeRegex(searchTerm), '$options': 'i' }; // Regex search instead of text index
filter.$and = [{
$or: [
{ name: { $regex: escapeRegex(searchTerm), '$options': 'i' } },
{ libraryTags: searchTerm },
],
}];
// filter.$text = {$search: searchTerm}; // filter.$text = {$search: searchTerm};
options = { options = {
/* /*
@@ -105,7 +111,7 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
} }
} else { } else {
//delete filter.$text //delete filter.$text
delete filter.name; delete filter.$and;
options = { options = {
sort: { sort: {
'ancestors.0.id': 1, 'ancestors.0.id': 1,

View File

@@ -41,16 +41,21 @@ Meteor.publish('slotFillers', function (slotId, searchTerm, isDummySlot) {
sort: { name: 1 } sort: { name: 1 }
}); });
this.autorun(function () {
// Build a filter for nodes in those libraries that match the slot // Build a filter for nodes in those libraries that match the slot
let filter = getSlotFillFilter({ slot, libraryIds }); let filter = getSlotFillFilter({ slot, libraryIds });
this.autorun(function () {
// Get the limit of the documents the user can fetch // Get the limit of the documents the user can fetch
var limit = self.data('limit') || 50; var limit = self.data('limit') || 50;
check(limit, Number); check(limit, Number);
let options = undefined; let options = undefined;
if (searchTerm) { if (searchTerm) {
filter.name = { $regex: escapeRegex(searchTerm), '$options': 'i' }; filter.$and.push({
$or: [
{ name: { $regex: escapeRegex(searchTerm), '$options': 'i' } },
{ libraryTags: searchTerm }
]
});
//filter.$text = { $search: searchTerm }; //filter.$text = { $search: searchTerm };
options = { options = {
// relevant documents have a higher score. // relevant documents have a higher score.

View File

@@ -1,6 +1,6 @@
{ {
"name": "dicecloud", "name": "dicecloud",
"version": "2.0.52", "version": "2.0.55",
"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": {

View File

@@ -153,7 +153,7 @@
"order": 8, "order": 8,
"urlName": "buff", "urlName": "buff",
"href": "/docs/property/buff", "href": "/docs/property/buff",
"description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `$target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `$target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\n\nThe name of the buff.\n\n### Description\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Target\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't show in log\n\nIf set, the buff will not show its name and description in the log when applied.\n\n### Don't freeze variables\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)", "description": "Buffs are temporary changes to a character sheet that can be applied by actions. When a buff is applied, it is copied to the target character along with all of its children properties. \n\nBuffs can either be manually removed from the stats page, or be removed by an action applying a [buff remover](/docs/property/remove-buff/) property.\n\n### Variable freezing\n\nWhen a buff is applied, all the calculations in the child properties have their variables frozen to their values at the time the buff is applied. You can prevent this behavior for the whole buff by using the `don't freeze variables` option, or on an individual variable reference by prefixing the variable with the keyword `~target.`.\n\nFor example, if a character has 10 strength and 16 dexterity, and applies a buff with some child property containing the calculation `~target.strength + dexterity` the property's calculation will become `strength + 16` when it is copied to the target character.\n\n---\n\n### Name\n\nThe name of the buff.\n\n### Description\n\nDescription of the applied buff.\n\nAllows [inline calculations](/docs/concepts/inline-calculations).\n\n### Target\n\n- **Target** Apply the buff to the target of the action\n- **Self** Apply the buff to the creature taking the action\n\n### Hide remove button\n\nIf this is set, the remove button next to the buff on the stats page will be hidden. Use this when you expect the buff to be removed automatically by another action.\n\n### Don't show in log\n\nIf set, the buff will not show its name and description in the log when applied.\n\n### Don't freeze variables\n\nPrevent the buff from freezing variables in child property calculations to their value at the time the buff was applied.\n\n### Tags\n\nSee [Tags](/docs/concepts/tags)",
"published": true "published": true
}, },
{ {