Compare commits
54 Commits
2.0-beta.4
...
2.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c55d572134 | ||
|
|
0a2b60990e | ||
|
|
a437ff5aef | ||
|
|
3d31d62860 | ||
|
|
8377231254 | ||
|
|
1ec29365cb | ||
|
|
60b21c1901 | ||
|
|
03f87b0afa | ||
|
|
48291d2c8f | ||
|
|
1cedf55fbf | ||
|
|
bed4d4b162 | ||
|
|
a1d992ec8d | ||
|
|
008ef62517 | ||
|
|
c436309ba8 | ||
|
|
0bfdb73b47 | ||
|
|
a462cc5ca2 | ||
|
|
5d57a74667 | ||
|
|
21b0029df7 | ||
|
|
c0ccafa787 | ||
|
|
d63ad9ea8f | ||
|
|
8f56a60fb1 | ||
|
|
358ae46627 | ||
|
|
0b1db3c40c | ||
|
|
0ad7e659d2 | ||
|
|
58c3875dc7 | ||
|
|
84f506f1fe | ||
|
|
d0a3ccc76a | ||
|
|
93ac9215c2 | ||
|
|
a6b501a62c | ||
|
|
e956bacf07 | ||
|
|
60b6b283b1 | ||
|
|
1c9b390551 | ||
|
|
21a487635d | ||
|
|
c92a26d5e6 | ||
|
|
49b514b8f3 | ||
|
|
5cb835c536 | ||
|
|
aa8f2d230d | ||
|
|
2fa913b09a | ||
|
|
de598c70a7 | ||
|
|
baecdeff24 | ||
|
|
d4b7d22b5f | ||
|
|
87f79737e8 | ||
|
|
9f0ffe13f8 | ||
|
|
adaa31d76c | ||
|
|
b051d764f8 | ||
|
|
ffb5b4a4f3 | ||
|
|
fd87b7fb75 | ||
|
|
f035902842 | ||
|
|
dbc5f7253f | ||
|
|
f0e7253374 | ||
|
|
ffe37bf907 | ||
|
|
a63e2099d3 | ||
|
|
0308e4e7a7 | ||
|
|
43f8df09f0 |
@@ -11,14 +11,14 @@ accounts-google@1.4.0
|
||||
email@2.2.1
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.15.0
|
||||
mongo@1.16.0
|
||||
session@1.2.0
|
||||
tracker@1.2.0
|
||||
logging@1.3.1
|
||||
reload@1.3.1
|
||||
ejson@1.1.2
|
||||
check@1.3.1
|
||||
standard-minifier-js@2.8.0
|
||||
standard-minifier-js@2.8.1
|
||||
shell-server@0.5.0
|
||||
ecmascript@0.16.2
|
||||
es5-shim@4.8.0
|
||||
@@ -48,3 +48,4 @@ simple:rest-bearer-token-parser
|
||||
simple:rest-json-error-handler
|
||||
littledata:synced-cron
|
||||
mdg:meteor-apm-agent
|
||||
typescript@4.5.4
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@2.7.3
|
||||
METEOR@2.8.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
accounts-base@2.2.3
|
||||
accounts-base@2.2.4
|
||||
accounts-google@1.4.0
|
||||
accounts-oauth@1.4.1
|
||||
accounts-password@2.3.1
|
||||
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
|
||||
aldeed:schema-index@3.0.0
|
||||
allow-deny@1.1.1
|
||||
autoupdate@1.8.0
|
||||
babel-compiler@7.9.0
|
||||
babel-compiler@7.9.2
|
||||
babel-runtime@1.5.1
|
||||
base64@1.0.12
|
||||
binary-heap@1.0.11
|
||||
@@ -27,10 +27,10 @@ coffeescript@2.4.1
|
||||
coffeescript-compiler@2.4.1
|
||||
dburles:mongo-collection-instances@0.3.6
|
||||
ddp@1.4.0
|
||||
ddp-client@2.5.0
|
||||
ddp-client@2.6.0
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.1.0
|
||||
ddp-server@2.5.0
|
||||
ddp-server@2.6.0
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.7.2
|
||||
ecmascript@0.16.2
|
||||
@@ -55,33 +55,33 @@ littledata:synced-cron@1.5.1
|
||||
livedata@1.0.18
|
||||
localstorage@1.2.0
|
||||
logging@1.3.1
|
||||
mdg:meteor-apm-agent@3.5.0
|
||||
mdg:meteor-apm-agent@3.5.1
|
||||
mdg:validated-method@1.2.0
|
||||
meteor@1.10.0
|
||||
meteor@1.10.1
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.3.5
|
||||
meteortesting:mocha@2.0.3
|
||||
meteortesting:mocha-core@8.1.2
|
||||
mikowals:batch-insert@1.3.0
|
||||
minifier-css@1.6.0
|
||||
minifier-js@2.7.4
|
||||
minimongo@1.8.0
|
||||
minifier-css@1.6.1
|
||||
minifier-js@2.7.5
|
||||
minimongo@1.9.0
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.8
|
||||
modules@0.18.0
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.0
|
||||
mongo@1.15.0
|
||||
mongo@1.16.0
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
npm-mongo@4.3.1
|
||||
npm-mongo@4.9.0
|
||||
oauth@2.1.2
|
||||
oauth2@1.3.1
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
ostrio:files@2.0.1
|
||||
ostrio:files@2.3.0
|
||||
patreon-oauth@0.1.0
|
||||
peerlibrary:assert@0.3.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
|
||||
socket-stream-client@0.5.0
|
||||
spacebars-compiler@1.3.1
|
||||
standard-minifier-js@2.8.0
|
||||
standard-minifier-js@2.8.1
|
||||
static-html@1.3.2
|
||||
templating-tools@1.2.2
|
||||
tmeasday:check-npm-versions@1.0.2
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
|
||||
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
|
||||
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
|
||||
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
|
||||
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
|
||||
<meta name="msapplication-TileColor" content="#b91d1d">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
|
||||
<meta name="theme-color" content="#d12929">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-touch-icon-57x57.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-touch-icon-60x60.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-touch-icon-72x72.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-touch-icon-76x76.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-touch-icon-114x114.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-touch-icon-144x144.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png?v=lk6WXp6Pmj">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png?v=lk6WXp6Pmj">
|
||||
<link rel="icon" type="image/png" href="/favicon-32x32.png?v=lk6WXp6Pmj" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/favicon-194x194.png?v=lk6WXp6Pmj" sizes="194x194">
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=lk6WXp6Pmj" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="/android-chrome-192x192.png?v=lk6WXp6Pmj" sizes="192x192">
|
||||
<link rel="icon" type="image/png" href="/favicon-16x16.png?v=lk6WXp6Pmj" sizes="16x16">
|
||||
<link rel="manifest" href="/manifest.json?v=lk6WXp6Pmj">
|
||||
<link rel="shortcut icon" href="/favicon.ico?v=lk6WXp6Pmj">
|
||||
<meta name="msapplication-TileColor" content="#b91d1d">
|
||||
<meta name="msapplication-TileImage" content="/mstile-144x144.png?v=lk6WXp6Pmj">
|
||||
<meta name="theme-color" content="#d12929">
|
||||
|
||||
<style type="text/css" media="print">
|
||||
@page {
|
||||
margin: 0mm;
|
||||
}
|
||||
html {
|
||||
margin: 0px;
|
||||
}
|
||||
* {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css" media="print">
|
||||
@page {
|
||||
margin: 0mm;
|
||||
}
|
||||
html {
|
||||
margin: 0px;
|
||||
}
|
||||
* {
|
||||
-webkit-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -4,25 +4,25 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let CreatureFolders = new Mongo.Collection('creatureFolders');
|
||||
|
||||
let creatureFolderSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
creatures: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'creatures.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
creatures: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'creatures.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
archived: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
|
||||
@@ -18,23 +18,23 @@ let CreaturePropertySchema = new SimpleSchema({
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: Object.keys(propertySchemasIndex),
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
icon: {
|
||||
type: storedIconsSchema,
|
||||
optional: true,
|
||||
@@ -93,20 +93,20 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
||||
|
||||
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
|
||||
|
||||
for (let key in propertySchemasIndex){
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(CreaturePropertySchema);
|
||||
for (let key in propertySchemasIndex) {
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(CreaturePropertySchema);
|
||||
schema.extend(ColorSchema);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
CreatureProperties.attachSchema(schema, {
|
||||
selector: {type: key}
|
||||
});
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
CreatureProperties.attachSchema(schema, {
|
||||
selector: { type: key }
|
||||
});
|
||||
}
|
||||
|
||||
export default CreatureProperties;
|
||||
export {
|
||||
DenormalisedOnlyCreaturePropertySchema,
|
||||
CreaturePropertySchema,
|
||||
CreaturePropertySchema,
|
||||
};
|
||||
|
||||
@@ -20,33 +20,33 @@ const adjustQuantity = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, operation, value}) {
|
||||
run({ _id, operation, value }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// 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
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('quantity')){
|
||||
if (!schema.allowsKey('quantity')) {
|
||||
throw new Meteor.Error(
|
||||
'Adjust quantity failed',
|
||||
`Property of type "${property.type}" doesn't have a quantity`
|
||||
);
|
||||
}
|
||||
if (operation === 'set'){
|
||||
if (operation === 'set') {
|
||||
CreatureProperties.update(property._id, {
|
||||
$set: {quantity: value, dirty: true}
|
||||
$set: { quantity: value, dirty: true }
|
||||
}, {
|
||||
selector: property
|
||||
});
|
||||
} else if (operation === 'increment'){
|
||||
} else if (operation === 'increment') {
|
||||
// value here is 'damage'
|
||||
value = -value;
|
||||
let currentQuantity = property.quantity;
|
||||
|
||||
@@ -22,7 +22,7 @@ const damageProperty = new ValidatedMethod({
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, operation, value }) {
|
||||
|
||||
|
||||
// Get action context
|
||||
let prop = CreatureProperties.findOne(_id);
|
||||
if (!prop) throw new Meteor.Error(
|
||||
@@ -30,17 +30,17 @@ const damageProperty = new ValidatedMethod({
|
||||
);
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
|
||||
// Check permissions
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
if (!schema.allowsKey('damage')){
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${prop.type}" can't be damaged`
|
||||
);
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
if (!schema.allowsKey('damage')) {
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${prop.type}" can't be damaged`
|
||||
);
|
||||
}
|
||||
|
||||
// Replace the prop by its actionContext counterpart if possible
|
||||
@@ -50,9 +50,9 @@ const damageProperty = new ValidatedMethod({
|
||||
prop = actionContextProp;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const result = damagePropertyWork({ prop, operation, value, actionContext });
|
||||
|
||||
|
||||
// Insert the log
|
||||
actionContext.writeLog();
|
||||
return result;
|
||||
@@ -86,7 +86,7 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
|
||||
}
|
||||
|
||||
let damage, newValue, increment;
|
||||
if (operation === 'set'){
|
||||
if (operation === 'set') {
|
||||
const total = prop.total || 0;
|
||||
// Set represents what we want the value to be after damage
|
||||
// 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
|
||||
prop.damage = damage;
|
||||
prop.value = newValue;
|
||||
} else if (operation === 'increment'){
|
||||
} else if (operation === 'increment') {
|
||||
let currentValue = prop.value || 0;
|
||||
let currentDamage = prop.damage || 0;
|
||||
increment = value;
|
||||
|
||||
@@ -5,12 +5,12 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
var snackbar;
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
@@ -31,7 +31,7 @@ const duplicateProperty = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let creature = getRootCreatureAncestor(property);
|
||||
|
||||
@@ -44,17 +44,17 @@ const duplicateProperty = new ValidatedMethod({
|
||||
|
||||
// Get all the descendants
|
||||
let nodes = CreatureProperties.find({
|
||||
'ancestors.id': _id,
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
'ancestors.id': _id,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: DUPLICATE_CHILDREN_LIMIT + 1,
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
// Alert the user if the limit was hit
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
|
||||
nodes.pop();
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar({
|
||||
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
|
||||
});
|
||||
@@ -63,25 +63,25 @@ const duplicateProperty = new ValidatedMethod({
|
||||
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry : [
|
||||
docArray: nodes,
|
||||
newAncestry: [
|
||||
...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
|
||||
renewDocIds({docArray: nodes});
|
||||
renewDocIds({ docArray: nodes });
|
||||
|
||||
// Order the root node
|
||||
property.order += 0.5;
|
||||
|
||||
|
||||
// Mark the sheet as needing recompute
|
||||
property.dirty = true;
|
||||
|
||||
// Insert the properties
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
|
||||
@@ -10,8 +10,8 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/
|
||||
// Equipping or unequipping an item will also change its parent
|
||||
const equipItem = new ValidatedMethod({
|
||||
name: 'creatureProperties.equip',
|
||||
validate({_id, equipped}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
validate({ _id, equipped }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
if (equipped !== true && equipped !== false) {
|
||||
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
|
||||
}
|
||||
@@ -21,20 +21,20 @@ const equipItem = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, equipped}) {
|
||||
run({ _id, equipped }) {
|
||||
let item = CreatureProperties.findOne(_id);
|
||||
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');
|
||||
let creature = getRootCreatureAncestor(item);
|
||||
assertEditPermission(creature, this.userId);
|
||||
CreatureProperties.update(_id, {
|
||||
$set: { equipped, dirty: true },
|
||||
}, {
|
||||
selector: {type: 'item'},
|
||||
});
|
||||
selector: { type: 'item' },
|
||||
});
|
||||
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
|
||||
let parentRef = getParentRefByTag(creature._id, tag);
|
||||
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
|
||||
if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' };
|
||||
|
||||
organizeDoc.call({
|
||||
docRef: {
|
||||
|
||||
@@ -6,24 +6,24 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
||||
|
||||
const flipToggle = new ValidatedMethod({
|
||||
name: 'creatureProperties.flipToggle',
|
||||
validate({_id}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
validate({ _id }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
// Permission
|
||||
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',
|
||||
'This method can only be applied to toggles');
|
||||
}
|
||||
if (!property.enabled && !property.disabled){
|
||||
if (!property.enabled && !property.disabled) {
|
||||
throw new Meteor.Error('Computed toggle',
|
||||
'Can\'t flip a toggle that is computed')
|
||||
}
|
||||
@@ -32,13 +32,15 @@ const flipToggle = new ValidatedMethod({
|
||||
|
||||
// Invert the current value, disabled is the canonical store of value
|
||||
const currentValue = !property.disabled;
|
||||
CreatureProperties.update(_id, {$set: {
|
||||
enabled: !currentValue,
|
||||
disabled: currentValue,
|
||||
dirty: true,
|
||||
}}, {
|
||||
selector: {type: 'toggle'},
|
||||
});
|
||||
CreatureProperties.update(_id, {
|
||||
$set: {
|
||||
enabled: !currentValue,
|
||||
disabled: currentValue,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: { type: 'toggle' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
|
||||
const insertProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.insert',
|
||||
validate: new SimpleSchema({
|
||||
validate: new SimpleSchema({
|
||||
creatureProperty: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
@@ -24,25 +24,25 @@ const insertProperty = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureProperty, parentRef}) {
|
||||
run({ creatureProperty, parentRef }) {
|
||||
// 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;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
creatureProperty.parent = parentRef;
|
||||
creatureProperty.ancestors = ancestors;
|
||||
|
||||
return insertPropertyWork({
|
||||
return insertPropertyWork({
|
||||
property: creatureProperty,
|
||||
creature: rootCreature,
|
||||
});
|
||||
@@ -75,31 +75,31 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureProperty, creatureId, tag, tagDefaultName}) {
|
||||
run({ creatureProperty, creatureId, tag, tagDefaultName }) {
|
||||
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
|
||||
var insertFolderFirst = true;
|
||||
parentRef = {id: creatureId, collection: 'creatures'};
|
||||
parentRef = { id: creatureId, collection: 'creatures' };
|
||||
}
|
||||
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Add the folder first if we need to
|
||||
if (insertFolderFirst){
|
||||
if (insertFolderFirst) {
|
||||
let order = getHighestOrder({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: parentRef.id,
|
||||
@@ -113,7 +113,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
order,
|
||||
});
|
||||
// Make the folder our new parent
|
||||
let newParentRef = {id, collection: 'creatureProperties'};
|
||||
let newParentRef = { id, collection: 'creatureProperties' };
|
||||
ancestors = [parentRef, newParentRef];
|
||||
parentRef = newParentRef;
|
||||
creatureProperty.order = order + 1;
|
||||
@@ -122,14 +122,14 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
creatureProperty.parent = parentRef;
|
||||
creatureProperty.ancestors = ancestors;
|
||||
|
||||
return insertPropertyWork({
|
||||
return insertPropertyWork({
|
||||
property: creatureProperty,
|
||||
creature: rootCreature,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function insertPropertyWork({property, creature}){
|
||||
export function insertPropertyWork({ property, creature }) {
|
||||
delete property._id;
|
||||
property.dirty = true;
|
||||
let _id = CreatureProperties.insert(property);
|
||||
|
||||
@@ -7,57 +7,57 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
getAncestry,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
getAncestry,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
nodeIds: {
|
||||
type: Array,
|
||||
max: 20,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parentRef: {
|
||||
type: RefSchema,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parentRef: {
|
||||
type: RefSchema,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({nodeIds, parentRef, order}) {
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
run({ nodeIds, parentRef, order }) {
|
||||
// get the new ancestry for the properties
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// {libraryId: hasViewPermission}
|
||||
//let libraryPermissionMemoir = {};
|
||||
let node;
|
||||
nodeIds.forEach(nodeId => {
|
||||
nodeIds.forEach(nodeId => {
|
||||
// TODO: Check library view permission for each node before starting
|
||||
node = insertPropertyFromNode(nodeId, ancestors, order);
|
||||
});
|
||||
@@ -70,18 +70,18 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: rootCreature._id,
|
||||
});
|
||||
// Return the docId of the last property, the inserted root property
|
||||
return rootId;
|
||||
},
|
||||
// Return the docId of the last property, the inserted root property
|
||||
return rootId;
|
||||
},
|
||||
});
|
||||
|
||||
function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
function insertPropertyFromNode(nodeId, ancestors, order) {
|
||||
// Fetch the library node and its decendents, provided they have not been
|
||||
// removed
|
||||
// TODO: Check permission to read the library this node is in
|
||||
let node = LibraryNodes.findOne({
|
||||
_id: nodeId,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
});
|
||||
if (!node) {
|
||||
if (Meteor.isClient) return;
|
||||
@@ -95,7 +95,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
let oldParent = node.parent;
|
||||
let nodes = LibraryNodes.find({
|
||||
'ancestors.id': nodeId,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
}).fetch();
|
||||
|
||||
// Convert all references into actual nodes
|
||||
@@ -118,11 +118,11 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({
|
||||
docArray: nodes,
|
||||
collectionMap: {'libraryNodes': 'creatureProperties'}
|
||||
collectionMap: { 'libraryNodes': 'creatureProperties' }
|
||||
});
|
||||
|
||||
// Order the root node
|
||||
if (order === undefined){
|
||||
if (order === undefined) {
|
||||
setDocToLastOrder({
|
||||
collection: CreatureProperties,
|
||||
doc: node,
|
||||
@@ -139,7 +139,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
return node;
|
||||
}
|
||||
|
||||
function storeLibraryNodeReferences(nodes){
|
||||
function storeLibraryNodeReferences(nodes) {
|
||||
nodes.forEach(node => {
|
||||
if (node.libraryNodeId) return;
|
||||
node.libraryNodeId = node._id;
|
||||
@@ -154,7 +154,7 @@ function dirtyNodes(nodes) {
|
||||
|
||||
// Covert node references into actual nodes
|
||||
// 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;
|
||||
// New nodes added this function
|
||||
let newNodes = [];
|
||||
@@ -165,9 +165,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
if (node.type !== 'reference') return true;
|
||||
|
||||
// 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');
|
||||
node.cache = {error: 'Reference depth limit exceeded'};
|
||||
node.cache = { error: 'Reference depth limit exceeded' };
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -177,17 +177,17 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
referencedNode.order = node.order;
|
||||
// We are definitely replacing this node, so add it to the list
|
||||
visitedRefs.add(node._id);
|
||||
} catch (e){
|
||||
node.cache = {error: e.reason || e.message || e.toString()};
|
||||
} catch (e) {
|
||||
node.cache = { error: e.reason || e.message || e.toString() };
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get all the descendants of the referenced node
|
||||
let descendents = LibraryNodes.find({
|
||||
'ancestors.id': referencedNode._id,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
// We are adding the referenced node and its descendants
|
||||
@@ -195,20 +195,20 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
|
||||
// re-map all the ancestors to parent the new sub-tree into our existing
|
||||
// node tree
|
||||
setLineageOfDocs({
|
||||
docArray: addedNodes,
|
||||
newAncestry: node.ancestors,
|
||||
oldParent: referencedNode.parent,
|
||||
});
|
||||
setLineageOfDocs({
|
||||
docArray: addedNodes,
|
||||
newAncestry: node.ancestors,
|
||||
oldParent: referencedNode.parent,
|
||||
});
|
||||
|
||||
// Filter all the looped references
|
||||
addedNodes = addedNodes.filter(addedNode => {
|
||||
// Add all non-reference nodes
|
||||
if (addedNode.type !== 'reference'){
|
||||
if (addedNode.type !== 'reference') {
|
||||
return true;
|
||||
}
|
||||
// If this exact reference has already been resolved before, filter it out
|
||||
if (visitedRefs.has(addedNode._id)){
|
||||
if (visitedRefs.has(addedNode._id)) {
|
||||
return false;
|
||||
} else {
|
||||
// Otherwise mark it as visited, and keep it
|
||||
|
||||
@@ -5,28 +5,28 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
|
||||
const pullFromProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.pull',
|
||||
validate: null,
|
||||
name: 'creatureProperties.pull',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, itemId}){
|
||||
run({ _id, path, itemId }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
CreatureProperties.update(_id, {
|
||||
CreatureProperties.update(_id, {
|
||||
$pull: { [path.join('.')]: { _id: itemId } },
|
||||
$set: { dirty: true }
|
||||
}, {
|
||||
selector: {type: property.type},
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
}, {
|
||||
selector: { type: property.type },
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default pullFromProperty;
|
||||
|
||||
@@ -6,16 +6,16 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
||||
import { get } from 'lodash';
|
||||
|
||||
const pushToProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.push',
|
||||
validate: null,
|
||||
name: 'creatureProperties.push',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}){
|
||||
run({ _id, path, value }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
@@ -25,10 +25,10 @@ const pushToProperty = new ValidatedMethod({
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
let maxCount = schema.get(joinedPath, 'maxCount');
|
||||
|
||||
if (Number.isFinite(maxCount)){
|
||||
if (Number.isFinite(maxCount)) {
|
||||
let array = get(property, path);
|
||||
let currentCount = array ? array.length : 0;
|
||||
if (currentCount >= maxCount){
|
||||
if (currentCount >= maxCount) {
|
||||
throw new Meteor.Error(
|
||||
'Array is full',
|
||||
`Cannot have more than ${maxCount} values`
|
||||
@@ -37,13 +37,13 @@ const pushToProperty = new ValidatedMethod({
|
||||
}
|
||||
|
||||
// Do work
|
||||
CreatureProperties.update(_id, {
|
||||
CreatureProperties.update(_id, {
|
||||
$push: { [joinedPath]: value },
|
||||
$set: { dirty: true },
|
||||
}, {
|
||||
selector: {type: property.type},
|
||||
});
|
||||
}
|
||||
}, {
|
||||
selector: { type: property.type },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default pushToProperty;
|
||||
|
||||
@@ -7,18 +7,18 @@ import { restore } from '/imports/api/parenting/softRemove.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
|
||||
const restoreProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'creatureProperties.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
@@ -30,7 +30,7 @@ const restoreProperty = new ValidatedMethod({
|
||||
$set: { dirty: true }
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default restoreProperty;
|
||||
|
||||
@@ -17,20 +17,20 @@ const selectAmmoItem = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, itemId, itemConsumedIndex}) {
|
||||
run({ actionId, itemId, itemConsumedIndex }) {
|
||||
// Permissions
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
let rootCreature = getRootCreatureAncestor(action);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Check that this index has a document to edit
|
||||
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
|
||||
if (!itemConsumed){
|
||||
if (!itemConsumed) {
|
||||
throw new Meteor.Error('Resouce not found',
|
||||
'Could not set ammo, because the ammo document was not found');
|
||||
}
|
||||
let itemToLink = CreatureProperties.findOne(itemId);
|
||||
if (!itemToLink){
|
||||
if (!itemToLink) {
|
||||
throw new Meteor.Error('Item not found',
|
||||
'Could not set ammo: the item was not found');
|
||||
}
|
||||
|
||||
@@ -7,24 +7,24 @@ import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
|
||||
const softRemoveProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'creatureProperties.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
softRemove({_id, collection: CreatureProperties});
|
||||
}
|
||||
softRemove({ _id, collection: CreatureProperties });
|
||||
}
|
||||
});
|
||||
|
||||
export default softRemoveProperty;
|
||||
|
||||
@@ -6,28 +6,28 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
||||
|
||||
const updateCreatureProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.update',
|
||||
validate({_id, path}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]){
|
||||
case 'type':
|
||||
validate({ _id, path }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]) {
|
||||
case 'type':
|
||||
case 'order':
|
||||
case 'parent':
|
||||
case 'ancestors':
|
||||
case 'damage':
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'This property can\'t be updated directly');
|
||||
}
|
||||
case 'damage':
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'This property can\'t be updated directly');
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
run({ _id, path, value }) {
|
||||
// Permission
|
||||
let property = CreatureProperties.findOne(_id, {
|
||||
fields: {type: 1, ancestors: 1}
|
||||
fields: { type: 1, ancestors: 1 }
|
||||
});
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
@@ -35,14 +35,14 @@ const updateCreatureProperty = new ValidatedMethod({
|
||||
let pathString = path.join('.');
|
||||
let modifier;
|
||||
// unset empty values
|
||||
if (value === null || value === undefined){
|
||||
modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } };
|
||||
if (value === null || value === undefined) {
|
||||
modifier = { $unset: { [pathString]: 1 }, $set: { dirty: true } };
|
||||
} else {
|
||||
modifier = { $set: {[pathString]: value, dirty: true } };
|
||||
modifier = { $set: { [pathString]: value, dirty: true } };
|
||||
}
|
||||
CreatureProperties.update(_id, modifier, {
|
||||
selector: {type: property.type},
|
||||
});
|
||||
CreatureProperties.update(_id, modifier, {
|
||||
selector: { type: property.type },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||
* Recomputes all ancestor creatures of this property
|
||||
*/
|
||||
export default function recomputeCreaturesByProperty(property){
|
||||
for (let ref of property.ancestors){
|
||||
if (ref.collection === 'creatures') {
|
||||
computeCreature.call(ref.id);
|
||||
}
|
||||
}
|
||||
for (let ref of property.ancestors){
|
||||
if (ref.collection === 'creatures') {
|
||||
computeCreature.call(ref.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Creatures = new Mongo.Collection('creatures');
|
||||
|
||||
let CreatureSettingsSchema = new SimpleSchema({
|
||||
//slowed down by carrying too much?
|
||||
useVariantEncumbrance: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide spellcasting tab
|
||||
hideSpellcasting: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//slowed down by carrying too much?
|
||||
useVariantEncumbrance: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide spellcasting tab
|
||||
hideSpellcasting: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide rest buttons
|
||||
hideRestButtons: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Hide all the unused stats
|
||||
hideUnusedStats: {
|
||||
type: Boolean,
|
||||
@@ -58,28 +63,28 @@ let CreatureSettingsSchema = new SimpleSchema({
|
||||
});
|
||||
|
||||
let CreatureSchema = new SimpleSchema({
|
||||
// Strings
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: '',
|
||||
optional: true,
|
||||
// Strings
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: '',
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
alignment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
alignment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.url,
|
||||
},
|
||||
},
|
||||
avatarPicture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -90,37 +95,37 @@ let CreatureSchema = new SimpleSchema({
|
||||
allowedLibraries: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraries.$': {
|
||||
type: String,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraries.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
allowedLibraryCollections: {
|
||||
type: Array,
|
||||
},
|
||||
allowedLibraryCollections: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraryCollections.$': {
|
||||
type: String,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraryCollections.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
|
||||
// Mechanics
|
||||
deathSave: {
|
||||
type: deathSaveSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Mechanics
|
||||
deathSave: {
|
||||
type: deathSaveSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Stats that are computed and denormalised outside of recomputation
|
||||
denormalizedStats: {
|
||||
type: Object,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Sum of all XP gained by this character
|
||||
'denormalizedStats.xp': {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
'denormalizedStats.xp': {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
// Sum of all levels granted by milestone XP
|
||||
'denormalizedStats.milestoneLevels': {
|
||||
type: SimpleSchema.Integer,
|
||||
@@ -133,24 +138,24 @@ let CreatureSchema = new SimpleSchema({
|
||||
},
|
||||
// Version of computation engine that was last used to compute this creature
|
||||
computeVersion: {
|
||||
type: String,
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
defaultValue: 'pc',
|
||||
allowedValues: ['pc', 'npc', 'monster'],
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
defaultValue: 'pc',
|
||||
allowedValues: ['pc', 'npc', 'monster'],
|
||||
},
|
||||
damageMultipliers: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
computeErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
@@ -161,7 +166,7 @@ let CreatureSchema = new SimpleSchema({
|
||||
'computeErrors.$.type': {
|
||||
type: String,
|
||||
},
|
||||
'computeErrors.$.details' : {
|
||||
'computeErrors.$.details': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
@@ -178,11 +183,11 @@ let CreatureSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
type: CreatureSettingsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Settings
|
||||
settings: {
|
||||
type: CreatureSettingsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
|
||||
CreatureSchema.extend(ColorSchema);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
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 simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||
|
||||
@@ -36,8 +36,8 @@ const changeAllowedLibraries = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, allowedLibraries, allowedLibraryCollections}) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
run({ _id, allowedLibraries, allowedLibraryCollections }) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
let $set;
|
||||
if (allowedLibraries) {
|
||||
@@ -48,7 +48,7 @@ const changeAllowedLibraries = new ValidatedMethod({
|
||||
$set.allowedLibraryCollections = allowedLibraryCollections;
|
||||
}
|
||||
if (!$set) return;
|
||||
Creatures.update(_id, {$set});
|
||||
Creatures.update(_id, { $set });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ const toggleAllUserLibraries = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, value}) {
|
||||
run({ _id, value }) {
|
||||
if (value) {
|
||||
Creatures.update(_id, {
|
||||
$unset: {
|
||||
@@ -87,4 +87,4 @@ const toggleAllUserLibraries = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
export {changeAllowedLibraries, toggleAllUserLibraries};
|
||||
export { changeAllowedLibraries, toggleAllUserLibraries };
|
||||
|
||||
@@ -51,7 +51,7 @@ const insertCreature = new ValidatedMethod({
|
||||
allowedLibraries,
|
||||
allowedLibraryCollections,
|
||||
});
|
||||
|
||||
|
||||
// Insert experience to get character to starting level
|
||||
if (startingLevel) {
|
||||
insertExperienceForCreature({
|
||||
@@ -70,7 +70,7 @@ const insertCreature = new ValidatedMethod({
|
||||
let baseId, rulesetSlot;
|
||||
defaultCharacterProperties(creatureId).forEach(prop => {
|
||||
let id = CreatureProperties.insert(prop);
|
||||
if (prop.name === 'Ruleset'){
|
||||
if (prop.name === 'Ruleset') {
|
||||
baseId = id;
|
||||
rulesetSlot = prop;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ const insertCreature = new ValidatedMethod({
|
||||
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
|
||||
}
|
||||
|
||||
return creatureId;
|
||||
return creatureId;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ function insertDefaultRuleset(creatureId, baseId, userId, slot) {
|
||||
const ruleset = fillCursor.fetch()[0]
|
||||
insertPropertyFromLibraryNode.call({
|
||||
nodeIds: [ruleset._id],
|
||||
parentRef: {id: baseId, collection: 'creatureProperties'},
|
||||
parentRef: { id: baseId, collection: 'creatureProperties' },
|
||||
order: 0.5,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const restCreature = new ValidatedMethod({
|
||||
applyTriggers(afterTriggers, null, actionContext);
|
||||
|
||||
// Insert log
|
||||
actionContext.writeLog();
|
||||
actionContext.writeLog();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,88 +57,124 @@ function doRestWork(restType, actionContext) {
|
||||
const creatureId = actionContext.creature._id;
|
||||
// Long rests reset short rest properties as well
|
||||
let resetFilter;
|
||||
if (restType === 'shortRest'){
|
||||
if (restType === 'shortRest') {
|
||||
resetFilter = 'shortRest'
|
||||
} else {
|
||||
resetFilter = {$in: ['shortRest', 'longRest']}
|
||||
resetFilter = { $in: ['shortRest', 'longRest'] }
|
||||
}
|
||||
resetProperties(creatureId, resetFilter, actionContext);
|
||||
|
||||
// Reset half hit dice on a long rest, starting with the highest dice
|
||||
if (restType === 'longRest') {
|
||||
resetHitDice(creatureId, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetProperties(creatureId, resetFilter, actionContext) {
|
||||
// Only apply to active properties
|
||||
let filter = {
|
||||
const filter = {
|
||||
'ancestors.id': creatureId,
|
||||
reset: resetFilter,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
};
|
||||
// update all attribute's damage
|
||||
filter.type = 'attribute';
|
||||
CreatureProperties.update(filter, {
|
||||
const attributeFilter = {
|
||||
...filter,
|
||||
type: 'attribute',
|
||||
damage: { $ne: 0 },
|
||||
}
|
||||
CreatureProperties.find(attributeFilter, {
|
||||
fields: { name: 1, damage: 1 }
|
||||
}).forEach(prop => {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}`
|
||||
});
|
||||
});
|
||||
CreatureProperties.update(attributeFilter, {
|
||||
$set: {
|
||||
damage: 0,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'attribute'},
|
||||
selector: { type: 'attribute' },
|
||||
multi: true,
|
||||
});
|
||||
// Update all action-like properties' usesUsed
|
||||
filter.type = {$in: [
|
||||
'action',
|
||||
'attack',
|
||||
'spell'
|
||||
]};
|
||||
CreatureProperties.update(filter, {
|
||||
const actionFilter = {
|
||||
...filter,
|
||||
type: {
|
||||
$in: ['action', 'spell']
|
||||
},
|
||||
usesUsed: { $ne: 0 },
|
||||
};
|
||||
CreatureProperties.find(actionFilter, {
|
||||
fields: { name: 1, usesUsed: 1 }
|
||||
}).forEach(prop => {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
|
||||
});
|
||||
});
|
||||
CreatureProperties.update(actionFilter, {
|
||||
$set: {
|
||||
usesUsed: 0,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'action'},
|
||||
selector: { type: 'action' },
|
||||
multi: true,
|
||||
});
|
||||
// Reset half hit dice on a long rest, starting with the highest dice
|
||||
if (restType === 'longRest'){
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
fields: {
|
||||
hitDiceSize: 1,
|
||||
damage: 1,
|
||||
total: 1,
|
||||
}
|
||||
|
||||
function resetHitDice(creatureId, actionContext) {
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
fields: {
|
||||
name: 1,
|
||||
hitDiceSize: 1,
|
||||
damage: 1,
|
||||
total: 1,
|
||||
}
|
||||
}).fetch();
|
||||
// Use a collator to do sorting in natural order
|
||||
let collator = new Intl.Collator('en', {
|
||||
numeric: true, sensitivity: 'base'
|
||||
});
|
||||
// Get the hit dice in decending order of hitDiceSize
|
||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||
hitDice.sort(compare);
|
||||
// Get the total number of hit dice that can be recovered this rest
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover, resultingDamage;
|
||||
hitDice.forEach(hd => {
|
||||
if (!recoverableHd) return;
|
||||
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
||||
if (!amountToRecover) return;
|
||||
recoverableHd -= amountToRecover;
|
||||
resultingDamage = hd.damage - amountToRecover;
|
||||
actionContext.addLog({
|
||||
name: hd.name,
|
||||
value: amountToRecover >= 0 ? `Restored ${amountToRecover} hit dice` : `Removed ${-amountToRecover} hit dice`
|
||||
});
|
||||
CreatureProperties.update(hd._id, {
|
||||
$set: {
|
||||
damage: resultingDamage,
|
||||
dirty: true,
|
||||
}
|
||||
}).fetch();
|
||||
// Use a collator to do sorting in natural order
|
||||
let collator = new Intl.Collator('en', {
|
||||
numeric: true, sensitivity: 'base'
|
||||
}, {
|
||||
selector: { type: 'attribute' },
|
||||
});
|
||||
// Get the hit dice in decending order of hitDiceSize
|
||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||
hitDice.sort(compare);
|
||||
// Get the total number of hit dice that can be recovered this rest
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover, resultingDamage;
|
||||
hitDice.forEach(hd => {
|
||||
if (!recoverableHd) return;
|
||||
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
||||
if (!amountToRecover) return;
|
||||
recoverableHd -= amountToRecover;
|
||||
resultingDamage = hd.damage - amountToRecover;
|
||||
CreatureProperties.update(hd._id, {
|
||||
$set: {
|
||||
damage: resultingDamage,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'attribute'},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default restCreature;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
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({
|
||||
name: 'creatures.update',
|
||||
validate({_id, path}){
|
||||
if (!_id) return false;
|
||||
// Allowed fields
|
||||
let allowedFields = [
|
||||
validate({ _id, path }) {
|
||||
if (!_id) return false;
|
||||
// Allowed fields
|
||||
let allowedFields = [
|
||||
'name',
|
||||
'alignment',
|
||||
'gender',
|
||||
@@ -17,26 +17,26 @@ const updateCreature = new ValidatedMethod({
|
||||
'color',
|
||||
'settings',
|
||||
];
|
||||
if (!allowedFields.includes(path[0])){
|
||||
throw new Meteor.Error('Creatures.methods.update.denied',
|
||||
'This field can\'t be updated using this method');
|
||||
}
|
||||
if (!allowedFields.includes(path[0])) {
|
||||
throw new Meteor.Error('Creatures.methods.update.denied',
|
||||
'This field can\'t be updated using this method');
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
run({ _id, path, value }) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
if (value === undefined || value === null){
|
||||
if (value === undefined || value === null) {
|
||||
Creatures.update(_id, {
|
||||
$unset: {[path.join('.')]: 1},
|
||||
$unset: { [path.join('.')]: 1 },
|
||||
});
|
||||
} else {
|
||||
Creatures.update(_id, {
|
||||
$set: {[path.join('.')]: value},
|
||||
$set: { [path.join('.')]: value },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,17 +8,17 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Experiences = new Mongo.Collection('experiences');
|
||||
|
||||
let ExperienceSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// The amount of XP this experience gives
|
||||
xp: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
},
|
||||
// The amount of XP this experience gives
|
||||
xp: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
// Setting levels instead of value grants whole levels
|
||||
levels: {
|
||||
type: SimpleSchema.Integer,
|
||||
@@ -26,17 +26,17 @@ let ExperienceSchema = new SimpleSchema({
|
||||
min: 0,
|
||||
index: 1,
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -46,8 +46,8 @@ let ExperienceSchema = new SimpleSchema({
|
||||
|
||||
Experiences.attachSchema(ExperienceSchema);
|
||||
|
||||
const insertExperienceForCreature = function({experience, creatureId, userId}){
|
||||
if (experience.xp){
|
||||
const insertExperienceForCreature = function ({ experience, creatureId }) {
|
||||
if (experience.xp) {
|
||||
Creatures.update(creatureId, {
|
||||
$inc: { 'denormalizedStats.xp': experience.xp },
|
||||
$set: { dirty: true },
|
||||
@@ -84,16 +84,16 @@ const insertExperience = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({experience, creatureIds}) {
|
||||
run({ experience, creatureIds }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.insert.denied',
|
||||
'You need to be logged in to insert an experience');
|
||||
'You need to be logged in to insert an experience');
|
||||
}
|
||||
let insertedIds = [];
|
||||
creatureIds.forEach(creatureId => {
|
||||
assertEditPermission(creatureId, userId);
|
||||
let id = insertExperienceForCreature({experience, creatureId, userId});
|
||||
let id = insertExperienceForCreature({ experience, creatureId, userId });
|
||||
insertedIds.push(id);
|
||||
});
|
||||
return insertedIds;
|
||||
@@ -113,17 +113,17 @@ const removeExperience = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({experienceId}) {
|
||||
run({ experienceId }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.remove.denied',
|
||||
'You need to be logged in to remove an experience');
|
||||
'You need to be logged in to remove an experience');
|
||||
}
|
||||
let experience = Experiences.findOne(experienceId);
|
||||
if (!experience) return;
|
||||
let creatureId = experience.creatureId
|
||||
assertEditPermission(creatureId, userId);
|
||||
if (experience.xp){
|
||||
if (experience.xp) {
|
||||
Creatures.update(creatureId, {
|
||||
$inc: { 'denormalizedStats.xp': -experience.xp },
|
||||
$set: { dirty: true },
|
||||
@@ -154,11 +154,11 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId}) {
|
||||
run({ creatureId }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.recompute.denied',
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
}
|
||||
assertEditPermission(creatureId, userId);
|
||||
|
||||
@@ -167,16 +167,18 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
Experiences.find({
|
||||
creatureId
|
||||
}, {
|
||||
fields: {xp: 1, levels: 1}
|
||||
fields: { xp: 1, levels: 1 }
|
||||
}).forEach(experience => {
|
||||
xp += experience.xp || 0;
|
||||
milestoneLevels += experience.levels || 0;
|
||||
});
|
||||
Creatures.update(creatureId, {$set: {
|
||||
'denormalizedStats.xp': xp,
|
||||
'denormalizedStats.milestoneLevels': milestoneLevels,
|
||||
dirty: true,
|
||||
}});
|
||||
Creatures.update(creatureId, {
|
||||
$set: {
|
||||
'denormalizedStats.xp': xp,
|
||||
'denormalizedStats.milestoneLevels': milestoneLevels,
|
||||
dirty: true,
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,33 +2,33 @@ import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
let ExperienceSchema = new SimpleSchema({
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// Potentially long description of the event
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// Potentially long description of the event
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
// The real-world date that it occured
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
// The date in-world of this event
|
||||
worldDate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// The real-world date that it occured
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
// The date in-world of this event
|
||||
worldDate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// Tags to better find this entry later
|
||||
tags: {
|
||||
type: Array,
|
||||
|
||||
@@ -4,13 +4,13 @@ import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables
|
||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import {parse, prettifyParseError} from '/imports/parser/parser.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
|
||||
}
|
||||
|
||||
@@ -25,17 +25,17 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
'content.$': {
|
||||
type: LogContentSchema,
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -50,23 +50,23 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
|
||||
CreatureLogs.attachSchema(CreatureLogSchema);
|
||||
|
||||
function removeOldLogs(creatureId){
|
||||
function removeOldLogs(creatureId) {
|
||||
// Find the first log that is over the limit
|
||||
let firstExpiredLog = CreatureLogs.find({
|
||||
creatureId
|
||||
}, {
|
||||
sort: {date: -1},
|
||||
sort: { date: -1 },
|
||||
skip: PER_CREATURE_LOG_LIMIT,
|
||||
});
|
||||
if (!firstExpiredLog) return;
|
||||
// Remove all logs older than the one over the limit
|
||||
CreatureLogs.remove({
|
||||
creatureId,
|
||||
date: {$lte: firstExpiredLog.date},
|
||||
date: { $lte: firstExpiredLog.date },
|
||||
});
|
||||
}
|
||||
|
||||
function logToMessageData(log){
|
||||
function logToMessageData(log) {
|
||||
let embed = {
|
||||
fields: [],
|
||||
};
|
||||
@@ -78,8 +78,8 @@ function logToMessageData(log){
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
function logWebhook({log, creature}){
|
||||
if (Meteor.isServer){
|
||||
function logWebhook({ log, creature }) {
|
||||
if (Meteor.isServer) {
|
||||
sendWebhookAsCreature({
|
||||
creature,
|
||||
data: logToMessageData(log),
|
||||
@@ -94,47 +94,49 @@ const insertCreatureLog = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({log}){
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({ log }) {
|
||||
const creatureId = log.creatureId;
|
||||
const creature = Creatures.findOne(creatureId, {fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}});
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
// Build the new log
|
||||
let id = insertCreatureLogWork({log, creature, method: this})
|
||||
let id = insertCreatureLogWork({ log, creature, method: this })
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export function insertCreatureLogWork({log, creature, method}){
|
||||
export function insertCreatureLogWork({ log, creature, method }) {
|
||||
// Build the new log
|
||||
if (typeof log === 'string'){
|
||||
log = {content: [{value: log}]};
|
||||
if (typeof log === 'string') {
|
||||
log = { content: [{ value: log }] };
|
||||
}
|
||||
if (!log.content?.length) return;
|
||||
log.date = new Date();
|
||||
// Insert it
|
||||
let id = CreatureLogs.insert(log);
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
method?.unblock();
|
||||
removeOldLogs(creature._id);
|
||||
logWebhook({log, creature});
|
||||
logWebhook({ log, creature });
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
function equalIgnoringWhitespace(a, b){
|
||||
function equalIgnoringWhitespace(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({
|
||||
@@ -144,33 +146,35 @@ const logRoll = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
run({roll, creatureId}){
|
||||
const creature = Creatures.findOne(creatureId, {fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}});
|
||||
validate: new SimpleSchema({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
run({ roll, creatureId }) {
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
try {
|
||||
parsedResult = parse(roll);
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
let error = prettifyParseError(e);
|
||||
logContent.push({name: 'Parse Error', value: error});
|
||||
logContent.push({ name: 'Parse Error', value: error });
|
||||
}
|
||||
if (parsedResult) try {
|
||||
let {
|
||||
@@ -184,19 +188,19 @@ const logRoll = new ValidatedMethod({
|
||||
logContent.push({
|
||||
value: compiledString
|
||||
});
|
||||
let {result: rolled} = resolve('roll', compiled, variables, context);
|
||||
let { result: rolled } = resolve('roll', compiled, variables, context);
|
||||
let rolledString = toString(rolled);
|
||||
if (rolledString !== compiledString) logContent.push({
|
||||
value: rolledString
|
||||
});
|
||||
let {result} = resolve('reduce', rolled, variables, context);
|
||||
let { result } = resolve('reduce', rolled, variables, context);
|
||||
let resultString = toString(result);
|
||||
if (resultString !== rolledString) logContent.push({
|
||||
value: resultString
|
||||
});
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logContent = [{name: 'Calculation error'}];
|
||||
logContent = [{ name: 'Calculation error' }];
|
||||
}
|
||||
const log = {
|
||||
content: logContent,
|
||||
@@ -204,11 +208,11 @@ const logRoll = new ValidatedMethod({
|
||||
date: new Date(),
|
||||
};
|
||||
|
||||
let id = insertCreatureLogWork({log, creature, method: this});
|
||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
||||
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export default CreatureLogs;
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT};
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT };
|
||||
|
||||
3
app/imports/api/docs/Docs.js
Normal file
3
app/imports/api/docs/Docs.js
Normal 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;
|
||||
@@ -4,6 +4,7 @@ import branch from './applyPropertyByType/applyBranch.js';
|
||||
import buff from './applyPropertyByType/applyBuff.js';
|
||||
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
|
||||
import damage from './applyPropertyByType/applyDamage.js';
|
||||
import folder from './applyPropertyByType/applyFolder.js';
|
||||
import note from './applyPropertyByType/applyNote.js';
|
||||
import roll from './applyPropertyByType/applyRoll.js';
|
||||
import savingThrow from './applyPropertyByType/applySavingThrow.js';
|
||||
@@ -16,6 +17,7 @@ const applyPropertyByType = {
|
||||
buff,
|
||||
buffRemover,
|
||||
damage,
|
||||
folder,
|
||||
note,
|
||||
roll,
|
||||
savingThrow,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
|
||||
|
||||
export default function applyAction(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) {
|
||||
|
||||
// Log the name and summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
if (prop.summary?.text) {
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
|
||||
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||
|
||||
// Attack if there is an attack roll
|
||||
if (attack && attack.calculation){
|
||||
if (targets.length){
|
||||
if (attack && attack.calculation) {
|
||||
if (targets.length) {
|
||||
targets.forEach(target => {
|
||||
applyAttackToTarget({attack, target, actionContext});
|
||||
applyAttackToTarget({ attack, target, actionContext });
|
||||
// Apply the children, but only to the current target
|
||||
actionContext.targets = [target];
|
||||
applyChildren(node, actionContext);
|
||||
});
|
||||
} else {
|
||||
applyAttackWithoutTarget({attack, actionContext});
|
||||
applyAttackWithoutTarget({ attack, actionContext });
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
} else {
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
if (prop.actionType === 'event' && prop.variableName) {
|
||||
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttackWithoutTarget({attack, actionContext}){
|
||||
function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||
delete actionContext.scope['$attackHit'];
|
||||
delete actionContext.scope['$attackMiss'];
|
||||
delete actionContext.scope['$criticalHit'];
|
||||
@@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
if (!criticalMiss){
|
||||
scope['$attackHit'] = {value: true}
|
||||
if (!criticalMiss) {
|
||||
scope['$attackHit'] = { value: true }
|
||||
}
|
||||
if (!criticalHit){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
if (!criticalHit) {
|
||||
scope['$attackMiss'] = { value: true };
|
||||
}
|
||||
|
||||
actionContext.addLog({
|
||||
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
|
||||
});
|
||||
}
|
||||
|
||||
function applyAttackToTarget({attack, target, actionContext}){
|
||||
function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
const scope = actionContext.scope;
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
|
||||
if (target.variables.armor){
|
||||
if (target.variables.armor) {
|
||||
const armor = target.variables.armor.value;
|
||||
|
||||
let name = criticalHit ? 'Critical Hit!' :
|
||||
criticalMiss ? 'Critical Miss!' :
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
|
||||
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
if (criticalMiss || result < armor){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
if (criticalMiss || result < armor) {
|
||||
scope['$attackMiss'] = { value: true };
|
||||
} else {
|
||||
scope['$attackHit'] = {value: true};
|
||||
scope['$attackHit'] = { value: true };
|
||||
}
|
||||
} else {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value:'Target has no `armor`',
|
||||
value: 'Target has no `armor`',
|
||||
});
|
||||
actionContext.addLog({
|
||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||
@@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){
|
||||
}
|
||||
}
|
||||
|
||||
function rollAttack(attack, scope){
|
||||
function rollAttack(attack, scope) {
|
||||
const rollModifierText = numberToSignedString(attack.value, true);
|
||||
let value, resultPrefix;
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -159,25 +163,25 @@ function rollAttack(attack, scope){
|
||||
value = rollDice(1, 20)[0];
|
||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||
}
|
||||
scope['$attackRoll'] = {value};
|
||||
scope['$attackRoll'] = { value };
|
||||
const result = value + attack.value;
|
||||
const {criticalHit, criticalMiss} = applyCrits(value, scope);
|
||||
return {resultPrefix, result, value, criticalHit, criticalMiss};
|
||||
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
||||
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyCrits(value, scope){
|
||||
function applyCrits(value, scope) {
|
||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
||||
let criticalHit = value >= criticalHitTarget;
|
||||
let criticalMiss;
|
||||
if (criticalHit){
|
||||
scope['$criticalHit'] = {value: true};
|
||||
if (criticalHit) {
|
||||
scope['$criticalHit'] = { value: true };
|
||||
} else {
|
||||
criticalMiss = value === 1;
|
||||
if (criticalMiss){
|
||||
scope['$criticalMiss'] = {value: true};
|
||||
if (criticalMiss) {
|
||||
scope['$criticalMiss'] = { value: true };
|
||||
}
|
||||
}
|
||||
return {criticalHit, criticalMiss};
|
||||
return { criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyChildren(node, actionContext) {
|
||||
@@ -185,9 +189,9 @@ function applyChildren(node, actionContext) {
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function spendResources(prop, actionContext){
|
||||
function spendResources(prop, actionContext) {
|
||||
// Check Uses
|
||||
if (prop.usesLeft <= 0){
|
||||
if (prop.usesLeft <= 0) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||
@@ -195,7 +199,7 @@ function spendResources(prop, actionContext){
|
||||
return true;
|
||||
}
|
||||
// Resources
|
||||
if (prop.insufficientResources){
|
||||
if (prop.insufficientResources) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||
@@ -209,14 +213,14 @@ function spendResources(prop, actionContext){
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
if (!itemConsumed.itemId){
|
||||
if (!itemConsumed.itemId) {
|
||||
throw 'No ammo was selected for this prop';
|
||||
}
|
||||
let item = CreatureProperties.findOne(itemConsumed.itemId);
|
||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
|
||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
|
||||
throw 'The prop\'s ammo was not found on the creature';
|
||||
}
|
||||
if (!item.equipped){
|
||||
if (!item.equipped) {
|
||||
throw 'The selected ammo is not equipped';
|
||||
}
|
||||
if (
|
||||
@@ -229,16 +233,16 @@ function spendResources(prop, actionContext){
|
||||
value: itemConsumed.quantity.value,
|
||||
});
|
||||
let logName = item.name;
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
|
||||
logName = item.plural || logName;
|
||||
}
|
||||
if (itemConsumed.quantity.value > 0){
|
||||
if (itemConsumed.quantity.value > 0) {
|
||||
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
|
||||
} else if (itemConsumed.quantity.value < 0){
|
||||
} else if (itemConsumed.quantity.value < 0) {
|
||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||
}
|
||||
});
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
@@ -251,9 +255,9 @@ function spendResources(prop, actionContext){
|
||||
itemQuantityAdjustments.forEach(adjustQuantityWork);
|
||||
|
||||
// Use uses
|
||||
if (prop.usesLeft){
|
||||
if (prop.usesLeft) {
|
||||
CreatureProperties.update(prop._id, {
|
||||
$inc: {usesUsed: 1}
|
||||
$inc: { usesUsed: 1 }
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
@@ -270,7 +274,7 @@ function spendResources(prop, actionContext){
|
||||
|
||||
if (!attConsumed.quantity?.value) return;
|
||||
let stat = actionContext.scope[attConsumed.variableName];
|
||||
if (!stat){
|
||||
if (!stat) {
|
||||
spendLog.push(stat.name + ': ' + ' not found');
|
||||
return;
|
||||
}
|
||||
@@ -280,9 +284,9 @@ function spendResources(prop, actionContext){
|
||||
value: attConsumed.quantity.value,
|
||||
actionContext,
|
||||
});
|
||||
if (attConsumed.quantity.value > 0){
|
||||
if (attConsumed.quantity.value > 0) {
|
||||
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
|
||||
} else if (attConsumed.quantity.value < 0){
|
||||
} else if (attConsumed.quantity.value < 0) {
|
||||
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
} 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 computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.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 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);
|
||||
const prop = node.node;
|
||||
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
// Then copy the decendants of the buff to the targets
|
||||
let propList = [prop];
|
||||
function addChildrenToPropList(children, { skipCrystalize } = {}){
|
||||
function addChildrenToPropList(children, { skipCrystalize } = {}) {
|
||||
children.forEach(child => {
|
||||
if (skipCrystalize) child.node._skipCrystalize = true;
|
||||
propList.push(child.node);
|
||||
@@ -34,7 +34,7 @@ export default function applyBuff(node, actionContext){
|
||||
}
|
||||
addChildrenToPropList(node.children);
|
||||
if (!prop.skipCrystalization) {
|
||||
crystalizeVariables({propList, actionContext});
|
||||
crystalizeVariables({ propList, actionContext });
|
||||
}
|
||||
|
||||
let oldParent = {
|
||||
@@ -46,8 +46,8 @@ export default function applyBuff(node, actionContext){
|
||||
copyNodeListToTarget(propList, target, oldParent);
|
||||
|
||||
//Log the buff
|
||||
if ((prop.name || prop.description?.value) && !prop.silent){
|
||||
if (target._id === actionContext.creature._id){
|
||||
if ((prop.name || prop.description?.value) && !prop.silent) {
|
||||
if (target._id === actionContext.creature._id) {
|
||||
// Targeting self
|
||||
actionContext.addLog({
|
||||
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
|
||||
}
|
||||
|
||||
function copyNodeListToTarget(propList, target, oldParent){
|
||||
let ancestry = [{collection: 'creatures', id: target._id}];
|
||||
function copyNodeListToTarget(propList, target, oldParent) {
|
||||
let ancestry = [{ collection: 'creatures', id: target._id }];
|
||||
setLineageOfDocs({
|
||||
docArray: propList,
|
||||
newAncestry: ancestry,
|
||||
@@ -93,14 +93,14 @@ function copyNodeListToTarget(propList, target, oldParent){
|
||||
* Replaces all variables with their resolved values
|
||||
* except variables of the form `$target.thing.total` become `thing.total`
|
||||
*/
|
||||
function crystalizeVariables({propList, actionContext}){
|
||||
function crystalizeVariables({ propList, actionContext }) {
|
||||
propList.forEach(prop => {
|
||||
if (prop._skipCrystalize) {
|
||||
delete prop._skipCrystalize;
|
||||
return;
|
||||
}
|
||||
// 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) => {
|
||||
const calcObj = get(prop, key);
|
||||
if (!calcObj?.parseNode) return;
|
||||
@@ -110,12 +110,12 @@ function crystalizeVariables({propList, actionContext}){
|
||||
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
||||
) return node;
|
||||
// Handle variables
|
||||
if (node.name === '$target'){
|
||||
if (node.name === '$target') {
|
||||
// strip $target
|
||||
if (node.parseType === 'accessor'){
|
||||
if (node.parseType === 'accessor') {
|
||||
node.name = node.path.shift();
|
||||
if (!node.path.length){
|
||||
return symbol.create({name: node.name})
|
||||
if (!node.path.length) {
|
||||
return symbol.create({ name: node.name })
|
||||
}
|
||||
} else {
|
||||
// Can't strip symbols
|
||||
@@ -127,7 +127,7 @@ function crystalizeVariables({propList, actionContext}){
|
||||
return node;
|
||||
} else {
|
||||
// Resolve all other variables
|
||||
const {result, context} = resolve('reduce', node, actionContext.scope);
|
||||
const { result, context } = resolve('reduce', node, actionContext.scope);
|
||||
logErrors(context.errors, actionContext);
|
||||
return result;
|
||||
}
|
||||
@@ -137,14 +137,14 @@ function crystalizeVariables({propList, actionContext}){
|
||||
});
|
||||
});
|
||||
// For each key in the schema
|
||||
computedSchemas[prop.type].inlineCalculationFields().forEach( calcKey => {
|
||||
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
|
||||
// That ends in .inlineCalculations
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const inlineCalcObj = get(prop, key);
|
||||
if (!inlineCalcObj) return;
|
||||
|
||||
// If there is no text, skip
|
||||
if (!inlineCalcObj.text){
|
||||
if (!inlineCalcObj.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { some, intersection, difference, remove } from 'lodash';
|
||||
import { some, intersection, difference, remove, includes } from 'lodash';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import resolve, { Context, toString } from '/imports/parser/resolve.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyDamage(node, actionContext){
|
||||
export default function applyDamage(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function(){
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
@@ -28,10 +28,10 @@ export default function applyDamage(node, actionContext){
|
||||
// Determine if the hit is critical
|
||||
let criticalHit = scope['$criticalHit']?.value &&
|
||||
prop.damageType !== 'healing' // Can't critically heal
|
||||
;
|
||||
;
|
||||
// Double the damage rolls if the hit is critical
|
||||
let context = new Context({
|
||||
options: {doubleRolls: criticalHit},
|
||||
options: { doubleRolls: criticalHit },
|
||||
});
|
||||
|
||||
// Gather all the lines we need to log into an array
|
||||
@@ -40,8 +40,8 @@ export default function applyDamage(node, actionContext){
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
|
||||
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
if (rolled.parseType !== 'constant') {
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, actionContext);
|
||||
@@ -50,13 +50,13 @@ export default function applyDamage(node, actionContext){
|
||||
context.errors = [];
|
||||
|
||||
// Resolve the roll to a final value
|
||||
const {result: reduced} = resolve('reduce', rolled, scope, context);
|
||||
const { result: reduced } = resolve('reduce', rolled, scope, context);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
if (reduced.parseType === 'constant') {
|
||||
prop.amount.value = reduced.value;
|
||||
} else if (reduced.parseType === 'error'){
|
||||
} else if (reduced.parseType === 'error') {
|
||||
prop.amount.value = null;
|
||||
} else {
|
||||
prop.amount.value = toString(reduced);
|
||||
@@ -64,7 +64,7 @@ export default function applyDamage(node, actionContext){
|
||||
let damage = +reduced.value;
|
||||
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function applyDamage(node, actionContext){
|
||||
// Memoise the damage suffix for the log
|
||||
let suffix = (criticalHit ? ' critical ' : ' ') +
|
||||
prop.damageType +
|
||||
(prop.damageType !== 'healing' ? ' damage ': '');
|
||||
(prop.damageType !== 'healing' ? ' damage ' : '');
|
||||
|
||||
if (damageTargets && damageTargets.length) {
|
||||
// Iterate through all the targets
|
||||
@@ -107,7 +107,7 @@ export default function applyDamage(node, actionContext){
|
||||
});
|
||||
|
||||
// Log the damage done
|
||||
if (target._id === actionContext.creature._id){
|
||||
if (target._id === actionContext.creature._id) {
|
||||
// Target is same as self, log damage as such
|
||||
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
||||
} else {
|
||||
@@ -136,33 +136,33 @@ export default function applyDamage(node, actionContext){
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
||||
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
|
||||
const damageType = damageProp?.damageType;
|
||||
if (!damageType) return damage;
|
||||
|
||||
const multiplier = target?.variables?.[damageType];
|
||||
if (!multiplier) return damage;
|
||||
|
||||
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
|
||||
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
|
||||
|
||||
if (
|
||||
multiplier.immunity &&
|
||||
some(multiplier.immunities, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
|
||||
) {
|
||||
logValue.push(`Immune to ${damageTypeText}`);
|
||||
return 0;
|
||||
} else {
|
||||
if (
|
||||
multiplier.resistance &&
|
||||
some(multiplier.resistances, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
|
||||
) {
|
||||
logValue.push(`Resistant to ${damageTypeText}`);
|
||||
damage = Math.floor(damage / 2);
|
||||
}
|
||||
if (
|
||||
multiplier.vulnerability &&
|
||||
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp))
|
||||
){
|
||||
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
|
||||
) {
|
||||
logValue.push(`Vulnerable to ${damageTypeText}`);
|
||||
damage = Math.floor(damage * 2);
|
||||
}
|
||||
@@ -170,8 +170,11 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
||||
return damage;
|
||||
}
|
||||
|
||||
function multiplierAppliesTo(damageProp){
|
||||
function multiplierAppliesTo(damageProp, multiplierType) {
|
||||
return multiplier => {
|
||||
// Apply the default 'ignore x' tags
|
||||
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
|
||||
|
||||
const hasRequiredTags = difference(
|
||||
multiplier.includeTags, damageProp.tags
|
||||
).length === 0;
|
||||
@@ -184,7 +187,7 @@ function multiplierAppliesTo(damageProp){
|
||||
}
|
||||
}
|
||||
|
||||
function dealDamage({target, damageType, amount, actionContext}){
|
||||
function dealDamage({ target, damageType, amount, actionContext }) {
|
||||
// Get all the health bars and do damage to them
|
||||
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
||||
|
||||
@@ -236,6 +239,14 @@ function dealDamage({target, damageType, amount, actionContext}){
|
||||
actionContext
|
||||
});
|
||||
damageLeft -= damageAdded;
|
||||
// Prevent overflow
|
||||
if (
|
||||
damageType === 'healing' ?
|
||||
healthBar.healthBarNoHealingOverflow :
|
||||
healthBar.healthBarNoDamageOverflow
|
||||
) {
|
||||
damageLeft = 0;
|
||||
}
|
||||
});
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import logErrors from './logErrors.js';
|
||||
export default function recalculateCalculation(calc, actionContext, context){
|
||||
if (!calc?.parseNode) return;
|
||||
calc._parseLevel = 'reduce';
|
||||
applyEffectsToCalculationParseNode(calc, actionContext.log);
|
||||
applyEffectsToCalculationParseNode(calc, actionContext);
|
||||
evaluateCalculation(calc, actionContext.scope, context);
|
||||
logErrors(calc.errors, actionContext.log);
|
||||
logErrors(calc.errors, actionContext);
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ const doAction = new ValidatedMethod({
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
const creatureId = action.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
@@ -56,13 +56,13 @@ const doAction = new ValidatedMethod({
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Do the action
|
||||
doActionWork({properties, ancestors, actionContext, methodScope: scope});
|
||||
doActionWork({ properties, ancestors, actionContext, methodScope: scope });
|
||||
|
||||
// Recompute all involved creatures
|
||||
Creatures.update({
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: {dirty: true},
|
||||
$set: { dirty: true },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -71,11 +71,11 @@ export default doAction;
|
||||
|
||||
export function doActionWork({
|
||||
properties, ancestors, actionContext, methodScope = {},
|
||||
}){
|
||||
}) {
|
||||
// get the docs
|
||||
const ancestorScope = getAncestorScope(ancestors);
|
||||
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`);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export function doActionWork({
|
||||
}
|
||||
|
||||
// Assumes ancestors are in tree order already
|
||||
function getAncestorScope(ancestors){
|
||||
function getAncestorScope(ancestors) {
|
||||
let scope = {};
|
||||
ancestors.forEach(prop => {
|
||||
scope[`#${prop.type}`] = prop;
|
||||
|
||||
@@ -20,6 +20,10 @@ const doAction = new ValidatedMethod({
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
ritual: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
@@ -41,13 +45,13 @@ const doAction = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ spellId, slotId, targetIds = [], scope = {} }) {
|
||||
// Get action context
|
||||
run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
|
||||
// Get action context
|
||||
let spell = CreatureProperties.findOne(spellId);
|
||||
const creatureId = spell.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
@@ -64,25 +68,26 @@ const doAction = new ValidatedMethod({
|
||||
let slotLevel = spell.level || 0;
|
||||
let slot;
|
||||
|
||||
if (slotId && !spell.castWithoutSpellSlots){
|
||||
// If a spell requires a slot, make sure a slot is spent
|
||||
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
|
||||
slot = CreatureProperties.findOne(slotId);
|
||||
if (!slot){
|
||||
if (!slot) {
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot not found to cast spell');
|
||||
}
|
||||
if (!slot.value){
|
||||
if (!slot.value) {
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot depleted');
|
||||
}
|
||||
if (slot.attributeType !== 'spellSlot'){
|
||||
if (slot.attributeType !== 'spellSlot') {
|
||||
throw new Meteor.Error('Not a 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',
|
||||
'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',
|
||||
'Slot is not large enough to cast spell');
|
||||
}
|
||||
@@ -96,14 +101,20 @@ const doAction = new ValidatedMethod({
|
||||
}
|
||||
|
||||
// Post the slot level spent to the log
|
||||
if (slot?.spellSlotLevel?.value){
|
||||
if (slot?.spellSlotLevel?.value) {
|
||||
actionContext.addLog({
|
||||
name: `Casting using a level ${slotLevel} spell slot`
|
||||
});
|
||||
} else if (slotLevel) {
|
||||
actionContext.addLog({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
if (ritual) {
|
||||
actionContext.addLog({
|
||||
name: `Ritual casting at level ${slotLevel}`
|
||||
});
|
||||
} else {
|
||||
actionContext.addLog({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
actionContext.scope['slotLevel'] = slotLevel;
|
||||
|
||||
@@ -7,6 +7,7 @@ import rollDice from '/imports/parser/rollDice.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
|
||||
|
||||
const doCheck = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCheck',
|
||||
@@ -22,7 +23,7 @@ const doCheck = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({propId, scope}) {
|
||||
run({ propId, scope }) {
|
||||
const prop = CreatureProperties.findOne(propId);
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
@@ -32,13 +33,13 @@ const doCheck = new ValidatedMethod({
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Do the check
|
||||
doCheckWork({prop, actionContext});
|
||||
doCheckWork({ prop, actionContext });
|
||||
},
|
||||
});
|
||||
|
||||
export default doCheck;
|
||||
|
||||
export function doCheckWork({prop, actionContext}){
|
||||
export function doCheckWork({ prop, actionContext }) {
|
||||
|
||||
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
||||
rollCheck(prop, actionContext);
|
||||
@@ -53,17 +54,17 @@ function rollCheck(prop, actionContext) {
|
||||
// get the modifier for the roll
|
||||
let rollModifier;
|
||||
let logName = `${prop.name} check`;
|
||||
if (prop.type === 'skill'){
|
||||
if (prop.type === 'skill') {
|
||||
rollModifier = prop.value;
|
||||
if (prop.skillType === 'save'){
|
||||
if (prop.name.match(/save/i)){
|
||||
if (prop.skillType === 'save') {
|
||||
if (prop.name.match(/save/i)) {
|
||||
logName = prop.name;
|
||||
} else {
|
||||
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
|
||||
}
|
||||
}
|
||||
} else if (prop.type === 'attribute'){
|
||||
if (prop.attributeType === 'ability'){
|
||||
} else if (prop.type === 'attribute') {
|
||||
if (prop.attributeType === 'ability') {
|
||||
rollModifier = prop.modifier;
|
||||
} else {
|
||||
rollModifier = prop.value;
|
||||
@@ -72,10 +73,14 @@ function rollCheck(prop, actionContext) {
|
||||
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;
|
||||
if (scope['$checkAdvantage'] === 1){
|
||||
if (scope['$checkAdvantage'] === 1) {
|
||||
logName += ' (Advantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
@@ -85,7 +90,7 @@ function rollCheck(prop, actionContext) {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else if (scope['$checkAdvantage'] === -1){
|
||||
} else if (scope['$checkAdvantage'] === -1) {
|
||||
logName += ' (Disadvantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
@@ -101,8 +106,29 @@ function rollCheck(prop, actionContext) {
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
}
|
||||
const result = (value + rollModifier) || 0;
|
||||
scope['$checkDiceRoll'] = value;
|
||||
scope['$checkRoll'] = result;
|
||||
scope['$checkModifier'] = rollModifier;
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
});
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
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';
|
||||
|
||||
interface CreatureProperty {
|
||||
_id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
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
|
||||
this.originalPropsById = {};
|
||||
this.propsById = {};
|
||||
this.originalPropsById = {};
|
||||
this.propsById = {};
|
||||
this.propsWithTag = {};
|
||||
this.scope = {};
|
||||
this.props = properties;
|
||||
this.props = properties;
|
||||
this.dependencyGraph = createGraph();
|
||||
this.errors = [];
|
||||
this.creature = creature;
|
||||
@@ -29,8 +29,8 @@ function childrenActive(prop){
|
||||
// Children of disabled properties are always inactive
|
||||
if (prop.disabled) return false;
|
||||
switch (prop.type){
|
||||
// Only equipped items have active children
|
||||
case 'item': return !!prop.equipped;
|
||||
// Only equipped items with non-zero quantity have active children
|
||||
case 'item': return !!prop.equipped && prop.quantity !== 0;
|
||||
// The children of actions, spells, and triggers are always inactive
|
||||
case 'action': return false;
|
||||
case 'spell': return false;
|
||||
|
||||
@@ -10,8 +10,6 @@ export default function computeToggleDependencies(node, dependencyGraph){
|
||||
prop.enabled
|
||||
) return;
|
||||
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
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
|
||||
@@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
|
||||
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
|
||||
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
|
||||
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
|
||||
import CreatureComputation from './CreatureComputation.js';
|
||||
import CreatureComputation from './CreatureComputation.ts';
|
||||
import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import skill from './computeByType/computeSkill.js';
|
||||
import pointBuy from './computeByType/computePointBuy.js';
|
||||
import propertySlot from './computeByType/computeSlot.js';
|
||||
import container from './computeByType/computeContainer.js';
|
||||
import spellList from './computeByType/computeSpellList.js';
|
||||
import _calculation from './computeByType/computeCalculation.js';
|
||||
|
||||
export default Object.freeze({
|
||||
@@ -17,4 +18,5 @@ export default Object.freeze({
|
||||
pointBuy,
|
||||
propertySlot,
|
||||
spell: action,
|
||||
spellList,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default function computeSpelllist(computation, node) {
|
||||
const prop = node.data;
|
||||
|
||||
const ability = computation.scope[prop.ability];
|
||||
prop.abilityMod = ability?.modifier || 0;
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export default function aggregateEffect({node, linkedNode, link}){
|
||||
import { pick } from 'lodash';
|
||||
|
||||
export default function aggregateEffect({ node, linkedNode, link }) {
|
||||
if (link.data !== 'effect') return;
|
||||
// store the effect aggregator, its presence indicates that the variable is
|
||||
// targeted by effects
|
||||
@@ -19,12 +21,24 @@ export default function aggregateEffect({node, linkedNode, link}){
|
||||
|
||||
// Store a summary of the effect itself
|
||||
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({
|
||||
_id: linkedNode.data._id,
|
||||
name: linkedNode.data.name,
|
||||
operation: linkedNode.data.operation,
|
||||
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value},
|
||||
amount: effectAmount,
|
||||
type: linkedNode.data.type,
|
||||
text: linkedNode.data.text,
|
||||
// ancestors: linkedNode.data.ancestors,
|
||||
});
|
||||
|
||||
@@ -32,14 +46,13 @@ export default function aggregateEffect({node, linkedNode, link}){
|
||||
const aggregator = node.data.effectAggregator;
|
||||
// Get the result of the effect
|
||||
const result = linkedNode.data.amount?.value;
|
||||
// Skip aggregating if the result is not resolved completely
|
||||
if (typeof result === 'string') return;
|
||||
|
||||
// Aggregate the effect based on its operation
|
||||
switch(linkedNode.data.operation){
|
||||
switch (linkedNode.data.operation) {
|
||||
case 'base':
|
||||
// Take the largest base value
|
||||
if (Number.isFinite(result)){
|
||||
if(Number.isFinite(aggregator.base)){
|
||||
if (Number.isFinite(result)) {
|
||||
if (Number.isFinite(aggregator.base)) {
|
||||
aggregator.base = Math.max(aggregator.base, result);
|
||||
} else {
|
||||
aggregator.base = result;
|
||||
|
||||
@@ -7,19 +7,19 @@ import getAggregatorResult from './getAggregatorResult.js';
|
||||
export default function computeImplicitVariable(node){
|
||||
const prop = {};
|
||||
|
||||
// Combine damage multipliers
|
||||
if (node.data.immunity){
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
// Combine damage multipliers
|
||||
if (node.data.immunity){
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
|
||||
const result = getAggregatorResult(node);
|
||||
if (result !== undefined){
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function evaluateToggles(computation, node){
|
||||
let toggles = prop._computationDetails?.toggleAncestors;
|
||||
if (!toggles) return;
|
||||
toggles.forEach(toggle => {
|
||||
if (prop.inactive || !toggle.condition) return;
|
||||
if (!toggle.condition) return;
|
||||
if (!toggle.condition.value){
|
||||
prop.inactive = true;
|
||||
prop.deactivatedByToggle = true;
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function getEffectivePropTags(prop) {
|
||||
if (prop.variableName) tags.push(prop.variableName);
|
||||
if (prop.damageType) tags.push(prop.damageType);
|
||||
if (prop.skillType) tags.push(prop.skillType);
|
||||
if (prop.actionType) tags.push(prop.actionType);
|
||||
if (prop.attributeType) tags.push(prop.attributeType);
|
||||
if (prop.reset) tags.push(prop.reset);
|
||||
return tags;
|
||||
|
||||
@@ -7,17 +7,17 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Icons = new Mongo.Collection('icons');
|
||||
|
||||
let iconsSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
name: {
|
||||
type: String,
|
||||
unique: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
index: 1,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
@@ -38,7 +38,7 @@ if (Meteor.isServer) {
|
||||
Icons._ensureIndex({
|
||||
'name': 'text',
|
||||
'description': 'text',
|
||||
'tags': 'text',
|
||||
'tags': 'text',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,15 +55,15 @@ Icons.attachSchema(iconsSchema);
|
||||
|
||||
// This method does not validate icons against the schema, use wisely;
|
||||
const writeIcons = new ValidatedMethod({
|
||||
name: 'icons.write',
|
||||
validate: null,
|
||||
run(icons){
|
||||
name: 'icons.write',
|
||||
validate: null,
|
||||
run(icons) {
|
||||
assertAdmin(this.userId);
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
this.unblock();
|
||||
Icons.rawCollection().insert(icons, {ordered: false});
|
||||
Icons.rawCollection().insert(icons, { ordered: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const findIcons = new ValidatedMethod({
|
||||
@@ -80,11 +80,11 @@ const findIcons = new ValidatedMethod({
|
||||
numRequests: 20,
|
||||
timeInterval: 10000,
|
||||
},
|
||||
run({search}){
|
||||
run({ search }) {
|
||||
if (!search) return [];
|
||||
if (!Meteor.isServer) return;
|
||||
return Icons.find(
|
||||
{ $text: {$search: search} },
|
||||
{ $text: { $search: search } },
|
||||
{
|
||||
// relevant documents have a higher score.
|
||||
fields: {
|
||||
|
||||
@@ -20,7 +20,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Libraries = new Mongo.Collection('libraries');
|
||||
|
||||
let LibrarySchema = new SimpleSchema({
|
||||
name: {
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
@@ -39,95 +39,95 @@ export default Libraries;
|
||||
|
||||
const insertLibrary = new ValidatedMethod({
|
||||
name: 'libraries.insert',
|
||||
mixins: [
|
||||
simpleSchemaMixin,
|
||||
mixins: [
|
||||
simpleSchemaMixin,
|
||||
],
|
||||
schema: LibrarySchema.omit('owner'),
|
||||
run(library) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('Libraries.methods.insert.denied',
|
||||
'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);
|
||||
if (!tier.paidBenefits){
|
||||
if (!tier.paidBenefits) {
|
||||
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`);
|
||||
}
|
||||
library.owner = this.userId;
|
||||
return Libraries.insert(library);
|
||||
return Libraries.insert(library);
|
||||
},
|
||||
});
|
||||
|
||||
const updateLibraryName = new ValidatedMethod({
|
||||
name: 'libraries.updateName',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
name: 'libraries.updateName',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, name}){
|
||||
let library = Libraries.findOne(_id);
|
||||
assertEditPermission(library, this.userId);
|
||||
Libraries.update(_id, {$set: {name}});
|
||||
},
|
||||
run({ _id, name }) {
|
||||
let library = Libraries.findOne(_id);
|
||||
assertEditPermission(library, this.userId);
|
||||
Libraries.update(_id, { $set: { name } });
|
||||
},
|
||||
});
|
||||
|
||||
const updateLibraryDescription = new ValidatedMethod({
|
||||
name: 'libraries.updateDescription',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
name: 'libraries.updateDescription',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, description}){
|
||||
let library = Libraries.findOne(_id);
|
||||
assertEditPermission(library, this.userId);
|
||||
Libraries.update(_id, {$set: {description}});
|
||||
},
|
||||
run({ _id, description }) {
|
||||
let library = Libraries.findOne(_id);
|
||||
assertEditPermission(library, this.userId);
|
||||
Libraries.update(_id, { $set: { description } });
|
||||
},
|
||||
});
|
||||
|
||||
const removeLibrary = new ValidatedMethod({
|
||||
name: 'libraries.remove',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
}).validator(),
|
||||
name: 'libraries.remove',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
let library = Libraries.findOne(_id);
|
||||
assertOwnership(library, this.userId);
|
||||
run({ _id }) {
|
||||
let library = Libraries.findOne(_id);
|
||||
assertOwnership(library, this.userId);
|
||||
this.unblock();
|
||||
removeLibaryWork(_id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function removeLibaryWork(libraryId){
|
||||
export function removeLibaryWork(libraryId) {
|
||||
Libraries.remove(libraryId);
|
||||
LibraryNodes.remove({'ancestors.id': libraryId});
|
||||
LibraryNodes.remove({ 'ancestors.id': libraryId });
|
||||
}
|
||||
|
||||
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };
|
||||
|
||||
@@ -23,28 +23,28 @@ let LibraryNodeSchema = new SimpleSchema({
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
type: {
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: Object.keys(propertySchemasIndex),
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
},
|
||||
libraryTags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'libraryTags.$': {
|
||||
type: String,
|
||||
},
|
||||
'libraryTags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
type: storedIconsSchema,
|
||||
optional: true,
|
||||
@@ -56,37 +56,37 @@ let LibraryNodeSchema = new SimpleSchema({
|
||||
if (Meteor.isServer) {
|
||||
LibraryNodes._ensureIndex({
|
||||
'name': 'text',
|
||||
'tags': 'text',
|
||||
'tags': 'text',
|
||||
});
|
||||
}
|
||||
|
||||
for (let key in propertySchemasIndex){
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(LibraryNodeSchema);
|
||||
for (let key in propertySchemasIndex) {
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(LibraryNodeSchema);
|
||||
schema.extend(ColorSchema);
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
LibraryNodes.attachSchema(schema, {
|
||||
selector: {type: key}
|
||||
});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
LibraryNodes.attachSchema(schema, {
|
||||
selector: { type: key }
|
||||
});
|
||||
}
|
||||
|
||||
function getLibrary(node){
|
||||
function getLibrary(node) {
|
||||
if (!node) throw new Meteor.Error('No node provided');
|
||||
let library = Libraries.findOne(node.ancestors[0].id);
|
||||
if (!library) throw new Meteor.Error('Library does not exist');
|
||||
return library;
|
||||
}
|
||||
|
||||
function assertNodeEditPermission(node, userId){
|
||||
function assertNodeEditPermission(node, userId) {
|
||||
let lib = getLibrary(node);
|
||||
return assertEditPermission(lib, userId);
|
||||
}
|
||||
|
||||
const insertNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.insert',
|
||||
validate: null,
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
@@ -95,8 +95,8 @@ const insertNode = new ValidatedMethod({
|
||||
run(libraryNode) {
|
||||
delete libraryNode._id;
|
||||
assertNodeEditPermission(libraryNode, this.userId);
|
||||
let nodeId = LibraryNodes.insert(libraryNode);
|
||||
if (libraryNode.type == 'reference'){
|
||||
let nodeId = LibraryNodes.insert(libraryNode);
|
||||
if (libraryNode.type == 'reference') {
|
||||
libraryNode._id = nodeId;
|
||||
updateReferenceNodeWork(libraryNode, this.userId);
|
||||
}
|
||||
@@ -106,37 +106,37 @@ const insertNode = new ValidatedMethod({
|
||||
|
||||
const updateLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.update',
|
||||
validate({_id, path}){
|
||||
if (!_id) return false;
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]){
|
||||
case 'type':
|
||||
validate({ _id, path }) {
|
||||
if (!_id) return false;
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]) {
|
||||
case 'type':
|
||||
case 'order':
|
||||
case 'parent':
|
||||
case 'ancestors':
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
run({ _id, path, value }) {
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
assertNodeEditPermission(node, this.userId);
|
||||
let pathString = path.join('.');
|
||||
let modifier;
|
||||
// unset empty values
|
||||
if (value === null || value === undefined){
|
||||
modifier = {$unset: {[pathString]: 1}};
|
||||
if (value === null || value === undefined) {
|
||||
modifier = { $unset: { [pathString]: 1 } };
|
||||
} else {
|
||||
modifier = {$set: {[pathString]: value}};
|
||||
modifier = { $set: { [pathString]: value } };
|
||||
}
|
||||
let numUpdated = LibraryNodes.update(_id, modifier, {
|
||||
selector: {type: node.type},
|
||||
});
|
||||
if (node.type == 'reference'){
|
||||
let numUpdated = LibraryNodes.update(_id, modifier, {
|
||||
selector: { type: node.type },
|
||||
});
|
||||
if (node.type == 'reference') {
|
||||
node = LibraryNodes.findOne(_id);
|
||||
updateReferenceNodeWork(node, this.userId);
|
||||
}
|
||||
@@ -145,87 +145,87 @@ const updateLibraryNode = new ValidatedMethod({
|
||||
});
|
||||
|
||||
const pushToLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.push',
|
||||
validate: null,
|
||||
name: 'libraryNodes.push',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}){
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
run({ _id, path, value }) {
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
assertNodeEditPermission(node, this.userId);
|
||||
return LibraryNodes.update(_id, {
|
||||
$push: {[path.join('.')]: value},
|
||||
}, {
|
||||
selector: {type: node.type},
|
||||
});
|
||||
}
|
||||
return LibraryNodes.update(_id, {
|
||||
$push: { [path.join('.')]: value },
|
||||
}, {
|
||||
selector: { type: node.type },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pullFromLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.pull',
|
||||
validate: null,
|
||||
name: 'libraryNodes.pull',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, itemId}){
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
run({ _id, path, itemId }) {
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
assertNodeEditPermission(node, this.userId);
|
||||
return LibraryNodes.update(_id, {
|
||||
$pull: {[path.join('.')]: {_id: itemId}},
|
||||
}, {
|
||||
selector: {type: node.type},
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
return LibraryNodes.update(_id, {
|
||||
$pull: { [path.join('.')]: { _id: itemId } },
|
||||
}, {
|
||||
selector: { type: node.type },
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const softRemoveLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'libraryNodes.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
run({ _id }) {
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
assertNodeEditPermission(node, this.userId);
|
||||
softRemove({_id, collection: LibraryNodes});
|
||||
}
|
||||
softRemove({ _id, collection: LibraryNodes });
|
||||
}
|
||||
});
|
||||
|
||||
const restoreLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'libraryNodes.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let node = LibraryNodes.findOne(_id);
|
||||
assertNodeEditPermission(node, this.userId);
|
||||
// Do work
|
||||
restore({_id, collection: LibraryNodes});
|
||||
}
|
||||
restore({ _id, collection: LibraryNodes });
|
||||
}
|
||||
});
|
||||
|
||||
export default LibraryNodes;
|
||||
export {
|
||||
LibraryNodeSchema,
|
||||
insertNode,
|
||||
updateLibraryNode,
|
||||
pullFromLibraryNode,
|
||||
pushToLibraryNode,
|
||||
softRemoveLibraryNode,
|
||||
LibraryNodeSchema,
|
||||
insertNode,
|
||||
updateLibraryNode,
|
||||
pullFromLibraryNode,
|
||||
pushToLibraryNode,
|
||||
softRemoveLibraryNode,
|
||||
restoreLibraryNode,
|
||||
};
|
||||
|
||||
97
app/imports/api/library/methods/copyLibraryNodeTo.js
Normal file
97
app/imports/api/library/methods/copyLibraryNodeTo.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import {
|
||||
assertDocCopyPermission,
|
||||
assertDocEditPermission
|
||||
} from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
var snackbar;
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
}
|
||||
|
||||
const DUPLICATE_CHILDREN_LIMIT = 500;
|
||||
|
||||
const copyLibraryNodeTo = new ValidatedMethod({
|
||||
name: 'libraryNodes.copyTo',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parent: {
|
||||
type: RefSchema,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 10000,
|
||||
},
|
||||
run({ _id, parent }) {
|
||||
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
|
||||
throw new Meteor.Error('Invalid destination',
|
||||
'Library documents can only be copied to destinations inside other libraries'
|
||||
);
|
||||
}
|
||||
const libraryNode = LibraryNodes.findOne(_id);
|
||||
const parentDoc = fetchDocByRef(parent);
|
||||
assertDocCopyPermission(libraryNode, this.userId);
|
||||
assertDocEditPermission(parentDoc, this.userId);
|
||||
|
||||
let decendants = LibraryNodes.find({
|
||||
'ancestors.id': _id,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: DUPLICATE_CHILDREN_LIMIT + 1,
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
if (decendants.length > DUPLICATE_CHILDREN_LIMIT) {
|
||||
decendants.pop();
|
||||
if (Meteor.isClient) {
|
||||
snackbar({
|
||||
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const nodes = [libraryNode, ...decendants];
|
||||
|
||||
const newAncestry = parentDoc.ancestors || [];
|
||||
newAncestry.push(parent);
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry,
|
||||
oldParent: libraryNode.parent,
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({ docArray: nodes });
|
||||
|
||||
// Order the root node
|
||||
libraryNode.order = (parentDoc.order || 0) + 0.5;
|
||||
|
||||
LibraryNodes.batchInsert(nodes);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
collection: LibraryNodes,
|
||||
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default copyLibraryNodeTo;
|
||||
@@ -4,23 +4,23 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
|
||||
var snackbar;
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
}
|
||||
|
||||
const DUPLICATE_CHILDREN_LIMIT = 50;
|
||||
const DUPLICATE_CHILDREN_LIMIT = 500;
|
||||
|
||||
const duplicateLibraryNode = new ValidatedMethod({
|
||||
name: 'libraryNodes.duplicate',
|
||||
validate: new SimpleSchema({
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -28,10 +28,10 @@ const duplicateLibraryNode = new ValidatedMethod({
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
let libraryNode = LibraryNodes.findOne(_id);
|
||||
assertDocEditPermission(libraryNode, this.userId);
|
||||
|
||||
@@ -40,16 +40,16 @@ const duplicateLibraryNode = new ValidatedMethod({
|
||||
libraryNode._id = libraryNodeId;
|
||||
|
||||
let nodes = LibraryNodes.find({
|
||||
'ancestors.id': _id,
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
'ancestors.id': _id,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: DUPLICATE_CHILDREN_LIMIT + 1,
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
|
||||
nodes.pop();
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar({
|
||||
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
|
||||
});
|
||||
@@ -58,21 +58,21 @@ const duplicateLibraryNode = new ValidatedMethod({
|
||||
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry : [
|
||||
docArray: nodes,
|
||||
newAncestry: [
|
||||
...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
|
||||
renewDocIds({docArray: nodes});
|
||||
renewDocIds({ docArray: nodes });
|
||||
|
||||
// Order the root node
|
||||
libraryNode.order += 0.5;
|
||||
|
||||
LibraryNodes.batchInsert([libraryNode, ...nodes]);
|
||||
LibraryNodes.batchInsert([libraryNode, ...nodes]);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import '/imports/api/library/methods/copyLibraryNodeTo.js';
|
||||
import '/imports/api/library/methods/duplicateLibraryNode.js';
|
||||
import '/imports/api/library/methods/updateReferenceNode.js';
|
||||
|
||||
@@ -22,7 +22,7 @@ let ChildSchema = new SimpleSchema({
|
||||
order: {
|
||||
type: Number,
|
||||
},
|
||||
parent: {
|
||||
parent: {
|
||||
type: RefSchema,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
let SoftRemovableSchema = new SimpleSchema({
|
||||
"removed": {
|
||||
'removed': {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
index: 1,
|
||||
},
|
||||
"removedAt": {
|
||||
'removedAt': {
|
||||
type: Date,
|
||||
optional: true,
|
||||
index: 1,
|
||||
},
|
||||
"removedWith": {
|
||||
'removedWith': {
|
||||
optional: true,
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
|
||||
@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
/*
|
||||
* Actions are things a character can do
|
||||
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
|
||||
// long actions take longer than 1 round to cast
|
||||
actionType: {
|
||||
type: String,
|
||||
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
|
||||
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
|
||||
defaultValue: 'action',
|
||||
},
|
||||
// If the action type is an event, what is the variable name of that event?
|
||||
variableName: {
|
||||
type: String,
|
||||
optional: true,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// Who is the action directed at
|
||||
target: {
|
||||
type: String,
|
||||
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
|
||||
// How this action's uses are reset automatically
|
||||
reset: {
|
||||
type: String,
|
||||
allowedValues: ['longRest', 'shortRest'],
|
||||
optional: true,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// Resources
|
||||
resources: {
|
||||
@@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({
|
||||
'resources.itemsConsumed.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
@@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({
|
||||
'resources.attributesConsumed.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
@@ -218,4 +229,4 @@ const ComputedActionSchema = new SimpleSchema()
|
||||
.extend(ActionSchema)
|
||||
.extend(ComputedOnlyActionSchema);
|
||||
|
||||
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
|
||||
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema };
|
||||
|
||||
@@ -3,7 +3,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
const AdjustmentSchema = createPropertySchema({
|
||||
// The roll that determines how much to change the attribute
|
||||
// The roll that determines how much to change the attribute
|
||||
// This can be simplified, but should only compute when activated
|
||||
amount: {
|
||||
type: 'fieldToCompute',
|
||||
@@ -11,21 +11,21 @@ const AdjustmentSchema = createPropertySchema({
|
||||
optional: true,
|
||||
defaultValue: 1,
|
||||
},
|
||||
// Who this adjustment applies to
|
||||
target: {
|
||||
type: String,
|
||||
// Who this adjustment applies to
|
||||
target: {
|
||||
type: String,
|
||||
defaultValue: 'target',
|
||||
allowedValues: [
|
||||
allowedValues: [
|
||||
'self',
|
||||
'target',
|
||||
],
|
||||
},
|
||||
// The stat this rolls applies to
|
||||
stat: {
|
||||
type: String,
|
||||
},
|
||||
// The stat this rolls applies to
|
||||
stat: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
},
|
||||
operation: {
|
||||
type: String,
|
||||
allowedValues: ['set', 'increment'],
|
||||
|
||||
@@ -8,34 +8,33 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
|
||||
*/
|
||||
let AttributeSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
variableName: {
|
||||
type: String,
|
||||
optional: true,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// How it is displayed and computed is determined by type
|
||||
// How it is displayed and computed is determined by type
|
||||
attributeType: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
'ability', //Strength, Dex, Con, etc.
|
||||
'stat', // Speed, Armor Class
|
||||
'modifier', // Proficiency Bonus, displayed as +x
|
||||
'modifier', // Proficiency Bonus, displayed as +x
|
||||
'hitDice', // d12 hit dice
|
||||
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage
|
||||
'bar', // Displayed as a health bar, can't take damage
|
||||
'healthBar', // Hitpoints, Temporary Hitpoints
|
||||
'resource', // Rages, sorcery points
|
||||
'spellSlot', // Level 1, 2, 3... spell slots
|
||||
'utility', // Aren't displayed, Jump height, Carry capacity
|
||||
],
|
||||
defaultValue: 'stat',
|
||||
index: 1,
|
||||
index: 1,
|
||||
},
|
||||
// For type hitDice, the size needs to be stored separately
|
||||
hitDiceSize: {
|
||||
@@ -46,19 +45,19 @@ let AttributeSchema = createPropertySchema({
|
||||
// For type spellSlot, the level needs to be stored separately
|
||||
spellSlotLevel: {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
optional: true,
|
||||
},
|
||||
// For type healthBar midColor, and lowColor can be set separately from the
|
||||
// property's color, which is used as the undamaged color
|
||||
'healthBarColorMid': {
|
||||
type: String,
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
optional: true,
|
||||
type: String,
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
optional: true,
|
||||
},
|
||||
'healthBarColorLow': {
|
||||
type: String,
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
optional: true,
|
||||
type: String,
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
optional: true,
|
||||
},
|
||||
// Control how the health bar takes damage or healing
|
||||
healthBarNoDamage: {
|
||||
@@ -68,7 +67,17 @@ let AttributeSchema = createPropertySchema({
|
||||
healthBarNoHealing: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
// Control how the health bar handles overflow
|
||||
healthBarNoDamageOverflow: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
healthBarNoHealingOverflow: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Control when the health bar takes damage or healing
|
||||
healthBarDamageOrder: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
@@ -77,17 +86,17 @@ let AttributeSchema = createPropertySchema({
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
},
|
||||
// The starting value, before effects
|
||||
baseValue: {
|
||||
// The starting value, before effects
|
||||
baseValue: {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
optional: true,
|
||||
},
|
||||
// Description of what the attribute is used for
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// The damage done to the attribute, should always compute as positive
|
||||
optional: true,
|
||||
},
|
||||
// The damage done to the attribute, should always compute as positive
|
||||
damage: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
@@ -107,11 +116,21 @@ let AttributeSchema = createPropertySchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Automatically zero the adjustment on these conditions
|
||||
hideWhenTotalZero: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
hideWhenValueZero: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Automatically zero the adjustment on these conditions
|
||||
reset: {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: ['shortRest', 'longRest'],
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -126,9 +145,9 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
|
||||
},
|
||||
spellSlotLevel: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
optional: true,
|
||||
},
|
||||
// The computed value of the attribute
|
||||
// The computed value of the attribute
|
||||
total: {
|
||||
type: SimpleSchema.oneOf(Number, String, Boolean),
|
||||
optional: true,
|
||||
@@ -137,27 +156,27 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
|
||||
// The computed value of the attribute minus the damage
|
||||
value: {
|
||||
type: SimpleSchema.oneOf(Number, String, Boolean),
|
||||
defaultValue: 0,
|
||||
defaultValue: 0,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// The computed modifier, provided the attribute type is `ability`
|
||||
modifier: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
// The computed modifier, provided the attribute type is `ability`
|
||||
modifier: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
},
|
||||
// Attributes with proficiency grant it to all skills based on the attribute
|
||||
proficiency: {
|
||||
type: Number,
|
||||
type: Number,
|
||||
allowedValues: [0, 0.49, 0.5, 1, 2],
|
||||
optional: true,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
},
|
||||
// The computed creature constitution modifier for hit dice
|
||||
constitutionMod: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Should this attribute hide
|
||||
|
||||
@@ -3,8 +3,8 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
let BranchSchema = createPropertySchema({
|
||||
branchType: {
|
||||
type: String,
|
||||
branchType: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
// Uses the condition field to determine whether to apply children
|
||||
'if',
|
||||
@@ -26,7 +26,7 @@ let BranchSchema = createPropertySchema({
|
||||
//'option',
|
||||
],
|
||||
defaultValue: 'if',
|
||||
},
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
optional: true,
|
||||
|
||||
@@ -4,31 +4,31 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
|
||||
|
||||
let BuffRemoverSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// This will remove just the nearest ancestor buff
|
||||
targetParentBuff: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// The following only applies when not targeting the parent buff
|
||||
// Which character to remove buffs from
|
||||
target: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
},
|
||||
// This will remove just the nearest ancestor buff
|
||||
targetParentBuff: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// The following only applies when not targeting the parent buff
|
||||
// Which character to remove buffs from
|
||||
target: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
'self',
|
||||
'target',
|
||||
],
|
||||
defaultValue: 'target',
|
||||
},
|
||||
// remove 1 or remove all
|
||||
removeAll: {
|
||||
type: Boolean,
|
||||
defaultValue: 'target',
|
||||
},
|
||||
// remove 1 or remove all
|
||||
removeAll: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
// Buffs to remove based on tags:
|
||||
targetTags: {
|
||||
type: Array,
|
||||
@@ -50,7 +50,7 @@ let BuffRemoverSchema = createPropertySchema({
|
||||
'extraTags.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,37 +4,37 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
|
||||
|
||||
let BuffSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
hideRemoveButton: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
hideRemoveButton: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// How many rounds this buff lasts
|
||||
duration: {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
duration: {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
type: String,
|
||||
allowedValues: [
|
||||
'self',
|
||||
'target',
|
||||
],
|
||||
defaultValue: 'target',
|
||||
},
|
||||
// Prevent the property from showing up in the log
|
||||
defaultValue: 'target',
|
||||
},
|
||||
// Prevent the property from showing up in the log
|
||||
silent: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Prevent the children from being crystalized
|
||||
},
|
||||
// Prevent the children from being crystalized
|
||||
skipCrystalization: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
@@ -42,36 +42,36 @@ let BuffSchema = createPropertySchema({
|
||||
});
|
||||
|
||||
let ComputedOnlyBuffSchema = createPropertySchema({
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
duration: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
durationSpent: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
min: 0,
|
||||
},
|
||||
appliedBy: {
|
||||
type: Object,
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
},
|
||||
'appliedBy.name': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
duration: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
durationSpent: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
min: 0,
|
||||
},
|
||||
appliedBy: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
'appliedBy.name': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
'appliedBy.id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
'appliedBy.collection': {
|
||||
type: String,
|
||||
},
|
||||
'appliedBy.id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
'appliedBy.collection': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.collectionName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedBuffSchema = new SimpleSchema()
|
||||
|
||||
@@ -4,26 +4,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
const ClassLevelSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// The name of this class level's variable
|
||||
variableName: {
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// The name of this class level's variable
|
||||
variableName: {
|
||||
type: String,
|
||||
min: 2,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
optional: true,
|
||||
},
|
||||
level: {
|
||||
level: {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 1,
|
||||
defaultValue: 1,
|
||||
max: STORAGE_LIMITS.levelMax,
|
||||
},
|
||||
// Filters out of UI if condition isn't met, but isn't otherwise enforced
|
||||
@@ -34,7 +34,7 @@ const ClassLevelSchema = createPropertySchema({
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlyClassLevelSchema = createPropertySchema({
|
||||
const ComputedOnlyClassLevelSchema = createPropertySchema({
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
|
||||
@@ -13,29 +13,29 @@ import resolve, { Context, traverse } from '/imports/parser/resolve.js';
|
||||
*/
|
||||
let ConstantSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
variableName: {
|
||||
type: String,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
defaultValue: 'newConstant',
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// The input value to be parsed, must return a constant node or an array
|
||||
// The input value to be parsed, must return a constant node or an array
|
||||
// of constant nodes to be valid
|
||||
calculation: {
|
||||
type: String,
|
||||
optional: true,
|
||||
calculation: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
type: Array,
|
||||
maxCount: STORAGE_LIMITS.errorCount,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
let calc = this.field('calculation');
|
||||
if (!calc.isSet && this.isModifier) {
|
||||
this.unset()
|
||||
@@ -44,27 +44,27 @@ let ConstantSchema = new SimpleSchema({
|
||||
let string = calc.value;
|
||||
if (!string) return [];
|
||||
// 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
|
||||
if (context && context.errors.length) return context.errors;
|
||||
// Ban variables in constants if necessary
|
||||
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');
|
||||
}
|
||||
});
|
||||
return context && context.errors || [];
|
||||
}
|
||||
},
|
||||
'errors.$':{
|
||||
'errors.$': {
|
||||
type: ErrorSchema,
|
||||
},
|
||||
});
|
||||
|
||||
function parseString(string, fn = 'compile'){
|
||||
function parseString(string, fn = 'compile') {
|
||||
let context = new Context();
|
||||
if (!string){
|
||||
return {result: string, context};
|
||||
if (!string) {
|
||||
return { result: string, context };
|
||||
}
|
||||
|
||||
// Parse the string using mathjs
|
||||
@@ -74,11 +74,11 @@ function parseString(string, fn = 'compile'){
|
||||
} catch (e) {
|
||||
let message = prettifyParseError(e);
|
||||
context.error(message);
|
||||
return {context};
|
||||
return { context };
|
||||
}
|
||||
if (!node) return {context};
|
||||
let {result} = resolve(fn, node, {/*empty scope*/}, context);
|
||||
return {result, context}
|
||||
if (!node) return { context };
|
||||
let { result } = resolve(fn, node, {/*empty scope*/ }, context);
|
||||
return { result, context }
|
||||
}
|
||||
|
||||
const ComputedOnlyConstantSchema = new SimpleSchema({});
|
||||
|
||||
@@ -3,61 +3,61 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
let ContainerSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
trim: false,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
carried: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
optional: true,
|
||||
},
|
||||
contentsWeightless: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
weight: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
carried: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
contentsWeightless: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
weight: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
optional: true,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlyContainerSchema = createPropertySchema({
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
},
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
},
|
||||
// Weight of all the contents, zero if `contentsWeightless` is true
|
||||
contentsWeight:{
|
||||
contentsWeight: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Weight of all the carried contents (some sub-containers might not be carried)
|
||||
// zero if `contentsWeightless` is true
|
||||
carriedWeight:{
|
||||
carriedWeight: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
contentsValue:{
|
||||
contentsValue: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
carriedValue:{
|
||||
carriedValue: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
@@ -65,7 +65,7 @@ const ComputedOnlyContainerSchema = createPropertySchema({
|
||||
});
|
||||
|
||||
const ComputedContainerSchema = new SimpleSchema()
|
||||
.extend(ComputedOnlyContainerSchema)
|
||||
.extend(ContainerSchema);
|
||||
.extend(ComputedOnlyContainerSchema)
|
||||
.extend(ContainerSchema);
|
||||
|
||||
export { ContainerSchema, ComputedOnlyContainerSchema, ComputedContainerSchema };
|
||||
|
||||
@@ -8,10 +8,10 @@ import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
*/
|
||||
let DamageMultiplierSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
damageTypes: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
@@ -23,11 +23,11 @@ let DamageMultiplierSchema = new SimpleSchema({
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
},
|
||||
// The value of the damage multiplier
|
||||
value: {
|
||||
// The value of the damage multiplier
|
||||
value: {
|
||||
type: Number,
|
||||
defaultValue: 0.5,
|
||||
allowedValues: [0, 0.5, 2],
|
||||
defaultValue: 0.5,
|
||||
allowedValues: [0, 0.5, 2],
|
||||
},
|
||||
// Tags which bypass this multiplier (OR)
|
||||
excludeTags: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
const DamageSchema = createPropertySchema({
|
||||
// The roll that determines how much to damage the attribute
|
||||
// The roll that determines how much to damage the attribute
|
||||
// This can be simplified, but only computed when applied
|
||||
amount: {
|
||||
type: 'fieldToCompute',
|
||||
@@ -12,19 +12,19 @@ const DamageSchema = createPropertySchema({
|
||||
defaultValue: '1d8 + strength.modifier',
|
||||
parseLevel: 'compile',
|
||||
},
|
||||
// Who this damage applies to
|
||||
target: {
|
||||
type: String,
|
||||
// Who this damage applies to
|
||||
target: {
|
||||
type: String,
|
||||
defaultValue: 'target',
|
||||
allowedValues: [
|
||||
allowedValues: [
|
||||
'self',
|
||||
'target',
|
||||
],
|
||||
},
|
||||
damageType: {
|
||||
type: String,
|
||||
},
|
||||
damageType: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
defaultValue: 'slashing',
|
||||
defaultValue: 'slashing',
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
},
|
||||
// Prevent the property from showing up in the log
|
||||
|
||||
@@ -3,19 +3,19 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
let FeatureSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
optional: true,
|
||||
},
|
||||
summary: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
summary: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyFeatureSchema = createPropertySchema({
|
||||
|
||||
@@ -7,6 +7,11 @@ let FolderSchema = new createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
optional: true,
|
||||
},
|
||||
groupStats: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,58 +3,58 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
const ItemSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// Plural name of the item, if there is more than one
|
||||
plural: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// Plural name of the item, if there is more than one
|
||||
plural: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// Number currently held
|
||||
quantity: {
|
||||
type: SimpleSchema.Integer,
|
||||
min: 0,
|
||||
defaultValue: 1
|
||||
},
|
||||
// Weight per item in the stack
|
||||
weight: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// Value per item in the stack, in gold pieces
|
||||
value: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
},
|
||||
// Number currently held
|
||||
quantity: {
|
||||
type: SimpleSchema.Integer,
|
||||
min: 0,
|
||||
defaultValue: 1
|
||||
},
|
||||
// Weight per item in the stack
|
||||
weight: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
optional: true,
|
||||
},
|
||||
// If this item is equipped, it requires attunement
|
||||
requiresAttunement: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
// Value per item in the stack, in gold pieces
|
||||
value: {
|
||||
type: Number,
|
||||
min: 0,
|
||||
optional: true,
|
||||
},
|
||||
// If this item is equipped, it requires attunement
|
||||
requiresAttunement: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
attuned: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Show increment/decrement buttons in item lists
|
||||
showIncrement: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Unequipped items shouldn't affect creature stats
|
||||
equipped: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Show increment/decrement buttons in item lists
|
||||
showIncrement: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Unequipped items shouldn't affect creature stats
|
||||
equipped: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyItemSchema = createPropertySchema({
|
||||
|
||||
@@ -3,19 +3,19 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
let NoteSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
summary: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
summary: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyNoteSchema = createPropertySchema({
|
||||
|
||||
@@ -2,28 +2,28 @@ import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
let ProficiencySchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// The variableNames of the skills, tags, or attributes to apply proficiency to
|
||||
stats: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
// The variableNames of the skills, tags, or attributes to apply proficiency to
|
||||
stats: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.statsToTarget,
|
||||
},
|
||||
'stats.$': {
|
||||
type: String,
|
||||
},
|
||||
'stats.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// A number representing how proficient the character is
|
||||
},
|
||||
// A number representing how proficient the character is
|
||||
// where 0.49 is half rounded down and 0.5 is half rounded up
|
||||
value: {
|
||||
type: Number,
|
||||
allowedValues: [0.49, 0.5, 1, 2],
|
||||
defaultValue: 1,
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
allowedValues: [0.49, 0.5, 1, 2],
|
||||
defaultValue: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlyProficiencySchema = new SimpleSchema({});
|
||||
|
||||
@@ -23,14 +23,14 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
|
||||
*/
|
||||
let RollSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
type: String,
|
||||
defaultValue: 'New Roll',
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
variableName: {
|
||||
type: String,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
defaultValue: 'newRoll',
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
|
||||
@@ -16,14 +16,14 @@ let SavingThrowSchema = createPropertySchema({
|
||||
optional: true,
|
||||
},
|
||||
// Who this saving throw applies to
|
||||
target: {
|
||||
type: String,
|
||||
target: {
|
||||
type: String,
|
||||
defaultValue: 'target',
|
||||
allowedValues: [
|
||||
allowedValues: [
|
||||
'self',
|
||||
'target',
|
||||
],
|
||||
},
|
||||
},
|
||||
// The variable name of save to roll
|
||||
stat: {
|
||||
type: String,
|
||||
|
||||
@@ -9,10 +9,10 @@ import createPropertySchema from '/imports/api/properties/subSchemas/createPrope
|
||||
*/
|
||||
let SkillSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// The technical, lowercase, single-word name used in formulae
|
||||
// Ignored for skilltype = save
|
||||
variableName: {
|
||||
@@ -22,33 +22,33 @@ let SkillSchema = createPropertySchema({
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
optional: true,
|
||||
},
|
||||
// The variable name of the ability this skill relies on
|
||||
// The variable name of the ability this skill relies on
|
||||
ability: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// What type of skill is this
|
||||
// What type of skill is this
|
||||
skillType: {
|
||||
type: String,
|
||||
allowedValues: [
|
||||
'skill',
|
||||
'save',
|
||||
'check',
|
||||
'check',
|
||||
'tool',
|
||||
'weapon',
|
||||
'armor',
|
||||
'language',
|
||||
'utility', //not displayed anywhere
|
||||
'utility', //not displayed anywhere
|
||||
],
|
||||
defaultValue: 'skill',
|
||||
},
|
||||
// The base proficiency of this skill
|
||||
baseProficiency: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
// The base proficiency of this skill
|
||||
baseProficiency: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
allowedValues: [0.49, 0.5, 1, 2],
|
||||
},
|
||||
},
|
||||
// The starting value, before effects
|
||||
baseValue: {
|
||||
type: 'fieldToCompute',
|
||||
@@ -56,16 +56,16 @@ let SkillSchema = createPropertySchema({
|
||||
},
|
||||
// Description of what the skill is used for
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlySkillSchema = createPropertySchema({
|
||||
// Computed value of skill to be added to skill rolls
|
||||
// Computed value of skill to be added to skill rolls
|
||||
value: {
|
||||
type: Number,
|
||||
defaultValue: 0,
|
||||
defaultValue: 0,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
@@ -75,33 +75,33 @@ let ComputedOnlySkillSchema = createPropertySchema({
|
||||
optional: true,
|
||||
},
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
},
|
||||
// Computed value added by the ability
|
||||
abilityMod: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
},
|
||||
// Computed value added by the ability
|
||||
abilityMod: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Computed advantage/disadvantage
|
||||
},
|
||||
// Computed advantage/disadvantage
|
||||
advantage: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
allowedValues: [-1, 0, 1],
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Computed bonus to passive checks
|
||||
// Computed bonus to passive checks
|
||||
passiveBonus: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Computed proficiency multiplier
|
||||
// Computed proficiency multiplier
|
||||
proficiency: {
|
||||
type: Number,
|
||||
allowedValues: [0, 0.49, 0.5, 1, 2],
|
||||
defaultValue: 0,
|
||||
defaultValue: 0,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Compiled text of all conditional benefits
|
||||
@@ -113,7 +113,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
|
||||
'conditionalBenefits.$': {
|
||||
type: String,
|
||||
},
|
||||
// Computed number of things forcing this skill to fail
|
||||
// Computed number of things forcing this skill to fail
|
||||
fail: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
|
||||
@@ -3,20 +3,26 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
|
||||
let SpellListSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// Calculation of how many spells in this list can be prepared
|
||||
maxPrepared: {
|
||||
type: 'inlineCalculationFieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// Calculation of how many spells in this list can be prepared
|
||||
maxPrepared: {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// The variable name of the ability this spell relies on
|
||||
ability: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// Calculation of The attack roll bonus used by spell attacks in this list
|
||||
attackRollBonus: {
|
||||
type: 'fieldToCompute',
|
||||
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
// Computed value determined by the ability
|
||||
abilityMod: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
attackRollBonus: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
|
||||
@@ -3,93 +3,93 @@ import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
const magicSchools = [
|
||||
'abjuration',
|
||||
'conjuration',
|
||||
'divination',
|
||||
'enchantment',
|
||||
'evocation',
|
||||
'illusion',
|
||||
'necromancy',
|
||||
'transmutation',
|
||||
'abjuration',
|
||||
'conjuration',
|
||||
'divination',
|
||||
'enchantment',
|
||||
'evocation',
|
||||
'illusion',
|
||||
'necromancy',
|
||||
'transmutation',
|
||||
];
|
||||
|
||||
let SpellSchema = new SimpleSchema({})
|
||||
.extend(ActionSchema)
|
||||
.extend({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// If it's always prepared, it doesn't count against the number of spells
|
||||
// prepared in a spell list, and enabled should be true
|
||||
alwaysPrepared: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
prepared: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// This spell ignores spell slot rules
|
||||
castWithoutSpellSlots: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
hasAttackRoll: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
castingTime: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: 'action',
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
range: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
duration: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: 'Instantaneous',
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
verbal: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
somatic: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
concentration: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
material: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
ritual: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
level: {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 1,
|
||||
max: 9,
|
||||
min: 0,
|
||||
},
|
||||
school: {
|
||||
type: String,
|
||||
defaultValue: 'abjuration',
|
||||
allowedValues: magicSchools,
|
||||
},
|
||||
});
|
||||
.extend(ActionSchema)
|
||||
.extend({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// If it's always prepared, it doesn't count against the number of spells
|
||||
// prepared in a spell list, and enabled should be true
|
||||
alwaysPrepared: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
prepared: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// This spell ignores spell slot rules
|
||||
castWithoutSpellSlots: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
hasAttackRoll: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
castingTime: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: 'action',
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
range: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
duration: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: 'Instantaneous',
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
verbal: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
somatic: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
concentration: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
material: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.spellDetail,
|
||||
},
|
||||
ritual: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
level: {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 1,
|
||||
max: 9,
|
||||
min: 0,
|
||||
},
|
||||
school: {
|
||||
type: String,
|
||||
defaultValue: 'abjuration',
|
||||
allowedValues: magicSchools,
|
||||
},
|
||||
});
|
||||
|
||||
const ComputedOnlySpellSchema = new SimpleSchema()
|
||||
.extend(ComputedOnlyActionSchema);
|
||||
|
||||
@@ -41,7 +41,7 @@ const ComputedOnlyToggleSchema = createPropertySchema({
|
||||
});
|
||||
|
||||
const ComputedToggleSchema = new SimpleSchema()
|
||||
.extend(ComputedOnlyToggleSchema)
|
||||
.extend(ToggleSchema);
|
||||
.extend(ComputedOnlyToggleSchema)
|
||||
.extend(ToggleSchema);
|
||||
|
||||
export { ToggleSchema, ComputedOnlyToggleSchema, ComputedToggleSchema };
|
||||
|
||||
@@ -5,31 +5,31 @@ const AdjustmentSchema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
// The roll that determines how much to change the attribute
|
||||
// The roll that determines how much to change the attribute
|
||||
adjustment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
defaultValue: '1',
|
||||
},
|
||||
// Who this adjustment applies to
|
||||
target: {
|
||||
type: String,
|
||||
// Who this adjustment applies to
|
||||
target: {
|
||||
type: String,
|
||||
defaultValue: 'every',
|
||||
allowedValues: [
|
||||
allowedValues: [
|
||||
'self', // the character who took the action
|
||||
'each', // rolled once for `each` target
|
||||
'every', // rolled once and applied to `every` target
|
||||
],
|
||||
},
|
||||
// The stat this rolls applies to, if damage type is set, this is ignored
|
||||
stat: {
|
||||
type: String,
|
||||
},
|
||||
// The stat this rolls applies to, if damage type is set, this is ignored
|
||||
stat: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default AdjustmentSchema;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
const ColorSchema = new SimpleSchema({
|
||||
color: {
|
||||
type: String,
|
||||
// match hex colors of the form #A23 or #A23f56
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
optional: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
// match hex colors of the form #A23 or #A23f56
|
||||
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default ColorSchema;
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
const DeathSavesSchema = new SimpleSchema({
|
||||
pass: {
|
||||
type: SimpleSchema.Integer,
|
||||
min: 0,
|
||||
max: 3,
|
||||
defaultValue: 0,
|
||||
},
|
||||
fail: {
|
||||
type: SimpleSchema.Integer,
|
||||
min: 0,
|
||||
max: 3,
|
||||
defaultValue: 0,
|
||||
},
|
||||
canDeathSave: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
stable: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
pass: {
|
||||
type: SimpleSchema.Integer,
|
||||
min: 0,
|
||||
max: 3,
|
||||
defaultValue: 0,
|
||||
},
|
||||
fail: {
|
||||
type: SimpleSchema.Integer,
|
||||
min: 0,
|
||||
max: 3,
|
||||
defaultValue: 0,
|
||||
},
|
||||
canDeathSave: {
|
||||
type: Boolean,
|
||||
defaultValue: true,
|
||||
},
|
||||
stable: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default DeathSavesSchema;
|
||||
|
||||
@@ -6,10 +6,10 @@ const ErrorSchema = new SimpleSchema({
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.errorMessage,
|
||||
},
|
||||
type: {
|
||||
type: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default ErrorSchema;
|
||||
|
||||
@@ -3,7 +3,7 @@ import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
const InlineComputationSchema = new SimpleSchema({
|
||||
// The part between bracers {}
|
||||
// The part between bracers {}
|
||||
calculation: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.calculation,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import ResultsSchema from '/imports/api/properties/subSchemas/ResultsSchema.js';
|
||||
|
||||
let RollResultsSchema = new SimpleSchema ({
|
||||
let RollResultsSchema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
@@ -17,9 +17,9 @@ let RollResultsSchema = new SimpleSchema ({
|
||||
optional: true,
|
||||
},
|
||||
results: {
|
||||
type: ResultsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
type: ResultsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
|
||||
export default RollResultsSchema ;
|
||||
export default RollResultsSchema;
|
||||
|
||||
@@ -4,35 +4,39 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
let SharingSchema = new SimpleSchema({
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1
|
||||
},
|
||||
readers: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
index: 1,
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1
|
||||
},
|
||||
readers: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
index: 1,
|
||||
maxCount: STORAGE_LIMITS.readersCount,
|
||||
},
|
||||
'readers.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id
|
||||
},
|
||||
writers: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
index: 1,
|
||||
},
|
||||
'readers.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id
|
||||
},
|
||||
writers: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
index: 1,
|
||||
maxCount: STORAGE_LIMITS.writersCount,
|
||||
},
|
||||
'writers.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id
|
||||
},
|
||||
public: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
'writers.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id
|
||||
},
|
||||
public: {
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
index: 1,
|
||||
},
|
||||
readersCanCopy: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default SharingSchema;
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
|
||||
const setPublic = new ValidatedMethod({
|
||||
name: 'sharing.setPublic',
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
isPublic: { type: Boolean },
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
@@ -18,19 +18,39 @@ const setPublic = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({docRef, isPublic}){
|
||||
let doc = fetchDocByRef(docRef);
|
||||
assertOwnership(doc, this.userId);
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$set: {public: isPublic},
|
||||
run({ docRef, isPublic }) {
|
||||
let doc = fetchDocByRef(docRef);
|
||||
assertOwnership(doc, this.userId);
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$set: { public: isPublic },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const setReadersCanCopy = new ValidatedMethod({
|
||||
name: 'sharing.setReadersCanCopy',
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
readersCanCopy: { type: Boolean },
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ docRef, readersCanCopy }) {
|
||||
let doc = fetchDocByRef(docRef);
|
||||
assertOwnership(doc, this.userId);
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$set: { readersCanCopy },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateUserSharePermissions = new ValidatedMethod({
|
||||
name: 'sharing.updateUserSharePermissions',
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
userId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -45,40 +65,40 @@ const updateUserSharePermissions = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({docRef, userId, role}){
|
||||
let doc = fetchDocByRef(docRef);
|
||||
if (role === 'none'){
|
||||
run({ docRef, userId, role }) {
|
||||
let doc = fetchDocByRef(docRef);
|
||||
if (role === 'none') {
|
||||
// only assert ownership if you aren't removing yourself
|
||||
if (this.userId !== userId){
|
||||
if (this.userId !== userId) {
|
||||
assertOwnership(doc, this.userId);
|
||||
}
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$pullAll: { readers: userId, writers: userId },
|
||||
});
|
||||
}
|
||||
if (doc.owner === userId){
|
||||
if (doc.owner === userId) {
|
||||
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);
|
||||
if (role === 'reader'){
|
||||
if (role === 'reader') {
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$addToSet: { readers: userId },
|
||||
$pullAll: { writers: userId },
|
||||
});
|
||||
} else if (role === 'writer'){
|
||||
} else if (role === 'writer') {
|
||||
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||
$addToSet: { writers: userId },
|
||||
$pullAll: { readers: userId },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const transferOwnership = new ValidatedMethod({
|
||||
name: 'sharing.transferOwnership',
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
validate: new SimpleSchema({
|
||||
docRef: RefSchema,
|
||||
userId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -89,31 +109,31 @@ const transferOwnership = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({docRef, userId}){
|
||||
run({ docRef, userId }) {
|
||||
let doc = fetchDocByRef(docRef);
|
||||
assertOwnership(doc, this.userId);
|
||||
|
||||
let collection = getCollectionByName(docRef.collection);
|
||||
|
||||
let tier = getUserTier(userId);
|
||||
if (docRef.collection === 'creatures'){
|
||||
if (docRef.collection === 'creatures') {
|
||||
let currentCharacterCount = collection.find({
|
||||
owner: userId,
|
||||
}, {
|
||||
fields: {_id: 1},
|
||||
fields: { _id: 1 },
|
||||
}).count();
|
||||
|
||||
if (
|
||||
tier.characterSlots !== -1 &&
|
||||
currentCharacterCount >= tier.characterSlots
|
||||
){
|
||||
) {
|
||||
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'){
|
||||
if (!tier.paidBenefits){
|
||||
} else if (docRef.collection === 'libraries') {
|
||||
if (!tier.paidBenefits) {
|
||||
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,10 +143,10 @@ const transferOwnership = new ValidatedMethod({
|
||||
});
|
||||
// Then make the user the owner and the current owner a writer
|
||||
return collection.update(docRef.id, {
|
||||
$set: {owner: userId},
|
||||
$set: { owner: userId },
|
||||
$addToSet: { writers: this.userId },
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { setPublic, updateUserSharePermissions, transferOwnership };
|
||||
export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
function assertIdValid(userId){
|
||||
if (!userId || typeof userId !== 'string'){
|
||||
function assertIdValid(userId) {
|
||||
if (!userId || typeof userId !== 'string') {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'No user ID. Are you logged in?');
|
||||
}
|
||||
}
|
||||
|
||||
function assertdocExists(doc){
|
||||
if (!doc){
|
||||
function assertdocExists(doc) {
|
||||
if (!doc) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'Permission denied: No such document exists');
|
||||
}
|
||||
}
|
||||
|
||||
export function assertOwnership(doc, userId){
|
||||
export function assertOwnership(doc, userId) {
|
||||
assertIdValid(userId);
|
||||
assertdocExists(doc);
|
||||
if (doc.owner === userId ){
|
||||
|
||||
if (doc.owner === userId) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
@@ -37,13 +38,12 @@ export function assertEditPermission(doc, userId) {
|
||||
assertdocExists(doc);
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {
|
||||
'services.patreon': 1,
|
||||
'roles': 1,
|
||||
}
|
||||
});
|
||||
|
||||
// Admin override
|
||||
if (user.roles && user.roles.includes('admin')){
|
||||
if (user.roles && user.roles.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function assertEditPermission(doc, userId) {
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.writers, userId)
|
||||
){
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Meteor.Error('Edit permission denied',
|
||||
@@ -59,9 +59,46 @@ export function assertEditPermission(doc, userId) {
|
||||
}
|
||||
}
|
||||
|
||||
function getRoot(doc){
|
||||
/**
|
||||
* Assert that the user can edit the root document which manages its own sharing
|
||||
* permissions.
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertCopyPermission(doc, userId) {
|
||||
assertIdValid(userId);
|
||||
assertdocExists(doc);
|
||||
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {
|
||||
'roles': 1,
|
||||
}
|
||||
});
|
||||
|
||||
// Admin override
|
||||
if (user.roles && user.roles.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure the user is authorized for this specific document
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.writers, userId)
|
||||
) {
|
||||
return true;
|
||||
} else if (
|
||||
(_.contains(doc.readers, userId) || doc.public) &&
|
||||
doc.readersCanCopy
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Meteor.Error('Copy permission denied',
|
||||
'You do not have permission to copy this document');
|
||||
}
|
||||
}
|
||||
|
||||
function getRoot(doc) {
|
||||
assertdocExists(doc);
|
||||
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
|
||||
return fetchDocByRef(doc.ancestors[0]);
|
||||
} else {
|
||||
return doc;
|
||||
@@ -74,11 +111,22 @@ function getRoot(doc){
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertDocEditPermission(doc, userId){
|
||||
export function assertDocEditPermission(doc, userId) {
|
||||
let root = getRoot(doc);
|
||||
assertEditPermission(root, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the user can copy a descendant document whose root ancestor
|
||||
* implements sharing permissions.
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertDocCopyPermission(doc, userId) {
|
||||
let root = getRoot(doc);
|
||||
assertCopyPermission(root, userId);
|
||||
}
|
||||
|
||||
export function assertViewPermission(doc, userId) {
|
||||
assertdocExists(doc);
|
||||
if (doc.public) return true;
|
||||
@@ -88,17 +136,17 @@ export function assertViewPermission(doc, userId) {
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.readers, userId) ||
|
||||
_.contains(doc.writers, userId)
|
||||
){
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
|
||||
// Admin override
|
||||
const user = Meteor.users.findOne(userId, {
|
||||
fields: {
|
||||
'roles': 1,
|
||||
}
|
||||
});
|
||||
if (user.roles && user.roles.includes('admin')){
|
||||
if (user.roles && user.roles.includes('admin')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
|
||||
*
|
||||
* Warning: the doc and userId must be set by a trusted source
|
||||
*/
|
||||
export function assertDocViewPermission(doc, userId){
|
||||
export function assertDocViewPermission(doc, userId) {
|
||||
let root = getRoot(doc);
|
||||
assertViewPermission(root, userId);
|
||||
}
|
||||
|
||||
export function assertAdmin(userId){
|
||||
export function assertAdmin(userId) {
|
||||
assertIdValid(userId);
|
||||
let user = Meteor.users.findOne(userId, {fields: {roles: 1}});
|
||||
if (!user){
|
||||
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
|
||||
if (!user) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'UserId does not match any existing user');
|
||||
}
|
||||
let isAdmin = user.roles && user.roles.includes('admin')
|
||||
if (!isAdmin){
|
||||
if (!isAdmin) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'User does not have the admin role');
|
||||
}
|
||||
|
||||
@@ -51,21 +51,21 @@ const sendMessage = new ValidatedMethod({
|
||||
timeInterval: 5000,
|
||||
},
|
||||
|
||||
run({content, tabletopId}) {
|
||||
run({ content, tabletopId }) {
|
||||
let user = Meteor.user();
|
||||
if (!user) {
|
||||
throw new Meteor.Error('messages.send.denied',
|
||||
'You need to be logged in to send a message');
|
||||
'You need to be logged in to send a message');
|
||||
}
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
|
||||
return Messages.insert({
|
||||
content,
|
||||
content,
|
||||
tabletopId,
|
||||
timestamp: new Date(),
|
||||
userId: user._id,
|
||||
username: user.username,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
@@ -87,24 +87,24 @@ const removeMessages = new ValidatedMethod({
|
||||
timeInterval: 5000,
|
||||
},
|
||||
|
||||
run({messageId, tabletopId}) {
|
||||
run({ messageId, tabletopId }) {
|
||||
if (!this.userId) {
|
||||
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 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',
|
||||
'You don\'t have permission to remove this message');
|
||||
'You don\'t have permission to remove this message');
|
||||
}
|
||||
let removed = Messages.remove({
|
||||
_id: messageId,
|
||||
});
|
||||
_id: messageId,
|
||||
});
|
||||
Creatures.update({
|
||||
tabletop: tabletopId,
|
||||
}, {
|
||||
$unset: {tabletop: 1},
|
||||
$unset: { tabletop: 1 },
|
||||
});
|
||||
return removed;
|
||||
},
|
||||
|
||||
@@ -19,14 +19,14 @@ const insertTabletop = new ValidatedMethod({
|
||||
run() {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('tabletops.insert.denied',
|
||||
'You need to be logged in to insert a tabletop');
|
||||
'You need to be logged in to insert a tabletop');
|
||||
}
|
||||
assertUserHasPaidBenefits(this.userId);
|
||||
assertAdmin(this.userId);
|
||||
|
||||
return Tabletops.insert({
|
||||
gameMaster: this.userId,
|
||||
});
|
||||
gameMaster: this.userId,
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -24,22 +24,22 @@ const removeTabletop = new ValidatedMethod({
|
||||
timeInterval: 5000,
|
||||
},
|
||||
|
||||
run({tabletopId}) {
|
||||
run({ tabletopId }) {
|
||||
if (!this.userId) {
|
||||
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');
|
||||
}
|
||||
assertUserHasPaidBenefits(this.userId);
|
||||
assertUserIsTabletopOwner(tabletopId, this.userId);
|
||||
assertAdmin(this.userId);
|
||||
|
||||
let removed = Tabletops.remove({
|
||||
_id: tabletopId,
|
||||
});
|
||||
_id: tabletopId,
|
||||
});
|
||||
Creatures.update({
|
||||
tabletop: tabletopId,
|
||||
}, {
|
||||
$unset: {tabletop: 1},
|
||||
$unset: { tabletop: 1 },
|
||||
});
|
||||
return removed;
|
||||
},
|
||||
|
||||
@@ -11,94 +11,94 @@ const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LI
|
||||
const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || [];
|
||||
|
||||
const userSchema = new SimpleSchema({
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: 30,
|
||||
min: 4,
|
||||
},
|
||||
emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'emails.$': {
|
||||
type: Object,
|
||||
},
|
||||
'emails.$.address': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Email,
|
||||
},
|
||||
'emails.$.verified': {
|
||||
type: Boolean,
|
||||
},
|
||||
registered_emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'registered_emails.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date
|
||||
},
|
||||
services: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'roles.$': {
|
||||
type: String
|
||||
},
|
||||
// In order to avoid an 'Exception in setInterval callback' from Meteor
|
||||
heartbeat: {
|
||||
type: Date,
|
||||
optional: true,
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
index: 1,
|
||||
optional: true,
|
||||
},
|
||||
darkMode: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
subscribedLibraries: {
|
||||
type: Array,
|
||||
defaultValue: defaultLibraries,
|
||||
maxCount: 100,
|
||||
},
|
||||
'subscribedLibraries.$': {
|
||||
type: String,
|
||||
username: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: 30,
|
||||
min: 4,
|
||||
},
|
||||
emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'emails.$': {
|
||||
type: Object,
|
||||
},
|
||||
'emails.$.address': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Email,
|
||||
},
|
||||
'emails.$.verified': {
|
||||
type: Boolean,
|
||||
},
|
||||
registered_emails: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'registered_emails.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
createdAt: {
|
||||
type: Date
|
||||
},
|
||||
services: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
blackbox: true,
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'roles.$': {
|
||||
type: String
|
||||
},
|
||||
// In order to avoid an 'Exception in setInterval callback' from Meteor
|
||||
heartbeat: {
|
||||
type: Date,
|
||||
optional: true,
|
||||
},
|
||||
apiKey: {
|
||||
type: String,
|
||||
index: 1,
|
||||
optional: true,
|
||||
},
|
||||
darkMode: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
subscribedLibraries: {
|
||||
type: Array,
|
||||
defaultValue: defaultLibraries,
|
||||
maxCount: 100,
|
||||
},
|
||||
'subscribedLibraries.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
subscribedLibraryCollections: {
|
||||
type: Array,
|
||||
defaultValue: defaultLibraryCollections,
|
||||
maxCount: 100,
|
||||
},
|
||||
'subscribedLibraryCollections.$': {
|
||||
type: String,
|
||||
},
|
||||
subscribedLibraryCollections: {
|
||||
type: Array,
|
||||
defaultValue: defaultLibraryCollections,
|
||||
maxCount: 100,
|
||||
},
|
||||
'subscribedLibraryCollections.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
},
|
||||
subscribedCharacters: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
max: 100,
|
||||
},
|
||||
'subscribedCharacters.$': {
|
||||
type: String,
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
max: 100,
|
||||
},
|
||||
'subscribedCharacters.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
fileStorageUsed: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
fileStorageUsed: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
profile: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
@@ -123,25 +123,25 @@ Meteor.users.attachSchema(userSchema);
|
||||
|
||||
Meteor.users.generateApiKey = new ValidatedMethod({
|
||||
name: 'users.generateApiKey',
|
||||
validate: null,
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run(){
|
||||
if(Meteor.isClient) return;
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (!user) return;
|
||||
if (user && user.apiKey) return;
|
||||
var apiKey = Random.id(30);
|
||||
Meteor.users.update(this.userId, {$set: {apiKey}});
|
||||
},
|
||||
run() {
|
||||
if (Meteor.isClient) return;
|
||||
var user = Meteor.users.findOne(this.userId);
|
||||
if (!user) return;
|
||||
if (user && user.apiKey) return;
|
||||
var apiKey = Random.id(30);
|
||||
Meteor.users.update(this.userId, { $set: { apiKey } });
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.setDarkMode = new ValidatedMethod({
|
||||
name: 'users.setDarkMode',
|
||||
validate: new SimpleSchema({
|
||||
validate: new SimpleSchema({
|
||||
darkMode: { type: Boolean },
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
@@ -149,81 +149,81 @@ Meteor.users.setDarkMode = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({darkMode}){
|
||||
if (!this.userId) return;
|
||||
Meteor.users.update(this.userId, {$set: {darkMode}});
|
||||
},
|
||||
run({ darkMode }) {
|
||||
if (!this.userId) return;
|
||||
Meteor.users.update(this.userId, { $set: { darkMode } });
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.sendVerificationEmail = new ValidatedMethod({
|
||||
name: 'users.sendVerificationEmail',
|
||||
validate: new SimpleSchema({
|
||||
userId:{
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
name: 'users.sendVerificationEmail',
|
||||
validate: new SimpleSchema({
|
||||
userId: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({userId, address}){
|
||||
userId = this.userId || userId;
|
||||
let user = Meteor.users.findOne(userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('User not found',
|
||||
'Can\'t send a validation email to a user that does not exist');
|
||||
}
|
||||
if (!some(user.emails, email => email.address === address)) {
|
||||
throw new Meteor.Error('Email address not found',
|
||||
'The specified email address wasn\'t found on this user account');
|
||||
}
|
||||
Accounts.sendVerificationEmail(userId, address);
|
||||
}
|
||||
run({ userId, address }) {
|
||||
userId = this.userId || userId;
|
||||
let user = Meteor.users.findOne(userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('User not found',
|
||||
'Can\'t send a validation email to a user that does not exist');
|
||||
}
|
||||
if (!some(user.emails, email => email.address === address)) {
|
||||
throw new Meteor.Error('Email address not found',
|
||||
'The specified email address wasn\'t found on this user account');
|
||||
}
|
||||
Accounts.sendVerificationEmail(userId, address);
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.users.canPickUsername = new ValidatedMethod({
|
||||
name: 'users.canPickUsername',
|
||||
validate: userSchema.pick('username').validator(),
|
||||
name: 'users.canPickUsername',
|
||||
validate: userSchema.pick('username').validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({username}){
|
||||
if (Meteor.isClient) return;
|
||||
let user = Accounts.findUserByUsername(username);
|
||||
run({ username }) {
|
||||
if (Meteor.isClient) return;
|
||||
let user = Accounts.findUserByUsername(username);
|
||||
// You can pick your own username
|
||||
if (user && user._id === this.userId){
|
||||
if (user && user._id === this.userId) {
|
||||
return false;
|
||||
}
|
||||
return !!user;
|
||||
}
|
||||
return !!user;
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.users.setUsername = new ValidatedMethod({
|
||||
name: 'users.setUsername',
|
||||
validate: userSchema.pick('username').validator(),
|
||||
validate: userSchema.pick('username').validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({username}){
|
||||
if (!this.userId) throw 'Can only set your username if logged in';
|
||||
run({ username }) {
|
||||
if (!this.userId) throw 'Can only set your username if logged in';
|
||||
if (Meteor.isClient) return;
|
||||
return Accounts.setUsername(this.userId, username)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.users.setPreference = new ValidatedMethod({
|
||||
name: 'users.setPreference',
|
||||
validate: new SimpleSchema({
|
||||
preference:{
|
||||
validate: new SimpleSchema({
|
||||
preference: {
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
@@ -235,97 +235,97 @@ Meteor.users.setPreference = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({preference, value}){
|
||||
if (!this.userId) throw 'You can only set preferences once logged in';
|
||||
run({ preference, value }) {
|
||||
if (!this.userId) throw 'You can only set preferences once logged in';
|
||||
let prefPath = `preferences.${preference}`
|
||||
if (value == true){
|
||||
if (value == true) {
|
||||
return Meteor.users.update(this.userId, {
|
||||
$set: {[prefPath]: true},
|
||||
$set: { [prefPath]: true },
|
||||
});
|
||||
} else {
|
||||
return Meteor.users.update(this.userId, {
|
||||
$unset: {[prefPath]: 1},
|
||||
$unset: { [prefPath]: 1 },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Meteor.users.subscribeToLibrary = new ValidatedMethod({
|
||||
name: 'users.subscribeToLibrary',
|
||||
validate: new SimpleSchema({
|
||||
libraryId:{
|
||||
type: String,
|
||||
validate: new SimpleSchema({
|
||||
libraryId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
},
|
||||
subscribe: {
|
||||
type: Boolean,
|
||||
},
|
||||
}).validator(),
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({libraryId, subscribe}){
|
||||
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||
if (subscribe){
|
||||
run({ libraryId, subscribe }) {
|
||||
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||
if (subscribe) {
|
||||
return Meteor.users.update(this.userId, {
|
||||
$addToSet: {subscribedLibraries: libraryId},
|
||||
$addToSet: { subscribedLibraries: libraryId },
|
||||
});
|
||||
} else {
|
||||
return Meteor.users.update(this.userId, {
|
||||
$pullAll: {subscribedLibraries: libraryId},
|
||||
$pullAll: { subscribedLibraries: libraryId },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
|
||||
name: 'users.subscribeToLibraryCollection',
|
||||
validate: new SimpleSchema({
|
||||
libraryCollectionId:{
|
||||
type: String,
|
||||
validate: new SimpleSchema({
|
||||
libraryCollectionId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
},
|
||||
subscribe: {
|
||||
type: Boolean,
|
||||
},
|
||||
}).validator(),
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({libraryCollectionId, subscribe}){
|
||||
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||
if (subscribe){
|
||||
run({ libraryCollectionId, subscribe }) {
|
||||
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||
if (subscribe) {
|
||||
return Meteor.users.update(this.userId, {
|
||||
$addToSet: {subscribedLibraryCollections: libraryCollectionId},
|
||||
$addToSet: { subscribedLibraryCollections: libraryCollectionId },
|
||||
});
|
||||
} else {
|
||||
return Meteor.users.update(this.userId, {
|
||||
$pullAll: {subscribedLibraryCollections: libraryCollectionId},
|
||||
$pullAll: { subscribedLibraryCollections: libraryCollectionId },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
|
||||
name: 'users.findUserByUsernameOrEmail',
|
||||
validate: new SimpleSchema({
|
||||
usernameOrEmail:{
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
name: 'users.findUserByUsernameOrEmail',
|
||||
validate: new SimpleSchema({
|
||||
usernameOrEmail: {
|
||||
type: String,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({usernameOrEmail}){
|
||||
if (Meteor.isClient) return;
|
||||
let user = Accounts.findUserByUsername(usernameOrEmail) ||
|
||||
Accounts.findUserByEmail(usernameOrEmail);
|
||||
return user && user._id;
|
||||
}
|
||||
run({ usernameOrEmail }) {
|
||||
if (Meteor.isClient) return;
|
||||
let user = Accounts.findUserByUsername(usernameOrEmail) ||
|
||||
Accounts.findUserByEmail(usernameOrEmail);
|
||||
return user && user._id;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
|
||||
const addEmail = new ValidatedMethod({
|
||||
name: 'users.addEmail',
|
||||
validate: new SimpleSchema({
|
||||
name: 'users.addEmail',
|
||||
validate: new SimpleSchema({
|
||||
email: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Email,
|
||||
@@ -15,20 +15,20 @@ const addEmail = new ValidatedMethod({
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({email}){
|
||||
run({ email }) {
|
||||
const userId = Meteor.userId();
|
||||
const user = Meteor.users.findOne(userId);
|
||||
if (!user) throw new Meteor.Error('No user',
|
||||
'You must be logged in to add an email address');
|
||||
if (user.emails && user.emails.length >= 2){
|
||||
'You must be logged in to add an email address');
|
||||
if (user.emails && user.emails.length >= 2) {
|
||||
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.sendVerificationEmail(userId, email);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default addEmail;
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
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 {removeCreatureWork} from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
|
||||
Meteor.users.deleteMyAccount = new ValidatedMethod({
|
||||
name: 'users.deleteMyAccount',
|
||||
validate: null,
|
||||
name: 'users.deleteMyAccount',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run(){
|
||||
run() {
|
||||
let userId = Meteor.userId();
|
||||
if (!userId) throw new Meteor.Error('No user',
|
||||
'You must be logged in to delete your account');
|
||||
|
||||
// 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));
|
||||
|
||||
// Remove permissions from all creatures
|
||||
Creatures.update({
|
||||
$or: [
|
||||
{writers: userId},
|
||||
{readers: userId},
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
],
|
||||
}, {
|
||||
$pull: {
|
||||
@@ -37,14 +37,14 @@ Meteor.users.deleteMyAccount = new ValidatedMethod({
|
||||
});
|
||||
|
||||
// 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));
|
||||
|
||||
// Remove permissions from all creatures
|
||||
Libraries.update({
|
||||
$or: [
|
||||
{writers: userId},
|
||||
{readers: userId},
|
||||
{ writers: userId },
|
||||
{ readers: userId },
|
||||
],
|
||||
}, {
|
||||
$pull: {
|
||||
@@ -57,5 +57,5 @@ Meteor.users.deleteMyAccount = new ValidatedMethod({
|
||||
|
||||
// delete the account
|
||||
Meteor.users.remove(userId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
|
||||
const removeEmail = new ValidatedMethod({
|
||||
name: 'users.removeEmail',
|
||||
validate: new SimpleSchema({
|
||||
name: 'users.removeEmail',
|
||||
validate: new SimpleSchema({
|
||||
email: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Email,
|
||||
@@ -15,23 +15,23 @@ const removeEmail = new ValidatedMethod({
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({email}){
|
||||
run({ email }) {
|
||||
const userId = Meteor.userId();
|
||||
const user = Meteor.users.findOne(userId);
|
||||
if (!user) throw new Meteor.Error('No user',
|
||||
'You must be logged in to remove an email address');
|
||||
if (!user.emails){
|
||||
'You must be logged in to remove an email address');
|
||||
if (!user.emails) {
|
||||
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',
|
||||
'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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default removeEmail;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { Migrations } from 'meteor/percolate:migrations';
|
||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
||||
|
||||
if (Meteor.isServer){
|
||||
Meteor.startup(()=>{
|
||||
if (Meteor.isServer) {
|
||||
Meteor.startup(() => {
|
||||
const dbVersion = Migrations.getVersion();
|
||||
// If there are no users, this is a new DB, set the version to latest
|
||||
const aUser = Meteor.users.findOne({});
|
||||
const latestVersion = Migrations._list[Migrations._list.length - 1].version
|
||||
if (!aUser && dbVersion !== latestVersion) {
|
||||
Migrations._collection.update({ _id: 'control' }, { version: latestVersion });
|
||||
return;
|
||||
}
|
||||
// Otherwise put the app in maintenance mode if it's not the right version
|
||||
if (
|
||||
!Meteor.settings.public.maintenanceMode &&
|
||||
dbVersion !== undefined &&
|
||||
SCHEMA_VERSION !== dbVersion
|
||||
){
|
||||
Meteor.settings.public.maintenanceMode = {
|
||||
) {
|
||||
Meteor.settings.public.maintenanceMode = {
|
||||
reason: 'App data needs to be migrated to the latest version'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ const PROPERTIES = Object.freeze({
|
||||
action: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: ['classLevel', 'feature', 'item'],
|
||||
},
|
||||
attribute: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
examples: 'Ability scores, speed, hit points, ki',
|
||||
suggestedParents: ['classLevel', 'buff'],
|
||||
@@ -15,48 +17,56 @@ const PROPERTIES = Object.freeze({
|
||||
adjustment: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
buff: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
buffRemover: {
|
||||
icon: '$vuetify.icons.buffRemover',
|
||||
name: 'Remove Buff',
|
||||
docsPath: 'property/remove-buff',
|
||||
helpText: 'Removes a buff from the target character',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
branch: {
|
||||
icon: 'mdi-file-tree',
|
||||
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.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
|
||||
},
|
||||
class: {
|
||||
icon: 'mdi-card-account-details',
|
||||
name: 'Class',
|
||||
docsPath: 'property/class',
|
||||
helpText: 'Your character should ideally have one starting class. Classes hold class levels',
|
||||
suggestedParents: [],
|
||||
},
|
||||
classLevel: {
|
||||
icon: '$vuetify.icons.class_level',
|
||||
name: 'Class level',
|
||||
docsPath: 'property/class-level',
|
||||
helpText: 'Class levels represent a single level gained in a class',
|
||||
suggestedParents: ['class'],
|
||||
},
|
||||
constant: {
|
||||
icon: 'mdi-anchor',
|
||||
name: 'Constant',
|
||||
docsPath: 'property/constant',
|
||||
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
|
||||
suggestedParents: [],
|
||||
},
|
||||
container: {
|
||||
icon: 'mdi-bag-personal-outline',
|
||||
name: 'Container',
|
||||
docsPath: 'property/container',
|
||||
helpText: 'A container holds items in the inventory',
|
||||
examples: 'Coin pouch, backpack',
|
||||
suggestedParents: ['folder'],
|
||||
@@ -64,18 +74,21 @@ const PROPERTIES = Object.freeze({
|
||||
damage: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
damageMultiplier: {
|
||||
icon: '$vuetify.icons.damage_multiplier',
|
||||
name: 'Damage multiplier',
|
||||
docsPath: 'property/damage-multiplier',
|
||||
helpText: 'Resistance, vulnerability, and immunity.',
|
||||
suggestedParents: ['classLevel', 'feature', 'item'],
|
||||
},
|
||||
effect: {
|
||||
icon: '$vuetify.icons.effect',
|
||||
name: 'Effect',
|
||||
docsPath: 'property/effect',
|
||||
helpText: 'Effects change the value or state of attributes and skills.',
|
||||
examples: '+2 Strength, Advantage on dexterity saving throws',
|
||||
suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'],
|
||||
@@ -83,42 +96,49 @@ const PROPERTIES = Object.freeze({
|
||||
feature: {
|
||||
icon: 'mdi-text-subject',
|
||||
name: 'Feature',
|
||||
docsPath: 'property/feature',
|
||||
helpText: 'Descriptive or narrative features your character has access to',
|
||||
suggestedParents: ['classLevel', 'folder'],
|
||||
},
|
||||
folder: {
|
||||
icon: 'mdi-folder-outline',
|
||||
name: 'Folder',
|
||||
docsPath: 'property/feature',
|
||||
helpText: 'A way to organise other properties on the character',
|
||||
suggestedParents: ['folder'],
|
||||
suggestedParents: ['action', 'folder'],
|
||||
},
|
||||
item: {
|
||||
icon: 'mdi-cube-outline',
|
||||
name: 'Item',
|
||||
docsPath: 'property/item',
|
||||
helpText: 'Objects and equipment your charcter finds on their adventures',
|
||||
suggestedParents: ['container'],
|
||||
},
|
||||
note: {
|
||||
icon: 'mdi-note-outline',
|
||||
name: 'Note',
|
||||
docsPath: 'property/note',
|
||||
helpText: 'Notes about your character and their adventures',
|
||||
suggestedParents: ['note', 'folder'],
|
||||
},
|
||||
pointBuy: {
|
||||
icon: 'mdi-table',
|
||||
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',
|
||||
suggestedParents: [],
|
||||
},
|
||||
proficiency: {
|
||||
icon: 'mdi-brightness-1',
|
||||
name: 'Proficiency',
|
||||
docsPath: 'property/proficiency',
|
||||
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
|
||||
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
|
||||
},
|
||||
roll: {
|
||||
icon: '$vuetify.icons.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',
|
||||
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
|
||||
},
|
||||
@@ -132,48 +152,56 @@ const PROPERTIES = Object.freeze({
|
||||
savingThrow: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: ['action', 'attack', 'spell'],
|
||||
},
|
||||
skill: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: ['classLevel', 'folder'],
|
||||
},
|
||||
propertySlot: {
|
||||
icon: 'mdi-power-socket-eu',
|
||||
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',
|
||||
suggestedParents: [],
|
||||
},
|
||||
slotFiller: {
|
||||
icon: 'mdi-power-plug-outline',
|
||||
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.',
|
||||
suggestedParents: ['propertySlot'],
|
||||
},
|
||||
spellList: {
|
||||
icon: '$vuetify.icons.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',
|
||||
suggestedParents: [],
|
||||
},
|
||||
spell: {
|
||||
icon: '$vuetify.icons.spell',
|
||||
name: 'Spell',
|
||||
docsPath: 'property/spell',
|
||||
helpText: 'A spell your character can potentially cast',
|
||||
suggestedParents: ['spellList'],
|
||||
},
|
||||
toggle: {
|
||||
icon: '$vuetify.icons.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.',
|
||||
suggestedParents: [],
|
||||
},
|
||||
trigger: {
|
||||
icon: 'mdi-electric-switch',
|
||||
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',
|
||||
suggestedParents: [],
|
||||
},
|
||||
@@ -188,3 +216,17 @@ export function getPropertyName(type){
|
||||
export function getPropertyIcon(type){
|
||||
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 };
|
||||
|
||||
@@ -10,7 +10,7 @@ const dbVersionToGitVersion = {
|
||||
|
||||
const getVersion = new ValidatedMethod({
|
||||
name: 'admin.getVersion',
|
||||
validate: null,
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Migrations } from 'meteor/percolate:migrations';
|
||||
|
||||
const migrateTo = new ValidatedMethod({
|
||||
name: 'admin.migrateTo',
|
||||
validate: new SimpleSchema({
|
||||
validate: new SimpleSchema({
|
||||
version: {
|
||||
type: SimpleSchema.oneOf(
|
||||
SimpleSchema.Integer,
|
||||
@@ -19,7 +19,7 @@ const migrateTo = new ValidatedMethod({
|
||||
numRequests: 1,
|
||||
timeInterval: 10000,
|
||||
},
|
||||
run({version}) {
|
||||
run({ version }) {
|
||||
if (Meteor.isClient) return;
|
||||
assertAdmin(this.userId);
|
||||
Migrations.migrateTo(version);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
|
||||
|
||||
const validateDatabase = new ValidatedMethod({
|
||||
name: 'validateDatabase',
|
||||
validate: null,
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
@@ -23,8 +23,8 @@ const validateDatabase = new ValidatedMethod({
|
||||
const schema = collection.instance.simpleSchema(doc);
|
||||
let cleanDoc = schema.clean(doc);
|
||||
try {
|
||||
schema.validate(cleanDoc, {modifier: false});
|
||||
} catch (e){
|
||||
schema.validate(cleanDoc, { modifier: false });
|
||||
} catch (e) {
|
||||
console.log(collection.name, doc._id, e.message || e.reason || e.toString());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -112,10 +112,10 @@ export default {
|
||||
}
|
||||
},
|
||||
'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: [
|
||||
{input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'},
|
||||
{input: 'resolve(3d6)', result: '2'},
|
||||
{input: 'resolve(1d6)', result: '4'},
|
||||
],
|
||||
arguments: ['parseNode'],
|
||||
fn: function resolveFn(node){
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user