Merge branch 'version-2-dev' into version-2

This commit is contained in:
Stefan Zermatten
2022-11-03 20:52:26 +02:00
17 changed files with 427 additions and 69 deletions

View File

@@ -1,6 +1,6 @@
import { some, intersection, difference, remove, includes } from 'lodash'; import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js'; import applyProperty from '../applyProperty.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js'; import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js'; import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js'; import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js'; import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
@@ -10,9 +10,9 @@ import {
} from '/imports/api/engine/loadCreatures.js'; } from '/imports/api/engine/loadCreatures.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyDamage(node, actionContext){ export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function(){ const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext); applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext)); node.children.forEach(child => applyProperty(child, actionContext));
}; };
@@ -28,10 +28,10 @@ export default function applyDamage(node, actionContext){
// Determine if the hit is critical // Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value && let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal prop.damageType !== 'healing' // Can't critically heal
; ;
// Double the damage rolls if the hit is critical // Double the damage rolls if the hit is critical
let context = new Context({ let context = new Context({
options: {doubleRolls: criticalHit}, options: { doubleRolls: criticalHit },
}); });
// Gather all the lines we need to log into an array // Gather all the lines we need to log into an array
@@ -40,8 +40,8 @@ export default function applyDamage(node, actionContext){
// roll the dice only and store that string // roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, actionContext.log); applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context); const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){ if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled)); logValue.push(toString(rolled));
} }
logErrors(context.errors, actionContext); logErrors(context.errors, actionContext);
@@ -50,13 +50,13 @@ export default function applyDamage(node, actionContext){
context.errors = []; context.errors = [];
// Resolve the roll to a final value // Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context); const { result: reduced } = resolve('reduce', rolled, scope, context);
logErrors(context.errors, actionContext); logErrors(context.errors, actionContext);
// Store the result // Store the result
if (reduced.parseType === 'constant'){ if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value; prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){ } else if (reduced.parseType === 'error') {
prop.amount.value = null; prop.amount.value = null;
} else { } else {
prop.amount.value = toString(reduced); prop.amount.value = toString(reduced);
@@ -64,7 +64,7 @@ export default function applyDamage(node, actionContext){
let damage = +reduced.value; let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up // If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){ if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren(); return applyChildren();
} }
@@ -83,7 +83,7 @@ export default function applyDamage(node, actionContext){
// Memoise the damage suffix for the log // Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') + let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType + prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': ''); (prop.damageType !== 'healing' ? ' damage ' : '');
if (damageTargets && damageTargets.length) { if (damageTargets && damageTargets.length) {
// Iterate through all the targets // Iterate through all the targets
@@ -107,7 +107,7 @@ export default function applyDamage(node, actionContext){
}); });
// Log the damage done // Log the damage done
if (target._id === actionContext.creature._id){ if (target._id === actionContext.creature._id) {
// Target is same as self, log damage as such // Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`); logValue.push(`**${damageDealt}** ${suffix} to self`);
} else { } else {
@@ -136,33 +136,33 @@ export default function applyDamage(node, actionContext){
return applyChildren(); return applyChildren();
} }
function applyDamageMultipliers({target, damage, damageProp, logValue}){ function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
const damageType = damageProp?.damageType; const damageType = damageProp?.damageType;
if (!damageType) return damage; if (!damageType) return damage;
const multiplier = target?.variables?.[damageType]; const multiplier = target?.variables?.[damageType];
if (!multiplier) return damage; if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`; const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
if ( if (
multiplier.immunity && multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity')) some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
){ ) {
logValue.push(`Immune to ${damageTypeText}`); logValue.push(`Immune to ${damageTypeText}`);
return 0; return 0;
} else { } else {
if ( if (
multiplier.resistance && multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance')) some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
){ ) {
logValue.push(`Resistant to ${damageTypeText}`); logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2); damage = Math.floor(damage / 2);
} }
if ( if (
multiplier.vulnerability && multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability')) some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
){ ) {
logValue.push(`Vulnerable to ${damageTypeText}`); logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2); damage = Math.floor(damage * 2);
} }
@@ -170,7 +170,7 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage; return damage;
} }
function multiplierAppliesTo(damageProp, multiplierType){ function multiplierAppliesTo(damageProp, multiplierType) {
return multiplier => { return multiplier => {
// Apply the default 'ignore x' tags // Apply the default 'ignore x' tags
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false; if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
@@ -187,7 +187,7 @@ function multiplierAppliesTo(damageProp, multiplierType){
} }
} }
function dealDamage({target, damageType, amount, actionContext}){ function dealDamage({ target, damageType, amount, actionContext }) {
// Get all the health bars and do damage to them // Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute'); let healthBars = getPropertiesOfType(target._id, 'attribute');
@@ -239,6 +239,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext actionContext
}); });
damageLeft -= damageAdded; damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
}); });
return totalDamage; return totalDamage;
} }

View File

@@ -0,0 +1,97 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import {
assertDocCopyPermission,
assertDocEditPermission
} from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
var snackbar;
if (Meteor.isClient) {
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyLibraryNodeTo = new ValidatedMethod({
name: 'libraryNodes.copyTo',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parent: {
type: RefSchema,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 10000,
},
run({ _id, parent }) {
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
throw new Meteor.Error('Invalid destination',
'Library documents can only be copied to destinations inside other libraries'
);
}
const libraryNode = LibraryNodes.findOne(_id);
const parentDoc = fetchDocByRef(parent);
assertDocCopyPermission(libraryNode, this.userId);
assertDocEditPermission(parentDoc, this.userId);
let decendants = LibraryNodes.find({
'ancestors.id': _id,
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
}).fetch();
if (decendants.length > DUPLICATE_CHILDREN_LIMIT) {
decendants.pop();
if (Meteor.isClient) {
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
const nodes = [libraryNode, ...decendants];
const newAncestry = parentDoc.ancestors || [];
newAncestry.push(parent);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry,
oldParent: libraryNode.parent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({ docArray: nodes });
// Order the root node
libraryNode.order = (parentDoc.order || 0) + 0.5;
LibraryNodes.batchInsert(nodes);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
});
},
});
export default copyLibraryNodeTo;

View File

@@ -16,7 +16,7 @@ if (Meteor.isClient) {
).snackbar ).snackbar
} }
const DUPLICATE_CHILDREN_LIMIT = 50; const DUPLICATE_CHILDREN_LIMIT = 500;
const duplicateLibraryNode = new ValidatedMethod({ const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate', name: 'libraryNodes.duplicate',
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
rateLimit: { rateLimit: {
numRequests: 5, numRequests: 1,
timeInterval: 5000, timeInterval: 5000,
}, },
run({ _id }) { run({ _id }) {

View File

@@ -1,2 +1,3 @@
import '/imports/api/library/methods/copyLibraryNodeTo.js';
import '/imports/api/library/methods/duplicateLibraryNode.js'; import '/imports/api/library/methods/duplicateLibraryNode.js';
import '/imports/api/library/methods/updateReferenceNode.js'; import '/imports/api/library/methods/updateReferenceNode.js';

View File

@@ -69,6 +69,16 @@ let AttributeSchema = createPropertySchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
// Control how the health bar handles overflow
healthBarNoDamageOverflow: {
type: Boolean,
optional: true,
},
healthBarNoHealingOverflow: {
type: Boolean,
optional: true,
},
// Control when the health bar takes damage or healing
healthBarDamageOrder: { healthBarDamageOrder: {
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
optional: true, optional: true,
@@ -107,6 +117,14 @@ let AttributeSchema = createPropertySchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
hideWhenTotalZero: {
type: Boolean,
optional: true,
},
hideWhenValueZero: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions // Automatically zero the adjustment on these conditions
reset: { reset: {
type: String, type: String,

View File

@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
defaultValue: false, defaultValue: false,
index: 1, index: 1,
}, },
readersCanCopy: {
type: Boolean,
optional: true,
},
}); });
export default SharingSchema; export default SharingSchema;

View File

@@ -27,6 +27,26 @@ const setPublic = new ValidatedMethod({
}, },
}); });
const setReadersCanCopy = new ValidatedMethod({
name: 'sharing.setReadersCanCopy',
validate: new SimpleSchema({
docRef: RefSchema,
readersCanCopy: { type: Boolean },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ docRef, readersCanCopy }) {
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
return getCollectionByName(docRef.collection).update(docRef.id, {
$set: { readersCanCopy },
});
},
});
const updateUserSharePermissions = new ValidatedMethod({ const updateUserSharePermissions = new ValidatedMethod({
name: 'sharing.updateUserSharePermissions', name: 'sharing.updateUserSharePermissions',
validate: new SimpleSchema({ validate: new SimpleSchema({
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
}, },
}); });
export { setPublic, updateUserSharePermissions, transferOwnership }; export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };

View File

@@ -1,24 +1,25 @@
import { _ } from 'meteor/underscore'; import { _ } from 'meteor/underscore';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId){ function assertIdValid(userId) {
if (!userId || typeof userId !== 'string'){ if (!userId || typeof userId !== 'string') {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'No user ID. Are you logged in?'); 'No user ID. Are you logged in?');
} }
} }
function assertdocExists(doc){ function assertdocExists(doc) {
if (!doc){ if (!doc) {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'Permission denied: No such document exists'); 'Permission denied: No such document exists');
} }
} }
export function assertOwnership(doc, userId){ export function assertOwnership(doc, userId) {
assertIdValid(userId); assertIdValid(userId);
assertdocExists(doc); assertdocExists(doc);
if (doc.owner === userId ){
if (doc.owner === userId) {
return true; return true;
} else { } else {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
@@ -37,13 +38,12 @@ export function assertEditPermission(doc, userId) {
assertdocExists(doc); assertdocExists(doc);
const user = Meteor.users.findOne(userId, { const user = Meteor.users.findOne(userId, {
fields: { fields: {
'services.patreon': 1,
'roles': 1, 'roles': 1,
} }
}); });
// Admin override // Admin override
if (user.roles && user.roles.includes('admin')){ if (user.roles && user.roles.includes('admin')) {
return true; return true;
} }
@@ -51,7 +51,7 @@ export function assertEditPermission(doc, userId) {
if ( if (
doc.owner === userId || doc.owner === userId ||
_.contains(doc.writers, userId) _.contains(doc.writers, userId)
){ ) {
return true; return true;
} else { } else {
throw new Meteor.Error('Edit permission denied', throw new Meteor.Error('Edit permission denied',
@@ -59,9 +59,46 @@ export function assertEditPermission(doc, userId) {
} }
} }
function getRoot(doc){ /**
* Assert that the user can edit the root document which manages its own sharing
* permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertCopyPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc); assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){ const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')) {
return true;
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||
_.contains(doc.writers, userId)
) {
return true;
} else if (
(_.contains(doc.readers, userId) || doc.public) &&
doc.readersCanCopy
) {
return true;
} else {
throw new Meteor.Error('Copy permission denied',
'You do not have permission to copy this document');
}
}
function getRoot(doc) {
assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
return fetchDocByRef(doc.ancestors[0]); return fetchDocByRef(doc.ancestors[0]);
} else { } else {
return doc; return doc;
@@ -74,11 +111,22 @@ function getRoot(doc){
* *
* Warning: the doc and userId must be set by a trusted source * Warning: the doc and userId must be set by a trusted source
*/ */
export function assertDocEditPermission(doc, userId){ export function assertDocEditPermission(doc, userId) {
let root = getRoot(doc); let root = getRoot(doc);
assertEditPermission(root, userId); assertEditPermission(root, userId);
} }
/**
* Assert that the user can copy a descendant document whose root ancestor
* implements sharing permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocCopyPermission(doc, userId) {
let root = getRoot(doc);
assertCopyPermission(root, userId);
}
export function assertViewPermission(doc, userId) { export function assertViewPermission(doc, userId) {
assertdocExists(doc); assertdocExists(doc);
if (doc.public) return true; if (doc.public) return true;
@@ -88,17 +136,17 @@ export function assertViewPermission(doc, userId) {
doc.owner === userId || doc.owner === userId ||
_.contains(doc.readers, userId) || _.contains(doc.readers, userId) ||
_.contains(doc.writers, userId) _.contains(doc.writers, userId)
){ ) {
return true; return true;
} else { } else {
// Admin override // Admin override
const user = Meteor.users.findOne(userId, { const user = Meteor.users.findOne(userId, {
fields: { fields: {
'roles': 1, 'roles': 1,
} }
}); });
if (user.roles && user.roles.includes('admin')){ if (user.roles && user.roles.includes('admin')) {
return true; return true;
} }
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
* *
* Warning: the doc and userId must be set by a trusted source * Warning: the doc and userId must be set by a trusted source
*/ */
export function assertDocViewPermission(doc, userId){ export function assertDocViewPermission(doc, userId) {
let root = getRoot(doc); let root = getRoot(doc);
assertViewPermission(root, userId); assertViewPermission(root, userId);
} }
export function assertAdmin(userId){ export function assertAdmin(userId) {
assertIdValid(userId); assertIdValid(userId);
let user = Meteor.users.findOne(userId, {fields: {roles: 1}}); let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
if (!user){ if (!user) {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'UserId does not match any existing user'); 'UserId does not match any existing user');
} }
let isAdmin = user.roles && user.roles.includes('admin') let isAdmin = user.roles && user.roles.includes('admin')
if (!isAdmin){ if (!isAdmin) {
throw new Meteor.Error('Permission denied', throw new Meteor.Error('Permission denied',
'User does not have the admin role'); 'User does not have the admin role');
} }

View File

@@ -10,6 +10,7 @@
:outlined="!!label" :outlined="!!label"
:icon="!label" :icon="!label"
:min-width="label && 108" :min-width="label && 108"
:disabled="context.editPermission === false"
v-on="on" v-on="on"
> >
{{ label }} {{ label }}
@@ -124,6 +125,9 @@
} }
export default { export default {
inject: {
context: { default: {} }
},
props: { props: {
//hex string //hex string
value: { value: {

View File

@@ -69,6 +69,7 @@
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="$listeners && $listeners.duplicate" v-if="$listeners && $listeners.duplicate"
:disabled="context.editPermission === false"
@click="$emit('duplicate')" @click="$emit('duplicate')"
> >
<v-list-item-content> <v-list-item-content>
@@ -80,8 +81,23 @@
<v-icon>mdi-content-copy</v-icon> <v-icon>mdi-content-copy</v-icon>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
<v-list-item
v-if="$listeners && $listeners.copy"
:disabled="context.copyPermission === false"
@click="$emit('copy')"
>
<v-list-item-content>
<v-list-item-title>
Copy To
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-content-duplicate</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"
@click="$emit('move')" @click="$emit('move')"
> >
<v-list-item-content> <v-list-item-content>
@@ -95,6 +111,7 @@
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="$listeners && $listeners.remove" v-if="$listeners && $listeners.remove"
:disabled="context.editPermission === false"
@click="$emit('remove')" @click="$emit('remove')"
> >
<v-list-item-content> <v-list-item-content>
@@ -157,6 +174,9 @@ export default {
PropertyIcon, PropertyIcon,
ColorPicker, ColorPicker,
}, },
inject: {
context: { default: {} }
},
props: { props: {
model: { model: {
type: Object, type: Object,

View File

@@ -365,6 +365,10 @@ const getProperties = function (creature, filter, options = {
filter.removed = { $ne: true }; filter.removed = { $ne: true };
filter.inactive = { $ne: true }; filter.inactive = { $ne: true };
filter.overridden = { $ne: true }; filter.overridden = { $ne: true };
filter.$nor = [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
];
return CreatureProperties.find(filter, options); return CreatureProperties.find(filter, options);
}; };

View File

@@ -8,6 +8,7 @@
:embedded="embedded" :embedded="embedded"
@duplicate="duplicate" @duplicate="duplicate"
@move="move" @move="move"
@copy="copy"
@remove="remove" @remove="remove"
@toggle-editing="editing = !editing" @toggle-editing="editing = !editing"
@color-changed="value => change({path: ['color'], value})" @color-changed="value => change({path: ['color'], value})"
@@ -95,10 +96,13 @@
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js'; import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js'; import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
import { get } from 'lodash'; import { get } from 'lodash';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js'; import {
assertDocEditPermission, assertDocCopyPermission
} from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js'; import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js'; import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
import copyLibraryNodeTo from '/imports/api/library/methods/copyLibraryNodeTo.js';
let formIndex = {}; let formIndex = {};
for (let key in propertyFormIndex){ for (let key in propertyFormIndex){
@@ -126,7 +130,7 @@
}, },
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['editPermission', 'isLibraryForm'], include: ['editPermission', 'copyPermission', 'isLibraryForm'],
}, },
data(){return { data(){return {
editing: !!this.startInEditTab, editing: !!this.startInEditTab,
@@ -162,6 +166,14 @@
return false; return false;
} }
}, },
copyPermission(){
try {
assertDocCopyPermission(this.model, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
}, },
methods: { methods: {
getPropertyName, getPropertyName,
@@ -200,6 +212,37 @@
} }
}); });
}, },
copy(){
const thisId = this._id;
this.$store.commit('pushDialogStack', {
component: 'move-library-node-dialog',
elementId: 'property-toolbar-menu-button',
data: {
action: 'Copy',
},
callback(parentId){
if (!parentId) return;
copyLibraryNodeTo.call({
_id: thisId,
parent: {
collection: 'libraryNodes',
id: parentId
},
}, (error) => {
if (error) {
console.error(error);
snackbar({
text: error.reason || error.message || error.toString(),
});
} else {
snackbar({
text: 'Copied successfully',
});
}
});
}
});
},
change({path, value, ack}){ change({path, value, ack}){
updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{ updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{
if (ack){ if (ack){

View File

@@ -16,7 +16,7 @@
color="primary" color="primary"
@click="$store.dispatch('popDialogStack', node._id)" @click="$store.dispatch('popDialogStack', node._id)"
> >
Move {{ action || 'Move' }}
</v-btn> </v-btn>
</template> </template>
</dialog-base> </dialog-base>
@@ -30,6 +30,12 @@ export default {
DialogBase, DialogBase,
LibraryAndNode, LibraryAndNode,
}, },
props: {
action: {
type: String,
default: undefined,
},
},
data() { data() {
return { return {
node: undefined, node: undefined,

View File

@@ -42,6 +42,10 @@ export default {
removed: { $ne: true }, removed: { $ne: true },
inactive: { $ne: true }, inactive: { $ne: true },
overridden: { $ne: true }, overridden: { $ne: true },
$nor: [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
],
}; };
if (creature.settings.hideUnusedStats) { if (creature.settings.hideUnusedStats) {
filter.hide = { $ne: true }; filter.hide = { $ne: true };

View File

@@ -106,10 +106,17 @@
/> />
<smart-switch <smart-switch
label="Ignore damage" label="Ignore damage"
class="mr-4"
:value="model.healthBarNoDamage" :value="model.healthBarNoDamage"
:error-messages="errors.healthBarNoDamage" :error-messages="errors.healthBarNoDamage"
@change="change('healthBarNoDamage', ...arguments)" @change="change('healthBarNoDamage', ...arguments)"
/> />
<smart-switch
label="Prevent damage overflow"
:value="model.healthBarNoDamageOverflow"
:error-messages="errors.healthBarNoDamageOverflow"
@change="change('healthBarNoDamageOverflow', ...arguments)"
/>
</v-layout> </v-layout>
<v-layout wrap> <v-layout wrap>
<text-field <text-field
@@ -125,14 +132,20 @@
/> />
<smart-switch <smart-switch
label="Ignore healing" label="Ignore healing"
class="mr-4"
:value="model.healthBarNoHealing" :value="model.healthBarNoHealing"
:error-messages="errors.healthBarNoHealing" :error-messages="errors.healthBarNoHealing"
@change="change('healthBarNoHealing', ...arguments)" @change="change('healthBarNoHealing', ...arguments)"
/> />
<smart-switch
label="Prevent healing overflow"
:value="model.healthBarNoHealingOverflow"
:error-messages="errors.healthBarNoHealingOverflow"
@change="change('healthBarNoHealingOverflow', ...arguments)"
/>
</v-layout> </v-layout>
</form-section> </form-section>
</v-expand-transition> </v-expand-transition>
<form-section <form-section
v-if="$slots.children" v-if="$slots.children"
name="Children" name="Children"
@@ -151,26 +164,74 @@
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<div class="layout column align-center"> <div class="layout column align-center">
<smart-switch <v-row dense>
v-if="model.attributeType !== 'hitDice'" <v-col
label="Allow decimal values" cols="12"
class="no-flex" sm="6"
:value="model.decimal" md="4"
:error-messages="errors.decimal" >
@change="change('decimal', ...arguments)" <smart-switch
/> v-if="model.attributeType !== 'hitDice'"
<smart-switch label="Allow decimal values"
label="Can be damaged into negative values" class="mx-4"
:value="model.ignoreLowerLimit" :value="model.decimal"
:error-messages="errors.ignoreLowerLimit" :error-messages="errors.decimal"
@change="change('ignoreLowerLimit', ...arguments)" @change="change('decimal', ...arguments)"
/> />
<smart-switch </v-col>
label="Can be incremented above total" <v-col
:value="model.ignoreUpperLimit" cols="12"
:error-messages="errors.ignoreUpperLimit" sm="6"
@change="change('ignoreUpperLimit', ...arguments)" md="4"
/> >
<smart-switch
label="Can be damaged into negative values"
class="mx-4"
:value="model.ignoreLowerLimit"
:error-messages="errors.ignoreLowerLimit"
@change="change('ignoreLowerLimit', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Can be incremented above total"
class="mx-4"
:value="model.ignoreUpperLimit"
:error-messages="errors.ignoreUpperLimit"
@change="change('ignoreUpperLimit', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Hide when total is zero"
class="mx-4"
:value="model.hideWhenTotalZero"
:error-messages="errors.hideWhenTotalZero"
@change="change('hideWhenTotalZero', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Hide when value is zero"
class="mx-4"
:value="model.hideWhenValueZero"
:error-messages="errors.hideWhenValueZero"
@change="change('hideWhenValueZero', ...arguments)"
/>
</v-col>
</v-row>
<div <div
class="layout justify-center" class="layout justify-center"
style="align-self: stretch;" style="align-self: stretch;"

View File

@@ -13,6 +13,16 @@
:value="!!model.public + ''" :value="!!model.public + ''"
@change="(value, ack) => setSheetPublic({value, ack})" @change="(value, ack) => setSheetPublic({value, ack})"
/> />
<smart-select
v-if="docRef.collection === 'libraries'"
label="Who can copy from this library"
:items="[
{text: 'Only people with edit permission', value: 'false'},
{text: 'Anyone with read permission', value: 'true'}
]"
:value="!!model.readersCanCopy + ''"
@change="(value, ack) => setReadersCanCopy({value, ack})"
/>
<text-field <text-field
v-if="model.public && docRef.collection === 'libraries'" v-if="model.public && docRef.collection === 'libraries'"
readonly readonly
@@ -30,6 +40,7 @@
@change="(value, ack) => getUser({value, ack})" @change="(value, ack) => getUser({value, ack})"
/> />
<v-btn <v-btn
class="ml-2 mt-2"
:disabled="userFoundState !== 'found'" :disabled="userFoundState !== 'found'"
@click="updateSharing(userId, 'reader')" @click="updateSharing(userId, 'reader')"
> >
@@ -126,6 +137,7 @@
<script lang="js"> <script lang="js">
import { import {
setPublic, setPublic,
setReadersCanCopy,
updateUserSharePermissions updateUserSharePermissions
} from '/imports/api/sharing/sharing.js'; } from '/imports/api/sharing/sharing.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
@@ -157,6 +169,14 @@ export default {
ack(error && error.reason || error); ack(error && error.reason || error);
}); });
}, },
setReadersCanCopy({ value, ack }) {
setReadersCanCopy.call({
docRef: this.docRef,
readersCanCopy: value === 'true',
}, (error) => {
ack(error && error.reason || error);
});
},
getUser({ value, ack }) { getUser({ value, ack }) {
this.userSearched = value; this.userSearched = value;
if (!value) { if (!value) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "dicecloud", "name": "dicecloud",
"version": "2.0.42", "version": "2.0.43",
"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": {
@@ -124,4 +124,4 @@
"vuetify/no-deprecated-classes": "error" "vuetify/no-deprecated-classes": "error"
} }
} }
} }