Compare commits

...

27 Commits

Author SHA1 Message Date
Stefan Zermatten
93ac9215c2 Merge branch 'version-2-dev' into version-2 2022-10-10 16:53:10 +02:00
Stefan Zermatten
a6b501a62c Fixed error on missing group prop in tree node 2022-10-10 16:51:02 +02:00
Stefan Zermatten
e956bacf07 Added actionType to effective tags 2022-10-10 16:49:10 +02:00
Stefan Zermatten
60b6b283b1 Folders now get their children applied by actions 2022-10-10 16:45:53 +02:00
Stefan Zermatten
1c9b390551 Added ritual casting 2022-10-09 23:11:06 +02:00
Stefan Zermatten
21a487635d Removed unused code from action cards 2022-10-09 21:56:42 +02:00
Stefan Zermatten
c92a26d5e6 Action cards no longer display folder or the descendants of buffs 2022-10-09 21:56:01 +02:00
Stefan Zermatten
49b514b8f3 Load common dialogs more aggressively 2022-10-09 20:55:50 +02:00
Stefan Zermatten
5cb835c536 Got basic typescript tools working 2022-10-09 17:33:43 +02:00
Stefan Zermatten
aa8f2d230d Hunted the last of the \t's to extinction 2022-10-09 16:56:28 +02:00
Stefan Zermatten
2fa913b09a Applied style rules to genocide all \t characters 2022-10-09 16:01:36 +02:00
Stefan Zermatten
de598c70a7 Fixed rolled effects not applying to checks 2022-10-09 11:10:50 +02:00
Stefan Zermatten
baecdeff24 Fixed bug where items with zero quantity have active children 2022-10-09 10:10:21 +02:00
Stefan Zermatten
d4b7d22b5f Fixed toggled off spells showing in spell list 2022-09-26 09:43:00 +02:00
Stefan Zermatten
87f79737e8 Fixed empty calculated inline fields showing calc 2022-09-25 12:39:49 +02:00
Stefan Zermatten
9f0ffe13f8 Updated meteor to fix observer bugs 2022-09-13 17:34:46 +02:00
Stefan Zermatten
adaa31d76c damage tags to ignore multipliers 2022-09-13 17:34:30 +02:00
Stefan Zermatten
b051d764f8 Slot cards have slot color as outline 2022-09-13 15:47:31 +02:00
Stefan Zermatten
ffb5b4a4f3 Libraries show name in page title 2022-09-13 15:44:37 +02:00
Stefan Zermatten
fd87b7fb75 Added advantage popup to spell cast 2022-09-09 13:20:54 +02:00
Stefan Zermatten
f035902842 Removed unused file 2022-09-08 14:47:12 +02:00
Stefan Zermatten
dbc5f7253f Finished basic docs 2022-09-05 14:36:39 +02:00
Stefan Zermatten
f0e7253374 Updated docs 2022-09-01 13:33:28 +02:00
Stefan Zermatten
ffe37bf907 Added more property help docs 2022-09-01 12:18:29 +02:00
Stefan Zermatten
a63e2099d3 Added documentation UI and began documenting props 2022-08-31 14:43:38 +02:00
Stefan Zermatten
0308e4e7a7 Merge branch 'version-2' into version-2-dev 2022-08-29 11:30:55 +02:00
Stefan Zermatten
43f8df09f0 Fixed client crash when effects target calcs 2022-08-26 09:42:34 +02:00
280 changed files with 8796 additions and 6249 deletions

View File

@@ -11,14 +11,14 @@ accounts-google@1.4.0
email@2.2.1 email@2.2.1
meteor-base@1.5.1 meteor-base@1.5.1
mobile-experience@1.1.0 mobile-experience@1.1.0
mongo@1.15.0 mongo@1.16.0-beta280.7
session@1.2.0 session@1.2.0
tracker@1.2.0 tracker@1.2.0
logging@1.3.1 logging@1.3.1
reload@1.3.1 reload@1.3.1
ejson@1.1.2 ejson@1.1.2
check@1.3.1 check@1.3.1
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
shell-server@0.5.0 shell-server@0.5.0
ecmascript@0.16.2 ecmascript@0.16.2
es5-shim@4.8.0 es5-shim@4.8.0
@@ -48,3 +48,4 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler simple:rest-json-error-handler
littledata:synced-cron littledata:synced-cron
mdg:meteor-apm-agent mdg:meteor-apm-agent
typescript

View File

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

View File

@@ -1,4 +1,4 @@
accounts-base@2.2.3 accounts-base@2.2.4
accounts-google@1.4.0 accounts-google@1.4.0
accounts-oauth@1.4.1 accounts-oauth@1.4.1
accounts-password@2.3.1 accounts-password@2.3.1
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0 aldeed:schema-index@3.0.0
allow-deny@1.1.1 allow-deny@1.1.1
autoupdate@1.8.0 autoupdate@1.8.0
babel-compiler@7.9.0 babel-compiler@7.9.2
babel-runtime@1.5.1 babel-runtime@1.5.1
base64@1.0.12 base64@1.0.12
binary-heap@1.0.11 binary-heap@1.0.11
@@ -55,33 +55,33 @@ littledata:synced-cron@1.5.1
livedata@1.0.18 livedata@1.0.18
localstorage@1.2.0 localstorage@1.2.0
logging@1.3.1 logging@1.3.1
mdg:meteor-apm-agent@3.5.0 mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0 mdg:validated-method@1.2.0
meteor@1.10.0 meteor@1.10.1-beta280.7
meteor-base@1.5.1 meteor-base@1.5.1
meteortesting:browser-tests@1.3.5 meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3 meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2 meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0 mikowals:batch-insert@1.3.0
minifier-css@1.6.0 minifier-css@1.6.1
minifier-js@2.7.4 minifier-js@2.7.5
minimongo@1.8.0 minimongo@1.9.0-beta280.7
mobile-experience@1.1.0 mobile-experience@1.1.0
mobile-status-bar@1.1.0 mobile-status-bar@1.1.0
modern-browsers@0.1.8 modern-browsers@0.1.8
modules@0.18.0 modules@0.19.0-beta280.7
modules-runtime@0.13.0 modules-runtime@0.13.0
mongo@1.15.0 mongo@1.16.0-beta280.7
mongo-decimal@0.1.3 mongo-decimal@0.1.3
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.8 mongo-id@1.0.8
mongo-livedata@1.0.12 mongo-livedata@1.0.12
npm-mongo@4.3.1 npm-mongo@4.9.0-beta280.7
oauth@2.1.2 oauth@2.1.2
oauth2@1.3.1 oauth2@1.3.1
ordered-dict@1.1.0 ordered-dict@1.1.0
ostrio:cookies@2.7.2 ostrio:cookies@2.7.2
ostrio:files@2.0.1 ostrio:files@2.3.0
patreon-oauth@0.1.0 patreon-oauth@0.1.0
peerlibrary:assert@0.3.0 peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0 peerlibrary:check-extension@0.7.0
@@ -116,7 +116,7 @@ simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0 simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.0 socket-stream-client@0.5.0
spacebars-compiler@1.3.1 spacebars-compiler@1.3.1
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
static-html@1.3.2 static-html@1.3.2
templating-tools@1.2.2 templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2 tmeasday:check-npm-versions@1.0.2

View File

@@ -93,7 +93,7 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema); CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
for (let key in propertySchemasIndex){ for (let key in propertySchemasIndex) {
let schema = new SimpleSchema({}); let schema = new SimpleSchema({});
schema.extend(propertySchemasIndex[key]); schema.extend(propertySchemasIndex[key]);
schema.extend(CreaturePropertySchema); schema.extend(CreaturePropertySchema);
@@ -101,7 +101,7 @@ for (let key in propertySchemasIndex){
schema.extend(ChildSchema); schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema); schema.extend(SoftRemovableSchema);
CreatureProperties.attachSchema(schema, { CreatureProperties.attachSchema(schema, {
selector: {type: key} selector: { type: key }
}); });
} }

View File

@@ -20,33 +20,33 @@ const adjustQuantity = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, operation, value}) { run({ _id, operation, value }) {
// Permissions // Permissions
let property = CreatureProperties.findOne(_id); let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
// Do work // Do work
adjustQuantityWork({property, operation, value}); adjustQuantityWork({ property, operation, value });
}, },
}); });
export function adjustQuantityWork({property, operation, value}){ export function adjustQuantityWork({ property, operation, value }) {
// Check if property has quantity // Check if property has quantity
let schema = CreatureProperties.simpleSchema(property); let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('quantity')){ if (!schema.allowsKey('quantity')) {
throw new Meteor.Error( throw new Meteor.Error(
'Adjust quantity failed', 'Adjust quantity failed',
`Property of type "${property.type}" doesn't have a quantity` `Property of type "${property.type}" doesn't have a quantity`
); );
} }
if (operation === 'set'){ if (operation === 'set') {
CreatureProperties.update(property._id, { CreatureProperties.update(property._id, {
$set: {quantity: value, dirty: true} $set: { quantity: value, dirty: true }
}, { }, {
selector: property selector: property
}); });
} else if (operation === 'increment'){ } else if (operation === 'increment') {
// value here is 'damage' // value here is 'damage'
value = -value; value = -value;
let currentQuantity = property.quantity; let currentQuantity = property.quantity;

View File

@@ -36,7 +36,7 @@ const damageProperty = new ValidatedMethod({
// Check if property can take damage // Check if property can take damage
let schema = CreatureProperties.simpleSchema(prop); let schema = CreatureProperties.simpleSchema(prop);
if (!schema.allowsKey('damage')){ if (!schema.allowsKey('damage')) {
throw new Meteor.Error( throw new Meteor.Error(
'Damage property failed', 'Damage property failed',
`Property of type "${prop.type}" can't be damaged` `Property of type "${prop.type}" can't be damaged`
@@ -86,7 +86,7 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
} }
let damage, newValue, increment; let damage, newValue, increment;
if (operation === 'set'){ if (operation === 'set') {
const total = prop.total || 0; const total = prop.total || 0;
// Set represents what we want the value to be after damage // Set represents what we want the value to be after damage
// So we need the actual damage to get to that value // So we need the actual damage to get to that value
@@ -105,7 +105,7 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
// Also write it straight to the prop so that it is updated in the actionContext // Also write it straight to the prop so that it is updated in the actionContext
prop.damage = damage; prop.damage = damage;
prop.value = newValue; prop.value = newValue;
} else if (operation === 'increment'){ } else if (operation === 'increment') {
let currentValue = prop.value || 0; let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0; let currentDamage = prop.damage || 0;
increment = value; increment = value;

View File

@@ -10,7 +10,7 @@ import {
} from '/imports/api/parenting/parenting.js'; } from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js'; import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar; var snackbar;
if (Meteor.isClient){ if (Meteor.isClient) {
snackbar = require( snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js' '/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar ).snackbar
@@ -31,7 +31,7 @@ const duplicateProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}) { run({ _id }) {
let property = CreatureProperties.findOne(_id); let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property); let creature = getRootCreatureAncestor(property);
@@ -45,16 +45,16 @@ const duplicateProperty = new ValidatedMethod({
// Get all the descendants // Get all the descendants
let nodes = CreatureProperties.find({ let nodes = CreatureProperties.find({
'ancestors.id': _id, 'ancestors.id': _id,
removed: {$ne: true}, removed: { $ne: true },
}, { }, {
limit: DUPLICATE_CHILDREN_LIMIT + 1, limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1}, sort: { order: 1 },
}).fetch(); }).fetch();
// Alert the user if the limit was hit // Alert the user if the limit was hit
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){ if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
nodes.pop(); nodes.pop();
if (Meteor.isClient){ if (Meteor.isClient) {
snackbar({ snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`, text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
}); });
@@ -64,15 +64,15 @@ const duplicateProperty = new ValidatedMethod({
// re-map all the ancestors // re-map all the ancestors
setLineageOfDocs({ setLineageOfDocs({
docArray: nodes, docArray: nodes,
newAncestry : [ newAncestry: [
...property.ancestors, ...property.ancestors,
{id: propertyId, collection: 'creatureProperties'} { id: propertyId, collection: 'creatureProperties' }
], ],
oldParent : {id: _id, collection: 'creatureProperties'}, oldParent: { id: _id, collection: 'creatureProperties' },
}); });
// Give the docs new IDs without breaking internal references // Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes}); renewDocIds({ docArray: nodes });
// Order the root node // Order the root node
property.order += 0.5; property.order += 0.5;

View File

@@ -10,7 +10,7 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/
// Equipping or unequipping an item will also change its parent // Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({ const equipItem = new ValidatedMethod({
name: 'creatureProperties.equip', name: 'creatureProperties.equip',
validate({_id, equipped}){ validate({ _id, equipped }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required'); if (!_id) throw new Meteor.Error('No _id', '_id is required');
if (equipped !== true && equipped !== false) { if (equipped !== true && equipped !== false) {
throw new Meteor.Error('No equipped', 'equipped is required to be true or false'); throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
@@ -21,7 +21,7 @@ const equipItem = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, equipped}) { run({ _id, equipped }) {
let item = CreatureProperties.findOne(_id); let item = CreatureProperties.findOne(_id);
if (item.type !== 'item') throw new Meteor.Error('wrong type', if (item.type !== 'item') throw new Meteor.Error('wrong type',
'Equip and unequip can only be performed on items'); 'Equip and unequip can only be performed on items');
@@ -30,11 +30,11 @@ const equipItem = new ValidatedMethod({
CreatureProperties.update(_id, { CreatureProperties.update(_id, {
$set: { equipped, dirty: true }, $set: { equipped, dirty: true },
}, { }, {
selector: {type: 'item'}, selector: { type: 'item' },
}); });
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried; let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag); let parentRef = getParentRefByTag(creature._id, tag);
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'}; if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' };
organizeDoc.call({ organizeDoc.call({
docRef: { docRef: {

View File

@@ -6,7 +6,7 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
const flipToggle = new ValidatedMethod({ const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle', name: 'creatureProperties.flipToggle',
validate({_id}){ validate({ _id }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required'); if (!_id) throw new Meteor.Error('No _id', '_id is required');
}, },
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
@@ -14,16 +14,16 @@ const flipToggle = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}) { run({ _id }) {
// Permission // Permission
let property = CreatureProperties.findOne(_id, { let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1} fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 }
}); });
if (property.type !== 'toggle'){ if (property.type !== 'toggle') {
throw new Meteor.Error('wrong property', throw new Meteor.Error('wrong property',
'This method can only be applied to toggles'); 'This method can only be applied to toggles');
} }
if (!property.enabled && !property.disabled){ if (!property.enabled && !property.disabled) {
throw new Meteor.Error('Computed toggle', throw new Meteor.Error('Computed toggle',
'Can\'t flip a toggle that is computed') 'Can\'t flip a toggle that is computed')
} }
@@ -32,12 +32,14 @@ const flipToggle = new ValidatedMethod({
// Invert the current value, disabled is the canonical store of value // Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled; const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: { CreatureProperties.update(_id, {
$set: {
enabled: !currentValue, enabled: !currentValue,
disabled: currentValue, disabled: currentValue,
dirty: true, dirty: true,
}}, { }
selector: {type: 'toggle'}, }, {
selector: { type: 'toggle' },
}); });
}, },
}); });

View File

@@ -24,15 +24,15 @@ const insertProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({creatureProperty, parentRef}) { run({ creatureProperty, parentRef }) {
// get the new ancestry for the properties // get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef}); let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit // Check permission to edit
let rootCreature; let rootCreature;
if (parentRef.collection === 'creatures'){ if (parentRef.collection === 'creatures') {
rootCreature = parentDoc; rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){ } else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc); rootCreature = getRootCreatureAncestor(parentDoc);
} else { } else {
throw `${parentRef.collection} is not a valid parent collection` throw `${parentRef.collection} is not a valid parent collection`
@@ -75,23 +75,23 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({creatureProperty, creatureId, tag, tagDefaultName}) { run({ creatureProperty, creatureId, tag, tagDefaultName }) {
let parentRef = getParentRefByTag(creatureId, tag); let parentRef = getParentRefByTag(creatureId, tag);
if (!parentRef){ if (!parentRef) {
// Use the creature as the parent and mark that we need to insert the folder first later // Use the creature as the parent and mark that we need to insert the folder first later
var insertFolderFirst = true; var insertFolderFirst = true;
parentRef = {id: creatureId, collection: 'creatures'}; parentRef = { id: creatureId, collection: 'creatures' };
} }
// get the new ancestry for the properties // get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef}); let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit // Check permission to edit
let rootCreature; let rootCreature;
if (parentRef.collection === 'creatures'){ if (parentRef.collection === 'creatures') {
rootCreature = parentDoc; rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){ } else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc); rootCreature = getRootCreatureAncestor(parentDoc);
} else { } else {
throw `${parentRef.collection} is not a valid parent collection` throw `${parentRef.collection} is not a valid parent collection`
@@ -99,7 +99,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
// Add the folder first if we need to // Add the folder first if we need to
if (insertFolderFirst){ if (insertFolderFirst) {
let order = getHighestOrder({ let order = getHighestOrder({
collection: CreatureProperties, collection: CreatureProperties,
ancestorId: parentRef.id, ancestorId: parentRef.id,
@@ -113,7 +113,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
order, order,
}); });
// Make the folder our new parent // Make the folder our new parent
let newParentRef = {id, collection: 'creatureProperties'}; let newParentRef = { id, collection: 'creatureProperties' };
ancestors = [parentRef, newParentRef]; ancestors = [parentRef, newParentRef];
parentRef = newParentRef; parentRef = newParentRef;
creatureProperty.order = order + 1; creatureProperty.order = order + 1;
@@ -129,7 +129,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
}, },
}); });
export function insertPropertyWork({property, creature}){ export function insertPropertyWork({ property, creature }) {
delete property._id; delete property._id;
property.dirty = true; property.dirty = true;
let _id = CreatureProperties.insert(property); let _id = CreatureProperties.insert(property);

View File

@@ -39,15 +39,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({nodeIds, parentRef, order}) { run({ nodeIds, parentRef, order }) {
// get the new ancestry for the properties // get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef}); let { parentDoc, ancestors } = getAncestry({ parentRef });
// Check permission to edit // Check permission to edit
let rootCreature; let rootCreature;
if (parentRef.collection === 'creatures'){ if (parentRef.collection === 'creatures') {
rootCreature = parentDoc; rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){ } else if (parentRef.collection === 'creatureProperties') {
rootCreature = getRootCreatureAncestor(parentDoc); rootCreature = getRootCreatureAncestor(parentDoc);
} else { } else {
throw `${parentRef.collection} is not a valid parent collection` throw `${parentRef.collection} is not a valid parent collection`
@@ -75,13 +75,13 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}, },
}); });
function insertPropertyFromNode(nodeId, ancestors, order){ function insertPropertyFromNode(nodeId, ancestors, order) {
// Fetch the library node and its decendents, provided they have not been // Fetch the library node and its decendents, provided they have not been
// removed // removed
// TODO: Check permission to read the library this node is in // TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({ let node = LibraryNodes.findOne({
_id: nodeId, _id: nodeId,
removed: {$ne: true}, removed: { $ne: true },
}); });
if (!node) { if (!node) {
if (Meteor.isClient) return; if (Meteor.isClient) return;
@@ -95,7 +95,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
let oldParent = node.parent; let oldParent = node.parent;
let nodes = LibraryNodes.find({ let nodes = LibraryNodes.find({
'ancestors.id': nodeId, 'ancestors.id': nodeId,
removed: {$ne: true}, removed: { $ne: true },
}).fetch(); }).fetch();
// Convert all references into actual nodes // Convert all references into actual nodes
@@ -118,11 +118,11 @@ function insertPropertyFromNode(nodeId, ancestors, order){
// Give the docs new IDs without breaking internal references // Give the docs new IDs without breaking internal references
renewDocIds({ renewDocIds({
docArray: nodes, docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'} collectionMap: { 'libraryNodes': 'creatureProperties' }
}); });
// Order the root node // Order the root node
if (order === undefined){ if (order === undefined) {
setDocToLastOrder({ setDocToLastOrder({
collection: CreatureProperties, collection: CreatureProperties,
doc: node, doc: node,
@@ -139,7 +139,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
return node; return node;
} }
function storeLibraryNodeReferences(nodes){ function storeLibraryNodeReferences(nodes) {
nodes.forEach(node => { nodes.forEach(node => {
if (node.libraryNodeId) return; if (node.libraryNodeId) return;
node.libraryNodeId = node._id; node.libraryNodeId = node._id;
@@ -154,7 +154,7 @@ function dirtyNodes(nodes) {
// Covert node references into actual nodes // Covert node references into actual nodes
// TODO: check permissions for each library a reference node references // TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
depth += 1; depth += 1;
// New nodes added this function // New nodes added this function
let newNodes = []; let newNodes = [];
@@ -165,9 +165,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
if (node.type !== 'reference') return true; if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error // We have gone too deep, keep the reference node as an error
if (depth >= 10){ if (depth >= 10) {
if (Meteor.isClient) console.warn('Reference depth limit exceeded'); if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'}; node.cache = { error: 'Reference depth limit exceeded' };
return true; return true;
} }
@@ -177,17 +177,17 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
referencedNode.order = node.order; referencedNode.order = node.order;
// We are definitely replacing this node, so add it to the list // We are definitely replacing this node, so add it to the list
visitedRefs.add(node._id); visitedRefs.add(node._id);
} catch (e){ } catch (e) {
node.cache = {error: e.reason || e.message || e.toString()}; node.cache = { error: e.reason || e.message || e.toString() };
return true; return true;
} }
// Get all the descendants of the referenced node // Get all the descendants of the referenced node
let descendents = LibraryNodes.find({ let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id, 'ancestors.id': referencedNode._id,
removed: {$ne: true}, removed: { $ne: true },
}, { }, {
sort: {order: 1}, sort: { order: 1 },
}).fetch(); }).fetch();
// We are adding the referenced node and its descendants // We are adding the referenced node and its descendants
@@ -204,11 +204,11 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
// Filter all the looped references // Filter all the looped references
addedNodes = addedNodes.filter(addedNode => { addedNodes = addedNodes.filter(addedNode => {
// Add all non-reference nodes // Add all non-reference nodes
if (addedNode.type !== 'reference'){ if (addedNode.type !== 'reference') {
return true; return true;
} }
// If this exact reference has already been resolved before, filter it out // If this exact reference has already been resolved before, filter it out
if (visitedRefs.has(addedNode._id)){ if (visitedRefs.has(addedNode._id)) {
return false; return false;
} else { } else {
// Otherwise mark it as visited, and keep it // Otherwise mark it as visited, and keep it

View File

@@ -12,7 +12,7 @@ const pullFromProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, itemId}){ run({ _id, path, itemId }) {
// Permissions // Permissions
let property = CreatureProperties.findOne(_id); let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);
@@ -23,7 +23,7 @@ const pullFromProperty = new ValidatedMethod({
$pull: { [path.join('.')]: { _id: itemId } }, $pull: { [path.join('.')]: { _id: itemId } },
$set: { dirty: true } $set: { dirty: true }
}, { }, {
selector: {type: property.type}, selector: { type: property.type },
getAutoValues: false, getAutoValues: false,
}); });
} }

View File

@@ -13,7 +13,7 @@ const pushToProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, value}){ run({ _id, path, value }) {
// Permissions // Permissions
let property = CreatureProperties.findOne(_id); let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);
@@ -25,10 +25,10 @@ const pushToProperty = new ValidatedMethod({
let schema = CreatureProperties.simpleSchema(property); let schema = CreatureProperties.simpleSchema(property);
let maxCount = schema.get(joinedPath, 'maxCount'); let maxCount = schema.get(joinedPath, 'maxCount');
if (Number.isFinite(maxCount)){ if (Number.isFinite(maxCount)) {
let array = get(property, path); let array = get(property, path);
let currentCount = array ? array.length : 0; let currentCount = array ? array.length : 0;
if (currentCount >= maxCount){ if (currentCount >= maxCount) {
throw new Meteor.Error( throw new Meteor.Error(
'Array is full', 'Array is full',
`Cannot have more than ${maxCount} values` `Cannot have more than ${maxCount} values`
@@ -41,7 +41,7 @@ const pushToProperty = new ValidatedMethod({
$push: { [joinedPath]: value }, $push: { [joinedPath]: value },
$set: { dirty: true }, $set: { dirty: true },
}, { }, {
selector: {type: property.type}, selector: { type: property.type },
}); });
} }
}); });

View File

@@ -16,7 +16,7 @@ const restoreProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}){ run({ _id }) {
// Permissions // Permissions
let property = CreatureProperties.findOne(_id); let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);

View File

@@ -17,7 +17,7 @@ const selectAmmoItem = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({actionId, itemId, itemConsumedIndex}) { run({ actionId, itemId, itemConsumedIndex }) {
// Permissions // Permissions
let action = CreatureProperties.findOne(actionId); let action = CreatureProperties.findOne(actionId);
let rootCreature = getRootCreatureAncestor(action); let rootCreature = getRootCreatureAncestor(action);
@@ -25,12 +25,12 @@ const selectAmmoItem = new ValidatedMethod({
// Check that this index has a document to edit // Check that this index has a document to edit
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex]; let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
if (!itemConsumed){ if (!itemConsumed) {
throw new Meteor.Error('Resouce not found', throw new Meteor.Error('Resouce not found',
'Could not set ammo, because the ammo document was not found'); 'Could not set ammo, because the ammo document was not found');
} }
let itemToLink = CreatureProperties.findOne(itemId); let itemToLink = CreatureProperties.findOne(itemId);
if (!itemToLink){ if (!itemToLink) {
throw new Meteor.Error('Item not found', throw new Meteor.Error('Item not found',
'Could not set ammo: the item was not found'); 'Could not set ammo: the item was not found');
} }

View File

@@ -16,14 +16,14 @@ const softRemoveProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}){ run({ _id }) {
// Permissions // Permissions
let property = CreatureProperties.findOne(_id); let property = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
// Do work // Do work
softRemove({_id, collection: CreatureProperties}); softRemove({ _id, collection: CreatureProperties });
} }
}); });

View File

@@ -6,10 +6,10 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
const updateCreatureProperty = new ValidatedMethod({ const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update', name: 'creatureProperties.update',
validate({_id, path}){ validate({ _id, path }) {
if (!_id) throw new Meteor.Error('No _id', '_id is required'); if (!_id) throw new Meteor.Error('No _id', '_id is required');
// We cannot change these fields with a simple update // We cannot change these fields with a simple update
switch (path[0]){ switch (path[0]) {
case 'type': case 'type':
case 'order': case 'order':
case 'parent': case 'parent':
@@ -24,10 +24,10 @@ const updateCreatureProperty = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, value}) { run({ _id, path, value }) {
// Permission // Permission
let property = CreatureProperties.findOne(_id, { let property = CreatureProperties.findOne(_id, {
fields: {type: 1, ancestors: 1} fields: { type: 1, ancestors: 1 }
}); });
let rootCreature = getRootCreatureAncestor(property); let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
@@ -35,13 +35,13 @@ const updateCreatureProperty = new ValidatedMethod({
let pathString = path.join('.'); let pathString = path.join('.');
let modifier; let modifier;
// unset empty values // unset empty values
if (value === null || value === undefined){ if (value === null || value === undefined) {
modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } }; modifier = { $unset: { [pathString]: 1 }, $set: { dirty: true } };
} else { } else {
modifier = { $set: {[pathString]: value, dirty: true } }; modifier = { $set: { [pathString]: value, dirty: true } };
} }
CreatureProperties.update(_id, modifier, { CreatureProperties.update(_id, modifier, {
selector: {type: property.type}, selector: { type: property.type },
}); });
}, },
}); });

View File

@@ -161,7 +161,7 @@ let CreatureSchema = new SimpleSchema({
'computeErrors.$.type': { 'computeErrors.$.type': {
type: String, type: String,
}, },
'computeErrors.$.details' : { 'computeErrors.$.details': {
type: Object, type: Object,
blackbox: true, blackbox: true,
optional: true, optional: true,

View File

@@ -1,7 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
@@ -36,7 +36,7 @@ const changeAllowedLibraries = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, allowedLibraries, allowedLibraryCollections}) { run({ _id, allowedLibraries, allowedLibraryCollections }) {
let creature = Creatures.findOne(_id); let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
let $set; let $set;
@@ -48,7 +48,7 @@ const changeAllowedLibraries = new ValidatedMethod({
$set.allowedLibraryCollections = allowedLibraryCollections; $set.allowedLibraryCollections = allowedLibraryCollections;
} }
if (!$set) return; if (!$set) return;
Creatures.update(_id, {$set}); Creatures.update(_id, { $set });
}, },
}); });
@@ -68,7 +68,7 @@ const toggleAllUserLibraries = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, value}) { run({ _id, value }) {
if (value) { if (value) {
Creatures.update(_id, { Creatures.update(_id, {
$unset: { $unset: {
@@ -87,4 +87,4 @@ const toggleAllUserLibraries = new ValidatedMethod({
}, },
}); });
export {changeAllowedLibraries, toggleAllUserLibraries}; export { changeAllowedLibraries, toggleAllUserLibraries };

View File

@@ -70,7 +70,7 @@ const insertCreature = new ValidatedMethod({
let baseId, rulesetSlot; let baseId, rulesetSlot;
defaultCharacterProperties(creatureId).forEach(prop => { defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop); let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){ if (prop.name === 'Ruleset') {
baseId = id; baseId = id;
rulesetSlot = prop; rulesetSlot = prop;
} }
@@ -95,7 +95,7 @@ function insertDefaultRuleset(creatureId, baseId, userId, slot) {
const ruleset = fillCursor.fetch()[0] const ruleset = fillCursor.fetch()[0]
insertPropertyFromLibraryNode.call({ insertPropertyFromLibraryNode.call({
nodeIds: [ruleset._id], nodeIds: [ruleset._id],
parentRef: {id: baseId, collection: 'creatureProperties'}, parentRef: { id: baseId, collection: 'creatureProperties' },
order: 0.5, order: 0.5,
}); });
} }

View File

@@ -1,11 +1,11 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js'; import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
const updateCreature = new ValidatedMethod({ const updateCreature = new ValidatedMethod({
name: 'creatures.update', name: 'creatures.update',
validate({_id, path}){ validate({ _id, path }) {
if (!_id) return false; if (!_id) return false;
// Allowed fields // Allowed fields
let allowedFields = [ let allowedFields = [
@@ -17,7 +17,7 @@ const updateCreature = new ValidatedMethod({
'color', 'color',
'settings', 'settings',
]; ];
if (!allowedFields.includes(path[0])){ if (!allowedFields.includes(path[0])) {
throw new Meteor.Error('Creatures.methods.update.denied', throw new Meteor.Error('Creatures.methods.update.denied',
'This field can\'t be updated using this method'); 'This field can\'t be updated using this method');
} }
@@ -27,16 +27,16 @@ const updateCreature = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, value}) { run({ _id, path, value }) {
let creature = Creatures.findOne(_id); let creature = Creatures.findOne(_id);
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
if (value === undefined || value === null){ if (value === undefined || value === null) {
Creatures.update(_id, { Creatures.update(_id, {
$unset: {[path.join('.')]: 1}, $unset: { [path.join('.')]: 1 },
}); });
} else { } else {
Creatures.update(_id, { Creatures.update(_id, {
$set: {[path.join('.')]: value}, $set: { [path.join('.')]: value },
}); });
} }
}, },

View File

@@ -29,7 +29,7 @@ let ExperienceSchema = new SimpleSchema({
// The real-world date that it occured, usually sorted by date // The real-world date that it occured, usually sorted by date
date: { date: {
type: Date, type: Date,
autoValue: function() { autoValue: function () {
// If the date isn't set, set it to now // If the date isn't set, set it to now
if (!this.isSet) { if (!this.isSet) {
return new Date(); return new Date();
@@ -46,8 +46,8 @@ let ExperienceSchema = new SimpleSchema({
Experiences.attachSchema(ExperienceSchema); Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){ const insertExperienceForCreature = function ({ experience, creatureId }) {
if (experience.xp){ if (experience.xp) {
Creatures.update(creatureId, { Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': experience.xp }, $inc: { 'denormalizedStats.xp': experience.xp },
$set: { dirty: true }, $set: { dirty: true },
@@ -84,7 +84,7 @@ const insertExperience = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({experience, creatureIds}) { run({ experience, creatureIds }) {
let userId = this.userId; let userId = this.userId;
if (!userId) { if (!userId) {
throw new Meteor.Error('Experiences.methods.insert.denied', throw new Meteor.Error('Experiences.methods.insert.denied',
@@ -93,7 +93,7 @@ const insertExperience = new ValidatedMethod({
let insertedIds = []; let insertedIds = [];
creatureIds.forEach(creatureId => { creatureIds.forEach(creatureId => {
assertEditPermission(creatureId, userId); assertEditPermission(creatureId, userId);
let id = insertExperienceForCreature({experience, creatureId, userId}); let id = insertExperienceForCreature({ experience, creatureId, userId });
insertedIds.push(id); insertedIds.push(id);
}); });
return insertedIds; return insertedIds;
@@ -113,7 +113,7 @@ const removeExperience = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({experienceId}) { run({ experienceId }) {
let userId = this.userId; let userId = this.userId;
if (!userId) { if (!userId) {
throw new Meteor.Error('Experiences.methods.remove.denied', throw new Meteor.Error('Experiences.methods.remove.denied',
@@ -123,7 +123,7 @@ const removeExperience = new ValidatedMethod({
if (!experience) return; if (!experience) return;
let creatureId = experience.creatureId let creatureId = experience.creatureId
assertEditPermission(creatureId, userId); assertEditPermission(creatureId, userId);
if (experience.xp){ if (experience.xp) {
Creatures.update(creatureId, { Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': -experience.xp }, $inc: { 'denormalizedStats.xp': -experience.xp },
$set: { dirty: true }, $set: { dirty: true },
@@ -154,7 +154,7 @@ const recomputeExperiences = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({creatureId}) { run({ creatureId }) {
let userId = this.userId; let userId = this.userId;
if (!userId) { if (!userId) {
throw new Meteor.Error('Experiences.methods.recompute.denied', throw new Meteor.Error('Experiences.methods.recompute.denied',
@@ -167,16 +167,18 @@ const recomputeExperiences = new ValidatedMethod({
Experiences.find({ Experiences.find({
creatureId creatureId
}, { }, {
fields: {xp: 1, levels: 1} fields: { xp: 1, levels: 1 }
}).forEach(experience => { }).forEach(experience => {
xp += experience.xp || 0; xp += experience.xp || 0;
milestoneLevels += experience.levels || 0; milestoneLevels += experience.levels || 0;
}); });
Creatures.update(creatureId, {$set: { Creatures.update(creatureId, {
$set: {
'denormalizedStats.xp': xp, 'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels, 'denormalizedStats.milestoneLevels': milestoneLevels,
dirty: true, dirty: true,
}}); }
});
}, },
}); });

View File

@@ -16,7 +16,7 @@ let ExperienceSchema = new SimpleSchema({
// The real-world date that it occured // The real-world date that it occured
date: { date: {
type: Date, type: Date,
autoValue: function() { autoValue: function () {
// If the date isn't set, set it to now // If the date isn't set, set it to now
if (!this.isSet) { if (!this.isSet) {
return new Date(); return new Date();

View File

@@ -4,13 +4,13 @@ import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js'; import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import {parse, prettifyParseError} from '/imports/parser/parser.js'; import { parse, prettifyParseError } from '/imports/parser/parser.js';
import resolve, { toString } from '/imports/parser/resolve.js'; import resolve, { toString } from '/imports/parser/resolve.js';
const PER_CREATURE_LOG_LIMIT = 100; const PER_CREATURE_LOG_LIMIT = 100;
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
if (Meteor.isServer){ if (Meteor.isServer) {
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature; var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
} }
@@ -28,7 +28,7 @@ let CreatureLogSchema = new SimpleSchema({
// The real-world date that it occured, usually sorted by date // The real-world date that it occured, usually sorted by date
date: { date: {
type: Date, type: Date,
autoValue: function() { autoValue: function () {
// If the date isn't set, set it to now // If the date isn't set, set it to now
if (!this.isSet) { if (!this.isSet) {
return new Date(); return new Date();
@@ -50,23 +50,23 @@ let CreatureLogSchema = new SimpleSchema({
CreatureLogs.attachSchema(CreatureLogSchema); CreatureLogs.attachSchema(CreatureLogSchema);
function removeOldLogs(creatureId){ function removeOldLogs(creatureId) {
// Find the first log that is over the limit // Find the first log that is over the limit
let firstExpiredLog = CreatureLogs.find({ let firstExpiredLog = CreatureLogs.find({
creatureId creatureId
}, { }, {
sort: {date: -1}, sort: { date: -1 },
skip: PER_CREATURE_LOG_LIMIT, skip: PER_CREATURE_LOG_LIMIT,
}); });
if (!firstExpiredLog) return; if (!firstExpiredLog) return;
// Remove all logs older than the one over the limit // Remove all logs older than the one over the limit
CreatureLogs.remove({ CreatureLogs.remove({
creatureId, creatureId,
date: {$lte: firstExpiredLog.date}, date: { $lte: firstExpiredLog.date },
}); });
} }
function logToMessageData(log){ function logToMessageData(log) {
let embed = { let embed = {
fields: [], fields: [],
}; };
@@ -78,8 +78,8 @@ function logToMessageData(log){
return { embeds: [embed] }; return { embeds: [embed] };
} }
function logWebhook({log, creature}){ function logWebhook({ log, creature }) {
if (Meteor.isServer){ if (Meteor.isServer) {
sendWebhookAsCreature({ sendWebhookAsCreature({
creature, creature,
data: logToMessageData(log), data: logToMessageData(log),
@@ -97,44 +97,46 @@ const insertCreatureLog = new ValidatedMethod({
validate: new SimpleSchema({ validate: new SimpleSchema({
log: CreatureLogSchema.omit('date'), log: CreatureLogSchema.omit('date'),
}).validator(), }).validator(),
run({log}){ run({ log }) {
const creatureId = log.creatureId; const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: { const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1, readers: 1,
writers: 1, writers: 1,
owner: 1, owner: 1,
'settings.discordWebhook': 1, 'settings.discordWebhook': 1,
name: 1, name: 1,
avatarPicture: 1, avatarPicture: 1,
}}); }
});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
// Build the new log // Build the new log
let id = insertCreatureLogWork({log, creature, method: this}) let id = insertCreatureLogWork({ log, creature, method: this })
return id; return id;
}, },
}); });
export function insertCreatureLogWork({log, creature, method}){ export function insertCreatureLogWork({ log, creature, method }) {
// Build the new log // Build the new log
if (typeof log === 'string'){ if (typeof log === 'string') {
log = {content: [{value: log}]}; log = { content: [{ value: log }] };
} }
if (!log.content?.length) return; if (!log.content?.length) return;
log.date = new Date(); log.date = new Date();
// Insert it // Insert it
let id = CreatureLogs.insert(log); let id = CreatureLogs.insert(log);
if (Meteor.isServer){ if (Meteor.isServer) {
method?.unblock(); method?.unblock();
removeOldLogs(creature._id); removeOldLogs(creature._id);
logWebhook({log, creature}); logWebhook({ log, creature });
} }
return id; return id;
} }
function equalIgnoringWhitespace(a, b){ function equalIgnoringWhitespace(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') return a === b; if (typeof a !== 'string' || typeof b !== 'string') return a === b;
return a.replace(/\s/g,'') === b.replace(/\s/g, ''); return a.replace(/\s/g, '') === b.replace(/\s/g, '');
} }
const logRoll = new ValidatedMethod({ const logRoll = new ValidatedMethod({
@@ -153,24 +155,26 @@ const logRoll = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
}).validator(), }).validator(),
run({roll, creatureId}){ run({ roll, creatureId }) {
const creature = Creatures.findOne(creatureId, {fields: { const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1, readers: 1,
writers: 1, writers: 1,
owner: 1, owner: 1,
'settings.discordWebhook': 1, 'settings.discordWebhook': 1,
name: 1, name: 1,
avatarPicture: 1, avatarPicture: 1,
}}); }
});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
const variables = CreatureVariables.findOne({ _creatureId: creatureId }); const variables = CreatureVariables.findOne({ _creatureId: creatureId });
let logContent = [] let logContent = []
let parsedResult = undefined; let parsedResult = undefined;
try { try {
parsedResult = parse(roll); parsedResult = parse(roll);
} catch (e){ } catch (e) {
let error = prettifyParseError(e); let error = prettifyParseError(e);
logContent.push({name: 'Parse Error', value: error}); logContent.push({ name: 'Parse Error', value: error });
} }
if (parsedResult) try { if (parsedResult) try {
let { let {
@@ -184,19 +188,19 @@ const logRoll = new ValidatedMethod({
logContent.push({ logContent.push({
value: compiledString value: compiledString
}); });
let {result: rolled} = resolve('roll', compiled, variables, context); let { result: rolled } = resolve('roll', compiled, variables, context);
let rolledString = toString(rolled); let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({ if (rolledString !== compiledString) logContent.push({
value: rolledString value: rolledString
}); });
let {result} = resolve('reduce', rolled, variables, context); let { result } = resolve('reduce', rolled, variables, context);
let resultString = toString(result); let resultString = toString(result);
if (resultString !== rolledString) logContent.push({ if (resultString !== rolledString) logContent.push({
value: resultString value: resultString
}); });
} catch (e){ } catch (e) {
console.error(e); console.error(e);
logContent = [{name: 'Calculation error'}]; logContent = [{ name: 'Calculation error' }];
} }
const log = { const log = {
content: logContent, content: logContent,
@@ -204,11 +208,11 @@ const logRoll = new ValidatedMethod({
date: new Date(), date: new Date(),
}; };
let id = insertCreatureLogWork({log, creature, method: this}); let id = insertCreatureLogWork({ log, creature, method: this });
return id; return id;
}, },
}); });
export default CreatureLogs; export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT}; export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT };

View File

@@ -0,0 +1,3 @@
if (Meteor.isServer) throw 'Client side only collection, don\'t import on server';
const Docs = new Mongo.Collection('docs');
export default Docs;

View File

@@ -4,6 +4,7 @@ import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js'; import buff from './applyPropertyByType/applyBuff.js';
import buffRemover from './applyPropertyByType/applyBuffRemover.js'; import buffRemover from './applyPropertyByType/applyBuffRemover.js';
import damage from './applyPropertyByType/applyDamage.js'; import damage from './applyPropertyByType/applyDamage.js';
import folder from './applyPropertyByType/applyFolder.js';
import note from './applyPropertyByType/applyNote.js'; import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js'; import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js'; import savingThrow from './applyPropertyByType/applySavingThrow.js';
@@ -16,6 +17,7 @@ const applyPropertyByType = {
buff, buff,
buffRemover, buffRemover,
damage, damage,
folder,
note, note,
roll, roll,
savingThrow, savingThrow,

View File

@@ -2,7 +2,7 @@ import {
setLineageOfDocs, setLineageOfDocs,
renewDocIds renewDocIds
} from '/imports/api/parenting/parenting.js'; } from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js'; import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
@@ -15,14 +15,14 @@ import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
export default function applyBuff(node, actionContext){ export default function applyBuff(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
const prop = node.node; const prop = node.node;
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets; let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
// Then copy the decendants of the buff to the targets // Then copy the decendants of the buff to the targets
let propList = [prop]; let propList = [prop];
function addChildrenToPropList(children, { skipCrystalize } = {}){ function addChildrenToPropList(children, { skipCrystalize } = {}) {
children.forEach(child => { children.forEach(child => {
if (skipCrystalize) child.node._skipCrystalize = true; if (skipCrystalize) child.node._skipCrystalize = true;
propList.push(child.node); propList.push(child.node);
@@ -34,7 +34,7 @@ export default function applyBuff(node, actionContext){
} }
addChildrenToPropList(node.children); addChildrenToPropList(node.children);
if (!prop.skipCrystalization) { if (!prop.skipCrystalization) {
crystalizeVariables({propList, actionContext}); crystalizeVariables({ propList, actionContext });
} }
let oldParent = { let oldParent = {
@@ -46,8 +46,8 @@ export default function applyBuff(node, actionContext){
copyNodeListToTarget(propList, target, oldParent); copyNodeListToTarget(propList, target, oldParent);
//Log the buff //Log the buff
if ((prop.name || prop.description?.value) && !prop.silent){ if ((prop.name || prop.description?.value) && !prop.silent) {
if (target._id === actionContext.creature._id){ if (target._id === actionContext.creature._id) {
// Targeting self // Targeting self
actionContext.addLog({ actionContext.addLog({
name: prop.name, name: prop.name,
@@ -72,8 +72,8 @@ export default function applyBuff(node, actionContext){
// Don't apply the children of the buff, they get copied to the target instead // Don't apply the children of the buff, they get copied to the target instead
} }
function copyNodeListToTarget(propList, target, oldParent){ function copyNodeListToTarget(propList, target, oldParent) {
let ancestry = [{collection: 'creatures', id: target._id}]; let ancestry = [{ collection: 'creatures', id: target._id }];
setLineageOfDocs({ setLineageOfDocs({
docArray: propList, docArray: propList,
newAncestry: ancestry, newAncestry: ancestry,
@@ -93,14 +93,14 @@ function copyNodeListToTarget(propList, target, oldParent){
* Replaces all variables with their resolved values * Replaces all variables with their resolved values
* except variables of the form `$target.thing.total` become `thing.total` * except variables of the form `$target.thing.total` become `thing.total`
*/ */
function crystalizeVariables({propList, actionContext}){ function crystalizeVariables({ propList, actionContext }) {
propList.forEach(prop => { propList.forEach(prop => {
if (prop._skipCrystalize) { if (prop._skipCrystalize) {
delete prop._skipCrystalize; delete prop._skipCrystalize;
return; return;
} }
// Iterate through all the calculations and crystalize them // Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach( calcKey => { computedSchemas[prop.type].computedFields().forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key); const calcObj = get(prop, key);
if (!calcObj?.parseNode) return; if (!calcObj?.parseNode) return;
@@ -110,12 +110,12 @@ function crystalizeVariables({propList, actionContext}){
node.parseType !== 'accessor' && node.parseType !== 'symbol' node.parseType !== 'accessor' && node.parseType !== 'symbol'
) return node; ) return node;
// Handle variables // Handle variables
if (node.name === '$target'){ if (node.name === '$target') {
// strip $target // strip $target
if (node.parseType === 'accessor'){ if (node.parseType === 'accessor') {
node.name = node.path.shift(); node.name = node.path.shift();
if (!node.path.length){ if (!node.path.length) {
return symbol.create({name: node.name}) return symbol.create({ name: node.name })
} }
} else { } else {
// Can't strip symbols // Can't strip symbols
@@ -127,7 +127,7 @@ function crystalizeVariables({propList, actionContext}){
return node; return node;
} else { } else {
// Resolve all other variables // Resolve all other variables
const {result, context} = resolve('reduce', node, actionContext.scope); const { result, context } = resolve('reduce', node, actionContext.scope);
logErrors(context.errors, actionContext); logErrors(context.errors, actionContext);
return result; return result;
} }
@@ -137,14 +137,14 @@ function crystalizeVariables({propList, actionContext}){
}); });
}); });
// For each key in the schema // For each key in the schema
computedSchemas[prop.type].inlineCalculationFields().forEach( calcKey => { computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
// That ends in .inlineCalculations // That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key); const inlineCalcObj = get(prop, key);
if (!inlineCalcObj) return; if (!inlineCalcObj) return;
// If there is no text, skip // If there is no text, skip
if (!inlineCalcObj.text){ if (!inlineCalcObj.text) {
return; return;
} }

View File

@@ -1,4 +1,4 @@
import { some, intersection, difference, remove } 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';
@@ -147,21 +147,21 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
if ( if (
multiplier.immunity && multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp)) 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)) 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)) 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,8 +170,11 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage; return damage;
} }
function multiplierAppliesTo(damageProp){ function multiplierAppliesTo(damageProp, multiplierType){
return multiplier => { return multiplier => {
// Apply the default 'ignore x' tags
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
const hasRequiredTags = difference( const hasRequiredTags = difference(
multiplier.includeTags, damageProp.tags multiplier.includeTags, damageProp.tags
).length === 0; ).length === 0;

View File

@@ -0,0 +1,11 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyFolder(node, actionContext) {
// Apply triggers
applyNodeTriggers(node, 'before', actionContext);
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -5,7 +5,7 @@ import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, actionContext, context){ export default function recalculateCalculation(calc, actionContext, context){
if (!calc?.parseNode) return; if (!calc?.parseNode) return;
calc._parseLevel = 'reduce'; calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, actionContext.log); applyEffectsToCalculationParseNode(calc, actionContext);
evaluateCalculation(calc, actionContext.scope, context); evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext.log); logErrors(calc.errors, actionContext);
} }

View File

@@ -56,13 +56,13 @@ const doAction = new ValidatedMethod({
properties.sort((a, b) => a.order - b.order); properties.sort((a, b) => a.order - b.order);
// Do the action // Do the action
doActionWork({properties, ancestors, actionContext, methodScope: scope}); doActionWork({ properties, ancestors, actionContext, methodScope: scope });
// Recompute all involved creatures // Recompute all involved creatures
Creatures.update({ Creatures.update({
_id: { $in: [creatureId, ...targetIds] } _id: { $in: [creatureId, ...targetIds] }
}, { }, {
$set: {dirty: true}, $set: { dirty: true },
}); });
}, },
}); });
@@ -71,11 +71,11 @@ export default doAction;
export function doActionWork({ export function doActionWork({
properties, ancestors, actionContext, methodScope = {}, properties, ancestors, actionContext, methodScope = {},
}){ }) {
// get the docs // get the docs
const ancestorScope = getAncestorScope(ancestors); const ancestorScope = getAncestorScope(ancestors);
const propertyForest = nodeArrayToTree(properties); const propertyForest = nodeArrayToTree(properties);
if (propertyForest.length !== 1){ if (propertyForest.length !== 1) {
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`); throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
} }
@@ -91,7 +91,7 @@ export function doActionWork({
} }
// Assumes ancestors are in tree order already // Assumes ancestors are in tree order already
function getAncestorScope(ancestors){ function getAncestorScope(ancestors) {
let scope = {}; let scope = {};
ancestors.forEach(prop => { ancestors.forEach(prop => {
scope[`#${prop.type}`] = prop; scope[`#${prop.type}`] = prop;

View File

@@ -20,6 +20,10 @@ const doAction = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
optional: true, optional: true,
}, },
ritual: {
type: Boolean,
optional: true,
},
targetIds: { targetIds: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
@@ -41,7 +45,7 @@ const doAction = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({ spellId, slotId, targetIds = [], scope = {} }) { run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
// Get action context // Get action context
let spell = CreatureProperties.findOne(spellId); let spell = CreatureProperties.findOne(spellId);
const creatureId = spell.ancestors[0].id; const creatureId = spell.ancestors[0].id;
@@ -64,25 +68,26 @@ const doAction = new ValidatedMethod({
let slotLevel = spell.level || 0; let slotLevel = spell.level || 0;
let slot; let slot;
if (slotId && !spell.castWithoutSpellSlots){ // If a spell requires a slot, make sure a slot is spent
if (!spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
slot = CreatureProperties.findOne(slotId); slot = CreatureProperties.findOne(slotId);
if (!slot){ if (!slot) {
throw new Meteor.Error('No slot', throw new Meteor.Error('No slot',
'Slot not found to cast spell'); 'Slot not found to cast spell');
} }
if (!slot.value){ if (!slot.value) {
throw new Meteor.Error('No slot', throw new Meteor.Error('No slot',
'Slot depleted'); 'Slot depleted');
} }
if (slot.attributeType !== 'spellSlot'){ if (slot.attributeType !== 'spellSlot') {
throw new Meteor.Error('Not a slot', throw new Meteor.Error('Not a slot',
'The given property is not a valid spell slot'); 'The given property is not a valid spell slot');
} }
if (!slot.spellSlotLevel?.value){ if (!slot.spellSlotLevel?.value) {
throw new Meteor.Error('No slot level', throw new Meteor.Error('No slot level',
'Slot does not have a spell slot level'); 'Slot does not have a spell slot level');
} }
if (slot.spellSlotLevel.value < spell.level){ if (slot.spellSlotLevel.value < spell.level) {
throw new Meteor.Error('Slot too small', throw new Meteor.Error('Slot too small',
'Slot is not large enough to cast spell'); 'Slot is not large enough to cast spell');
} }
@@ -96,15 +101,21 @@ const doAction = new ValidatedMethod({
} }
// Post the slot level spent to the log // Post the slot level spent to the log
if (slot?.spellSlotLevel?.value){ if (slot?.spellSlotLevel?.value) {
actionContext.addLog({ actionContext.addLog({
name: `Casting using a level ${slotLevel} spell slot` name: `Casting using a level ${slotLevel} spell slot`
}); });
} else if (slotLevel) { } else if (slotLevel) {
if (ritual) {
actionContext.addLog({
name: `Ritual casting at level ${slotLevel}`
});
} else {
actionContext.addLog({ actionContext.addLog({
name: `Casting at level ${slotLevel}` name: `Casting at level ${slotLevel}`
}); });
} }
}
actionContext.scope['slotLevel'] = slotLevel; actionContext.scope['slotLevel'] = slotLevel;

View File

@@ -7,6 +7,7 @@ import rollDice from '/imports/parser/rollDice.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
const doCheck = new ValidatedMethod({ const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck', name: 'creatureProperties.doCheck',
@@ -72,7 +73,11 @@ function rollCheck(prop, actionContext) {
throw (`${prop.type} not supported for checks`); throw (`${prop.type} not supported for checks`);
} }
const rollModifierText = numberToSignedString(rollModifier, true); let rollModifierText = numberToSignedString(rollModifier, true);
const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix; let value, values, resultPrefix;
if (scope['$checkAdvantage'] === 1){ if (scope['$checkAdvantage'] === 1){
@@ -106,3 +111,21 @@ function rollCheck(prop, actionContext) {
value: `${resultPrefix} **${result}**`, value: `${resultPrefix} **${result}**`,
}); });
} }
function applyUnresolvedEffects(prop, scope) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {
return { effectBonus, effectString};
}
prop.effects.forEach(effect => {
if (!effect.amount?.parseNode) return;
if (effect.operation !== 'add') return;
effect.amount._parseLevel = 'reduce';
evaluateCalculation(effect.amount, scope);
if (typeof effect.amount?.value !== 'number') return;
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
});
return { effectBonus, effectString};
}

View File

@@ -1,9 +1,24 @@
import { EJSON } from 'meteor/ejson'; import { EJSON } from 'meteor/ejson';
import createGraph from 'ngraph.graph'; import createGraph, { Graph } from 'ngraph.graph';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
interface CreatureProperty {
_id: string;
type: string;
}
export default class CreatureComputation { export default class CreatureComputation {
constructor(properties, creature, variables){ originalPropsById: object;
propsById: object;
propsWithTag: object;
scope: object;
props: Array<CreatureProperty>;
dependencyGraph: Graph;
errors: Array<object>;
creature: object;
variables: object;
constructor(properties: Array<CreatureProperty>, creature: object, variables: object) {
// Set up fields // Set up fields
this.originalPropsById = {}; this.originalPropsById = {};
this.propsById = {}; this.propsById = {};

View File

@@ -29,8 +29,8 @@ function childrenActive(prop){
// Children of disabled properties are always inactive // Children of disabled properties are always inactive
if (prop.disabled) return false; if (prop.disabled) return false;
switch (prop.type){ switch (prop.type){
// Only equipped items have active children // Only equipped items with non-zero quantity have active children
case 'item': return !!prop.equipped; case 'item': return !!prop.equipped && prop.quantity !== 0;
// The children of actions, spells, and triggers are always inactive // The children of actions, spells, and triggers are always inactive
case 'action': return false; case 'action': return false;
case 'spell': return false; case 'spell': return false;

View File

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

View File

@@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
import CreatureComputation from './CreatureComputation.js'; import CreatureComputation from './CreatureComputation.ts';
import removeSchemaFields from './buildComputation/removeSchemaFields.js'; import removeSchemaFields from './buildComputation/removeSchemaFields.js';
/** /**

View File

@@ -1,3 +1,5 @@
import { pick } from 'lodash';
export default function aggregateEffect({node, linkedNode, link}){ export default function aggregateEffect({node, linkedNode, link}){
if (link.data !== 'effect') return; if (link.data !== 'effect') return;
// store the effect aggregator, its presence indicates that the variable is // store the effect aggregator, its presence indicates that the variable is
@@ -19,11 +21,22 @@ export default function aggregateEffect({node, linkedNode, link}){
// Store a summary of the effect itself // Store a summary of the effect itself
node.data.effects = node.data.effects || []; node.data.effects = node.data.effects || [];
// Store either just
let effectAmount;
if (!linkedNode.data.amount) {
effectAmount = undefined;
} else if (typeof linkedNode.data.amount.value === 'string') {
effectAmount = pick(linkedNode.data.amount, [
'calculation', 'parseNode', 'parseError', 'value'
]);
} else {
effectAmount = pick(linkedNode.data.amount, ['value']);
}
node.data.effects.push({ node.data.effects.push({
_id: linkedNode.data._id, _id: linkedNode.data._id,
name: linkedNode.data.name, name: linkedNode.data.name,
operation: linkedNode.data.operation, operation: linkedNode.data.operation,
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value}, amount: effectAmount,
type: linkedNode.data.type, type: linkedNode.data.type,
// ancestors: linkedNode.data.ancestors, // ancestors: linkedNode.data.ancestors,
}); });
@@ -33,7 +46,7 @@ export default function aggregateEffect({node, linkedNode, link}){
// Get the result of the effect // Get the result of the effect
const result = linkedNode.data.amount?.value; const result = linkedNode.data.amount?.value;
// Skip aggregating if the result is not resolved completely // Skip aggregating if the result is not resolved completely
if (typeof result === 'string') return; if (typeof result === 'string' || result === undefined) return;
// Aggregate the effect based on its operation // Aggregate the effect based on its operation
switch(linkedNode.data.operation){ switch(linkedNode.data.operation){
case 'base': case 'base':

View File

@@ -4,7 +4,7 @@ export default function evaluateToggles(computation, node){
let toggles = prop._computationDetails?.toggleAncestors; let toggles = prop._computationDetails?.toggleAncestors;
if (!toggles) return; if (!toggles) return;
toggles.forEach(toggle => { toggles.forEach(toggle => {
if (prop.inactive || !toggle.condition) return; if (!toggle.condition) return;
if (!toggle.condition.value){ if (!toggle.condition.value){
prop.inactive = true; prop.inactive = true;
prop.deactivatedByToggle = true; prop.deactivatedByToggle = true;

View File

@@ -12,6 +12,7 @@ export default function getEffectivePropTags(prop) {
if (prop.variableName) tags.push(prop.variableName); if (prop.variableName) tags.push(prop.variableName);
if (prop.damageType) tags.push(prop.damageType); if (prop.damageType) tags.push(prop.damageType);
if (prop.skillType) tags.push(prop.skillType); if (prop.skillType) tags.push(prop.skillType);
if (prop.actionType) tags.push(prop.actionType);
if (prop.attributeType) tags.push(prop.attributeType); if (prop.attributeType) tags.push(prop.attributeType);
if (prop.reset) tags.push(prop.reset); if (prop.reset) tags.push(prop.reset);
return tags; return tags;

View File

@@ -57,11 +57,11 @@ Icons.attachSchema(iconsSchema);
const writeIcons = new ValidatedMethod({ const writeIcons = new ValidatedMethod({
name: 'icons.write', name: 'icons.write',
validate: null, validate: null,
run(icons){ run(icons) {
assertAdmin(this.userId); assertAdmin(this.userId);
if (Meteor.isServer){ if (Meteor.isServer) {
this.unblock(); this.unblock();
Icons.rawCollection().insert(icons, {ordered: false}); Icons.rawCollection().insert(icons, { ordered: false });
} }
} }
}); });
@@ -80,11 +80,11 @@ const findIcons = new ValidatedMethod({
numRequests: 20, numRequests: 20,
timeInterval: 10000, timeInterval: 10000,
}, },
run({search}){ run({ search }) {
if (!search) return []; if (!search) return [];
if (!Meteor.isServer) return; if (!Meteor.isServer) return;
return Icons.find( return Icons.find(
{ $text: {$search: search} }, { $text: { $search: search } },
{ {
// relevant documents have a higher score. // relevant documents have a higher score.
fields: { fields: {

View File

@@ -49,7 +49,7 @@ const insertLibrary = new ValidatedMethod({
'You need to be logged in to insert a library'); 'You need to be logged in to insert a library');
} }
let tier = getUserTier(this.userId); let tier = getUserTier(this.userId);
if (!tier.paidBenefits){ if (!tier.paidBenefits) {
throw new Meteor.Error('Libraries.methods.insert.denied', throw new Meteor.Error('Libraries.methods.insert.denied',
`The ${tier.name} tier does not allow you to insert a library`); `The ${tier.name} tier does not allow you to insert a library`);
} }
@@ -74,10 +74,10 @@ const updateLibraryName = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, name}){ run({ _id, name }) {
let library = Libraries.findOne(_id); let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId); assertEditPermission(library, this.userId);
Libraries.update(_id, {$set: {name}}); Libraries.update(_id, { $set: { name } });
}, },
}); });
@@ -97,10 +97,10 @@ const updateLibraryDescription = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, description}){ run({ _id, description }) {
let library = Libraries.findOne(_id); let library = Libraries.findOne(_id);
assertEditPermission(library, this.userId); assertEditPermission(library, this.userId);
Libraries.update(_id, {$set: {description}}); Libraries.update(_id, { $set: { description } });
}, },
}); });
@@ -117,7 +117,7 @@ const removeLibrary = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}){ run({ _id }) {
let library = Libraries.findOne(_id); let library = Libraries.findOne(_id);
assertOwnership(library, this.userId); assertOwnership(library, this.userId);
this.unblock(); this.unblock();
@@ -125,9 +125,9 @@ const removeLibrary = new ValidatedMethod({
} }
}); });
export function removeLibaryWork(libraryId){ export function removeLibaryWork(libraryId) {
Libraries.remove(libraryId); Libraries.remove(libraryId);
LibraryNodes.remove({'ancestors.id': libraryId}); LibraryNodes.remove({ 'ancestors.id': libraryId });
} }
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary }; export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };

View File

@@ -60,7 +60,7 @@ if (Meteor.isServer) {
}); });
} }
for (let key in propertySchemasIndex){ for (let key in propertySchemasIndex) {
let schema = new SimpleSchema({}); let schema = new SimpleSchema({});
schema.extend(LibraryNodeSchema); schema.extend(LibraryNodeSchema);
schema.extend(ColorSchema); schema.extend(ColorSchema);
@@ -68,18 +68,18 @@ for (let key in propertySchemasIndex){
schema.extend(ChildSchema); schema.extend(ChildSchema);
schema.extend(SoftRemovableSchema); schema.extend(SoftRemovableSchema);
LibraryNodes.attachSchema(schema, { LibraryNodes.attachSchema(schema, {
selector: {type: key} selector: { type: key }
}); });
} }
function getLibrary(node){ function getLibrary(node) {
if (!node) throw new Meteor.Error('No node provided'); if (!node) throw new Meteor.Error('No node provided');
let library = Libraries.findOne(node.ancestors[0].id); let library = Libraries.findOne(node.ancestors[0].id);
if (!library) throw new Meteor.Error('Library does not exist'); if (!library) throw new Meteor.Error('Library does not exist');
return library; return library;
} }
function assertNodeEditPermission(node, userId){ function assertNodeEditPermission(node, userId) {
let lib = getLibrary(node); let lib = getLibrary(node);
return assertEditPermission(lib, userId); return assertEditPermission(lib, userId);
} }
@@ -96,7 +96,7 @@ const insertNode = new ValidatedMethod({
delete libraryNode._id; delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId); assertNodeEditPermission(libraryNode, this.userId);
let nodeId = LibraryNodes.insert(libraryNode); let nodeId = LibraryNodes.insert(libraryNode);
if (libraryNode.type == 'reference'){ if (libraryNode.type == 'reference') {
libraryNode._id = nodeId; libraryNode._id = nodeId;
updateReferenceNodeWork(libraryNode, this.userId); updateReferenceNodeWork(libraryNode, this.userId);
} }
@@ -106,10 +106,10 @@ const insertNode = new ValidatedMethod({
const updateLibraryNode = new ValidatedMethod({ const updateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.update', name: 'libraryNodes.update',
validate({_id, path}){ validate({ _id, path }) {
if (!_id) return false; if (!_id) return false;
// We cannot change these fields with a simple update // We cannot change these fields with a simple update
switch (path[0]){ switch (path[0]) {
case 'type': case 'type':
case 'order': case 'order':
case 'parent': case 'parent':
@@ -122,21 +122,21 @@ const updateLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, value}) { run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id); let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertNodeEditPermission(node, this.userId);
let pathString = path.join('.'); let pathString = path.join('.');
let modifier; let modifier;
// unset empty values // unset empty values
if (value === null || value === undefined){ if (value === null || value === undefined) {
modifier = {$unset: {[pathString]: 1}}; modifier = { $unset: { [pathString]: 1 } };
} else { } else {
modifier = {$set: {[pathString]: value}}; modifier = { $set: { [pathString]: value } };
} }
let numUpdated = LibraryNodes.update(_id, modifier, { let numUpdated = LibraryNodes.update(_id, modifier, {
selector: {type: node.type}, selector: { type: node.type },
}); });
if (node.type == 'reference'){ if (node.type == 'reference') {
node = LibraryNodes.findOne(_id); node = LibraryNodes.findOne(_id);
updateReferenceNodeWork(node, this.userId); updateReferenceNodeWork(node, this.userId);
} }
@@ -152,13 +152,13 @@ const pushToLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, value}){ run({ _id, path, value }) {
let node = LibraryNodes.findOne(_id); let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertNodeEditPermission(node, this.userId);
return LibraryNodes.update(_id, { return LibraryNodes.update(_id, {
$push: {[path.join('.')]: value}, $push: { [path.join('.')]: value },
}, { }, {
selector: {type: node.type}, selector: { type: node.type },
}); });
} }
}); });
@@ -171,13 +171,13 @@ const pullFromLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id, path, itemId}){ run({ _id, path, itemId }) {
let node = LibraryNodes.findOne(_id); let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertNodeEditPermission(node, this.userId);
return LibraryNodes.update(_id, { return LibraryNodes.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}}, $pull: { [path.join('.')]: { _id: itemId } },
}, { }, {
selector: {type: node.type}, selector: { type: node.type },
getAutoValues: false, getAutoValues: false,
}); });
} }
@@ -193,10 +193,10 @@ const softRemoveLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}){ run({ _id }) {
let node = LibraryNodes.findOne(_id); let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertNodeEditPermission(node, this.userId);
softRemove({_id, collection: LibraryNodes}); softRemove({ _id, collection: LibraryNodes });
} }
}); });
@@ -210,12 +210,12 @@ const restoreLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}){ run({ _id }) {
// Permissions // Permissions
let node = LibraryNodes.findOne(_id); let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId); assertNodeEditPermission(node, this.userId);
// Do work // Do work
restore({_id, collection: LibraryNodes}); restore({ _id, collection: LibraryNodes });
} }
}); });

View File

@@ -10,7 +10,7 @@ import {
import { reorderDocs } from '/imports/api/parenting/order.js'; import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar; var snackbar;
if (Meteor.isClient){ if (Meteor.isClient) {
snackbar = require( snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js' '/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar ).snackbar
@@ -31,7 +31,7 @@ const duplicateLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({_id}) { run({ _id }) {
let libraryNode = LibraryNodes.findOne(_id); let libraryNode = LibraryNodes.findOne(_id);
assertDocEditPermission(libraryNode, this.userId); assertDocEditPermission(libraryNode, this.userId);
@@ -41,15 +41,15 @@ const duplicateLibraryNode = new ValidatedMethod({
let nodes = LibraryNodes.find({ let nodes = LibraryNodes.find({
'ancestors.id': _id, 'ancestors.id': _id,
removed: {$ne: true}, removed: { $ne: true },
}, { }, {
limit: DUPLICATE_CHILDREN_LIMIT + 1, limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1}, sort: { order: 1 },
}).fetch(); }).fetch();
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){ if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
nodes.pop(); nodes.pop();
if (Meteor.isClient){ if (Meteor.isClient) {
snackbar({ snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`, text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
}); });
@@ -59,15 +59,15 @@ const duplicateLibraryNode = new ValidatedMethod({
// re-map all the ancestors // re-map all the ancestors
setLineageOfDocs({ setLineageOfDocs({
docArray: nodes, docArray: nodes,
newAncestry : [ newAncestry: [
...libraryNode.ancestors, ...libraryNode.ancestors,
{id: libraryNodeId, collection: 'libraryNodes'} { id: libraryNodeId, collection: 'libraryNodes' }
], ],
oldParent : {id: _id, collection: 'libraryNodes'}, oldParent: { id: _id, collection: 'libraryNodes' },
}); });
// Give the docs new IDs without breaking internal references // Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes}); renewDocIds({ docArray: nodes });
// Order the root node // Order the root node
libraryNode.order += 0.5; libraryNode.order += 0.5;

View File

@@ -1,17 +1,17 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
let SoftRemovableSchema = new SimpleSchema({ let SoftRemovableSchema = new SimpleSchema({
"removed": { 'removed': {
type: Boolean, type: Boolean,
optional: true, optional: true,
index: 1, index: 1,
}, },
"removedAt": { 'removedAt': {
type: Date, type: Date,
optional: true, optional: true,
index: 1, index: 1,
}, },
"removedWith": { 'removedWith': {
optional: true, optional: true,
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,

View File

@@ -50,7 +50,7 @@ let BuffRemoverSchema = createPropertySchema({
'extraTags.$._id': { 'extraTags.$._id': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
autoValue(){ autoValue() {
if (!this.isSet) return Random.id(); if (!this.isSet) return Random.id();
} }
}, },

View File

@@ -35,7 +35,7 @@ let ConstantSchema = new SimpleSchema({
errors: { errors: {
type: Array, type: Array,
maxCount: STORAGE_LIMITS.errorCount, maxCount: STORAGE_LIMITS.errorCount,
autoValue(){ autoValue() {
let calc = this.field('calculation'); let calc = this.field('calculation');
if (!calc.isSet && this.isModifier) { if (!calc.isSet && this.isModifier) {
this.unset() this.unset()
@@ -44,27 +44,27 @@ let ConstantSchema = new SimpleSchema({
let string = calc.value; let string = calc.value;
if (!string) return []; if (!string) return [];
// Evaluate the calculation with no scope // Evaluate the calculation with no scope
let {result, context} = parseString(string); let { result, context } = parseString(string);
// Any existing errors will result in an early failure // Any existing errors will result in an early failure
if (context && context.errors.length) return context.errors; if (context && context.errors.length) return context.errors;
// Ban variables in constants if necessary // Ban variables in constants if necessary
result && traverse(result, node => { result && traverse(result, node => {
if (node.parseType === 'symbol' || node.parseType === 'accessor'){ if (node.parseType === 'symbol' || node.parseType === 'accessor') {
context.error('Variables can\'t be used to define a constant'); context.error('Variables can\'t be used to define a constant');
} }
}); });
return context && context.errors || []; return context && context.errors || [];
} }
}, },
'errors.$':{ 'errors.$': {
type: ErrorSchema, type: ErrorSchema,
}, },
}); });
function parseString(string, fn = 'compile'){ function parseString(string, fn = 'compile') {
let context = new Context(); let context = new Context();
if (!string){ if (!string) {
return {result: string, context}; return { result: string, context };
} }
// Parse the string using mathjs // Parse the string using mathjs
@@ -74,11 +74,11 @@ function parseString(string, fn = 'compile'){
} catch (e) { } catch (e) {
let message = prettifyParseError(e); let message = prettifyParseError(e);
context.error(message); context.error(message);
return {context}; return { context };
} }
if (!node) return {context}; if (!node) return { context };
let {result} = resolve(fn, node, {/*empty scope*/}, context); let { result } = resolve(fn, node, {/*empty scope*/ }, context);
return {result, context} return { result, context }
} }
const ComputedOnlyConstantSchema = new SimpleSchema({}); const ComputedOnlyConstantSchema = new SimpleSchema({});

View File

@@ -40,24 +40,24 @@ const ComputedOnlyContainerSchema = createPropertySchema({
optional: true, optional: true,
}, },
// Weight of all the contents, zero if `contentsWeightless` is true // Weight of all the contents, zero if `contentsWeightless` is true
contentsWeight:{ contentsWeight: {
type: Number, type: Number,
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
// Weight of all the carried contents (some sub-containers might not be carried) // Weight of all the carried contents (some sub-containers might not be carried)
// zero if `contentsWeightless` is true // zero if `contentsWeightless` is true
carriedWeight:{ carriedWeight: {
type: Number, type: Number,
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
contentsValue:{ contentsValue: {
type: Number, type: Number,
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
carriedValue:{ carriedValue: {
type: Number, type: Number,
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,

View File

@@ -14,8 +14,8 @@ const magicSchools = [
]; ];
let SpellSchema = new SimpleSchema({}) let SpellSchema = new SimpleSchema({})
.extend(ActionSchema) .extend(ActionSchema)
.extend({ .extend({
name: { name: {
type: String, type: String,
optional: true, optional: true,
@@ -89,7 +89,7 @@ let SpellSchema = new SimpleSchema({})
defaultValue: 'abjuration', defaultValue: 'abjuration',
allowedValues: magicSchools, allowedValues: magicSchools,
}, },
}); });
const ComputedOnlySpellSchema = new SimpleSchema() const ComputedOnlySpellSchema = new SimpleSchema()
.extend(ComputedOnlyActionSchema); .extend(ComputedOnlyActionSchema);

View File

@@ -5,7 +5,7 @@ const AdjustmentSchema = new SimpleSchema({
_id: { _id: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
autoValue(){ autoValue() {
if (!this.isSet) return Random.id(); if (!this.isSet) return Random.id();
} }
}, },

View File

@@ -1,11 +1,11 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js'; import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js';
let RollResultsSchema = new SimpleSchema ({ let RollResultsSchema = new SimpleSchema({
_id: { _id: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
autoValue(){ autoValue() {
if (!this.isSet) return Random.id(); if (!this.isSet) return Random.id();
} }
}, },
@@ -22,4 +22,4 @@ let RollResultsSchema = new SimpleSchema ({
}, },
}); });
export default RollResultsSchema ; export default RollResultsSchema;

View File

@@ -18,11 +18,11 @@ const setPublic = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({docRef, isPublic}){ run({ docRef, isPublic }) {
let doc = fetchDocByRef(docRef); let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId); assertOwnership(doc, this.userId);
return getCollectionByName(docRef.collection).update(docRef.id, { return getCollectionByName(docRef.collection).update(docRef.id, {
$set: {public: isPublic}, $set: { public: isPublic },
}); });
}, },
}); });
@@ -45,28 +45,28 @@ const updateUserSharePermissions = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({docRef, userId, role}){ run({ docRef, userId, role }) {
let doc = fetchDocByRef(docRef); let doc = fetchDocByRef(docRef);
if (role === 'none'){ if (role === 'none') {
// only assert ownership if you aren't removing yourself // only assert ownership if you aren't removing yourself
if (this.userId !== userId){ if (this.userId !== userId) {
assertOwnership(doc, this.userId); assertOwnership(doc, this.userId);
} }
return getCollectionByName(docRef.collection).update(docRef.id, { return getCollectionByName(docRef.collection).update(docRef.id, {
$pullAll: { readers: userId, writers: userId }, $pullAll: { readers: userId, writers: userId },
}); });
} }
if (doc.owner === userId){ if (doc.owner === userId) {
throw new Meteor.Error('Sharing update failed', throw new Meteor.Error('Sharing update failed',
'User is already the owner of this document'); 'User is already the owner of this document');
} }
assertOwnership(doc, this.userId); assertOwnership(doc, this.userId);
if (role === 'reader'){ if (role === 'reader') {
return getCollectionByName(docRef.collection).update(docRef.id, { return getCollectionByName(docRef.collection).update(docRef.id, {
$addToSet: { readers: userId }, $addToSet: { readers: userId },
$pullAll: { writers: userId }, $pullAll: { writers: userId },
}); });
} else if (role === 'writer'){ } else if (role === 'writer') {
return getCollectionByName(docRef.collection).update(docRef.id, { return getCollectionByName(docRef.collection).update(docRef.id, {
$addToSet: { writers: userId }, $addToSet: { writers: userId },
$pullAll: { readers: userId }, $pullAll: { readers: userId },
@@ -89,29 +89,29 @@ const transferOwnership = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({docRef, userId}){ run({ docRef, userId }) {
let doc = fetchDocByRef(docRef); let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId); assertOwnership(doc, this.userId);
let collection = getCollectionByName(docRef.collection); let collection = getCollectionByName(docRef.collection);
let tier = getUserTier(userId); let tier = getUserTier(userId);
if (docRef.collection === 'creatures'){ if (docRef.collection === 'creatures') {
let currentCharacterCount = collection.find({ let currentCharacterCount = collection.find({
owner: userId, owner: userId,
}, { }, {
fields: {_id: 1}, fields: { _id: 1 },
}).count(); }).count();
if ( if (
tier.characterSlots !== -1 && tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots currentCharacterCount >= tier.characterSlots
){ ) {
throw new Meteor.Error('Sharing.methods.transferOwnership.denied', throw new Meteor.Error('Sharing.methods.transferOwnership.denied',
'The new owner is already at their character limit') 'The new owner is already at their character limit')
} }
} else if (docRef.collection === 'libraries'){ } else if (docRef.collection === 'libraries') {
if (!tier.paidBenefits){ if (!tier.paidBenefits) {
throw new Meteor.Error('Sharing.methods.transferOwnership.denied', throw new Meteor.Error('Sharing.methods.transferOwnership.denied',
'The new owner\'s Patreon tier does not have access to library ownership'); 'The new owner\'s Patreon tier does not have access to library ownership');
} }
@@ -123,7 +123,7 @@ const transferOwnership = new ValidatedMethod({
}); });
// Then make the user the owner and the current owner a writer // Then make the user the owner and the current owner a writer
return collection.update(docRef.id, { return collection.update(docRef.id, {
$set: {owner: userId}, $set: { owner: userId },
$addToSet: { writers: this.userId }, $addToSet: { writers: this.userId },
}); });
}, },

View File

@@ -51,7 +51,7 @@ const sendMessage = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
run({content, tabletopId}) { run({ content, tabletopId }) {
let user = Meteor.user(); let user = Meteor.user();
if (!user) { if (!user) {
throw new Meteor.Error('messages.send.denied', throw new Meteor.Error('messages.send.denied',
@@ -87,14 +87,14 @@ const removeMessages = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
run({messageId, tabletopId}) { run({ messageId, tabletopId }) {
if (!this.userId) { if (!this.userId) {
throw new Meteor.Error('messages.remove.denied', throw new Meteor.Error('messages.remove.denied',
'You need to be logged in to remove a tabletop'); 'You need to be logged in to remove a tabletop');
} }
let message = Messages.findOne(messageId); let message = Messages.findOne(messageId);
let tabletop = Tabletops.findOne(message.tabletopId); let tabletop = Tabletops.findOne(message.tabletopId);
if (this.userId !== message.userId && this.userId !== tabletop.gameMaster){ if (this.userId !== message.userId && this.userId !== tabletop.gameMaster) {
throw new Meteor.Error('messages.remove.denied', throw new Meteor.Error('messages.remove.denied',
'You don\'t have permission to remove this message'); 'You don\'t have permission to remove this message');
} }
@@ -104,7 +104,7 @@ const removeMessages = new ValidatedMethod({
Creatures.update({ Creatures.update({
tabletop: tabletopId, tabletop: tabletopId,
}, { }, {
$unset: {tabletop: 1}, $unset: { tabletop: 1 },
}); });
return removed; return removed;
}, },

View File

@@ -24,7 +24,7 @@ const removeTabletop = new ValidatedMethod({
timeInterval: 5000, timeInterval: 5000,
}, },
run({tabletopId}) { run({ tabletopId }) {
if (!this.userId) { if (!this.userId) {
throw new Meteor.Error('tabletops.remove.denied', throw new Meteor.Error('tabletops.remove.denied',
'You need to be logged in to remove a tabletop'); 'You need to be logged in to remove a tabletop');
@@ -39,7 +39,7 @@ const removeTabletop = new ValidatedMethod({
Creatures.update({ Creatures.update({
tabletop: tabletopId, tabletop: tabletopId,
}, { }, {
$unset: {tabletop: 1}, $unset: { tabletop: 1 },
}); });
return removed; return removed;
}, },

View File

@@ -129,13 +129,13 @@ Meteor.users.generateApiKey = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run(){ run() {
if(Meteor.isClient) return; if (Meteor.isClient) return;
var user = Meteor.users.findOne(this.userId); var user = Meteor.users.findOne(this.userId);
if (!user) return; if (!user) return;
if (user && user.apiKey) return; if (user && user.apiKey) return;
var apiKey = Random.id(30); var apiKey = Random.id(30);
Meteor.users.update(this.userId, {$set: {apiKey}}); Meteor.users.update(this.userId, { $set: { apiKey } });
}, },
}); });
@@ -149,16 +149,16 @@ Meteor.users.setDarkMode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({darkMode}){ run({ darkMode }) {
if (!this.userId) return; if (!this.userId) return;
Meteor.users.update(this.userId, {$set: {darkMode}}); Meteor.users.update(this.userId, { $set: { darkMode } });
}, },
}); });
Meteor.users.sendVerificationEmail = new ValidatedMethod({ Meteor.users.sendVerificationEmail = new ValidatedMethod({
name: 'users.sendVerificationEmail', name: 'users.sendVerificationEmail',
validate: new SimpleSchema({ validate: new SimpleSchema({
userId:{ userId: {
type: String, type: String,
optional: true, optional: true,
}, },
@@ -171,7 +171,7 @@ Meteor.users.sendVerificationEmail = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({userId, address}){ run({ userId, address }) {
userId = this.userId || userId; userId = this.userId || userId;
let user = Meteor.users.findOne(userId); let user = Meteor.users.findOne(userId);
if (!user) { if (!user) {
@@ -194,11 +194,11 @@ Meteor.users.canPickUsername = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({username}){ run({ username }) {
if (Meteor.isClient) return; if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(username); let user = Accounts.findUserByUsername(username);
// You can pick your own username // You can pick your own username
if (user && user._id === this.userId){ if (user && user._id === this.userId) {
return false; return false;
} }
return !!user; return !!user;
@@ -213,7 +213,7 @@ Meteor.users.setUsername = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({username}){ run({ username }) {
if (!this.userId) throw 'Can only set your username if logged in'; if (!this.userId) throw 'Can only set your username if logged in';
if (Meteor.isClient) return; if (Meteor.isClient) return;
return Accounts.setUsername(this.userId, username) return Accounts.setUsername(this.userId, username)
@@ -223,7 +223,7 @@ Meteor.users.setUsername = new ValidatedMethod({
Meteor.users.setPreference = new ValidatedMethod({ Meteor.users.setPreference = new ValidatedMethod({
name: 'users.setPreference', name: 'users.setPreference',
validate: new SimpleSchema({ validate: new SimpleSchema({
preference:{ preference: {
type: String, type: String,
}, },
value: { value: {
@@ -235,16 +235,16 @@ Meteor.users.setPreference = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({preference, value}){ run({ preference, value }) {
if (!this.userId) throw 'You can only set preferences once logged in'; if (!this.userId) throw 'You can only set preferences once logged in';
let prefPath = `preferences.${preference}` let prefPath = `preferences.${preference}`
if (value == true){ if (value == true) {
return Meteor.users.update(this.userId, { return Meteor.users.update(this.userId, {
$set: {[prefPath]: true}, $set: { [prefPath]: true },
}); });
} else { } else {
return Meteor.users.update(this.userId, { return Meteor.users.update(this.userId, {
$unset: {[prefPath]: 1}, $unset: { [prefPath]: 1 },
}); });
} }
}, },
@@ -253,7 +253,7 @@ Meteor.users.setPreference = new ValidatedMethod({
Meteor.users.subscribeToLibrary = new ValidatedMethod({ Meteor.users.subscribeToLibrary = new ValidatedMethod({
name: 'users.subscribeToLibrary', name: 'users.subscribeToLibrary',
validate: new SimpleSchema({ validate: new SimpleSchema({
libraryId:{ libraryId: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
@@ -266,15 +266,15 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({libraryId, subscribe}){ run({ libraryId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in'; if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe){ if (subscribe) {
return Meteor.users.update(this.userId, { return Meteor.users.update(this.userId, {
$addToSet: {subscribedLibraries: libraryId}, $addToSet: { subscribedLibraries: libraryId },
}); });
} else { } else {
return Meteor.users.update(this.userId, { return Meteor.users.update(this.userId, {
$pullAll: {subscribedLibraries: libraryId}, $pullAll: { subscribedLibraries: libraryId },
}); });
} }
} }
@@ -283,7 +283,7 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({
Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({ Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
name: 'users.subscribeToLibraryCollection', name: 'users.subscribeToLibraryCollection',
validate: new SimpleSchema({ validate: new SimpleSchema({
libraryCollectionId:{ libraryCollectionId: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
@@ -296,15 +296,15 @@ Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({libraryCollectionId, subscribe}){ run({ libraryCollectionId, subscribe }) {
if (!this.userId) throw 'Can only subscribe if logged in'; if (!this.userId) throw 'Can only subscribe if logged in';
if (subscribe){ if (subscribe) {
return Meteor.users.update(this.userId, { return Meteor.users.update(this.userId, {
$addToSet: {subscribedLibraryCollections: libraryCollectionId}, $addToSet: { subscribedLibraryCollections: libraryCollectionId },
}); });
} else { } else {
return Meteor.users.update(this.userId, { return Meteor.users.update(this.userId, {
$pullAll: {subscribedLibraryCollections: libraryCollectionId}, $pullAll: { subscribedLibraryCollections: libraryCollectionId },
}); });
} }
} }
@@ -313,7 +313,7 @@ Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({ Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
name: 'users.findUserByUsernameOrEmail', name: 'users.findUserByUsernameOrEmail',
validate: new SimpleSchema({ validate: new SimpleSchema({
usernameOrEmail:{ usernameOrEmail: {
type: String, type: String,
}, },
}).validator(), }).validator(),
@@ -322,7 +322,7 @@ Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({usernameOrEmail}){ run({ usernameOrEmail }) {
if (Meteor.isClient) return; if (Meteor.isClient) return;
let user = Accounts.findUserByUsername(usernameOrEmail) || let user = Accounts.findUserByUsername(usernameOrEmail) ||
Accounts.findUserByEmail(usernameOrEmail); Accounts.findUserByEmail(usernameOrEmail);

View File

@@ -15,16 +15,16 @@ const addEmail = new ValidatedMethod({
numRequests: 1, numRequests: 1,
timeInterval: 5000, timeInterval: 5000,
}, },
run({email}){ run({ email }) {
const userId = Meteor.userId(); const userId = Meteor.userId();
const user = Meteor.users.findOne(userId); const user = Meteor.users.findOne(userId);
if (!user) throw new Meteor.Error('No user', if (!user) throw new Meteor.Error('No user',
'You must be logged in to add an email address'); 'You must be logged in to add an email address');
if (user.emails && user.emails.length >= 2){ if (user.emails && user.emails.length >= 2) {
throw new Meteor.Error('Emails full', throw new Meteor.Error('Emails full',
'You may only have up to 2 email addresses per account'); 'You may only have up to 2 email addresses per account');
} }
if (Meteor.isServer){ if (Meteor.isServer) {
Accounts.addEmail(userId, email); Accounts.addEmail(userId, email);
Accounts.sendVerificationEmail(userId, email); Accounts.sendVerificationEmail(userId, email);
} }

View File

@@ -1,8 +1,8 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js'; import Libraries, { removeLibaryWork } from '/imports/api/library/Libraries.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/creatures/methods/removeCreature.js'; import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
Meteor.users.deleteMyAccount = new ValidatedMethod({ Meteor.users.deleteMyAccount = new ValidatedMethod({
name: 'users.deleteMyAccount', name: 'users.deleteMyAccount',
@@ -12,20 +12,20 @@ Meteor.users.deleteMyAccount = new ValidatedMethod({
numRequests: 1, numRequests: 1,
timeInterval: 5000, timeInterval: 5000,
}, },
run(){ run() {
let userId = Meteor.userId(); let userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user', if (!userId) throw new Meteor.Error('No user',
'You must be logged in to delete your account'); 'You must be logged in to delete your account');
// Delete all creatures // Delete all creatures
let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch(); let creatures = Creatures.find({ owner: userId }, { fields: { _id: 1 } }).fetch();
creatures.forEach(creature => removeCreatureWork(creature._id)); creatures.forEach(creature => removeCreatureWork(creature._id));
// Remove permissions from all creatures // Remove permissions from all creatures
Creatures.update({ Creatures.update({
$or: [ $or: [
{writers: userId}, { writers: userId },
{readers: userId}, { readers: userId },
], ],
}, { }, {
$pull: { $pull: {
@@ -37,14 +37,14 @@ Meteor.users.deleteMyAccount = new ValidatedMethod({
}); });
// Delete all libraries // Delete all libraries
let libraries = Libraries.find({owner: userId}, {fields: {_id: 1}}).fetch(); let libraries = Libraries.find({ owner: userId }, { fields: { _id: 1 } }).fetch();
libraries.forEach(library => removeLibaryWork(library._id)); libraries.forEach(library => removeLibaryWork(library._id));
// Remove permissions from all creatures // Remove permissions from all creatures
Libraries.update({ Libraries.update({
$or: [ $or: [
{writers: userId}, { writers: userId },
{readers: userId}, { readers: userId },
], ],
}, { }, {
$pull: { $pull: {

View File

@@ -15,20 +15,20 @@ const removeEmail = new ValidatedMethod({
numRequests: 1, numRequests: 1,
timeInterval: 5000, timeInterval: 5000,
}, },
run({email}){ run({ email }) {
const userId = Meteor.userId(); const userId = Meteor.userId();
const user = Meteor.users.findOne(userId); const user = Meteor.users.findOne(userId);
if (!user) throw new Meteor.Error('No user', if (!user) throw new Meteor.Error('No user',
'You must be logged in to remove an email address'); 'You must be logged in to remove an email address');
if (!user.emails){ if (!user.emails) {
throw new Meteor.Error('No email to remove', throw new Meteor.Error('No email to remove',
'No email addresses are associated with this account'); 'No email addresses are associated with this account');
} }
if (user.emails.length == 1){ if (user.emails.length == 1) {
throw new Meteor.Error('Can\'t remove last email', throw new Meteor.Error('Can\'t remove last email',
'You may not remove the last email address from your account'); 'You may not remove the last email address from your account');
} }
if (Meteor.isServer){ if (Meteor.isServer) {
Accounts.removeEmail(userId, email); Accounts.removeEmail(userId, email);
} }
} }

View File

@@ -2,12 +2,14 @@ const PROPERTIES = Object.freeze({
action: { action: {
icon: '$vuetify.icons.action', icon: '$vuetify.icons.action',
name: 'Action', name: 'Action',
docsPath: 'property/action',
helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.', helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.',
suggestedParents: ['classLevel', 'feature', 'item'], suggestedParents: ['classLevel', 'feature', 'item'],
}, },
attribute: { attribute: {
icon: '$vuetify.icons.attribute', icon: '$vuetify.icons.attribute',
name: 'Attribute', name: 'Attribute',
docsPath: 'property/attribute',
helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.', helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.',
examples: 'Ability scores, speed, hit points, ki', examples: 'Ability scores, speed, hit points, ki',
suggestedParents: ['classLevel', 'buff'], suggestedParents: ['classLevel', 'buff'],
@@ -15,48 +17,56 @@ const PROPERTIES = Object.freeze({
adjustment: { adjustment: {
icon: '$vuetify.icons.attribute_damage', icon: '$vuetify.icons.attribute_damage',
name: 'Attribute damage', name: 'Attribute damage',
docsPath: 'property/attribute-damage',
helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.', helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
buff: { buff: {
icon: '$vuetify.icons.buff', icon: '$vuetify.icons.buff',
name: 'Buff', name: 'Buff',
docsPath: 'property/buff',
helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.', helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
buffRemover: { buffRemover: {
icon: '$vuetify.icons.buffRemover', icon: '$vuetify.icons.buffRemover',
name: 'Remove Buff', name: 'Remove Buff',
docsPath: 'property/remove-buff',
helpText: 'Removes a buff from the target character', helpText: 'Removes a buff from the target character',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
branch: { branch: {
icon: 'mdi-file-tree', icon: 'mdi-file-tree',
name: 'Branch', name: 'Branch',
docsPath: 'property/branch',
helpText: 'When a branch is activated as a child of an action, it can control which of its children get activated.', helpText: 'When a branch is activated as a child of an action, it can control which of its children get activated.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
class: { class: {
icon: 'mdi-card-account-details', icon: 'mdi-card-account-details',
name: 'Class', name: 'Class',
docsPath: 'property/class',
helpText: 'Your character should ideally have one starting class. Classes hold class levels', helpText: 'Your character should ideally have one starting class. Classes hold class levels',
suggestedParents: [], suggestedParents: [],
}, },
classLevel: { classLevel: {
icon: '$vuetify.icons.class_level', icon: '$vuetify.icons.class_level',
name: 'Class level', name: 'Class level',
docsPath: 'property/class-level',
helpText: 'Class levels represent a single level gained in a class', helpText: 'Class levels represent a single level gained in a class',
suggestedParents: ['class'], suggestedParents: ['class'],
}, },
constant: { constant: {
icon: 'mdi-anchor', icon: 'mdi-anchor',
name: 'Constant', name: 'Constant',
docsPath: 'property/constant',
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet', helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
suggestedParents: [], suggestedParents: [],
}, },
container: { container: {
icon: 'mdi-bag-personal-outline', icon: 'mdi-bag-personal-outline',
name: 'Container', name: 'Container',
docsPath: 'property/container',
helpText: 'A container holds items in the inventory', helpText: 'A container holds items in the inventory',
examples: 'Coin pouch, backpack', examples: 'Coin pouch, backpack',
suggestedParents: ['folder'], suggestedParents: ['folder'],
@@ -64,18 +74,21 @@ const PROPERTIES = Object.freeze({
damage: { damage: {
icon: '$vuetify.icons.damage', icon: '$vuetify.icons.damage',
name: 'Damage', name: 'Damage',
docsPath: 'property/damage',
helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.', helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
damageMultiplier: { damageMultiplier: {
icon: '$vuetify.icons.damage_multiplier', icon: '$vuetify.icons.damage_multiplier',
name: 'Damage multiplier', name: 'Damage multiplier',
docsPath: 'property/damage-multiplier',
helpText: 'Resistance, vulnerability, and immunity.', helpText: 'Resistance, vulnerability, and immunity.',
suggestedParents: ['classLevel', 'feature', 'item'], suggestedParents: ['classLevel', 'feature', 'item'],
}, },
effect: { effect: {
icon: '$vuetify.icons.effect', icon: '$vuetify.icons.effect',
name: 'Effect', name: 'Effect',
docsPath: 'property/effect',
helpText: 'Effects change the value or state of attributes and skills.', helpText: 'Effects change the value or state of attributes and skills.',
examples: '+2 Strength, Advantage on dexterity saving throws', examples: '+2 Strength, Advantage on dexterity saving throws',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'], suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'],
@@ -83,42 +96,49 @@ const PROPERTIES = Object.freeze({
feature: { feature: {
icon: 'mdi-text-subject', icon: 'mdi-text-subject',
name: 'Feature', name: 'Feature',
docsPath: 'property/feature',
helpText: 'Descriptive or narrative features your character has access to', helpText: 'Descriptive or narrative features your character has access to',
suggestedParents: ['classLevel', 'folder'], suggestedParents: ['classLevel', 'folder'],
}, },
folder: { folder: {
icon: 'mdi-folder-outline', icon: 'mdi-folder-outline',
name: 'Folder', name: 'Folder',
docsPath: 'property/feature',
helpText: 'A way to organise other properties on the character', helpText: 'A way to organise other properties on the character',
suggestedParents: ['folder'], suggestedParents: ['action', 'folder'],
}, },
item: { item: {
icon: 'mdi-cube-outline', icon: 'mdi-cube-outline',
name: 'Item', name: 'Item',
docsPath: 'property/item',
helpText: 'Objects and equipment your charcter finds on their adventures', helpText: 'Objects and equipment your charcter finds on their adventures',
suggestedParents: ['container'], suggestedParents: ['container'],
}, },
note: { note: {
icon: 'mdi-note-outline', icon: 'mdi-note-outline',
name: 'Note', name: 'Note',
docsPath: 'property/note',
helpText: 'Notes about your character and their adventures', helpText: 'Notes about your character and their adventures',
suggestedParents: ['note', 'folder'], suggestedParents: ['note', 'folder'],
}, },
pointBuy: { pointBuy: {
icon: 'mdi-table', icon: 'mdi-table',
name: 'Point Buy', name: 'Point Buy',
docsPath: 'property/point-buy',
helpText: 'A point buy table that allows the user to select an array of values that match a given cost', helpText: 'A point buy table that allows the user to select an array of values that match a given cost',
suggestedParents: [], suggestedParents: [],
}, },
proficiency: { proficiency: {
icon: 'mdi-brightness-1', icon: 'mdi-brightness-1',
name: 'Proficiency', name: 'Proficiency',
docsPath: 'property/proficiency',
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.', helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'], suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
}, },
roll: { roll: {
icon: '$vuetify.icons.roll', icon: '$vuetify.icons.roll',
name: 'Roll', name: 'Roll',
docsPath: 'property/roll',
helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use', helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
@@ -132,48 +152,56 @@ const PROPERTIES = Object.freeze({
savingThrow: { savingThrow: {
icon: '$vuetify.icons.saving_throw', icon: '$vuetify.icons.saving_throw',
name: 'Saving throw', name: 'Saving throw',
docsPath: 'property/saving-throw',
helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.', helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.',
suggestedParents: ['action', 'attack', 'spell'], suggestedParents: ['action', 'attack', 'spell'],
}, },
skill: { skill: {
icon: '$vuetify.icons.skill', icon: '$vuetify.icons.skill',
name: 'Skill', name: 'Skill',
docsPath: 'property/skill',
helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.', helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.',
suggestedParents: ['classLevel', 'folder'], suggestedParents: ['classLevel', 'folder'],
}, },
propertySlot: { propertySlot: {
icon: 'mdi-power-socket-eu', icon: 'mdi-power-socket-eu',
name: 'Slot', name: 'Slot',
docsPath: 'property/slot',
helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot', helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot',
suggestedParents: [], suggestedParents: [],
}, },
slotFiller: { slotFiller: {
icon: 'mdi-power-plug-outline', icon: 'mdi-power-plug-outline',
name: 'Slot filler', name: 'Slot filler',
docsPath: 'property/slot-filler',
helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.', helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
suggestedParents: ['propertySlot'], suggestedParents: ['propertySlot'],
}, },
spellList: { spellList: {
icon: '$vuetify.icons.spell_list', icon: '$vuetify.icons.spell_list',
name: 'Spell list', name: 'Spell list',
docsPath: 'property/spell-list',
helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within', helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within',
suggestedParents: [], suggestedParents: [],
}, },
spell: { spell: {
icon: '$vuetify.icons.spell', icon: '$vuetify.icons.spell',
name: 'Spell', name: 'Spell',
docsPath: 'property/spell',
helpText: 'A spell your character can potentially cast', helpText: 'A spell your character can potentially cast',
suggestedParents: ['spellList'], suggestedParents: ['spellList'],
}, },
toggle: { toggle: {
icon: '$vuetify.icons.toggle', icon: '$vuetify.icons.toggle',
name: 'Toggle', name: 'Toggle',
docsPath: 'property/toggle',
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.', helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
suggestedParents: [], suggestedParents: [],
}, },
trigger: { trigger: {
icon: 'mdi-electric-switch', icon: 'mdi-electric-switch',
name: 'Trigger', name: 'Trigger',
docsPath: 'property/trigger',
helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage', helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage',
suggestedParents: [], suggestedParents: [],
}, },
@@ -188,3 +216,17 @@ export function getPropertyName(type){
export function getPropertyIcon(type){ export function getPropertyIcon(type){
return type && PROPERTIES[type] && PROPERTIES[type].icon; return type && PROPERTIES[type] && PROPERTIES[type].icon;
} }
const propsByDocsPath = new Map();
for (const key in PROPERTIES) {
const prop = PROPERTIES[key];
if (prop.docsPath) {
propsByDocsPath.set(prop.docsPath, {
...prop,
type: key,
});
}
}
export { propsByDocsPath };

View File

@@ -19,7 +19,7 @@ const migrateTo = new ValidatedMethod({
numRequests: 1, numRequests: 1,
timeInterval: 10000, timeInterval: 10000,
}, },
run({version}) { run({ version }) {
if (Meteor.isClient) return; if (Meteor.isClient) return;
assertAdmin(this.userId); assertAdmin(this.userId);
Migrations.migrateTo(version); Migrations.migrateTo(version);

View File

@@ -23,8 +23,8 @@ const validateDatabase = new ValidatedMethod({
const schema = collection.instance.simpleSchema(doc); const schema = collection.instance.simpleSchema(doc);
let cleanDoc = schema.clean(doc); let cleanDoc = schema.clean(doc);
try { try {
schema.validate(cleanDoc, {modifier: false}); schema.validate(cleanDoc, { modifier: false });
} catch (e){ } catch (e) {
console.log(collection.name, doc._id, e.message || e.reason || e.toString()); console.log(collection.name, doc._id, e.message || e.reason || e.toString());
} }
}); });

View File

@@ -112,10 +112,10 @@ export default {
} }
}, },
'resolve': { 'resolve': {
comment: 'Forces the given calcultion to resolve into a number', comment: 'Forces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is',
examples: [ examples: [
{input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'}, {input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'},
{input: 'resolve(3d6)', result: '2'}, {input: 'resolve(1d6)', result: '4'},
], ],
arguments: ['parseNode'], arguments: ['parseNode'],
fn: function resolveFn(node){ fn: function resolveFn(node){

View File

@@ -2,23 +2,23 @@ import resolve, { traverse, toString, map } from '../resolve';
import error from './error'; import error from './error';
const indexNode = { const indexNode = {
create({array, index}) { create({ array, index }) {
return { return {
parseType: 'index', parseType: 'index',
array, array,
index, index,
} }
}, },
resolve(fn, node, scope, context){ resolve(fn, node, scope, context) {
let {result: index} = resolve(fn, node.index, scope, context); let { result: index } = resolve(fn, node.index, scope, context);
let {result: array} = resolve(fn, node.array, scope, context); let { result: array } = resolve(fn, node.array, scope, context);
if ( if (
index.valueType === 'number' && index.valueType === 'number' &&
Number.isInteger(index.value) && Number.isInteger(index.value) &&
array.parseType === 'array' array.parseType === 'array'
){ ) {
if (index.value < 1 || index.value > array.values.length){ if (index.value < 1 || index.value > array.values.length) {
context.error({ context.error({
type: 'warning', type: 'warning',
message: `Index of ${index.value} is out of range for an array` + message: `Index of ${index.value} is out of range for an array` +
@@ -26,11 +26,11 @@ const indexNode = {
}); });
} }
let selection = array.values[index.value - 1]; let selection = array.values[index.value - 1];
if (selection){ if (selection) {
return resolve(fn, selection, scope, context); return resolve(fn, selection, scope, context);
} }
} else if (fn === 'reduce'){ } else if (fn === 'reduce') {
if (array.parseType !== 'array'){ if (array.parseType !== 'array') {
const message = `Can not get the index of a non-array node: ${toString(node.array)} = ${toString(array)}` const message = `Can not get the index of a non-array node: ${toString(node.array)} = ${toString(array)}`
context.error(message); context.error(message);
return { return {
@@ -40,7 +40,7 @@ const indexNode = {
}), }),
context, context,
}; };
} else if (!index.isInteger){ } else if (!index.isInteger) {
const message = `${toString(array)} is not an integer index of the array` const message = `${toString(array)} is not an integer index of the array`
context.error(message); context.error(message);
return { return {
@@ -60,17 +60,17 @@ const indexNode = {
context, context,
}; };
}, },
toString(node){ toString(node) {
return `${toString(node.array)}[${toString(node.index)}]`; return `${toString(node.array)}[${toString(node.index)}]`;
}, },
traverse(node, fn){ traverse(node, fn) {
fn(node); fn(node);
traverse(node.array, fn); traverse(node.array, fn);
traverse(node.index, fn); traverse(node.index, fn);
}, },
map(node, fn){ map(node, fn) {
const resultingNode = fn(node); const resultingNode = fn(node);
if (resultingNode === node){ if (resultingNode === node) {
node.array = map(node.array, fn); node.array = map(node.array, fn);
node.index = map(node.index, fn); node.index = map(node.index, fn);
} }

View File

@@ -1,7 +1,7 @@
import constant from './constant.js'; import constant from './constant.js';
const rollArray = { const rollArray = {
create({values, diceSize, diceNum}) { create({ values, diceSize, diceNum }) {
return { return {
parseType: 'rollArray', parseType: 'rollArray',
values, values,
@@ -9,16 +9,16 @@ const rollArray = {
diceNum, diceNum,
}; };
}, },
compile(node, scope, context){ compile(node, scope, context) {
return { return {
result: node, result: node,
context context
}; };
}, },
toString(node){ toString(node) {
return `${node.diceNum || ''}d${node.diceSize} [ ${node.values.join(', ')} ]`; return `${node.diceNum || ''}d${node.diceSize} [ ${node.values.join(', ')} ]`;
}, },
reduce(node, scope, context){ reduce(node, scope, context) {
const total = node.values.reduce((a, b) => a + b, 0); const total = node.values.reduce((a, b) => a + b, 0);
return { return {
result: constant.create({ result: constant.create({

View File

View File

@@ -14,15 +14,15 @@ Meteor.startup(() => {
* and were not restored * and were not restored
* @return {Number} Number of documents removed * @return {Number} Number of documents removed
*/ */
const deleteOldSoftRemovedDocs = function(){ const deleteOldSoftRemovedDocs = function () {
const now = new Date(); const now = new Date();
const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000)); const yesterday = new Date(now.getTime() - (24 * 60 * 60 * 1000));
collections.forEach(collection => { collections.forEach(collection => {
collection.remove({ collection.remove({
removed: true, removed: true,
removedAt: {$lt: yesterday} // dates *before* yesterday removedAt: { $lt: yesterday } // dates *before* yesterday
}, function(error){ }, function (error) {
if (error){ if (error) {
console.error(JSON.stringify(error, null, 2)); console.error(JSON.stringify(error, null, 2));
} }
}); });
@@ -31,7 +31,7 @@ Meteor.startup(() => {
SyncedCron.add({ SyncedCron.add({
name: 'deleteSoftRemovedDocs', name: 'deleteSoftRemovedDocs',
schedule: function(parser) { schedule: function (parser) {
return parser.text('every 10 minutes'); return parser.text('every 10 minutes');
}, },
job: deleteOldSoftRemovedDocs, job: deleteOldSoftRemovedDocs,

View File

@@ -0,0 +1,34 @@
import { propsByDocsPath } from '/imports/constants/PROPERTIES.js';
// Manual doc paths
const docPaths = [
'computed-fields',
'inline-calculations',
'dependency-loops',
'docs',
'tags',
];
const docs = new Map();
docPaths.forEach(path => {
docs.set(path, Assets.getText(`docs/${path}.md`))
});
// Doc paths for properties
propsByDocsPath.forEach(prop => {
docs.set(prop.docsPath, Assets.getText(`docs/${prop.docsPath}.md`));
});
Meteor.publish('docs', function (path) {
if (!path) {
docs.forEach((text, path) => {
this.added('docs', path, { text });
});
} else {
const text = docs.get(path);
if (text) {
this.added('docs', path, { text });
}
}
this.ready();
});

View File

@@ -1,16 +1,16 @@
import Icons from '/imports/api/icons/Icons.js'; import Icons from '/imports/api/icons/Icons.js';
Meteor.publish('sampleIcons', function(){ Meteor.publish('sampleIcons', function () {
return Icons.find({}, {limit: 50}); return Icons.find({}, { limit: 50 });
}); });
Meteor.publish('searchIcons', function(searchValue) { Meteor.publish('searchIcons', function (searchValue) {
// Don't publish anything if there's no search value // Don't publish anything if there's no search value
if (!searchValue) { if (!searchValue) {
return []; return [];
} }
return Icons.find( return Icons.find(
{ $text: {$search: searchValue} }, { $text: { $search: searchValue } },
{ {
// relevant documents have a higher score. // relevant documents have a higher score.
fields: { fields: {

View File

@@ -11,3 +11,4 @@ import '/imports/server/publications/ownedDocuments.js';
import '/imports/server/publications/searchLibraryNodes.js'; import '/imports/server/publications/searchLibraryNodes.js';
import '/imports/server/publications/archiveFiles.js'; import '/imports/server/publications/archiveFiles.js';
import '/imports/server/publications/userImages.js'; import '/imports/server/publications/userImages.js';
import '/imports/server/publications/docs.js';

View File

@@ -2,9 +2,10 @@ import SimpleSchema from 'simpl-schema';
import '/imports/api/users/Users.js'; import '/imports/api/users/Users.js';
import Invites from '/imports/api/users/Invites.js'; import Invites from '/imports/api/users/Invites.js';
Meteor.publish('user', function(){ Meteor.publish('user', function () {
return [ return [
Meteor.users.find(this.userId, {fields: { Meteor.users.find(this.userId, {
fields: {
roles: 1, roles: 1,
username: 1, username: 1,
apiKey: 1, apiKey: 1,
@@ -22,11 +23,12 @@ Meteor.publish('user', function(){
'services.google.name': 1, 'services.google.name': 1,
'services.google.email': 1, 'services.google.email': 1,
'services.google.locale': 1, 'services.google.locale': 1,
}}), }
}),
Invites.find({ Invites.find({
$or: [ $or: [
{inviter: this.userId}, { inviter: this.userId },
{invitee: this.userId} { invitee: this.userId }
], ],
}, { }, {
fields: { fields: {
@@ -41,19 +43,19 @@ let userIdsSchema = new SimpleSchema({
type: Array, type: Array,
optional: true, optional: true,
}, },
'ids.$':{ 'ids.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
} }
}) })
Meteor.publish('userPublicProfiles', function(ids){ Meteor.publish('userPublicProfiles', function (ids) {
userIdsSchema.validate({ids}); userIdsSchema.validate({ ids });
if (!this.userId || !ids) return this.ready(); if (!this.userId || !ids) return this.ready();
return Meteor.users.find({ return Meteor.users.find({
_id: {$in: ids} _id: { $in: ids }
},{ }, {
fields: {username: 1}, fields: { username: 1 },
sort: {username: 1}, sort: { username: 1 },
}); });
}); });

View File

@@ -19,34 +19,39 @@ export default {
</script> </script>
<style lang="css"> <style lang="css">
.column-layout { .column-layout {
column-count: 12; column-count: 12;
column-fill: balance; column-fill: balance;
column-gap: 0; column-gap: 0;
column-width: 240px; column-width: 240px;
transform: translateZ(0); transform: translateZ(0);
padding: 4px; padding: 4px;
} }
.column-layout.wide-columns {
.column-layout.wide-columns {
column-count: 12; column-count: 12;
column-fill: balance; column-fill: balance;
column-gap: 0; column-gap: 0;
column-width: 320px; column-width: 320px;
transform: translateZ(0); transform: translateZ(0);
padding: 4px; padding: 4px;
} }
.column-layout > div, .column-layout > span > div {
.column-layout>div,
.column-layout>span>div {
/* /*
Table and width set because firefox does not support break-inside: avoid Table and width set because firefox does not support break-inside: avoid
*/ */
display: table; display: table;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
transform: translateX(0);
-webkit-transform: translateX(0); -webkit-transform: translateX(0);
-webkit-column-break-inside: avoid; -webkit-column-break-inside: avoid;
page-break-inside: avoid; page-break-inside: avoid;
break-inside: avoid; break-inside: avoid;
padding: 4px; padding: 4px;
} }
</style> </style>

View File

@@ -65,7 +65,7 @@
</template> </template>
<script lang="js"> <script lang="js">
export default { export default {
inject: { inject: {
context: { default: {} } context: { default: {} }
}, },
@@ -93,12 +93,12 @@
}, },
}, },
methods: { methods: {
resetData(){ resetData() {
this.editValue = this.value; this.editValue = this.value;
this.operation = 'set'; this.operation = 'set';
// this.$nextTick didn't work, using timeout instead did // this.$nextTick didn't work, using timeout instead did
setTimeout(() => { setTimeout(() => {
if (this.$refs.editInput){ if (this.$refs.editInput) {
this.$refs.editInput.focus(); this.$refs.editInput.focus();
} }
}, 100); }, 100);
@@ -125,11 +125,11 @@
return 'mdi-minus'; return 'mdi-minus';
} }
}, },
toggleAdd(){ toggleAdd() {
this.operation = (this.operation === 'add') ? 'set': 'add'; this.operation = (this.operation === 'add') ? 'set' : 'add';
}, },
toggleSubtract(){ toggleSubtract() {
this.operation = (this.operation === 'subtract') ? 'set': 'subtract'; this.operation = (this.operation === 'subtract') ? 'set' : 'subtract';
}, },
keypress(event) { keypress(event) {
let digitsOnly = /[0-9]/; let digitsOnly = /[0-9]/;
@@ -142,25 +142,26 @@
event.preventDefault(); event.preventDefault();
} else if (key === 'Enter') { } else if (key === 'Enter') {
this.commitEdit(); this.commitEdit();
} else if (!digitsOnly.test(key)){ } else if (!digitsOnly.test(key)) {
event.preventDefault(); event.preventDefault();
} }
}, },
input(value){ input(value) {
if (+value < 0){ if (+value < 0) {
this.editValue = -value; this.editValue = -value;
this.operation = 'subtract'; this.operation = 'subtract';
} }
} }
} }
}; };
</script> </script>
<style scoped> <style scoped>
.filled.theme--light { .filled.theme--light {
background: #fff !important; background: #fff !important;
} }
.filled.theme--dark {
.filled.theme--dark {
background: #424242 !important; background: #424242 !important;
} }
</style> </style>

View File

@@ -1,15 +1,16 @@
<template lang="html"> <template lang="html">
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div
class="markdown" class="markdown"
@click="e => $emit('click', e)"
v-html="compiledMarkdown" v-html="compiledMarkdown"
/> />
</template> </template>
<script lang="js"> <script lang="js">
import { marked } from 'marked'; import { marked } from 'marked';
export default { export default {
props: { props: {
markdown: { markdown: {
type: String, type: String,
@@ -22,5 +23,5 @@
return marked(this.markdown); return marked(this.markdown);
}, },
}, },
} }
</script> </script>

View File

@@ -27,54 +27,58 @@
</template> </template>
<script lang="js"> <script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js'; import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import CardHighlight from '/imports/ui/components/CardHighlight.vue'; import CardHighlight from '/imports/ui/components/CardHighlight.vue';
export default { export default {
components: { components: {
CardHighlight, CardHighlight,
}, },
props: { props: {
color: { color: {
type: String, type: String,
default(){ default() {
return getThemeColor('secondary'); return getThemeColor('secondary');
}, },
}, },
transparentToolbar: Boolean, transparentToolbar: Boolean,
}, },
data(){ return { data() {
return {
hovering: false, hovering: false,
}}, }
},
computed: { computed: {
isDark(){ isDark() {
return isDarkColor(this.color); return isDarkColor(this.color);
}, },
hasClickListener(){ hasClickListener() {
return this.$listeners && !!this.$listeners.click; return this.$listeners && !!this.$listeners.click;
}, },
hasToolbarClickListener(){ hasToolbarClickListener() {
return this.$listeners && !!this.$listeners.toolbarclick; return this.$listeners && !!this.$listeners.toolbarclick;
}, },
}, },
methods: { methods: {
hoverToolbar(val){ hoverToolbar(val) {
this.hovering = this.$listeners && this.hovering = this.$listeners &&
!!this.$listeners.toolbarclick && !!this.$listeners.toolbarclick &&
val; val;
} }
} }
}; };
</script> </script>
<style lang="css"> <style lang="css">
.toolbar-card .v-toolbar__title { .toolbar-card .v-toolbar__title {
font-size: 15px; font-size: 15px;
} }
.toolbar-card { .toolbar-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
} }
.toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet { .toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet {
background-color: #303030; background-color: #303030;
} }

View File

@@ -35,16 +35,18 @@ import { format } from 'date-fns';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],
data(){return { data() {
return {
menu: false, menu: false,
};}, };
},
computed: { computed: {
formattedSafeValue(){ formattedSafeValue() {
return format(this.safeValue, 'YYYY-MM-DD') return format(this.safeValue, 'YYYY-MM-DD')
}, },
}, },
methods: { methods: {
dateInput(e){ dateInput(e) {
this.menu = false; this.menu = false;
this.input(e); this.input(e);
}, },
@@ -53,4 +55,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -97,16 +97,18 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
menu: false, menu: false,
searchString: '', searchString: '',
icons: [], icons: [],
};}, };
},
watch: { watch: {
menu(value){ menu(value) {
if (value){ if (value) {
setTimeout(() => { setTimeout(() => {
if (this.$refs.iconSearchField){ if (this.$refs.iconSearchField) {
this.$refs.iconSearchField.$children[0].focus(); this.$refs.iconSearchField.$children[0].focus();
} }
}, 100); }, 100);
@@ -114,15 +116,15 @@ export default {
}, },
}, },
methods: { methods: {
search(value, ack){ search(value, ack) {
this.searchString = value; this.searchString = value;
this.icons = []; this.icons = [];
findIcons.call({search: value}, (error, result) => { findIcons.call({ search: value }, (error, result) => {
ack(error); ack(error);
this.icons = result; this.icons = result;
}); });
}, },
select(icon){ select(icon) {
this.menu = false; this.menu = false;
this.change(icon); this.change(icon);
}, },
@@ -131,4 +133,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -21,23 +21,25 @@
</template> </template>
<script lang="js"> <script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],
props: { props: {
multiple: Boolean, multiple: Boolean,
}, },
data(){ return { data() {
return {
searchInput: '', searchInput: '',
}}, }
},
computed: { computed: {
// Multiple combobox gets a long default debounce time while single // Multiple combobox gets a long default debounce time while single
// value gets a shorter one // value gets a shorter one
debounceTime() { debounceTime() {
if (Number.isFinite(this.debounce)){ if (Number.isFinite(this.debounce)) {
return this.debounce; return this.debounce;
} else if (Number.isFinite(this.context.debounceTime)){ } else if (Number.isFinite(this.context.debounceTime)) {
return this.context.debounceTime; return this.context.debounceTime;
} else { } else {
return this.multiple ? 1000 : 100; return this.multiple ? 1000 : 100;
@@ -45,10 +47,10 @@
}, },
}, },
methods: { methods: {
customChange(val){ customChange(val) {
this.input(val); this.input(val);
this.searchInput = ''; this.searchInput = '';
}, },
} }
}; };
</script> </script>

View File

@@ -13,7 +13,8 @@ export default {
context: { default: {} } context: { default: {} }
}, },
inheritAttrs: false, inheritAttrs: false,
data(){ return { data() {
return {
error: false, error: false,
ackErrors: null, ackErrors: null,
rulesErrors: null, rulesErrors: null,
@@ -22,7 +23,8 @@ export default {
dirty: false, dirty: false,
safeValue: this.value, safeValue: this.value,
inputValue: this.value, inputValue: this.value,
};}, };
},
props: { props: {
value: [String, Number, Date, Array, Object, Boolean], value: [String, Number, Date, Array, Object, Boolean],
errorMessages: [String, Array], errorMessages: [String, Array],
@@ -34,11 +36,11 @@ export default {
rules: Array, rules: Array,
}, },
watch: { watch: {
focused(newFocus){ focused(newFocus) {
// If the value updated while we were focused, show it now on defocus // If the value updated while we were focused, show it now on defocus
// but not if we are waiting for our own writes to get persisted // but not if we are waiting for our own writes to get persisted
// and not if there is an error in our input // and not if there is an error in our input
if (!newFocus && !this.dirty && !this.error){ if (!newFocus && !this.dirty && !this.error) {
this.forceSafeValueUpdate(); this.forceSafeValueUpdate();
} }
// Start the loading bar on defocus if the input is dirty // Start the loading bar on defocus if the input is dirty
@@ -48,118 +50,118 @@ export default {
!newFocus && !newFocus &&
this.dirty && this.dirty &&
!(this.rulesErrors && this.rulesErrors.length) !(this.rulesErrors && this.rulesErrors.length)
){ ) {
if (this.hasChangeListener) this.loading = true; if (this.hasChangeListener) this.loading = true;
} }
}, },
dirty(newDirty){ dirty(newDirty) {
// Our changes were acknowledged, weren't in error, and we aren't focused, // Our changes were acknowledged, weren't in error, and we aren't focused,
// make sure the internal value matches the database value // make sure the internal value matches the database value
if (!newDirty && !this.focused && !this.error){ if (!newDirty && !this.focused && !this.error) {
this.forceSafeValueUpdate(); this.forceSafeValueUpdate();
} }
}, },
value(newValue){ value(newValue) {
if ( if (
!this.focused && !this.focused &&
!(this.rulesErrors && this.rulesErrors.length) !(this.rulesErrors && this.rulesErrors.length)
){ ) {
this.safeValue = newValue; this.safeValue = newValue;
} }
}, },
safeValue(){ safeValue() {
// The safe value only gets updated from the parent, so it must be valid // The safe value only gets updated from the parent, so it must be valid
this.error = false; this.error = false;
this.ackErrors = null; this.ackErrors = null;
}, },
}, },
methods: { methods: {
input(val){ input(val) {
this.$emit('input', val); this.$emit('input', val);
this.inputValue = val; this.inputValue = val;
this.dirty = true; this.dirty = true;
// Apply the rules if there are any // Apply the rules if there are any
this.rulesErrors = null; this.rulesErrors = null;
if (this.rules && this.rules.length){ if (this.rules && this.rules.length) {
this.rules.forEach(rule => { this.rules.forEach(rule => {
const result = rule(val); const result = rule(val);
if (typeof result === 'string'){ if (typeof result === 'string') {
if (!this.rulesErrors) this.rulesErrors = []; if (!this.rulesErrors) this.rulesErrors = [];
this.rulesErrors.push(result); this.rulesErrors.push(result);
} }
}); });
} }
if (this.rulesErrors){ if (this.rulesErrors) {
return; return;
} }
this.debouncedChange(val); this.debouncedChange(val);
}, },
acknowledgeChange(error){ acknowledgeChange(error) {
this.loading = false; this.loading = false;
this.dirty = false; this.dirty = false;
this.error = !!error; this.error = !!error;
if (!error){ if (!error) {
this.ackErrors = null; this.ackErrors = null;
} else if (typeof error === 'string'){ } else if (typeof error === 'string') {
this.ackErrors = error; this.ackErrors = error;
} else if (error.reason){ } else if (error.reason) {
this.ackErrors = error.reason; this.ackErrors = error.reason;
} else if (error.message){ } else if (error.message) {
this.ackErrors = error.message; this.ackErrors = error.message;
} else { } else {
this.ackErrors = 'Something went wrong' this.ackErrors = 'Something went wrong'
console.error(error); console.error(error);
} }
}, },
change(val){ change(val) {
this.dirty = true; this.dirty = true;
if (this.hasChangeListener()) this.loading = true; if (this.hasChangeListener()) this.loading = true;
this.$emit('change', val, this.acknowledgeChange); this.$emit('change', val, this.acknowledgeChange);
}, },
hasChangeListener(){ hasChangeListener() {
return this.$listeners && this.$listeners.change; return this.$listeners && this.$listeners.change;
}, },
forceSafeValueUpdate(){ forceSafeValueUpdate() {
// hack to force the value to update on the child component // hack to force the value to update on the child component
this.safeValue = null; this.safeValue = null;
this.$nextTick(() => this.safeValue = this.value); this.$nextTick(() => this.safeValue = this.value);
}, },
focus(){ focus() {
this.$refs.input.focus(); this.$refs.input.focus();
} }
}, },
computed: { computed: {
errors(){ errors() {
let errors = this.ackErrors ? [this.ackErrors] : []; let errors = this.ackErrors ? [this.ackErrors] : [];
if (Array.isArray(this.rulesErrors)){ if (Array.isArray(this.rulesErrors)) {
errors.push(...this.rulesErrors) errors.push(...this.rulesErrors)
} }
if (Array.isArray(this.errorMessages)){ if (Array.isArray(this.errorMessages)) {
errors.push(...this.errorMessages); errors.push(...this.errorMessages);
} else if (typeof this.errorMessages === 'string' && this.errorMessages){ } else if (typeof this.errorMessages === 'string' && this.errorMessages) {
errors.push(this.errorMessages); errors.push(this.errorMessages);
} }
return errors; return errors;
}, },
isDisabled(){ isDisabled() {
return this.context.editPermission === false || this.disabled; return this.context.editPermission === false || this.disabled;
}, },
debounceTime() { debounceTime() {
if (Number.isFinite(this.debounce)){ if (Number.isFinite(this.debounce)) {
return this.debounce; return this.debounce;
} else if (Number.isFinite(this.context.debounceTime)){ } else if (Number.isFinite(this.context.debounceTime)) {
return this.context.debounceTime; return this.context.debounceTime;
} else { } else {
return 750; return 750;
} }
}, },
}, },
created(){ created() {
this.debouncedChange = debounce(this.change, this.debounceTime); this.debouncedChange = debounce(this.change, this.debounceTime);
}, },
beforeDestroy(){ beforeDestroy() {
this.debouncedChange.flush(); this.debouncedChange.flush();
}, },
}; };

View File

@@ -23,9 +23,9 @@
</template> </template>
<script lang="js"> <script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],
}; };
</script> </script>

View File

@@ -14,9 +14,9 @@
</template> </template>
<script lang="js"> <script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js'; import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],
props: { props: {
autoGrow: { autoGrow: {
@@ -24,5 +24,5 @@
default: false, default: false,
}, },
}, },
}; };
</script> </script>

View File

@@ -54,6 +54,19 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-item
v-if="docsPath"
@click="helpDialog"
>
<v-list-item-content>
<v-list-item-title>
Help
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-help</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item <v-list-item
v-if="$listeners && $listeners.duplicate" v-if="$listeners && $listeners.duplicate"
@click="$emit('duplicate')" @click="$emit('duplicate')"
@@ -137,6 +150,7 @@ import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js'; import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue'; import ColorPicker from '/imports/ui/components/ColorPicker.vue';
import getThemeColor from '/imports/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
export default { export default {
components: { components: {
@@ -171,7 +185,11 @@ export default {
} }
} }
return model.name || getPropertyName(model.type); return model.name || getPropertyName(model.type);
} },
docsPath() {
const propDef = PROPERTIES[this.model.type];
return propDef && propDef.docsPath;
},
}, },
methods: { methods: {
colorChanged(value){ colorChanged(value){
@@ -180,6 +198,15 @@ export default {
back(){ back(){
this.$store.dispatch('popDialogStack'); this.$store.dispatch('popDialogStack');
}, },
helpDialog() {
this.$store.commit('pushDialogStack', {
component: 'help-dialog',
elementId: 'property-toolbar-menu-button',
data: {
path: this.docsPath,
},
});
},
} }
} }
</script> </script>

View File

@@ -77,18 +77,18 @@
</template> </template>
<script lang="js"> <script lang="js">
/** /**
* TreeNode's are list item views of character properties. Every property which * TreeNode's are list item views of character properties. Every property which
* can belong to the character is shown in the tree view of the character * can belong to the character is shown in the tree view of the character
* the tree view shows off the full character structure, and where each part of * the tree view shows off the full character structure, and where each part of
* character comes from. * character comes from.
**/ **/
import { canBeParent } from '/imports/api/parenting/parenting.js'; import { canBeParent } from '/imports/api/parenting/parenting.js';
import { getPropertyIcon } from '/imports/constants/PROPERTIES.js'; import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import { some } from 'lodash'; import { some } from 'lodash';
export default { export default {
name: 'TreeNode', name: 'TreeNode',
components: { components: {
TreeNodeView, TreeNodeView,
@@ -100,7 +100,7 @@
}, },
group: { group: {
type: String, type: String,
required: true, default: undefined,
}, },
organize: Boolean, organize: Boolean,
children: { children: {
@@ -118,38 +118,40 @@
selected: Boolean, selected: Boolean,
startExpanded: Boolean, startExpanded: Boolean,
}, },
data(){return { data() {
return {
expanded: this.startExpanded || this.node._ancestorOfMatchedDocument || expanded: this.startExpanded || this.node._ancestorOfMatchedDocument ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) || some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
false, false,
}}, }
},
computed: { computed: {
hasChildren(){ hasChildren() {
return this.children && !!this.children.length || this.lazy && !this.expanded; return this.children && !!this.children.length || this.lazy && !this.expanded;
}, },
showExpanded(){ showExpanded() {
return this.expanded && (this.organize || this.hasChildren) return this.expanded && (this.organize || this.hasChildren)
}, },
computedChildren(){ computedChildren() {
let children = []; let children = [];
if (this.children){ if (this.children) {
children.push(...this.children) children.push(...this.children)
} }
if (this.getChildren){ if (this.getChildren) {
children.push(...this.getChildren()) children.push(...this.getChildren())
} }
return children; return children;
}, },
canExpand(){ canExpand() {
return canBeParent(this.node.type); return canBeParent(this.node.type);
}, },
}, },
watch: { watch: {
'node._ancestorOfMatchedDocument'(value){ 'node._ancestorOfMatchedDocument'(value) {
this.expanded = !!value || this.expanded = !!value ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id); some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
}, },
'selectedNode.ancestors'(value){ 'selectedNode.ancestors'(value) {
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded; this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
}, },
}, },
@@ -157,54 +159,67 @@
this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default
}, },
methods: { methods: {
icon(type){ icon(type) {
return getPropertyIcon(type); return getPropertyIcon(type);
}, },
} }
}; };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
.rotate-90 { .rotate-90 {
transform: rotate(90deg) translateZ(0); transform: rotate(90deg) translateZ(0);
} }
.drag-area {
.drag-area {
box-shadow: -2px 0px 0px 0px #808080; box-shadow: -2px 0px 0px 0px #808080;
margin-left: 0; margin-left: 0;
min-height: 32px; min-height: 32px;
} }
.handle {
.handle {
cursor: move; cursor: move;
} }
.empty .drag-area {
.empty .drag-area {
box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4); box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4);
} }
.empty .v-btn {
.empty .v-btn {
opacity: 0.4; opacity: 0.4;
} }
.found {
.found {
background: rgba(200, 0, 0, 0.1) !important; background: rgba(200, 0, 0, 0.1) !important;
} }
.ghost {
.ghost {
opacity: 0.5; opacity: 0.5;
background: rgba(251, 0, 0, 0.3); background: rgba(251, 0, 0, 0.3);
} }
.v-icon.v-icon--disabled {
.v-icon.v-icon--disabled {
opacity: 0; opacity: 0;
} }
.v-icon {
.v-icon {
transition: none !important; transition: none !important;
} }
.theme--light .tree-node-title:hover {
background-color: rgba(0,0,0,.04); .theme--light .tree-node-title:hover {
} background-color: rgba(0, 0, 0, .04);
.theme--dark .tree-node-title:hover { }
background-color: rgba(255,255,255,.04);
} .theme--dark .tree-node-title:hover {
.tree-node-title{ background-color: rgba(255, 255, 255, .04);
}
.tree-node-title {
transition: background ease 0.3s, color ease 0.15s; transition: background ease 0.3s, color ease 0.15s;
} }
.tree-node-title, .dummy-node {
.tree-node-title,
.dummy-node {
height: 40px; height: 40px;
} }
</style> </style>

View File

@@ -33,18 +33,24 @@
</template> </template>
<script lang="js"> <script lang="js">
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import TreeNode from '/imports/ui/components/tree/TreeNode.vue'; import TreeNode from '/imports/ui/components/tree/TreeNode.vue';
import { isParentAllowed } from '/imports/api/parenting/parenting.js'; import { isParentAllowed } from '/imports/api/parenting/parenting.js';
export default { export default {
components: { components: {
draggable, draggable,
TreeNode, TreeNode,
}, },
props: { props: {
node: Object, node: {
group: String, type: Object,
default: undefined,
},
group: {
type: String,
default: undefined,
},
organize: Boolean, organize: Boolean,
lazy: Boolean, lazy: Boolean,
children: { children: {
@@ -61,39 +67,41 @@
}, },
startExpanded: Boolean, startExpanded: Boolean,
}, },
data(){ return { data() {
return {
expanded: this.startExpanded || false, expanded: this.startExpanded || false,
displayedChildren: [], displayedChildren: [],
}}, }
},
computed: { computed: {
hasChildren(){ hasChildren() {
return this.children && this.children.length; return this.children && this.children.length;
}, },
showExpanded(){ showExpanded() {
return this.expanded && (this.organize || this.hasChildren) return this.expanded && (this.organize || this.hasChildren)
}, },
}, },
watch: { watch: {
children(value){ children(value) {
this.displayedChildren = value; this.displayedChildren = value;
} }
}, },
mounted(){ mounted() {
this.displayedChildren = this.children; this.displayedChildren = this.children;
}, },
methods: { methods: {
change({added, moved}){ change({ added, moved }) {
let event = moved || added; let event = moved || added;
if (event){ if (event) {
let doc = event.element.node; let doc = event.element.node;
let newIndex; let newIndex;
if (event.newIndex === 0){ if (event.newIndex === 0) {
newIndex = -0.5; newIndex = -0.5;
} else { } else {
if (event.newIndex < this.children.length){ if (event.newIndex < this.children.length) {
let childAtNewIndex = this.children[event.newIndex]; let childAtNewIndex = this.children[event.newIndex];
let indexOrder = childAtNewIndex.node.order; let indexOrder = childAtNewIndex.node.order;
if (event.newIndex > event.oldIndex){ if (event.newIndex > event.oldIndex) {
newIndex = indexOrder + 0.5; newIndex = indexOrder + 0.5;
} else { } else {
newIndex = indexOrder - 0.5; newIndex = indexOrder - 0.5;
@@ -103,32 +111,34 @@
newIndex = childBeforeNewIndex.node.order + 0.5; newIndex = childBeforeNewIndex.node.order + 0.5;
} }
} }
if (moved){ if (moved) {
this.$emit('reordered', {doc, newIndex}); this.$emit('reordered', { doc, newIndex });
} else if (added){ } else if (added) {
this.$emit('reorganized', {doc, parent: this.node, newIndex}); this.$emit('reorganized', { doc, parent: this.node, newIndex });
} }
} }
}, },
move(evt){ move(evt) {
let parentNode = evt.relatedContext.component.$parent.node let parentNode = evt.relatedContext.component.$parent.node
let parentType = parentNode && parentNode.type || 'root'; let parentType = parentNode && parentNode.type || 'root';
let childType = evt.draggedContext.element.node.type; let childType = evt.draggedContext.element.node.type;
let allowed = isParentAllowed({parentType, childType}); let allowed = isParentAllowed({ parentType, childType });
return allowed; return allowed;
}, },
}, },
}; };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
.flip-list-leave-active { .flip-list-leave-active {
display: none; display: none;
} }
.flip-list-move {
.flip-list-move {
transition: transform 0.5s; transition: transform 0.5s;
} }
.no-move {
.no-move {
transition: transform 0s; transition: transform 0s;
} }
</style> </style>

View File

@@ -121,10 +121,10 @@
<script lang="js"> <script lang="js">
import { union, without, debounce } from 'lodash'; import { union, without, debounce } from 'lodash';
import FormSection, {FormSections} from '/imports/ui/properties/forms/shared/FormSection.vue'; import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
import LibraryList from '/imports/ui/library/LibraryList.vue'; import LibraryList from '/imports/ui/library/LibraryList.vue';
import LibraryCollections from '/imports/api/library/LibraryCollections.js'; import LibraryCollections from '/imports/api/library/LibraryCollections.js';
import {changeAllowedLibraries, toggleAllUserLibraries} from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js'; import { changeAllowedLibraries, toggleAllUserLibraries } from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
export default { export default {
components: { components: {
@@ -149,7 +149,8 @@ export default {
}, },
disabled: Boolean, disabled: Boolean,
}, },
data() { return { data() {
return {
libraryCollections: this.model.allowedLibraryCollections, libraryCollections: this.model.allowedLibraryCollections,
libraries: this.model.allowedLibraries, libraries: this.model.allowedLibraries,
libraryWriteLoading: false, libraryWriteLoading: false,
@@ -211,29 +212,29 @@ export default {
}, },
}, },
methods: { methods: {
changeShowTreeTab(value){ changeShowTreeTab(value) {
this.$emit('change', { this.$emit('change', {
path: ['settings','showTreeTab'], path: ['settings', 'showTreeTab'],
value: !!value value: !!value
}); });
let currentTab = this.$store.getters.tabById(this.model._id); let currentTab = this.$store.getters.tabById(this.model._id);
if (!value && currentTab === 5){ if (!value && currentTab === 5) {
this.$store.commit( this.$store.commit(
'setTabForCharacterSheet', 'setTabForCharacterSheet',
{id: this.model._id, tab: 4} { id: this.model._id, tab: 4 }
); );
} }
}, },
changeHideSpellsTab(value){ changeHideSpellsTab(value) {
this.$emit('change', { this.$emit('change', {
path: ['settings','hideSpellsTab'], path: ['settings', 'hideSpellsTab'],
value: !value value: !value
}); });
let currentTab = this.$store.getters.tabById(this.model._id); let currentTab = this.$store.getters.tabById(this.model._id);
if (!value && currentTab === 3){ if (!value && currentTab === 3) {
this.$store.commit( this.$store.commit(
'setTabForCharacterSheet', 'setTabForCharacterSheet',
{id: this.model._id, tab: 4} { id: this.model._id, tab: 4 }
); );
} }
}, },
@@ -266,4 +267,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -1,5 +1,8 @@
<template lang="html"> <template lang="html">
<dialog-base v-if="model" :color="model.color"> <dialog-base
v-if="model"
:color="model.color"
>
<template slot="toolbar"> <template slot="toolbar">
<v-toolbar-title> <v-toolbar-title>
Character Details Character Details
@@ -50,10 +53,10 @@ export default {
startInEditTab: Boolean, startInEditTab: Boolean,
}, },
meteor: { meteor: {
model(){ model() {
return Creatures.findOne(this._id); return Creatures.findOne(this._id);
}, },
editPermission(){ editPermission() {
try { try {
assertEditPermission(this.model, Meteor.userId()); assertEditPermission(this.model, Meteor.userId());
return true; return true;
@@ -63,10 +66,10 @@ export default {
}, },
}, },
methods: { methods: {
change({path, value, ack}){ change({ path, value, ack }) {
updateCreature.call({_id: this._id, path, value}, (error) =>{ updateCreature.call({ _id: this._id, path, value }, (error) => {
if (error){ if (error) {
if(ack){ if (ack) {
ack(error && error.reason || error) ack(error && error.reason || error)
} else { } else {
console.error(error) console.error(error)
@@ -81,4 +84,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -12,9 +12,9 @@
</template> </template>
<script lang="js"> <script lang="js">
import BuildTreeNode from '/imports/ui/creature/buildTree/BuildTreeNode.vue'; import BuildTreeNode from '/imports/ui/creature/buildTree/BuildTreeNode.vue';
export default { export default {
components: { components: {
BuildTreeNode, BuildTreeNode,
}, },
@@ -28,8 +28,10 @@
default: undefined, default: undefined,
}, },
}, },
data(){ return { data() {
return {
expanded: false, expanded: false,
}}, }
}; },
};
</script> </script>

View File

@@ -43,11 +43,13 @@ export default {
props: { props: {
id: String, id: String,
}, },
data(){return { data() {
return {
inputName: undefined, inputName: undefined,
}}, }
},
computed: { computed: {
nameMatch(){ nameMatch() {
if (!this.name) return true; if (!this.name) return true;
let uppername = this.name.toUpperCase(); let uppername = this.name.toUpperCase();
let upperInputName = this.inputName && this.inputName.toUpperCase(); let upperInputName = this.inputName && this.inputName.toUpperCase();
@@ -55,19 +57,19 @@ export default {
}, },
}, },
meteor: { meteor: {
name(){ name() {
let creature = Creatures.findOne(this.id, {fields: {name: 1}}); let creature = Creatures.findOne(this.id, { fields: { name: 1 } });
return creature && creature.name; return creature && creature.name;
}, },
}, },
methods: { methods: {
remove(){ remove() {
this.$router.push('/characterList'); this.$router.push('/characterList');
this.$store.dispatch('popDialogStack'); this.$store.dispatch('popDialogStack');
removeCreature.call({charId: this.id}, (error) => { removeCreature.call({ charId: this.id }, (error) => {
if (error) { if (error) {
console.error(error); console.error(error);
snackbar({text: error.message || error.toString()}); snackbar({ text: error.message || error.toString() });
} }
}); });
} }
@@ -76,4 +78,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -12,9 +12,7 @@
size="64" size="64"
/> />
</div> </div>
<div <div v-else-if="!creature">
v-else-if="!creature"
>
<v-layout <v-layout
column column
align-center align-center
@@ -55,9 +53,7 @@
<v-tab-item> <v-tab-item>
<inventory-tab :creature-id="creatureId" /> <inventory-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item <v-tab-item v-if="!creature.settings.hideSpellsTab">
v-if="!creature.settings.hideSpellsTab"
>
<spells-tab :creature-id="creatureId" /> <spells-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item> <v-tab-item>
@@ -66,9 +62,7 @@
<v-tab-item> <v-tab-item>
<build-tab :creature-id="creatureId" /> <build-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item <v-tab-item v-if="creature.settings.showTreeTab">
v-if="creature.settings.showTreeTab"
>
<tree-tab :creature-id="creatureId" /> <tree-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
</v-tabs-items> </v-tabs-items>
@@ -78,21 +72,21 @@
</template> </template>
<script lang="js"> <script lang="js">
//TODO add a "no character found" screen if shown on a false address //TODO add a "no character found" screen if shown on a false address
// or on a character the user does not have permission to view // or on a character the user does not have permission to view
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue'; import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue';
import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue'; import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue'; import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue'; import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/JournalTab.vue'; import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/JournalTab.vue';
import BuildTab from '/imports/ui/creature/character/characterSheetTabs/BuildTab.vue'; import BuildTab from '/imports/ui/creature/character/characterSheetTabs/BuildTab.vue';
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue'; import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {
components: { components: {
StatsTab, StatsTab,
FeaturesTab, FeaturesTab,
@@ -114,27 +108,27 @@
}, },
computed: { computed: {
activeTab: { activeTab: {
get(){ get() {
return this.tabs; return this.tabs;
}, },
set(newTab){ set(newTab) {
this.$emit('update:tabs', newTab); this.$emit('update:tabs', newTab);
}, },
}, },
}, },
watch: { watch: {
'creature.name'(value){ 'creature.name'(value) {
this.$store.commit('setPageTitle', value || 'Character Sheet'); this.$store.commit('setPageTitle', value || 'Character Sheet');
}, },
}, },
mounted(){ mounted() {
this.$store.commit('setPageTitle', this.creature && this.creature.name || 'Character Sheet'); this.$store.commit('setPageTitle', this.creature && this.creature.name || 'Character Sheet');
this.nameObserver = Creatures.find({ this.nameObserver = Creatures.find({
creatureId: this.creatureId, creatureId: this.creatureId,
}, { }, {
fields: {name: 1}, fields: { name: 1 },
}).observe({ }).observe({
added: ({name}) => added: ({ name }) =>
this.$store.commit('setPageTitle', name || 'Character Sheet'), this.$store.commit('setPageTitle', name || 'Character Sheet'),
changed: ({ name }) => changed: ({ name }) =>
this.$store.commit('setPageTitle', name || 'Character Sheet'), this.$store.commit('setPageTitle', name || 'Character Sheet'),
@@ -143,29 +137,29 @@
this.logObserver = CreatureLogs.find({ this.logObserver = CreatureLogs.find({
creatureId: this.creatureId, creatureId: this.creatureId,
}).observe({ }).observe({
added({content}){ added({ content }) {
if (!that.$subReady.singleCharacter) return; if (!that.$subReady.singleCharacter) return;
if (that.$store.state.rightDrawer) return; if (that.$store.state.rightDrawer) return;
snackbar({content}); snackbar({ content });
}, },
}); });
}, },
beforeDestroy(){ beforeDestroy() {
this.nameObserver.stop(); this.nameObserver.stop();
this.logObserver.stop(); this.logObserver.stop();
}, },
meteor: { meteor: {
$subscribe: { $subscribe: {
'singleCharacter'(){ 'singleCharacter'() {
return [this.creatureId]; return [this.creatureId];
}, },
}, },
creature(){ creature() {
return Creatures.findOne(this.creatureId, { return Creatures.findOne(this.creatureId, {
fields: {variables: 0} fields: { variables: 0 }
}); });
}, },
editPermission(){ editPermission() {
try { try {
assertEditPermission(this.creature, Meteor.userId()); assertEditPermission(this.creature, Meteor.userId());
return true; return true;
@@ -174,12 +168,12 @@
} }
}, },
}, },
} }
</script> </script>
<style> <style>
.character-sheet .v-window-item { .character-sheet .v-window-item {
min-height: calc(100vh - 96px); min-height: calc(100vh - 96px);
overflow: hidden; overflow: hidden;
} }
</style> </style>

View File

@@ -11,17 +11,13 @@
dense dense
> >
<v-app-bar-nav-icon @click="toggleDrawer" /> <v-app-bar-nav-icon @click="toggleDrawer" />
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<v-toolbar-title :key="$store.state.pageTitle"> <v-toolbar-title :key="$store.state.pageTitle">
{{ $store.state.pageTitle }} {{ $store.state.pageTitle }}
</v-toolbar-title> </v-toolbar-title>
</v-fade-transition> </v-fade-transition>
<v-spacer /> <v-spacer />
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<v-layout <v-layout
:key="$route.meta.title" :key="$route.meta.title"
class="flex-shrink-0 flex-grow-0" class="flex-shrink-0 flex-grow-0"
@@ -151,17 +147,17 @@ export default {
context: { default: {} } context: { default: {} }
}, },
computed: { computed: {
creatureId(){ creatureId() {
return this.$route.params.id; return this.$route.params.id;
}, },
toolbarColor(){ toolbarColor() {
if (this.creature && this.creature.color){ if (this.creature && this.creature.color) {
return this.creature.color; return this.creature.color;
} else { } else {
return getThemeColor('secondary'); return getThemeColor('secondary');
} }
}, },
isDark(){ isDark() {
return isDarkColor(this.toolbarColor); return isDarkColor(this.toolbarColor);
}, },
}, },
@@ -170,7 +166,7 @@ export default {
'toggleDrawer', 'toggleDrawer',
'toggleRightDrawer', 'toggleRightDrawer',
]), ]),
showCharacterForm(){ showCharacterForm() {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-form-dialog', component: 'creature-form-dialog',
elementId: 'creature-menu', elementId: 'creature-menu',
@@ -179,7 +175,7 @@ export default {
}, },
}); });
}, },
showShareDialog(){ showShareDialog() {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'share-dialog', component: 'share-dialog',
elementId: 'creature-menu', elementId: 'creature-menu',
@@ -191,7 +187,7 @@ export default {
}, },
}); });
}, },
deleteCharacter(){ deleteCharacter() {
let that = this; let that = this;
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'delete-confirmation-dialog', component: 'delete-confirmation-dialog',
@@ -200,9 +196,9 @@ export default {
name: this.creature.name, name: this.creature.name,
typeName: 'Character' typeName: 'Character'
}, },
callback(confirmation){ callback(confirmation) {
if(!confirmation) return; if (!confirmation) return;
removeCreature.call({charId: that.creatureId}, (error) => { removeCreature.call({ charId: that.creatureId }, (error) => {
if (error) { if (error) {
console.error(error); console.error(error);
} else { } else {
@@ -212,7 +208,7 @@ export default {
} }
}); });
}, },
unshareWithMe(){ unshareWithMe() {
updateUserSharePermissions.call({ updateUserSharePermissions.call({
docRef: { docRef: {
collection: 'creatures', collection: 'creatures',
@@ -230,10 +226,10 @@ export default {
}, },
}, },
meteor: { meteor: {
creature(){ creature() {
return Creatures.findOne(this.creatureId); return Creatures.findOne(this.creatureId);
}, },
editPermission(){ editPermission() {
try { try {
assertEditPermission(this.creature, Meteor.userId()); assertEditPermission(this.creature, Meteor.userId());
return true; return true;
@@ -249,9 +245,11 @@ export default {
.character-sheet-toolbar .v-tabs__container--grow .v-tabs__div { .character-sheet-toolbar .v-tabs__container--grow .v-tabs__div {
max-width: 120px !important; max-width: 120px !important;
} }
.character-sheet-toolbar .v-tabs__bar { .character-sheet-toolbar .v-tabs__bar {
background: none !important; background: none !important;
} }
.character-sheet-fab { .character-sheet-fab {
bottom: -24px; bottom: -24px;
right: 8px; right: 8px;

View File

@@ -16,11 +16,11 @@
</template> </template>
<script lang="js"> <script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.vue'; import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.vue';
export default { export default {
components: { components: {
ColumnLayout, ColumnLayout,
FeatureCard, FeatureCard,
@@ -32,28 +32,29 @@
}, },
}, },
meteor: { meteor: {
features(){ features() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
type: 'feature', type: 'feature',
removed: {$ne: true}, removed: { $ne: true },
inactive: {$ne: true}, inactive: { $ne: true },
}, { }, {
sort: {order: 1} sort: { order: 1 }
}); });
}, },
}, },
methods: { methods: {
featureClicked({_id}){ featureClicked({ _id }) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: `${_id}`, elementId: `${_id}`,
data: {_id}, data: { _id },
}); });
}, },
}, },
}; };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -30,9 +30,7 @@
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-list-item-title> <v-list-item-title>
<coin-value <coin-value :value="variables && variables.valueTotal && variables.valueTotal.value|| 0" />
:value="variables && variables.valueTotal && variables.valueTotal.value|| 0"
/>
</v-list-item-title> </v-list-item-title>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
@@ -85,9 +83,7 @@
v-for="container in containersWithoutAncestorContainers" v-for="container in containersWithoutAncestorContainers"
:key="container._id" :key="container._id"
> >
<container-card <container-card :model="container" />
:model="container"
/>
</div> </div>
</column-layout> </column-layout>
</div> </div>
@@ -120,70 +116,74 @@ export default {
required: true, required: true,
}, },
}, },
data(){ return { data() {
return {
organize: false, organize: false,
}}, }
},
meteor: { meteor: {
containers(){ containers() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
type: 'container', type: 'container',
removed: {$ne: true}, removed: { $ne: true },
inactive: {$ne: true}, inactive: { $ne: true },
}, { }, {
sort: {order: 1}, sort: { order: 1 },
}); });
}, },
creature(){ creature() {
return Creatures.findOne(this.creatureId, {fields: { return Creatures.findOne(this.creatureId, {
fields: {
color: 1, color: 1,
variables: 1, variables: 1,
}}); }
});
}, },
variables() { variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
}, },
containersWithoutAncestorContainers(){ containersWithoutAncestorContainers() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': { 'ancestors.id': {
$eq: this.creatureId, $eq: this.creatureId,
$nin: this.containerIds $nin: this.containerIds
}, },
type: 'container', type: 'container',
removed: {$ne: true}, removed: { $ne: true },
inactive: {$ne: true}, inactive: { $ne: true },
}, { }, {
sort: {order: 1}, sort: { order: 1 },
}); });
}, },
carriedItems(){ carriedItems() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': { 'ancestors.id': {
$eq: this.creatureId, $eq: this.creatureId,
$nin: this.containerIds $nin: this.containerIds
}, },
type: 'item', type: 'item',
equipped: {$ne: true}, equipped: { $ne: true },
removed: {$ne: true}, removed: { $ne: true },
deactivatedByAncestor: {$ne: true}, deactivatedByAncestor: { $ne: true },
}, { }, {
sort: {order: 1}, sort: { order: 1 },
}); });
}, },
equippedItems(){ equippedItems() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': { 'ancestors.id': {
$eq: this.creatureId, $eq: this.creatureId,
}, },
type: 'item', type: 'item',
equipped: true, equipped: true,
removed: {$ne: true}, removed: { $ne: true },
inactive: {$ne: true}, inactive: { $ne: true },
}, { }, {
sort: {order: 1}, sort: { order: 1 },
}); });
}, },
equipmentParentRef(){ equipmentParentRef() {
return getParentRefByTag( return getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.equipment this.creatureId, BUILT_IN_TAGS.equipment
) || getParentRefByTag( ) || getParentRefByTag(
@@ -193,7 +193,7 @@ export default {
collection: 'creatures' collection: 'creatures'
}; };
}, },
carriedParentRef(){ carriedParentRef() {
return getParentRefByTag( return getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.carried this.creatureId, BUILT_IN_TAGS.carried
) || getParentRefByTag( ) || getParentRefByTag(
@@ -205,10 +205,10 @@ export default {
}, },
}, },
computed: { computed: {
containerIds(){ containerIds() {
return this.containers.map(container => container._id); return this.containers.map(container => container._id);
}, },
weightCarried(){ weightCarried() {
return stripFloatingPointOddities( return stripFloatingPointOddities(
this.variables && this.variables &&
this.variables.weightCarried && this.variables.weightCarried &&
@@ -217,11 +217,11 @@ export default {
}, },
}, },
methods: { methods: {
clickProperty(_id){ clickProperty(_id) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: `tree-node-${_id}`, elementId: `tree-node-${_id}`,
data: {_id}, data: { _id },
}); });
}, },
}, },
@@ -229,4 +229,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -40,30 +40,32 @@ export default {
required: true, required: true,
} }
}, },
data(){ return { data() {
return {
organize: false, organize: false,
}}, }
},
meteor: { meteor: {
spellLists(){ spellLists() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
type: 'spellList', type: 'spellList',
removed: {$ne: true}, removed: { $ne: true },
inactive: {$ne: true}, inactive: { $ne: true },
}, { }, {
sort: {order: 1} sort: { order: 1 }
}); });
}, },
spellsWithoutList(){ spellsWithoutList() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': { 'ancestors.id': {
$eq: this.creatureId, $eq: this.creatureId,
$nin: this.spellListIds, $nin: this.spellListIds,
}, },
type: 'spell', type: 'spell',
removed: {$ne: true}, removed: { $ne: true },
deactivatedByAncestor: {$ne: true}, deactivatedByAncestor: { $ne: true },
deactivatedByToggle: {$ne: true}, deactivatedByToggle: { $ne: true },
}, { }, {
sort: { sort: {
level: 1, level: 1,
@@ -71,31 +73,31 @@ export default {
} }
}); });
}, },
spellListsWithoutAncestorSpellLists(){ spellListsWithoutAncestorSpellLists() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': { 'ancestors.id': {
$eq: this.creatureId, $eq: this.creatureId,
$nin: this.spellListIds, $nin: this.spellListIds,
}, },
type: 'spellList', type: 'spellList',
removed: {$ne: true}, removed: { $ne: true },
inactive: {$ne: true}, inactive: { $ne: true },
}, { }, {
sort: {order: 1} sort: { order: 1 }
}); });
}, },
}, },
computed: { computed: {
spellListIds(){ spellListIds() {
return this.spellLists.map(spellList => spellList._id); return this.spellLists.map(spellList => spellList._id);
}, },
}, },
methods: { methods: {
clickProperty(_id){ clickProperty(_id) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: `spell-list-tile-${_id}`, elementId: `spell-list-tile-${_id}`,
data: {_id}, data: { _id },
}); });
}, },
}, },
@@ -103,4 +105,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -1,7 +1,5 @@
<template lang="html"> <template lang="html">
<div <div class="stats-tab ma-2">
class="stats-tab ma-2"
>
<health-bar-card-container :creature-id="creatureId" /> <health-bar-card-container :creature-id="creatureId" />
<column-layout> <column-layout>
@@ -171,9 +169,7 @@
v-if="spellSlots && spellSlots.length || hasSpells" v-if="spellSlots && spellSlots.length || hasSpells"
class="spell-slots" class="spell-slots"
> >
<v-card <v-card data-id="spell-slot-card">
data-id="spell-slot-card"
>
<v-list <v-list
v-if="spellSlots && spellSlots.length" v-if="spellSlots && spellSlots.length"
two-line two-line
@@ -253,18 +249,6 @@
@sub-click="_id => clickTreeProperty({_id})" @sub-click="_id => clickTreeProperty({_id})"
/> />
</div> </div>
<div
v-for="attack in attacks"
:key="attack._id"
class="attack"
>
<action-card
attack
:model="attack"
:data-id="attack._id"
@click="clickProperty({_id: attack._id})"
/>
</div>
<div <div
v-if="weapons && weapons.length" v-if="weapons && weapons.length"
@@ -351,55 +335,55 @@
</template> </template>
<script lang="js"> <script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js'; import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue'; import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue'; import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue'; import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue'; import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue';
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue'; import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue'; import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue'; import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue'; import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue'; import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
import RestButton from '/imports/ui/creature/RestButton.vue'; import RestButton from '/imports/ui/creature/RestButton.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue'; import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js'; import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
const getProperties = function(creature, filter, options = { const getProperties = function (creature, filter, options = {
sort: {order: 1} sort: { order: 1 }
}){ }) {
if (!creature) return; if (!creature) return;
if (creature.settings.hideUnusedStats){ if (creature.settings.hideUnusedStats) {
filter.hide = {$ne: true}; filter.hide = { $ne: true };
} }
filter['ancestors.id'] = creature._id; filter['ancestors.id'] = creature._id;
filter.removed = {$ne: true}; filter.removed = { $ne: true };
filter.inactive = {$ne: true}; filter.inactive = { $ne: true };
filter.overridden = {$ne: true}; filter.overridden = { $ne: true };
return CreatureProperties.find(filter, options); return CreatureProperties.find(filter, options);
}; };
const getAttributeOfType = function(creature, type){ const getAttributeOfType = function (creature, type) {
return getProperties(creature, { return getProperties(creature, {
type: 'attribute', type: 'attribute',
attributeType: type, attributeType: type,
}); });
}; };
const getSkillOfType = function(creature, type){ const getSkillOfType = function (creature, type) {
return getProperties(creature, { return getProperties(creature, {
type: 'skill', type: 'skill',
skillType: type, skillType: type,
}); });
} }
export default { export default {
components: { components: {
RestButton, RestButton,
AbilityListTile, AbilityListTile,
@@ -420,141 +404,138 @@
required: true, required: true,
}, },
}, },
data(){return { data() {
return {
doCheckLoading: false, doCheckLoading: false,
}}, }
meteor: {
creature(){
return Creatures.findOne(this.creatureId, {fields: {settings: 1}});
}, },
abilities(){ meteor: {
creature() {
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
},
abilities() {
return getAttributeOfType(this.creature, 'ability'); return getAttributeOfType(this.creature, 'ability');
}, },
stats(){ stats() {
return getAttributeOfType(this.creature, 'stat'); return getAttributeOfType(this.creature, 'stat');
}, },
toggles(){ toggles() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
type: 'toggle', type: 'toggle',
removed: {$ne: true}, removed: { $ne: true },
deactivatedByAncestor: {$ne: true}, deactivatedByAncestor: { $ne: true },
showUI: true, showUI: true,
}, { }, {
sort: {order: 1} sort: { order: 1 }
}); });
}, },
modifiers(){ modifiers() {
return getAttributeOfType(this.creature, 'modifier'); return getAttributeOfType(this.creature, 'modifier');
}, },
resources(){ resources() {
return getAttributeOfType(this.creature, 'resource'); return getAttributeOfType(this.creature, 'resource');
}, },
spellSlots(){ spellSlots() {
return getAttributeOfType(this.creature, 'spellSlot'); return getAttributeOfType(this.creature, 'spellSlot');
}, },
hasSpells(){ hasSpells() {
const cursor = getProperties(this.creature, { const cursor = getProperties(this.creature, {
type: 'spell', type: 'spell',
}) })
return cursor && cursor.count(); return cursor && cursor.count();
}, },
hitDice(){ hitDice() {
return getAttributeOfType(this.creature, 'hitDice'); return getAttributeOfType(this.creature, 'hitDice');
}, },
checks(){ checks() {
return getSkillOfType(this.creature, 'check'); return getSkillOfType(this.creature, 'check');
}, },
savingThrows(){ savingThrows() {
return getSkillOfType(this.creature, 'save'); return getSkillOfType(this.creature, 'save');
}, },
skills(){ skills() {
return getSkillOfType(this.creature, 'skill'); return getSkillOfType(this.creature, 'skill');
}, },
tools(){ tools() {
return getSkillOfType(this.creature, 'tool'); return getSkillOfType(this.creature, 'tool');
}, },
weapons(){ weapons() {
return getSkillOfType(this.creature, 'weapon'); return getSkillOfType(this.creature, 'weapon');
}, },
armors(){ armors() {
return getSkillOfType(this.creature, 'armor'); return getSkillOfType(this.creature, 'armor');
}, },
languages(){ languages() {
return getSkillOfType(this.creature, 'language'); return getSkillOfType(this.creature, 'language');
}, },
actions(){ actions() {
return getProperties(this.creature, {type: 'action'}); return getProperties(this.creature, { type: 'action' });
}, },
appliedBuffs(){ appliedBuffs() {
return getProperties(this.creature, {type: 'buff'}); return getProperties(this.creature, { type: 'buff' });
}, },
multipliers(){ multipliers() {
return getProperties(this.creature, { return getProperties(this.creature, {
type: 'damageMultiplier' type: 'damageMultiplier'
}, { }, {
sort: {value: 1, order: 1} sort: { value: 1, order: 1 }
});
},
attacks(){
let props = getProperties(this.creature, {type: 'attack'})
return props && props.map(attack => {
attack.children = CreatureProperties.find({
'ancestors.id': attack._id,
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
});
return attack;
}); });
}, },
}, },
methods: { methods: {
clickProperty({_id}){ clickProperty({ _id }) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: `${_id}`, elementId: `${_id}`,
data: {_id}, data: { _id },
}); });
}, },
clickTreeProperty({_id}){ clickTreeProperty({ _id }) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog', component: 'creature-property-dialog',
elementId: `tree-node-${_id}`, elementId: `tree-node-${_id}`,
data: {_id}, data: { _id },
}); });
}, },
incrementChange(_id, {type, value}){ incrementChange(_id, { type, value }) {
if (type === 'increment'){ if (type === 'increment') {
damageProperty.call({_id, operation: 'increment' ,value: -value}); damageProperty.call({ _id, operation: 'increment', value: -value });
} }
}, },
softRemove(_id){ softRemove(_id) {
softRemoveProperty.call({_id}, error => { softRemoveProperty.call({ _id }, error => {
if (error) console.error(error); if (error) console.error(error);
}); });
}, },
castSpell(){ castSpell() {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'cast-spell-with-slot-dialog', component: 'cast-spell-with-slot-dialog',
elementId: 'spell-slot-card', elementId: 'spell-slot-card',
data: { data: {
creatureId: this.creatureId, creatureId: this.creatureId,
}, },
callback({spellId, slotId} = {}){ callback({ spellId, slotId, advantage, ritual } = {}) {
if (!spellId) return; if (!spellId) return;
doCastSpell.call({spellId, slotId}, error => { doCastSpell.call({
spellId,
slotId,
ritual,
scope: {
$attackAdvantage: advantage,
},
}, error => {
if (!error) return; if (!error) return;
snackbar({text: error.reason || error.message || error.toString()}); snackbar({ text: error.reason || error.message || error.toString() });
console.error(error); console.error(error);
}); });
}, },
}); });
} }
}, },
}; };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -13,12 +13,12 @@
</template> </template>
<script lang="js"> <script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import nodesToTree from '/imports/api/parenting/nodesToTree.js' import nodesToTree from '/imports/api/parenting/nodesToTree.js'
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue'; import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js'; import { organizeDoc, reorderDoc } from '/imports/api/parenting/organizeMethods.js';
export default { export default {
components: { components: {
TreeNodeList, TreeNodeList,
}, },
@@ -43,7 +43,7 @@
expanded: Boolean, expanded: Boolean,
}, },
meteor: { meteor: {
children(){ children() {
const children = nodesToTree({ const children = nodesToTree({
collection: CreatureProperties, collection: CreatureProperties,
ancestorId: this.root.id, ancestorId: this.root.id,
@@ -56,7 +56,7 @@
}, },
}, },
methods: { methods: {
reordered({doc, newIndex}){ reordered({ doc, newIndex }) {
reorderDoc.call({ reorderDoc.call({
docRef: { docRef: {
id: doc._id, id: doc._id,
@@ -65,9 +65,9 @@
order: newIndex, order: newIndex,
}); });
}, },
reorganized({doc, parent, newIndex}){ reorganized({ doc, parent, newIndex }) {
let parentRef; let parentRef;
if (parent){ if (parent) {
parentRef = { parentRef = {
id: parent._id, id: parent._id,
collection: 'creatureProperties', collection: 'creatureProperties',
@@ -85,8 +85,9 @@
}); });
}, },
}, },
}; };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -28,13 +28,15 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data() { return { data() {
return {
type: undefined, type: undefined,
};}, };
},
methods: { methods: {
getPropertyName, getPropertyName,
back(){ back() {
if (this.forcedType){ if (this.forcedType) {
this.$store.dispatch('popDialogStack'); this.$store.dispatch('popDialogStack');
} else { } else {
this.type = undefined; this.type = undefined;
@@ -45,4 +47,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -23,18 +23,21 @@
</template> </template>
<script lang="js"> <script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue'; import LibraryAndNode from '/imports/ui/library/LibraryAndNode.vue';
export default { export default {
components: { components: {
DialogBase, DialogBase,
LibraryAndNode, LibraryAndNode,
}, },
data(){return { data() {
return {
node: undefined, node: undefined,
};},
}; };
},
};
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -52,10 +52,10 @@ export default {
type: Boolean, type: Boolean,
}, },
}, },
data(){ data() {
let schema = ExperienceSchema.omit('creatureId'); let schema = ExperienceSchema.omit('creatureId');
let startingModel = {}; let startingModel = {};
if (this.startAsMilestone){ if (this.startAsMilestone) {
startingModel.levels = 1; startingModel.levels = 1;
} }
return { return {
@@ -65,14 +65,14 @@ export default {
debounceTime: 0, debounceTime: 0,
}; };
}, },
methods:{ methods: {
insertExperience(){ insertExperience() {
let experience = this.schema.clean(this.model); let experience = this.schema.clean(this.model);
let id = insertExperience.call({ let id = insertExperience.call({
experience, experience,
creatureIds: this.creatureIds, creatureIds: this.creatureIds,
}, (error) => { }, (error) => {
if (error){ if (error) {
console.error(error); console.error(error);
} }
}); });
@@ -83,4 +83,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -192,7 +192,6 @@ import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue'; import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue'; import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { clone } from 'lodash'; import { clone } from 'lodash';
export default { export default {
@@ -203,7 +202,7 @@ export default {
LibraryNodeExpansionContent, LibraryNodeExpansionContent,
PropertyTags, PropertyTags,
}, },
props:{ props: {
classId: { classId: {
type: String, type: String,
default: undefined, default: undefined,
@@ -217,42 +216,44 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
selectedNodeIds: [], selectedNodeIds: [],
searchInput: undefined, searchInput: undefined,
searchValue: undefined, searchValue: undefined,
showDisabled: false, showDisabled: false,
disabledNodeCount: undefined, disabledNodeCount: undefined,
}}, }
},
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['creatureId'], include: ['creatureId'],
}, },
computed: { computed: {
tagsSearched(){ tagsSearched() {
let or = []; let or = [];
let not = []; let not = [];
if (this.model.slotTags && this.model.slotTags.length){ if (this.model.slotTags && this.model.slotTags.length) {
or.push(this.model.slotTags); or.push(this.model.slotTags);
} }
this.model.extraTags?.forEach(extras => { this.model.extraTags?.forEach(extras => {
if (extras.tags?.length){ if (extras.tags?.length) {
if(extras.operation === 'OR'){ if (extras.operation === 'OR') {
or.push(extras.tags); or.push(extras.tags);
} else if (extras.operation === 'NOT'){ } else if (extras.operation === 'NOT') {
not.push(extras.tags); not.push(extras.tags);
} }
} }
}); });
return {or, not}; return { or, not };
}, },
}, },
methods: { methods: {
loadMore(){ loadMore() {
if (this.currentLimit >= this.countAll) return; if (this.currentLimit >= this.countAll) return;
this._subs['classFillers'].setData('limit', this.currentLimit + 50); this._subs['classFillers'].setData('limit', this.currentLimit + 50);
}, },
openPropertyDetails(id){ openPropertyDetails(id) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'library-node-dialog', component: 'library-node-dialog',
elementId: id, elementId: id,
@@ -261,7 +262,7 @@ export default {
}, },
}); });
}, },
isDisabled(node){ isDisabled(node) {
return node._disabledBySlotFillerCondition || return node._disabledBySlotFillerCondition ||
node._disabledByAlreadyAdded || node._disabledByAlreadyAdded ||
( (
@@ -272,15 +273,15 @@ export default {
}, },
meteor: { meteor: {
$subscribe: { $subscribe: {
'classFillers'(){ 'classFillers'() {
return [this.classId, this.searchValue || undefined] return [this.classId, this.searchValue || undefined]
}, },
}, },
searchLoading(){ searchLoading() {
return !!this.searchValue && !this.$subReady.classFillers; return !!this.searchValue && !this.$subReady.classFillers;
}, },
model(){ model() {
if (this.classId){ if (this.classId) {
return CreatureProperties.findOne(this.classId); return CreatureProperties.findOne(this.classId);
} else if (this.dummySlot) { } else if (this.dummySlot) {
let model = clone(this.dummySlot) let model = clone(this.dummySlot)
@@ -294,40 +295,40 @@ export default {
if (!this.creatureId) return {}; if (!this.creatureId) return {};
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
}, },
currentLimit(){ currentLimit() {
return this._subs['classFillers'].data('limit') || 50; return this._subs['classFillers'].data('limit') || 50;
}, },
countAll(){ countAll() {
return this._subs['classFillers'].data('countAll'); return this._subs['classFillers'].data('countAll');
}, },
alreadyAdded(){ alreadyAdded() {
let added = new Set(); let added = new Set();
if (!this.model.unique) return added; if (!this.model.unique) return added;
let ancestorId; let ancestorId;
if (this.model.unique === 'uniqueInSlot'){ if (this.model.unique === 'uniqueInSlot') {
ancestorId = this.model._id; ancestorId = this.model._id;
} else if (this.model.unique === 'uniqueInCreature'){ } else if (this.model.unique === 'uniqueInCreature') {
ancestorId = this.creatureId; ancestorId = this.creatureId;
} }
CreatureProperties.find({ CreatureProperties.find({
'ancestors.id': ancestorId, 'ancestors.id': ancestorId,
libraryNodeId: {$exists: true}, libraryNodeId: { $exists: true },
removed: {$ne: true}, removed: { $ne: true },
}, { }, {
fields: {libraryNodeId: 1}, fields: { libraryNodeId: 1 },
}).forEach(prop => { }).forEach(prop => {
added.add(prop.libraryNodeId); added.add(prop.libraryNodeId);
}); });
return added; return added;
}, },
totalQuantitySelected(){ totalQuantitySelected() {
let quantitySelected = 0; let quantitySelected = 0;
LibraryNodes.find({ LibraryNodes.find({
_id: {$in: this.selectedNodeIds} _id: { $in: this.selectedNodeIds }
}, { }, {
fields: {slotQuantityFilled: 1}, fields: { slotQuantityFilled: 1 },
}).forEach(node => { }).forEach(node => {
if (Number.isFinite(node.slotQuantityFilled)){ if (Number.isFinite(node.slotQuantityFilled)) {
quantitySelected += node.slotQuantityFilled; quantitySelected += node.slotQuantityFilled;
} else { } else {
quantitySelected += 1; quantitySelected += 1;
@@ -335,30 +336,30 @@ export default {
}); });
return quantitySelected; return quantitySelected;
}, },
spaceLeft(){ spaceLeft() {
if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined; if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined;
return this.model.spaceLeft - this.totalQuantitySelected; return this.model.spaceLeft - this.totalQuantitySelected;
}, },
libraryNames(){ libraryNames() {
let names = {}; let names = {};
Libraries.find().forEach(lib => names[lib._id] = lib.name) Libraries.find().forEach(lib => names[lib._id] = lib.name)
return names; return names;
}, },
libraryNodes(){ libraryNodes() {
let filter = getSlotFillFilter({ slot: this.model }); let filter = getSlotFillFilter({ slot: this.model });
let nodes = LibraryNodes.find(filter, { let nodes = LibraryNodes.find(filter, {
sort: {name: 1, order: 1} sort: { name: 1, order: 1 }
}).fetch(); }).fetch();
let disabledNodeCount = 0; let disabledNodeCount = 0;
// Mark classFillers whose condition isn't met or are too big to fit // Mark classFillers whose condition isn't met or are too big to fit
// the quantity to fill // the quantity to fill
nodes.forEach(node => { nodes.forEach(node => {
if (node.slotFillerCondition){ if (node.slotFillerCondition) {
try { try {
let parseNode = parse(node.slotFillerCondition); let parseNode = parse(node.slotFillerCondition);
const {result: resultNode} = resolve('reduce', parseNode, this.variables); const { result: resultNode } = resolve('reduce', parseNode, this.variables);
if (resultNode?.parseType === 'constant'){ if (resultNode?.parseType === 'constant') {
if (!resultNode.value){ if (!resultNode.value) {
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
disabledNodeCount += 1; disabledNodeCount += 1;
} }
@@ -367,7 +368,7 @@ export default {
node._conditionError = toString(resultNode); node._conditionError = toString(resultNode);
disabledNodeCount += 1; disabledNodeCount += 1;
} }
} catch (e){ } catch (e) {
console.warn(e); console.warn(e);
let error = prettifyParseError(e); let error = prettifyParseError(e);
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
@@ -378,10 +379,10 @@ export default {
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1; let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
if ( if (
quantityToFill > this.spaceLeft quantityToFill > this.spaceLeft
){ ) {
node._disabledByQuantityFilled = true; node._disabledByQuantityFilled = true;
} }
if (this.alreadyAdded.has(node._id)){ if (this.alreadyAdded.has(node._id)) {
node._disabledByAlreadyAdded = true; node._disabledByAlreadyAdded = true;
} }
}); });
@@ -393,7 +394,7 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
.disabled { .disabled {
opacity: 0.7; opacity: 0.7;
} }
</style> </style>

View File

@@ -62,8 +62,10 @@ export default {
hover: false, hover: false,
}}, }},
computed: { computed: {
accentColor(){ accentColor() {
if (this.theme.isDark){ if (this.model.color) {
return this.model.color
} else if (this.theme.isDark){
return this.$vuetify.theme.themes.dark.primary; return this.$vuetify.theme.themes.dark.primary;
} else { } else {
return this.$vuetify.theme.themes.light.primary; return this.$vuetify.theme.themes.light.primary;

View File

@@ -204,7 +204,7 @@ export default {
LibraryNodeExpansionContent, LibraryNodeExpansionContent,
PropertyTags, PropertyTags,
}, },
props:{ props: {
slotId: { slotId: {
type: String, type: String,
default: undefined, default: undefined,
@@ -218,36 +218,38 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
selectedNodeIds: [], selectedNodeIds: [],
searchInput: undefined, searchInput: undefined,
searchValue: undefined, searchValue: undefined,
showDisabled: false, showDisabled: false,
disabledNodeCount: undefined, disabledNodeCount: undefined,
}}, }
},
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['creatureId'], include: ['creatureId'],
}, },
computed: { computed: {
tagsSearched(){ tagsSearched() {
let or = []; let or = [];
let not = []; let not = [];
if (this.model.slotTags && this.model.slotTags.length){ if (this.model.slotTags && this.model.slotTags.length) {
or.push(this.model.slotTags); or.push(this.model.slotTags);
} }
this.model.extraTags?.forEach(extras => { this.model.extraTags?.forEach(extras => {
if (extras.tags?.length){ if (extras.tags?.length) {
if(extras.operation === 'OR'){ if (extras.operation === 'OR') {
or.push(extras.tags); or.push(extras.tags);
} else if (extras.operation === 'NOT'){ } else if (extras.operation === 'NOT') {
not.push(extras.tags); not.push(extras.tags);
} }
} }
}); });
return {or, not}; return { or, not };
}, },
slotPropertyTypeName(){ slotPropertyTypeName() {
if (!this.model) return; if (!this.model) return;
if (!this.model.slotType) return 'Property'; if (!this.model.slotType) return 'Property';
let propName = getPropertyName(this.model.slotType); let propName = getPropertyName(this.model.slotType);
@@ -255,11 +257,11 @@ export default {
}, },
}, },
methods: { methods: {
loadMore(){ loadMore() {
if (this.currentLimit >= this.countAll) return; if (this.currentLimit >= this.countAll) return;
this._subs['slotFillers'].setData('limit', this.currentLimit + 50); this._subs['slotFillers'].setData('limit', this.currentLimit + 50);
}, },
openPropertyDetails(id){ openPropertyDetails(id) {
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'library-node-dialog', component: 'library-node-dialog',
elementId: id, elementId: id,
@@ -268,7 +270,7 @@ export default {
}, },
}); });
}, },
isDisabled(node){ isDisabled(node) {
return node._disabledBySlotFillerCondition || return node._disabledBySlotFillerCondition ||
node._disabledByAlreadyAdded || node._disabledByAlreadyAdded ||
( (
@@ -279,15 +281,15 @@ export default {
}, },
meteor: { meteor: {
$subscribe: { $subscribe: {
'slotFillers'(){ 'slotFillers'() {
return [this.slotId, this.searchValue || undefined] return [this.slotId, this.searchValue || undefined]
}, },
}, },
searchLoading(){ searchLoading() {
return !!this.searchValue && !this.$subReady.slotFillers; return !!this.searchValue && !this.$subReady.slotFillers;
}, },
model(){ model() {
if (this.slotId){ if (this.slotId) {
return CreatureProperties.findOne(this.slotId); return CreatureProperties.findOne(this.slotId);
} else if (this.dummySlot) { } else if (this.dummySlot) {
let model = clone(this.dummySlot) let model = clone(this.dummySlot)
@@ -301,40 +303,40 @@ export default {
if (!this.creatureId) return {}; if (!this.creatureId) return {};
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
}, },
currentLimit(){ currentLimit() {
return this._subs['slotFillers'].data('limit') || 50; return this._subs['slotFillers'].data('limit') || 50;
}, },
countAll(){ countAll() {
return this._subs['slotFillers'].data('countAll'); return this._subs['slotFillers'].data('countAll');
}, },
alreadyAdded(){ alreadyAdded() {
let added = new Set(); let added = new Set();
if (!this.model.unique) return added; if (!this.model.unique) return added;
let ancestorId; let ancestorId;
if (this.model.unique === 'uniqueInSlot'){ if (this.model.unique === 'uniqueInSlot') {
ancestorId = this.model._id; ancestorId = this.model._id;
} else if (this.model.unique === 'uniqueInCreature'){ } else if (this.model.unique === 'uniqueInCreature') {
ancestorId = this.creatureId; ancestorId = this.creatureId;
} }
CreatureProperties.find({ CreatureProperties.find({
'ancestors.id': ancestorId, 'ancestors.id': ancestorId,
libraryNodeId: {$exists: true}, libraryNodeId: { $exists: true },
removed: {$ne: true}, removed: { $ne: true },
}, { }, {
fields: {libraryNodeId: 1}, fields: { libraryNodeId: 1 },
}).forEach(prop => { }).forEach(prop => {
added.add(prop.libraryNodeId); added.add(prop.libraryNodeId);
}); });
return added; return added;
}, },
totalQuantitySelected(){ totalQuantitySelected() {
let quantitySelected = 0; let quantitySelected = 0;
LibraryNodes.find({ LibraryNodes.find({
_id: {$in: this.selectedNodeIds} _id: { $in: this.selectedNodeIds }
}, { }, {
fields: {slotQuantityFilled: 1}, fields: { slotQuantityFilled: 1 },
}).forEach(node => { }).forEach(node => {
if (Number.isFinite(node.slotQuantityFilled)){ if (Number.isFinite(node.slotQuantityFilled)) {
quantitySelected += node.slotQuantityFilled; quantitySelected += node.slotQuantityFilled;
} else { } else {
quantitySelected += 1; quantitySelected += 1;
@@ -342,30 +344,30 @@ export default {
}); });
return quantitySelected; return quantitySelected;
}, },
spaceLeft(){ spaceLeft() {
if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined; if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined;
return this.model.spaceLeft - this.totalQuantitySelected; return this.model.spaceLeft - this.totalQuantitySelected;
}, },
libraryNames(){ libraryNames() {
let names = {}; let names = {};
Libraries.find().forEach(lib => names[lib._id] = lib.name) Libraries.find().forEach(lib => names[lib._id] = lib.name)
return names; return names;
}, },
libraryNodes(){ libraryNodes() {
let filter = getSlotFillFilter({slot: this.model}); let filter = getSlotFillFilter({ slot: this.model });
let nodes = LibraryNodes.find(filter, { let nodes = LibraryNodes.find(filter, {
sort: {name: 1, order: 1} sort: { name: 1, order: 1 }
}).fetch(); }).fetch();
let disabledNodeCount = 0; let disabledNodeCount = 0;
// Mark slotFillers whose condition isn't met or are too big to fit // Mark slotFillers whose condition isn't met or are too big to fit
// the quantity to fill // the quantity to fill
nodes.forEach(node => { nodes.forEach(node => {
if (node.slotFillerCondition){ if (node.slotFillerCondition) {
try { try {
let parseNode = parse(node.slotFillerCondition); let parseNode = parse(node.slotFillerCondition);
const {result: resultNode} = resolve('reduce', parseNode, this.variables); const { result: resultNode } = resolve('reduce', parseNode, this.variables);
if (resultNode?.parseType === 'constant'){ if (resultNode?.parseType === 'constant') {
if (!resultNode.value){ if (!resultNode.value) {
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
disabledNodeCount += 1; disabledNodeCount += 1;
} }
@@ -374,7 +376,7 @@ export default {
node._conditionError = toString(resultNode); node._conditionError = toString(resultNode);
disabledNodeCount += 1; disabledNodeCount += 1;
} }
} catch (e){ } catch (e) {
console.warn(e); console.warn(e);
let error = prettifyParseError(e); let error = prettifyParseError(e);
node._disabledBySlotFillerCondition = true; node._disabledBySlotFillerCondition = true;
@@ -385,10 +387,10 @@ export default {
let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1; let quantityToFill = node.type === 'slotFiller' ? node.slotQuantityFilled : 1;
if ( if (
quantityToFill > this.spaceLeft quantityToFill > this.spaceLeft
){ ) {
node._disabledByQuantityFilled = true; node._disabledByQuantityFilled = true;
} }
if (this.alreadyAdded.has(node._id)){ if (this.alreadyAdded.has(node._id)) {
node._disabledByAlreadyAdded = true; node._disabledByAlreadyAdded = true;
} }
}); });
@@ -400,7 +402,7 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
.disabled { .disabled {
opacity: 0.7; opacity: 0.7;
} }
</style> </style>

Some files were not shown because too many files have changed in this diff Show More