Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5bf39958 | ||
|
|
5d8485123e | ||
|
|
85f13713f2 | ||
|
|
b0afc86ad4 | ||
|
|
30fabce7f1 | ||
|
|
4133a0f78c | ||
|
|
2b1a6de1e5 | ||
|
|
25e2523d51 | ||
|
|
7072e9ba97 | ||
|
|
b3ed77964f | ||
|
|
912fff64a8 | ||
|
|
22d51eacab | ||
|
|
7562e29fac | ||
|
|
d4cac831e6 | ||
|
|
5112ecd0c7 | ||
|
|
6c7308ebf8 | ||
|
|
c50c512587 | ||
|
|
93dfbc8a93 | ||
|
|
2c89323764 | ||
|
|
33576e02fa | ||
|
|
81cfc3919e | ||
|
|
bae621cd47 | ||
|
|
70edd7b2c0 |
@@ -151,11 +151,6 @@ let CreatureSchema = new SimpleSchema({
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
computeErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
|
||||
@@ -225,7 +225,7 @@ function spendResources(prop, actionContext) {
|
||||
throw 'The prop\'s ammo was not found on the creature';
|
||||
}
|
||||
if (
|
||||
!itemConsumed.quantity.value ||
|
||||
!itemConsumed?.quantity?.value ||
|
||||
!isFinite(itemConsumed.quantity.value)
|
||||
) return;
|
||||
itemQuantityAdjustments.push({
|
||||
@@ -247,8 +247,9 @@ function spendResources(prop, actionContext) {
|
||||
} catch (e) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
value: e.toString(),
|
||||
});
|
||||
console.error(e);
|
||||
return true;
|
||||
}
|
||||
// No more errors should be thrown after this line
|
||||
|
||||
@@ -100,7 +100,7 @@ function copyNodeListToTarget(propList, target, oldParent) {
|
||||
|
||||
/**
|
||||
* 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 }) {
|
||||
propList.forEach(prop => {
|
||||
@@ -119,8 +119,8 @@ function crystalizeVariables({ propList, actionContext }) {
|
||||
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
||||
) return node;
|
||||
// Handle variables
|
||||
if (node.name === '$target') {
|
||||
// strip $target
|
||||
if (node.name === '~target') {
|
||||
// strip ~target
|
||||
if (node.parseType === 'accessor') {
|
||||
node.name = node.path.shift();
|
||||
if (!node.path.length) {
|
||||
@@ -130,7 +130,7 @@ function crystalizeVariables({ propList, actionContext }) {
|
||||
// Can't strip symbols
|
||||
actionContext.addLog({
|
||||
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;
|
||||
|
||||
@@ -2,9 +2,8 @@ import operator from '/imports/parser/parseTree/operator.js';
|
||||
import { parse } from '/imports/parser/parser.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
|
||||
if (!calcObj.effects) return;
|
||||
calcObj.effects.forEach(effect => {
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, actionContext) {
|
||||
calcObj.effects?.forEach(effect => {
|
||||
if (effect.operation !== 'add') return;
|
||||
if (!effect.amount) return;
|
||||
if (effect.amount.value === null) return;
|
||||
@@ -17,8 +16,31 @@ export default function applyEffectsToCalculationParseNode(calcObj, actionContex
|
||||
operator: '+',
|
||||
fn: 'add'
|
||||
});
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function recalculateCalculation(calc, actionContext, context){
|
||||
export default function recalculateCalculation(calc, actionContext, context) {
|
||||
if (!calc?.parseNode) return;
|
||||
calc._parseLevel = 'reduce';
|
||||
applyEffectsToCalculationParseNode(calc, actionContext);
|
||||
|
||||
@@ -29,9 +29,9 @@ export default function linkTypeDependencies(dependencyGraph, prop, computation)
|
||||
|
||||
function dependOnCalc({ dependencyGraph, prop, key }) {
|
||||
let calc = get(prop, key);
|
||||
if (!calc) return;
|
||||
if (!calc?.type) return;
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
|
||||
export default function evaluateCalculation(calculation, scope, givenContext){
|
||||
export default function evaluateCalculation(calculation, scope, givenContext) {
|
||||
const parseNode = calculation.parseNode;
|
||||
const fn = calculation._parseLevel;
|
||||
const calculationScope = {...calculation._localScope, ...scope};
|
||||
const {result: resultNode, context} = resolve(fn, parseNode, calculationScope, givenContext);
|
||||
const calculationScope = { ...calculation._localScope, ...scope };
|
||||
const { result: resultNode, context } = resolve(fn, parseNode, calculationScope, givenContext);
|
||||
calculation.errors = context.errors;
|
||||
if (resultNode?.parseType === 'constant'){
|
||||
if (resultNode?.parseType === 'constant') {
|
||||
calculation.value = resultNode.value;
|
||||
} else if (resultNode?.parseType === 'error'){
|
||||
} else if (resultNode?.parseType === 'error') {
|
||||
calculation.value = null;
|
||||
} else {
|
||||
calculation.value = toString(resultNode);
|
||||
|
||||
@@ -28,8 +28,8 @@ const duplicateLibraryNode = new ValidatedMethod({
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
numRequests: 4,
|
||||
timeInterval: 6000,
|
||||
},
|
||||
run({ _id }) {
|
||||
let libraryNode = LibraryNodes.findOne(_id);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { union, difference, sortBy, findLast } from 'lodash';
|
||||
import { union, difference, sortBy, findLast, intersection } from 'lodash';
|
||||
|
||||
export function nodeArrayToTree(nodes) {
|
||||
// Store a dict and list of all the nodes
|
||||
@@ -83,9 +83,15 @@ export default function nodesToTree({
|
||||
docs.forEach(doc => {
|
||||
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);
|
||||
// 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
|
||||
ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => {
|
||||
// Mark that the nodes are ancestors of the found nodes
|
||||
|
||||
@@ -252,7 +252,7 @@ Meteor.users.setPreference = new ValidatedMethod({
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Accounts.onCreateUser(() => {
|
||||
Accounts.onCreateUser((options, user) => {
|
||||
if (defaultLibraries?.length) {
|
||||
Libraries.update({
|
||||
_id: { $in: defaultLibraries }
|
||||
@@ -271,6 +271,7 @@ if (Meteor.isServer) {
|
||||
multi: true,
|
||||
}, () => {/**/ });
|
||||
}
|
||||
return user;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '');
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -16,42 +16,27 @@ export default {
|
||||
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>
|
||||
|
||||
<style lang="css">
|
||||
.column-layout {
|
||||
column-count: 12;
|
||||
column-fill: balance;
|
||||
column-gap: 0;
|
||||
column-gap: 8px;
|
||||
column-width: 240px;
|
||||
transform: translateZ(0);
|
||||
padding: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.column-layout.wide-columns {
|
||||
column-count: 12;
|
||||
column-fill: balance;
|
||||
column-gap: 0;
|
||||
column-width: 320px;
|
||||
transform: translateZ(0);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.column-layout>div,
|
||||
.column-layout>*,
|
||||
.column-layout>span>div {
|
||||
width: 100%;
|
||||
backface-visibility: hidden;
|
||||
transform: translateX(0);
|
||||
page-break-inside: avoid;
|
||||
display: inline-block;
|
||||
break-inside: avoid;
|
||||
padding: 4px;
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,19 @@
|
||||
class="layout"
|
||||
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
|
||||
class="layout column justify-start"
|
||||
:style="computedTreeStyle"
|
||||
|
||||
@@ -90,6 +90,20 @@
|
||||
<v-icon>mdi-content-duplicate</v-icon>
|
||||
</v-list-item-action>
|
||||
</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-if="$listeners && $listeners.move"
|
||||
:disabled="context.editPermission === false"
|
||||
|
||||
@@ -1,19 +1,111 @@
|
||||
<template lang="html">
|
||||
<v-combobox
|
||||
v-model="filterTerms"
|
||||
:items="filterOptions"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
hide-no-data
|
||||
hide-selected
|
||||
multiple
|
||||
clearable
|
||||
small-chips
|
||||
deletable-chips
|
||||
/>
|
||||
<v-menu
|
||||
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"
|
||||
multiple
|
||||
clearable
|
||||
small-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>
|
||||
|
||||
<script lang="js">
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
import escapeRegex from '/imports/api/utility/escapeRegex.js';
|
||||
|
||||
const filterOptions = [];
|
||||
for (let key in PROPERTIES) {
|
||||
if (key === 'reference') continue;
|
||||
@@ -31,39 +123,62 @@ export default {
|
||||
},
|
||||
},
|
||||
data(){return {
|
||||
filterTerms: [],
|
||||
typeFilterInput: [],
|
||||
fieldFilters: [{field: 'name', value: undefined}],
|
||||
filterOptions,
|
||||
menu: false,
|
||||
}},
|
||||
computed: {
|
||||
filter(){
|
||||
if (!this.filterTerms.length) return;
|
||||
let typeFilters = [];
|
||||
let nameFilters = [];
|
||||
this.filterTerms.forEach(filter => {
|
||||
if (filter.value){
|
||||
typeFilters.push(filter.value);
|
||||
filter() {
|
||||
let filter = undefined;
|
||||
if (this.typeFilterInput?.length) {
|
||||
filter = filter || {};
|
||||
filter.type = {$in: this.typeFilterInput};
|
||||
}
|
||||
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 {
|
||||
// escape string
|
||||
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
|
||||
var reg = new RegExp( '.*' + term + '.*', 'i' );
|
||||
nameFilters.push(reg)
|
||||
// No dot notation, search fields and their likely sub-fields
|
||||
filter.$and = filter.$and || [];
|
||||
filter.$and.push({
|
||||
$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;
|
||||
},
|
||||
},
|
||||
watch:{
|
||||
filter(value){
|
||||
this.$emit('input', value);
|
||||
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: {
|
||||
menu(val) {
|
||||
if (!val) {
|
||||
this.$emit('input', this.filter);
|
||||
this.$emit('extra-fields-changed', this.extraFields);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -11,9 +11,14 @@
|
||||
<template slot="tree">
|
||||
<v-toolbar
|
||||
flat
|
||||
dense
|
||||
dark
|
||||
style="flex-grow: 0;"
|
||||
>
|
||||
<tree-search-input
|
||||
ref="searchBox"
|
||||
v-model="filter"
|
||||
class="mx-4"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
v-if="context.editPermission !== false"
|
||||
@@ -23,12 +28,6 @@
|
||||
:disabled="organizeDisabled"
|
||||
style="flex-grow: 0; height: 32px;"
|
||||
/>
|
||||
<tree-search-input
|
||||
ref="searchBox"
|
||||
slot="extension"
|
||||
v-model="filter"
|
||||
class="mx-4"
|
||||
/>
|
||||
</v-toolbar>
|
||||
<creature-properties-tree
|
||||
class="pt-2 flex"
|
||||
|
||||
@@ -596,7 +596,6 @@ export default {
|
||||
margin-top: 4px;
|
||||
margin-left: -30px;
|
||||
padding-left: 34px;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.number-label .number {
|
||||
|
||||
@@ -57,6 +57,7 @@ export default {
|
||||
hideLibraryTab: true,
|
||||
noBackdropClose: true,
|
||||
showLibraryOnlyProps: true,
|
||||
collection: 'libraryNodes',
|
||||
},
|
||||
callback(libraryNode){
|
||||
if (!libraryNode) return;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<template lang="html">
|
||||
<tree-detail-layout>
|
||||
<library-second-tree
|
||||
v-if="showSecondTree"
|
||||
slot="left-tree"
|
||||
:selected-node="selectedNode"
|
||||
@close="showSecondTree = false"
|
||||
@selected="clickNode"
|
||||
/>
|
||||
<div
|
||||
slot="tree"
|
||||
class="layout column"
|
||||
@@ -17,23 +24,44 @@
|
||||
:dark="isToolbarDark"
|
||||
:light="!isToolbarDark"
|
||||
>
|
||||
<tree-search-input
|
||||
ref="searchBox"
|
||||
v-model="filter"
|
||||
class="mx-4"
|
||||
@extra-fields-changed="val => extraFields = val"
|
||||
/>
|
||||
<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-if="!libraryId || canEditLibrary"
|
||||
v-model="organize"
|
||||
hide-details
|
||||
label="Organize"
|
||||
class="mx-3"
|
||||
class="ml-1 mr-3 mt-2"
|
||||
style="flex-grow: 0; height: 32px;"
|
||||
/>
|
||||
<tree-search-input
|
||||
ref="searchBox"
|
||||
slot="extension"
|
||||
v-model="filter"
|
||||
class="mx-4"
|
||||
/>
|
||||
<insert-library-node-button
|
||||
v-if="libraryId && canEditLibrary"
|
||||
slot="extension"
|
||||
style="bottom: -24px"
|
||||
fab
|
||||
:library-id="libraryId"
|
||||
@@ -49,6 +77,7 @@
|
||||
:library-id="libraryId"
|
||||
:organize-mode="organize"
|
||||
:selected-node="selectedNode"
|
||||
:extra-fields="extraFields"
|
||||
should-subscribe
|
||||
:filter="filter"
|
||||
@selected="clickNode"
|
||||
@@ -93,6 +122,7 @@ import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
||||
import TreeSearchInput from '/imports/client/ui/components/tree/TreeSearchInput.vue';
|
||||
import LibrarySecondTree from '/imports/client/ui/library/LibrarySecondTree.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -102,6 +132,7 @@ export default {
|
||||
LibraryContentsContainer,
|
||||
InsertLibraryNodeButton,
|
||||
TreeSearchInput,
|
||||
LibrarySecondTree,
|
||||
},
|
||||
props: {
|
||||
selection: Boolean,
|
||||
@@ -114,6 +145,8 @@ export default {
|
||||
organize: false,
|
||||
selectedNodeId: undefined,
|
||||
filter: undefined,
|
||||
extraFields: [],
|
||||
showSecondTree: false,
|
||||
};},
|
||||
computed: {
|
||||
isToolbarDark(){
|
||||
@@ -121,7 +154,7 @@ export default {
|
||||
this.selectedNode && this.selectedNode.color ||
|
||||
getThemeColor('secondary')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
watch:{
|
||||
selectedNode(val){
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:class="isSelected && !disabled && 'primary--text v-list-item--active'"
|
||||
>
|
||||
<v-list-item-action
|
||||
v-if="selection"
|
||||
v-if="selection && !singleSelect"
|
||||
>
|
||||
<v-checkbox
|
||||
:disabled="disabled"
|
||||
@@ -66,6 +66,7 @@ export default {
|
||||
},
|
||||
open: Boolean,
|
||||
selection: Boolean,
|
||||
singleSelect: Boolean,
|
||||
dense: Boolean,
|
||||
isSelected: Boolean,
|
||||
disabled: Boolean,
|
||||
|
||||
@@ -50,6 +50,10 @@ export default {
|
||||
type: Object,
|
||||
default: undefined,
|
||||
},
|
||||
extraFields: {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -75,7 +79,7 @@ export default {
|
||||
$subscribe: {
|
||||
'libraryNodes'() {
|
||||
if (this.slowShouldSubscribe) {
|
||||
return [this.libraryId];
|
||||
return [this.libraryId, this.extraFields];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:model="library"
|
||||
:to="{ name: 'singleLibrary', params: { id: library._id }}"
|
||||
:selection="selection"
|
||||
:single-select="singleSelect"
|
||||
:is-selected="librariesSelected && librariesSelected.includes(library._id)"
|
||||
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
|
||||
:disabled="disabled"
|
||||
@@ -26,6 +27,7 @@
|
||||
:open="openCollections[libraryCollection._id]"
|
||||
:model="libraryCollection"
|
||||
:selection="selection"
|
||||
:single-select="singleSelect"
|
||||
:is-selected="libraryCollectionsSelected && libraryCollectionsSelected.includes(libraryCollection._id)"
|
||||
:disabled="disabled"
|
||||
@select="val => $emit('select-library-collection', libraryCollection._id, val)"
|
||||
@@ -37,6 +39,7 @@
|
||||
:model="library"
|
||||
:to="{ name: 'singleLibrary', params: { id: library._id }}"
|
||||
:selection="selection"
|
||||
:single-select="singleSelect"
|
||||
:is-selected="librariesSelected && librariesSelected.includes(library._id)"
|
||||
:selected-by-collection="librariesSelectedByCollections && librariesSelectedByCollections.includes(library._id)"
|
||||
:disabled="disabled"
|
||||
@@ -44,6 +47,14 @@
|
||||
@select="val => $emit('select-library', library._id, val)"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -62,6 +73,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
selection: Boolean,
|
||||
singleSelect: Boolean,
|
||||
disabled: Boolean,
|
||||
librariesSelected: {
|
||||
type: Array,
|
||||
@@ -87,7 +99,7 @@ export default {
|
||||
libraryCollections(){
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) return;
|
||||
const subCollections = Meteor.user().subscribedLibraryCollections || [];
|
||||
const subCollections = Meteor.user()?.subscribedLibraryCollections || [];
|
||||
return LibraryCollections.find({
|
||||
$or: [
|
||||
{ owner: userId },
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
v-bind="$attrs"
|
||||
:class="(isSelected || selectedByCollection) && !disabled && 'primary--text v-list-item--active'"
|
||||
:to="selection ? undefined : to"
|
||||
@click="singleSelect && $emit('select')"
|
||||
>
|
||||
<v-list-item-action
|
||||
v-if="selection"
|
||||
v-if="selection && !singleSelect"
|
||||
>
|
||||
<v-checkbox
|
||||
:disabled="disabled"
|
||||
@@ -42,6 +43,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
selection: Boolean,
|
||||
singleSelect: Boolean,
|
||||
isSelected: Boolean,
|
||||
selectedByCollection: Boolean,
|
||||
disabled: Boolean,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@move="move"
|
||||
@copy="copy"
|
||||
@remove="remove"
|
||||
@make-reference="makeReference"
|
||||
@toggle-editing="editing = !editing"
|
||||
@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) {
|
||||
if (this.embedded) {
|
||||
this.$emit('select-sub-property', _id);
|
||||
@@ -324,6 +345,7 @@ export default {
|
||||
suggestedType,
|
||||
noBackdropClose: true,
|
||||
showLibraryOnlyProps: true,
|
||||
collection: 'libraryNodes',
|
||||
},
|
||||
callback(result){
|
||||
if (!result) return;
|
||||
|
||||
125
app/imports/client/ui/library/LibrarySecondTree.vue
Normal file
125
app/imports/client/ui/library/LibrarySecondTree.vue
Normal 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>
|
||||
@@ -75,7 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { sortBy } from 'lodash';
|
||||
import { orderBy } from 'lodash';
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
import Libraries from '/imports/api/library/Libraries.js';
|
||||
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
|
||||
@@ -122,7 +122,7 @@ export default {
|
||||
});
|
||||
},
|
||||
libraryCards() {
|
||||
return sortBy([...this.libraries, ...this.collections], 'name');
|
||||
return orderBy([...this.libraries, ...this.collections], ['subscriberCount', 'name'], ['desc', 'asc']);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
no-child-insert
|
||||
:model="model"
|
||||
:errors="errors"
|
||||
:collection="collection"
|
||||
@change="change"
|
||||
@push="push"
|
||||
@pull="pull"
|
||||
@@ -229,6 +230,10 @@ export default {
|
||||
type: Array,
|
||||
default: undefined,
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
suggestedType: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
@@ -250,7 +255,7 @@ export default {
|
||||
},
|
||||
reactiveProvide: {
|
||||
name: 'context',
|
||||
include: ['debounceTime'],
|
||||
include: ['debounceTime', 'isLibraryForm'],
|
||||
},
|
||||
data(){return {
|
||||
selectedNodeIds: [],
|
||||
@@ -274,6 +279,9 @@ export default {
|
||||
const propDef = PROPERTIES[this.type];
|
||||
return propDef && propDef.docsPath;
|
||||
},
|
||||
isLibraryForm() {
|
||||
return this.collection === 'libraryNodes' || undefined;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
type(newType){
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
v-else-if="model.attributeType === 'resource'"
|
||||
:model="model"
|
||||
@click="$emit('click')"
|
||||
@change="({ type, value }) => damageProperty({type, value: -value})"
|
||||
@change="({ type, value, ack }) => damageProperty({type, value: -value, ack})"
|
||||
@mouseover="hover = true"
|
||||
@mouseleave="hover = false"
|
||||
/>
|
||||
@@ -96,6 +96,9 @@ export default {
|
||||
_id: this.model._id,
|
||||
operation: change.type,
|
||||
value: change.value
|
||||
}, e => {
|
||||
console.log(change);
|
||||
change.ack?.(e);
|
||||
});
|
||||
},
|
||||
log({_id}) {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
RegExp.escape = function(s) {
|
||||
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const dollarSignRegex = /(\W)\$(\w+)/gi;
|
||||
const dollarSignRegex = /(\W|^)\$(\w+)/gi;
|
||||
|
||||
export default function migrate1To2(archive) {
|
||||
archive.properties = archive.properties.map(prop => {
|
||||
@@ -10,6 +10,10 @@ export default function migrate1To2(archive) {
|
||||
// Migrate slot fillers to folders
|
||||
if (prop.type === 'slotFiller') {
|
||||
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
|
||||
if (prop.slotType === 'slotFiller') {
|
||||
|
||||
@@ -32,12 +32,15 @@ Migrations.add({
|
||||
});
|
||||
|
||||
function migrateCollection(collection, migrateDoc) {
|
||||
const bulk = collection.rawCollection().initializeUnorderedBulkOp();
|
||||
collection.find({}).forEach(doc => migrateDoc(bulk, doc, collection));
|
||||
bulk.execute();
|
||||
collection.find({}).forEach((doc, index) => {
|
||||
if (index % 1000 === 0) {
|
||||
console.log(`Migrating document #${index}`);
|
||||
}
|
||||
migrateDoc(doc, collection)
|
||||
});
|
||||
}
|
||||
|
||||
export function migratePropUp(bulk, prop, collection) {
|
||||
export function migratePropUp(prop, collection) {
|
||||
let update;
|
||||
if (prop.type === 'slotFiller') {
|
||||
update = update || { $set: {} };
|
||||
@@ -48,6 +51,10 @@ export function migratePropUp(bulk, prop, collection) {
|
||||
update.$set.slotFillImage = prop.picture;
|
||||
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
|
||||
@@ -67,13 +74,19 @@ export function migratePropUp(bulk, prop, collection) {
|
||||
// Replace dollar sign with tilde in calculated fields
|
||||
update = dollarSignToTilde(prop, update);
|
||||
|
||||
// Add the update to the bulk op
|
||||
// update the document
|
||||
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 = {
|
||||
$unset: {
|
||||
slotFillImage: 1,
|
||||
@@ -88,7 +101,15 @@ export function migratePropDown(bulk, prop) {
|
||||
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() {
|
||||
@@ -104,7 +125,7 @@ function countSubscribers() {
|
||||
});
|
||||
bulkLib.execute();
|
||||
|
||||
const bulkLibCols = Libraries.rawCollection().initializeUnorderedBulkOp();
|
||||
const bulkLibCols = LibraryCollections.rawCollection().initializeUnorderedBulkOp();
|
||||
LibraryCollections.find({}, {
|
||||
fields: { _id: 1 }
|
||||
}).forEach(col => {
|
||||
@@ -117,7 +138,7 @@ function countSubscribers() {
|
||||
bulkLibCols.execute();
|
||||
}
|
||||
|
||||
const dollarSignRegex = /(\W)\$(\w+)/gi;
|
||||
const dollarSignRegex = /(\W|^)\$(\w+)/gi;
|
||||
function dollarSignToTilde(prop, update) {
|
||||
computedSchemas[prop.type]?.inlineCalculationFields()?.forEach(calcKey => {
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
|
||||
@@ -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 [];
|
||||
libraryIdSchema.validate({ libraryId });
|
||||
try {
|
||||
libraryIdSchema.validate({ libraryId });
|
||||
extraFieldsSchema.validate({ extraFields });
|
||||
} catch (e) {
|
||||
return this.error(e);
|
||||
}
|
||||
this.autorun(function () {
|
||||
let userId = this.userId;
|
||||
let library = Libraries.findOne(libraryId);
|
||||
try { assertViewPermission(library, userId) }
|
||||
catch (e) {
|
||||
try {
|
||||
assertViewPermission(library, userId)
|
||||
} catch (e) {
|
||||
return this.error(e);
|
||||
}
|
||||
const fields = { ...LIBRARY_NODE_TREE_FIELDS };
|
||||
extraFields?.forEach(field => {
|
||||
fields[field] = 1;
|
||||
});
|
||||
return [
|
||||
LibraryNodes.find({
|
||||
'ancestors.id': libraryId,
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
fields: LIBRARY_NODE_TREE_FIELDS,
|
||||
fields,
|
||||
}),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -86,7 +86,13 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
|
||||
|
||||
let options = undefined;
|
||||
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};
|
||||
options = {
|
||||
/*
|
||||
@@ -105,7 +111,7 @@ Meteor.publish('searchLibraryNodes', function (creatureId) {
|
||||
}
|
||||
} else {
|
||||
//delete filter.$text
|
||||
delete filter.name;
|
||||
delete filter.$and;
|
||||
options = {
|
||||
sort: {
|
||||
'ancestors.0.id': 1,
|
||||
|
||||
@@ -41,16 +41,21 @@ Meteor.publish('slotFillers', function (slotId, searchTerm, isDummySlot) {
|
||||
sort: { name: 1 }
|
||||
});
|
||||
|
||||
// Build a filter for nodes in those libraries that match the slot
|
||||
let filter = getSlotFillFilter({ slot, libraryIds });
|
||||
this.autorun(function () {
|
||||
// Build a filter for nodes in those libraries that match the slot
|
||||
let filter = getSlotFillFilter({ slot, libraryIds });
|
||||
// Get the limit of the documents the user can fetch
|
||||
var limit = self.data('limit') || 50;
|
||||
check(limit, Number);
|
||||
|
||||
let options = undefined;
|
||||
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 };
|
||||
options = {
|
||||
// relevant documents have a higher score.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dicecloud",
|
||||
"version": "2.0.52",
|
||||
"version": "2.0.54",
|
||||
"description": "Unofficial Online Realtime D&D 5e App",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"order": 8,
|
||||
"urlName": "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
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user