Compare commits

...

11 Commits

Author SHA1 Message Date
Stefan Zermatten
5d57a74667 Merge branch 'version-2-dev' into version-2 2022-11-03 20:52:26 +02:00
Stefan Zermatten
21b0029df7 bumped version 2022-11-03 20:51:58 +02:00
Stefan Zermatten
c0ccafa787 Added overflow stops to health bars 2022-11-03 20:50:10 +02:00
Stefan Zermatten
d63ad9ea8f Added hide when total/value zero to attributes 2022-11-03 20:39:02 +02:00
Stefan Zermatten
8f56a60fb1 Added copy-to and related sharing permissions 2022-11-03 20:18:59 +02:00
Stefan Zermatten
358ae46627 Began work on copy to for library nodes 2022-11-03 19:08:44 +02:00
Stefan Zermatten
0b1db3c40c Updated meteor 2022-10-18 15:40:41 +02:00
Stefan Zermatten
0ad7e659d2 updated docs to include create a class guide 2022-10-18 15:40:17 +02:00
Stefan Zermatten
58c3875dc7 Hotifix: Casting cantrips without a spell slot 2022-10-12 07:36:42 +02:00
Stefan Zermatten
84f506f1fe Added $checkDiceRoll $checkRoll $checkModifier variables 2022-10-12 07:32:39 +02:00
Stefan Zermatten
d0a3ccc76a bumped version 2022-10-10 16:54:57 +02:00
35 changed files with 1172 additions and 490 deletions

View File

@@ -11,7 +11,7 @@ accounts-google@1.4.0
email@2.2.1
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.16.0-beta280.7
mongo@1.16.0
session@1.2.0
tracker@1.2.0
logging@1.3.1
@@ -48,4 +48,4 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
mdg:meteor-apm-agent
typescript
typescript@4.5.4

View File

@@ -1 +1 @@
METEOR@2.8-beta.7
METEOR@2.8.0

View File

@@ -27,10 +27,10 @@ coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.5.0
ddp-client@2.6.0
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
ddp-server@2.6.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.2
@@ -57,7 +57,7 @@ localstorage@1.2.0
logging@1.3.1
mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0
meteor@1.10.1-beta280.7
meteor@1.10.1
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
@@ -65,18 +65,18 @@ meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.1
minifier-js@2.7.5
minimongo@1.9.0-beta280.7
minimongo@1.9.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.8
modules@0.19.0-beta280.7
modules@0.19.0
modules-runtime@0.13.0
mongo@1.16.0-beta280.7
mongo@1.16.0
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
npm-mongo@4.9.0-beta280.7
npm-mongo@4.9.0
oauth@2.1.2
oauth2@1.3.1
ordered-dict@1.1.0

View File

@@ -1,6 +1,6 @@
import { some, intersection, difference, remove, includes } from 'lodash';
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 logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
@@ -10,9 +10,9 @@ import {
} from '/imports/api/engine/loadCreatures.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);
const applyChildren = function(){
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
@@ -28,10 +28,10 @@ export default function applyDamage(node, actionContext){
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
;
// Double the damage rolls if the hit is critical
let context = new Context({
options: {doubleRolls: criticalHit},
options: { doubleRolls: criticalHit },
});
// 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
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
@@ -50,13 +50,13 @@ export default function applyDamage(node, actionContext){
context.errors = [];
// 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);
// Store the result
if (reduced.parseType === 'constant'){
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
} else if (reduced.parseType === 'error') {
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
@@ -64,7 +64,7 @@ export default function applyDamage(node, actionContext){
let damage = +reduced.value;
// 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();
}
@@ -83,7 +83,7 @@ export default function applyDamage(node, actionContext){
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
(prop.damageType !== 'healing' ? ' damage ' : '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
@@ -107,7 +107,7 @@ export default function applyDamage(node, actionContext){
});
// 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
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
@@ -136,33 +136,33 @@ export default function applyDamage(node, actionContext){
return applyChildren();
}
function applyDamageMultipliers({target, damage, damageProp, logValue}){
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
const damageType = damageProp?.damageType;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
if (
multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
){
) {
logValue.push(`Immune to ${damageTypeText}`);
return 0;
} else {
if (
multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
){
) {
logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2);
}
if (
multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
){
) {
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
@@ -170,7 +170,7 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage;
}
function multiplierAppliesTo(damageProp, multiplierType){
function multiplierAppliesTo(damageProp, multiplierType) {
return multiplier => {
// Apply the default 'ignore x' tags
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
let healthBars = getPropertiesOfType(target._id, 'attribute');
@@ -239,6 +239,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext
});
damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
});
return totalDamage;
}

View File

@@ -69,7 +69,7 @@ const doAction = new ValidatedMethod({
let slot;
// If a spell requires a slot, make sure a slot is spent
if (!spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
slot = CreatureProperties.findOne(slotId);
if (!slot) {
throw new Meteor.Error('No slot',

View File

@@ -23,7 +23,7 @@ const doCheck = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({propId, scope}) {
run({ propId, scope }) {
const prop = CreatureProperties.findOne(propId);
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
@@ -33,13 +33,13 @@ const doCheck = new ValidatedMethod({
assertEditPermission(actionContext.creature, this.userId);
// Do the check
doCheckWork({prop, actionContext});
doCheckWork({ prop, actionContext });
},
});
export default doCheck;
export function doCheckWork({prop, actionContext}){
export function doCheckWork({ prop, actionContext }) {
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
rollCheck(prop, actionContext);
@@ -54,17 +54,17 @@ function rollCheck(prop, actionContext) {
// get the modifier for the roll
let rollModifier;
let logName = `${prop.name} check`;
if (prop.type === 'skill'){
if (prop.type === 'skill') {
rollModifier = prop.value;
if (prop.skillType === 'save'){
if (prop.name.match(/save/i)){
if (prop.skillType === 'save') {
if (prop.name.match(/save/i)) {
logName = prop.name;
} else {
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
}
}
} else if (prop.type === 'attribute'){
if (prop.attributeType === 'ability'){
} else if (prop.type === 'attribute') {
if (prop.attributeType === 'ability') {
rollModifier = prop.modifier;
} else {
rollModifier = prop.value;
@@ -80,7 +80,7 @@ function rollCheck(prop, actionContext) {
rollModifier += effectBonus;
let value, values, resultPrefix;
if (scope['$checkAdvantage'] === 1){
if (scope['$checkAdvantage'] === 1) {
logName += ' (Advantage)';
const [a, b] = rollDice(2, 20);
if (a >= b) {
@@ -90,7 +90,7 @@ function rollCheck(prop, actionContext) {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else if (scope['$checkAdvantage'] === -1){
} else if (scope['$checkAdvantage'] === -1) {
logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20);
if (a <= b) {
@@ -106,6 +106,9 @@ function rollCheck(prop, actionContext) {
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
}
const result = (value + rollModifier) || 0;
scope['$checkDiceRoll'] = value;
scope['$checkRoll'] = result;
scope['$checkModifier'] = rollModifier;
actionContext.addLog({
name: logName,
value: `${resultPrefix} **${result}**`,
@@ -116,7 +119,7 @@ function applyUnresolvedEffects(prop, scope) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {
return { effectBonus, effectString};
return { effectBonus, effectString };
}
prop.effects.forEach(effect => {
if (!effect.amount?.parseNode) return;
@@ -127,5 +130,5 @@ function applyUnresolvedEffects(prop, scope) {
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
});
return { effectBonus, effectString};
return { effectBonus, effectString };
}

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
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const DUPLICATE_CHILDREN_LIMIT = 500;
const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
numRequests: 1,
timeInterval: 5000,
},
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/updateReferenceNode.js';

View File

@@ -69,6 +69,16 @@ let AttributeSchema = createPropertySchema({
type: Boolean,
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: {
type: SimpleSchema.Integer,
optional: true,
@@ -107,6 +117,14 @@ let AttributeSchema = createPropertySchema({
type: Boolean,
optional: true,
},
hideWhenTotalZero: {
type: Boolean,
optional: true,
},
hideWhenValueZero: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions
reset: {
type: String,

View File

@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
defaultValue: false,
index: 1,
},
readersCanCopy: {
type: Boolean,
optional: true,
},
});
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({
name: 'sharing.updateUserSharePermissions',
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 fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
function assertIdValid(userId) {
if (!userId || typeof userId !== 'string') {
throw new Meteor.Error('Permission denied',
'No user ID. Are you logged in?');
}
}
function assertdocExists(doc){
if (!doc){
function assertdocExists(doc) {
if (!doc) {
throw new Meteor.Error('Permission denied',
'Permission denied: No such document exists');
}
}
export function assertOwnership(doc, userId){
export function assertOwnership(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.owner === userId ){
if (doc.owner === userId) {
return true;
} else {
throw new Meteor.Error('Permission denied',
@@ -37,13 +38,12 @@ export function assertEditPermission(doc, userId) {
assertdocExists(doc);
const user = Meteor.users.findOne(userId, {
fields: {
'services.patreon': 1,
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')){
if (user.roles && user.roles.includes('admin')) {
return true;
}
@@ -51,7 +51,7 @@ export function assertEditPermission(doc, userId) {
if (
doc.owner === userId ||
_.contains(doc.writers, userId)
){
) {
return true;
} else {
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);
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]);
} else {
return doc;
@@ -74,11 +111,22 @@ function getRoot(doc){
*
* 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);
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) {
assertdocExists(doc);
if (doc.public) return true;
@@ -88,17 +136,17 @@ export function assertViewPermission(doc, userId) {
doc.owner === userId ||
_.contains(doc.readers, userId) ||
_.contains(doc.writers, userId)
){
) {
return true;
} else {
// Admin override
const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
if (user.roles && user.roles.includes('admin')){
if (user.roles && user.roles.includes('admin')) {
return true;
}
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
*
* 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);
assertViewPermission(root, userId);
}
export function assertAdmin(userId){
export function assertAdmin(userId) {
assertIdValid(userId);
let user = Meteor.users.findOne(userId, {fields: {roles: 1}});
if (!user){
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
if (!user) {
throw new Meteor.Error('Permission denied',
'UserId does not match any existing user');
}
let isAdmin = user.roles && user.roles.includes('admin')
if (!isAdmin){
if (!isAdmin) {
throw new Meteor.Error('Permission denied',
'User does not have the admin role');
}

View File

@@ -8,6 +8,7 @@ const docPaths = [
'dependency-loops',
'docs',
'tags',
'walkthroughs/create-a-class',
];
const docs = new Map();
docPaths.forEach(path => {

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@
:embedded="embedded"
@duplicate="duplicate"
@move="move"
@copy="copy"
@remove="remove"
@toggle-editing="editing = !editing"
@color-changed="value => change({path: ['color'], value})"
@@ -95,10 +96,13 @@
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
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 { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
import copyLibraryNodeTo from '/imports/api/library/methods/copyLibraryNodeTo.js';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -126,7 +130,7 @@
},
reactiveProvide: {
name: 'context',
include: ['editPermission', 'isLibraryForm'],
include: ['editPermission', 'copyPermission', 'isLibraryForm'],
},
data(){return {
editing: !!this.startInEditTab,
@@ -162,6 +166,14 @@
return false;
}
},
copyPermission(){
try {
assertDocCopyPermission(this.model, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
},
methods: {
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}){
updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{
if (ack){

View File

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

View File

@@ -1,7 +1,10 @@
<template>
<v-container class="documentation">
<v-row>
<v-col cols="12">
<v-row justify="center">
<v-col
cols="12"
lg="8"
>
<v-fade-transition mode="out-in">
<v-card
v-if="doc"

View File

@@ -1,7 +1,10 @@
<template>
<v-container class="documentation">
<v-row>
<v-col cols="12">
<v-row justify="center">
<v-col
cols="12"
lg="8"
>
<v-card>
<v-card-text class="markdown">
<h1>Functions</h1>

View File

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

View File

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

View File

@@ -13,6 +13,16 @@
:value="!!model.public + ''"
@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
v-if="model.public && docRef.collection === 'libraries'"
readonly
@@ -30,6 +40,7 @@
@change="(value, ack) => getUser({value, ack})"
/>
<v-btn
class="ml-2 mt-2"
:disabled="userFoundState !== 'found'"
@click="updateSharing(userId, 'reader')"
>
@@ -126,6 +137,7 @@
<script lang="js">
import {
setPublic,
setReadersCanCopy,
updateUserSharePermissions
} from '/imports/api/sharing/sharing.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
@@ -157,6 +169,14 @@ export default {
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 }) {
this.userSearched = value;
if (!value) {

1013
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "dicecloud",
"version": "2.0.38",
"version": "2.0.43",
"description": "Unofficial Online Realtime D&D 5e App",
"license": "GPL-3.0",
"repository": {
@@ -19,57 +19,57 @@
"npm": "6.13.x"
},
"dependencies": {
"@babel/runtime": "^7.18.3",
"@babel/runtime": "^7.19.4",
"@chenfengyuan/vue-countdown": "^1.1.5",
"@tozd/vue-observer-utils": "^0.5.0",
"animejs": "^2.2.0",
"aws-sdk": "^2.1148.0",
"bcrypt": "^5.0.0",
"aws-sdk": "^2.1234.0",
"bcrypt": "^5.1.0",
"chroma-js": "^2.4.2",
"core-js": "^2.6.11",
"css-box-shadow": "^1.0.0-3",
"date-fns": "^1.30.1",
"ddp-rate-limiter-mixin": "^1.1.10",
"discord.js": "^12.5.3",
"dompurify": "^2.3.8",
"dompurify": "^2.4.0",
"ignore": "^5.2.0",
"ignore-styles": "^5.0.1",
"lodash": "^4.17.20",
"marked": "^4.0.16",
"meteor-node-stubs": "^1.2.3",
"marked": "^4.1.1",
"meteor-node-stubs": "^1.2.5",
"minify-css-string": "^1.0.0",
"moo": "^0.5.1",
"moo": "^0.5.2",
"nearley": "^2.19.1",
"ngraph.graph": "^19.1.0",
"ngraph.path": "^1.4.0",
"pretty-bytes": "^6.0.0",
"qrcode": "^1.5.0",
"qrcode": "^1.5.1",
"request": "^2.88.2",
"sharp": "^0.30.4",
"simpl-schema": "^1.12.2",
"sharp": "^0.30.7",
"simpl-schema": "^1.13.1",
"source-map-support": "^0.5.21",
"speakingurl": "^14.0.1",
"styles": "^0.2.1",
"underscore": "^1.13.4",
"underscore": "^1.13.6",
"vue": "2.6.10",
"vue-meteor-tracker": "^2.0.0-beta.5",
"vue-meteor-tracker": "^2.0.0",
"vue-reactive-provide": "^0.3.0",
"vue-router": "^3.5.4",
"vue-router": "^3.6.5",
"vuedraggable": "^2.23.2",
"vuetify": "^2.6.6",
"vuetify": "^2.6.11",
"vuetify-upload-button": "^2.0.2",
"vuex": "^3.1.3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"@vue/compiler-dom": "^3.2.40",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"@vue/compiler-dom": "^3.2.41",
"chai": "^4.3.6",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.20.0",
"eslint-plugin-vuetify": "^1.1.0",
"mem": "^6.1.1",
"sass": "^1.52.2",
"sass": "^1.55.0",
"typescript": "^4.8.4"
},
"eslintConfig": {
@@ -124,4 +124,4 @@
"vuetify/no-deprecated-classes": "error"
}
}
}
}

View File

@@ -6,6 +6,10 @@ Leveling up a class means choosing, or manually adding, class level properties t
The total level of the class can be accessed in calculations using `classVariableName.level`.
## Making your own class
See [Create a Class](/docs/walkthroughs/create-a-class)
---
### Name

View File

@@ -1,6 +1,6 @@
# Skills
Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/efffect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).
Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).
---

View File

@@ -0,0 +1,47 @@
# Create a Class
This is a guide on creating a custom class in a character sheet. If possible, it is always faster to use an existing library that contains the class you want to use. Before continuing, check the #libraries channel of the [official discord](https://discord.gg/qEvdfeB) to see if a library exists with the class you are creating.
This guide assumes you are using the ruleset provided in the [5e System Reference Document library](/library/qkv8aptJH2fCXARcJ). If you are using a different ruleset for your character, there may be some discrepancies.
## Adding the class property
On the build tab of your character, in the card labeled **Slots**, expand the rulset, then click the slot where you would like to place the custom class, if it is your starting class in an SRD character, this would be the Class slot. Be sure to click the name of the slot, not the **+** button.
![Screenshot of Build Tab > Slots > Ruleset > Class](/images/docs/walkthroughs/create-a-class-1.png)
This opens the slot in detail view, showing you how the slot expected to be filled from a library, instead of filling the slot, we will be manually adding a class to the slot that we create ourselves.
Click the **Edit** button in the top right of the slot detail dialog.
![Screenshot of slot detail dialog](/images/docs/walkthroughs/create-a-class-2.png)
Expand the children of the class slot, and click the plus button to add a child property.
![Screenshot of adding a child property](/images/docs/walkthroughs/create-a-class-3.png)
This brings up the create a property dialog, we are creating a class, so select the class property type.
![Screenshot of choosing a class property](/images/docs/walkthroughs/create-a-class-4.png)
Now that we have selected the class property type, the create tab is selected where we can enter the details of our class, fill in the form and click **Create**.
![Screenshot of the class form](/images/docs/walkthroughs/create-a-class-5.png)
Now that our custom class is created, we can close the class slot dialog.
On the Build tab, in the card with the title **Level**, you will see your new class, with a button to **Level Up**, clicking the level up button would usually search your libraries for class levels that match the variable name of the class, however, since it's a custom class, it will probably not find any levels.
Instead, as we did with the class slot, click on the class name to bring up the class detail dialog, click **Edit**, expand children and click the **+** button to add a child to the class. Here we will add all of the things our class gives the character.
Add an [Effect](/docs/property/effect) which targets `hitPoints` to add the starting hitpoints of the class. Add a [proficiencies](/docs/property/proficiency) for all the skill and saving throw proficiencies the class gives. Add [skills](/docs/property/skill) for all the tool and weapon proficiencies of the class, making sure to set the base proficiency of those skills to proficient. Add any text [features](/docs/property/feature) the class gives you, along with [actions](/docs/property/action) which may be children of those features, or direct children of the class.
Once you have added Everything the class gives you, it's time to add class levels. As a child of the class, add a [class level](/docs/property/class-level) property. Set the level to 1 and the name and variable name to match the variable name of the class.
Once the class level is created, open the class level and edit it. Use the **+** button in the children of the class level to add all the properties the class level gives your character.
Repeat this for every level of the class until your character is at the correct level.
You can use a separate character with levels in a class that is available in your libraries as an example of what properties you may want to add to your class and class levels.
![Example wizard class](/images/docs/walkthroughs/create-a-class-6.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB