Compare commits

...

75 Commits

Author SHA1 Message Date
Stefan Zermatten
2849df1974 Merge branch 'version-2-dev' into version-2 2021-07-13 12:45:59 +02:00
Stefan Zermatten
3fa2cca7ae Locked dark mode to paid accounts only 2021-07-13 12:39:56 +02:00
Stefan Zermatten
a0b53af6d7 Fixed awkward padding in character limit alert 2021-07-13 12:25:15 +02:00
Stefan Zermatten
51c709f7a5 Changed tier limitations for closed beta 2021-07-13 12:23:56 +02:00
Stefan Zermatten
28d67409aa Changed some patreon nudging 2021-07-13 12:23:28 +02:00
Stefan Zermatten
7e97bcb6d8 Updated packages 2021-07-13 12:04:36 +02:00
Stefan Zermatten
4e87737a3e Fixed add-property button not re-appearing when adding a reference property 2021-07-12 18:11:48 +02:00
Stefan Zermatten
822fa4619f Fixed missing icons in calculation errors 2021-07-12 18:00:21 +02:00
Stefan Zermatten
80ba44a28f Added ancestor references to action context so that #spellList references work, closes #270 2021-07-12 17:56:35 +02:00
Stefan Zermatten
c3c079731e Fixed equipment creating ghost items on drag if the equipment folder is deleted from the character 2021-07-12 17:13:18 +02:00
Stefan Zermatten
039b7046b2 Fixed attributes not hiding when redundant 2021-07-12 17:03:32 +02:00
Stefan Zermatten
8eaad3600f Fixed computation error with base values 2021-07-12 14:56:12 +02:00
Stefan Zermatten
5cf78932e6 removed stray debug text 2021-07-12 14:47:41 +02:00
Stefan Zermatten
d43d364175 Removed stray console log 2021-07-12 14:46:37 +02:00
Stefan Zermatten
0ad4c71189 Fixed some missing icons 2021-07-12 14:45:24 +02:00
Stefan Zermatten
e8c6f26a0b fix skills UI bugs and icon consistency for skills 2021-07-12 14:39:05 +02:00
Stefan Zermatten
8804c80a56 Fixed skills not showing their base value in the effects list 2021-07-12 14:15:17 +02:00
Stefan Zermatten
0d21ab758e Fixed negative base values being ignored 2021-07-12 14:12:20 +02:00
Stefan Zermatten
2ecb0e2671 Fix: DC missing from spell list viewer in library 2021-07-12 13:39:08 +02:00
Stefan Zermatten
f3b9b62486 Fixed styling of inventory for denser item list tiles 2021-07-12 12:50:56 +02:00
Stefan Zermatten
29a4575760 Fixed a stray empty list item action taking space on creatures in lists 2021-06-22 15:19:46 +02:00
Stefan Zermatten
a6fbf71b36 Added folders to the sidebar character list 2021-06-22 15:13:59 +02:00
Stefan Zermatten
86d9383af0 Archive UI 2021-06-22 14:59:18 +02:00
Stefan Zermatten
3db589f775 Fixed button property for new Vuetify version 2021-06-22 14:59:02 +02:00
Stefan Zermatten
e96755927f Added archive dialog, empty for now 2021-06-21 16:42:47 +02:00
Stefan Zermatten
66dc0ee34f Added icon to indicate sharing status of characters 2021-06-21 16:37:02 +02:00
Stefan Zermatten
54bf21c57c Edit permission is no longer patreon-only 2021-06-21 16:32:24 +02:00
Stefan Zermatten
5f5fe801f6 Added method to transfer character ownership 2021-06-21 16:06:29 +02:00
Stefan Zermatten
a451afcbaf Prevented new characters being added if you are at your character limit 2021-06-21 16:06:10 +02:00
Stefan Zermatten
848e961e3b Refactored creature methods to their own folders 2021-06-21 15:20:04 +02:00
Stefan Zermatten
c5aca81131 Removed stray console logs 2021-06-20 13:35:23 +02:00
Stefan Zermatten
814e371148 Can now move creatures between folders using drag and drop 2021-06-20 13:32:28 +02:00
Stefan Zermatten
69f4bbf360 Added CRUD API and UI for creature folders 2021-06-20 12:41:08 +02:00
Stefan Zermatten
6b2d74a165 Fixed parties -> creatureFolders publications and ui 2021-06-18 12:59:39 +02:00
Stefan Zermatten
cf05aea80a Fixed Parties references 2021-06-18 10:49:01 +02:00
Stefan Zermatten
1a2d4b22bb Renamed parties to creatureFolders 2021-06-18 10:48:12 +02:00
Stefan Zermatten
81d52a1847 Enabled editing of attribute damage in library forms 2021-06-18 10:43:27 +02:00
Stefan Zermatten
e3fc56a844 Added backend architecture to archive and restore creatures. 2021-06-11 12:03:31 +02:00
Stefan Zermatten
64fceb9c38 Added environmental variables to readme for self-hosting 2021-06-10 15:19:14 +02:00
Stefan Zermatten
9994c1f32a New users now get subscribed to the default libraries as defined by env 2021-06-10 15:18:54 +02:00
Stefan Zermatten
7056c5b37b Added character slot limitations to tiers; added no-patreon tier for self hosting 2021-06-10 12:25:17 +02:00
Stefan Zermatten
1ad1d1f23d Migrated from Google material design icons to vuetify default MDI 2021-06-01 12:34:51 +02:00
Stefan Zermatten
c65c8f3299 Added note to attempt to keep children of reference nodes 2021-04-29 16:13:22 +02:00
Stefan Zermatten
4faea42371 Merge branch 'version-2-dev' into version-2 2021-04-29 15:53:24 +02:00
Stefan Zermatten
9825872576 Implemented Reference properties 2021-04-29 15:52:24 +02:00
Stefan Zermatten
85b536bc46 Added default array for stat proficiencies as well 2021-04-29 11:52:47 +02:00
Stefan Zermatten
9aa8203dcc Fixed bug where effects in stat computation could be undefined 2021-04-29 11:50:16 +02:00
Stefan Zermatten
217133137b Added note to improve query performance with root ancestor targeting 2021-04-29 11:34:58 +02:00
Stefan Zermatten
aef7dbcbb3 Fixed bug in stat computation dependency tracking 2021-04-29 11:22:13 +02:00
Stefan Zermatten
6ff750417f Fixed error in stat computation 2021-04-24 23:25:58 +02:00
Stefan Zermatten
a9eacfab03 Unprepared spells without lists now correctly show up when unprepared 2021-04-22 16:06:31 +02:00
Stefan Zermatten
1f633621b7 Fixed a bug with functions accepting rolled arguments 2021-04-22 15:59:12 +02:00
Stefan Zermatten
9f3c8bef34 Removed stray console log 2021-04-22 15:54:41 +02:00
Stefan Zermatten
8a83e7d8a1 Fixed back button appearing in embedded dialogs 2021-04-22 15:42:44 +02:00
Stefan Zermatten
a28182f3e9 Added missing half rounded down icon for skills in stats tab 2021-04-22 15:40:26 +02:00
Stefan Zermatten
3d122e062f Added the distinction between half rounded up or down for proficiencies 2021-04-22 15:39:14 +02:00
Stefan Zermatten
e9a273244a Improved Effect and Proficiency UI in attribute and skill viewers 2021-04-22 15:12:49 +02:00
Stefan Zermatten
1de3122254 Updated UI to hide extra attributes and skills with same variable name 2021-04-22 15:12:21 +02:00
Stefan Zermatten
298db01e5b Updated computation engine to handle multiple attributes and skills with the same variable name 2021-04-22 15:11:49 +02:00
Stefan Zermatten
727101cd63 Updated Meteor 2021-04-22 15:10:47 +02:00
Stefan Zermatten
d4d002cf31 Fixed an error when targeting an ability score with a proficiency 2021-04-15 12:00:11 +02:00
Stefan Zermatten
2150bd6da4 Added breadcrumbs to creature properties 2021-04-13 14:17:31 +02:00
Stefan Zermatten
e1df145675 Add property button now in creature property dialogs 2021-04-13 11:53:50 +02:00
Stefan Zermatten
1eb78756ac Fixed console error if creature is deleted while sheet is still showing 2021-04-13 11:53:18 +02:00
Stefan Zermatten
ce9b9199ec Fixed dialog stacking animation 2021-04-13 11:41:14 +02:00
Stefan Zermatten
cfb1414494 Start character sheet on the character details dialog instead of build 2021-04-13 11:06:46 +02:00
Stefan Zermatten
4abd689c9f Use DiceCloud 5e base as default for new characters 2021-04-13 11:06:25 +02:00
Stefan Zermatten
f0e443fba2 When hiding the spells tab or tree tab, only change tabs if on one of those 2021-04-13 11:05:25 +02:00
Stefan Zermatten
52e7deedc6 Leave character page before deleting to prevent UI errors 2021-04-13 10:50:35 +02:00
Stefan Zermatten
15d593db79 Properties quick-inserted from the sheet now go into folders in the tree 2021-04-12 16:04:04 +02:00
Stefan Zermatten
e30754ef26 Added method to insert property to a tagged parent 2021-04-12 15:35:25 +02:00
Stefan Zermatten
255ac529b3 Added more default properties to creatures 2021-04-12 15:35:12 +02:00
Stefan Zermatten
c8b5ada5b9 Changed all form input fields to outlined style instead of filled 2021-04-12 14:21:50 +02:00
Stefan Zermatten
0c24238069 Fixed not found page for Vuetify 2 2021-04-11 18:15:56 +02:00
Stefan Zermatten
66847430ad Fixed sign in and register pages not being built with Vuetify 2 components 2021-04-11 18:05:36 +02:00
192 changed files with 3699 additions and 1033 deletions

View File

@@ -66,5 +66,20 @@ You should see this:
=> App running at: http://localhost:3000/
```
Environmental Variables
-----------------------
```
MAIL_URL=smtp://<your smtp mail url>
METEOR_SETTINGS={ "public": { "environment": "production", "patreon": { "clientId": "<your patreon client ID>", "campaignId": "<your campaign id>" } }, "patreon": { "clientSecret": "<your client secret>", "creatorAccessToken": "<your creator access token>" } }
MONGO_OPLOG_URL=mongodb+srv://<your url for the oplog account of your mongo database>
MONGO_URL=mongodb+srv://<your url for the read/write account of your mongo database>
NPM_CONFIG_PRODUCTION=true
PROJECT_DIR=app
ROOT_URL=https://<url of your DiceCloud instance>
DEFAULT_LIBRARIES=<comma separated list of library ids that will be subscribed by default: "abc123,def456">
DISABLE_PATREON=<"true" if you want to prevent features being locked behind Patreon tiers>
```
Now, visiting [](http://localhost:3000/) should show you an empty instance of
DiceCloud running.

View File

@@ -3,42 +3,42 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.7.0
random@1.2.0
accounts-password
random
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
reactive-var
underscore
momentjs:moment
dburles:mongo-collection-instances
accounts-google@1.3.3
email@2.0.0
accounts-google
email
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
mobile-experience@1.1.0
mongo@1.10.1
session@1.2.0
tracker@1.2.0
logging@1.2.0
reload@1.3.1
ejson@1.1.1
check@1.3.1
standard-minifier-js@2.6.0
shell-server@0.5.0
meteor-base
mobile-experience
mongo
session
tracker
logging
reload
ejson
check
standard-minifier-js
shell-server
templates:array
ecmascript@0.15.0
es5-shim@4.8.0
reactive-dict@1.3.0
ecmascript
es5-shim
reactive-dict
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
dynamic-import@0.6.0
ddp-rate-limiter@1.0.9
rate-limit@1.0.9
service-configuration
dynamic-import
ddp-rate-limiter
rate-limit
mdg:validated-method
akryum:vue-router2
static-html
aldeed:collection2@3.0.0
aldeed:collection2
aldeed:schema-index
zer0th:meteor-vuetify-loader
accounts-patreon

View File

@@ -1 +1 @@
METEOR@2.1
METEOR@2.2.1

View File

@@ -1,7 +1,7 @@
accounts-base@1.8.0
accounts-base@1.9.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.7.0
accounts-password@1.7.1
accounts-patreon@0.1.0
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
@@ -9,19 +9,19 @@ akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
akryum:vue-sass@0.1.2
aldeed:collection2@3.2.1
aldeed:collection2@3.4.1
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.7.0
babel-compiler@7.6.0
babel-compiler@7.6.2
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.0.10
blaze-tools@1.1.2
boilerplate-generator@1.7.1
bozhao:link-accounts@2.3.2
bozhao:link-accounts@2.4.0
caching-compiler@1.2.2
caching-html-compiler@1.1.3
caching-html-compiler@1.2.1
callback-hook@1.3.0
check@1.3.1
chuangbo:marked@0.3.5_1
@@ -30,17 +30,17 @@ coffeescript-compiler@2.4.1
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.4.0
ddp-client@2.4.1
ddp-common@1.4.0
ddp-rate-limiter@1.0.9
ddp-server@2.3.2
ddp-server@2.3.3
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.6.0
ecmascript@0.15.0
ecmascript@0.15.1
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
ecmascript-runtime-client@0.11.1
ecmascript-runtime-server@0.10.1
ejson@1.1.1
email@2.0.0
es5-shim@4.8.0
@@ -48,13 +48,13 @@ fetch@0.1.1
geojson-utils@1.0.10
google-oauth@1.3.0
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.3
id-map@1.1.0
html-tools@1.1.2
htmljs@1.1.1
http@1.4.4
id-map@1.1.1
inter-process-messaging@0.1.1
lai:collection-extensions@0.2.1_1
launch-screen@1.2.0
launch-screen@1.2.1
livedata@1.0.18
localstorage@1.2.0
logging@1.2.0
@@ -63,21 +63,21 @@ meteor@1.9.3
meteor-base@1.4.0
meteorhacks:subs-manager@1.6.4
mikowals:batch-insert@1.2.0
minifier-css@1.5.3
minifier-js@2.6.0
minimongo@1.6.1
minifier-css@1.5.4
minifier-js@2.6.1
minimongo@1.6.2
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.16.0
modules-runtime@0.12.0
momentjs:moment@2.29.1
mongo@1.10.1
mongo@1.11.1
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-bcrypt@0.9.3
npm-mongo@3.8.1
mongo-id@1.0.8
npm-bcrypt@0.9.4
npm-mongo@3.9.0
oauth@1.3.2
oauth2@1.3.0
ongoworks:speakingurl@9.0.0
@@ -98,7 +98,7 @@ promise@0.11.2
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.1.0
react-fast-refresh@0.1.1
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.1
@@ -112,17 +112,18 @@ shell-server@0.5.0
simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.1
spacebars-compiler@1.1.3
socket-stream-client@0.3.3
spacebars-compiler@1.3.0
srp@1.1.0
standard-minifier-js@2.6.0
static-html@1.2.2
standard-minifier-js@2.6.1
static-html@1.3.2
templates:array@1.0.3
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
templating-tools@1.2.1
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.2.2
underscore@1.0.10
url@1.3.1
webapp@1.10.0
url@1.3.2
webapp@1.10.1
webapp-hashing@1.1.0
zer0th:meteor-vuetify-loader@0.1.30

View File

@@ -1,5 +1,6 @@
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<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">

View File

@@ -1,28 +0,0 @@
import SimpleSchema from 'simpl-schema';
let Parties = new Mongo.Collection('parties');
let partySchema = new SimpleSchema({
name: {
type: String,
defaultValue: 'New Party',
trim: false,
optional: true,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Parties.attachSchema(partySchema);
export default Parties;

View File

@@ -62,6 +62,7 @@ function applyPropertyAndWalkChildren({prop, children, targets, ...options}){
export default function applyProperties({ forest, targets, ...options}){
forest.forEach(node => {
let prop = node.node;
options.actionContext[`#${prop.type}`] = prop;
let children = node.children;
if (shouldSplit(prop) && targets.length){
targets.forEach(target => {

View File

@@ -2,12 +2,13 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { doActionWork } from '/imports/api/creature/actions/doAction.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
const castSpellWithSlot = new ValidatedMethod({
name: 'creatureProperties.castSpellWithSlot',
@@ -61,9 +62,11 @@ const castSpellWithSlot = new ValidatedMethod({
value: 1,
});
}
let actionContext = getAncestorContext(spell);
doActionWork({
action: spell,
context: {slotLevel},
actionContext: {slotLevel, ...actionContext},
creature,
targets: target ? [target] : [],
method: this,

View File

@@ -2,15 +2,16 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import getAncestorContext from '/imports/api/creature/actions/getAncestorContext.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -36,6 +37,10 @@ const doAction = new ValidatedMethod({
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
// Build ancestor context
let actionContext = getAncestorContext(action);
assertEditPermission(creature, this.userId);
let targets = [];
targetIds.forEach(targetId => {
@@ -43,7 +48,7 @@ const doAction = new ValidatedMethod({
assertEditPermission(target, this.userId);
targets.push(target);
});
doActionWork({action, creature, targets, method: this});
doActionWork({action, creature, targets, actionContext, method: this});
// The acting creature might have used ammo
recomputeInventory(creature._id);
@@ -64,7 +69,7 @@ export function doActionWork({
action,
creature,
targets,
context = {},
actionContext = {},
method
}){
// Create the log
@@ -83,7 +88,7 @@ export function doActionWork({
}];
applyProperties({
forest: startingForest,
actionContext: context,
actionContext,
creature,
targets,
log,

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import roll from '/imports/parser/roll.js';
const doCheck = new ValidatedMethod({

View File

@@ -0,0 +1,15 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getAncestorContext(prop){
// Build ancestor context
const actionContext = {};
let ancestorIds = prop.ancestors.map(ref => ref.id);
CreatureProperties.find({
_id: {$in: ancestorIds}
}, {
sort: {order: 1},
}).forEach(ancestor => {
actionContext[`#${ancestor.type}`] = ancestor;
});
return actionContext;
}

View File

@@ -0,0 +1,57 @@
import SimpleSchema from 'simpl-schema';
// Archived creatures is an immutable collection of creatures that are no longer
// in use and can be safely archived by the mongoDB hosting service.
// It keeps the working datasets like creatureProperties much smaller
// than they would otherwise be.
let ArchivedCreatures = new Mongo.Collection('archivedCreatures');
// We use blackbox objects for everything:
// - saves time checking every object against a schema
// - doesn't accidentaly create indices defined in subschemas
// - The objects we are archiving have already been checked against their
// own schemas
let ArchivedCreatureSchema = new SimpleSchema({
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
// The primary index on this collection
index: 1,
},
archiveDate: {
type: Date,
// Indexed so the archiving system can archive documents when they
// get to a certain age
index: 1,
},
creature: {
type: Object,
blackbox: true,
},
properties: {
type: Array,
},
'properties.$': {
type: Object,
blackbox: true,
},
experiences: {
type: Array,
},
'experiences.$': {
type: Object,
blackbox: true,
},
logs: {
type: Array,
},
'logs.$': {
type: Object,
blackbox: true,
},
});
ArchivedCreatures.attachSchema(ArchivedCreatureSchema);
import '/imports/api/creature/archive/methods/index.js';
export default ArchivedCreatures;

View File

@@ -0,0 +1,66 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
function archiveCreature(creatureId){
// Build the archive document
const creature = Creatures.findOne(creatureId);
const properties = CreatureProperties.find({'ancestors.id': creatureId}).fetch();
const experiences = Experiences.find({creatureId}).fetch();
const logs = CreatureLogs.find({creatureId}).fetch();
let archiveCreature = {
owner: creature.owner,
archiveDate: new Date(),
creature,
properties,
experiences,
logs,
};
// Insert it
let id = ArchivedCreatures.insert(archiveCreature);
// Remove the original creature
removeCreatureWork(creatureId);
return id;
}
const archiveCreatures = new ValidatedMethod({
name: 'Creatures.methods.archiveCreatures',
validate: new SimpleSchema({
creatureIds: {
type: Array,
max: 10,
},
'creatureIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run({creatureIds}) {
for (let id of creatureIds){
assertOwnership(id, this.userId)
}
let archivedIds = [];
for (let id of creatureIds){
let archivedId = archiveCreature(id);
archivedIds.push(archivedId);
}
return archivedIds;
},
});
export default archiveCreatures;

View File

@@ -0,0 +1,2 @@
import '/imports/api/creature/archive/methods/archiveCreatures.js';
import '/imports/api/creature/archive/methods/restoreCreatures.js';

View File

@@ -0,0 +1,77 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
function restoreCreature(archiveId){
// Get the archive
const archivedCreature = ArchivedCreatures.findOne(archiveId);
// Insert the creature sub documents
// They still have their original _id's
Creatures.insert(archivedCreature.creature);
try {
// Add all the properties
if (archivedCreature.properties && archivedCreature.properties.length){
CreatureProperties.batchInsert(archivedCreature.properties);
}
if (archivedCreature.experiences && archivedCreature.experiences.length){
Experiences.batchInsert(archivedCreature.experiences);
}
if (archivedCreature.logs && archivedCreature.logs.length){
CreatureLogs.batchInsert(archivedCreature.logs);
}
// Remove the archived creature
ArchivedCreatures.remove(archiveId);
} catch (e) {
// If the above fails, delete the inserted creature
removeCreatureWork(archivedCreature.creature._id);
throw e;
}
// Do not recompute. The creature was in a computed and ordered state when
// we archived it, just restore everything as-is
return archivedCreature.creature._id;
}
const restoreCreatures = new ValidatedMethod({
name: 'Creatures.methods.restoreCreatures',
validate: new SimpleSchema({
archiveIds: {
type: Array,
max: 10,
},
'archiveIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run({archiveIds}) {
for (let id of archiveIds){
let archivedCreature = ArchivedCreatures.findOne(id, {
fields: {owner: 1}
});
assertOwnership(archivedCreature, this.userId)
}
let creatureIds = [];
for (let id of archiveIds){
let creatureId = restoreCreature(id);
creatureIds.push(creatureId);
}
return creatureIds;
},
});
export default restoreCreatures;

View File

@@ -104,31 +104,10 @@ export default class ComputationMemo {
let variableName = prop.variableName;
if (!variableName) return;
let existingStat = this.statsByVariableName[variableName];
prop = this.registerProperty(prop);
if (existingStat){
existingStat.computationDetails.idsOfSameName.push(prop._id);
this.originalPropsById[prop._id] = cloneDeep(prop);
if (prop.baseValueCalculation){
existingStat.computationDetails.effects.push({
operation: 'base',
calculation: prop.baseValueCalculation,
stats: [variableName],
computationDetails: propDetailsByType.effect(),
statBase: true,
dependencies: [],
});
}
if (prop.baseProficiency){
existingStat.computationDetails.proficiencies.push({
value: prop.baseProficiency,
stats: [variableName],
computationDetails: propDetailsByType.proficiency(),
type: 'proficiency',
statBase: true,
dependencies: [],
});
}
} else {
prop = this.registerProperty(prop);
this.statsById[prop._id] = prop;
this.statsByVariableName[variableName] = prop;
if (
@@ -190,7 +169,9 @@ export default class ComputationMemo {
prop = this.registerProperty(prop);
let targets = this.getProficiencyTargets(prop);
targets.forEach(target => {
target.computationDetails.proficiencies.push(prop);
if(target.computationDetails.proficiencies){
target.computationDetails.proficiencies.push(prop);
}
});
}
getProficiencyTargets(prop){
@@ -267,6 +248,7 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
effects: [],
proficiencies: [],
toggleAncestors: [],
idsOfSameName: [],
};

View File

@@ -1,31 +1,6 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
export default class EffectAggregator{
constructor(stat, memo){
delete this.baseValueErrors;
if (stat.baseValueCalculation){
let {
result,
context,
dependencies
} = evaluateCalculation({
string: stat.baseValueCalculation,
prop: stat,
memo
});
this.statBaseValue = +result.value;
stat.dependencies = union(
stat.dependencies,
dependencies,
);
if (context.errors.length){
this.baseValueErrors = context.errors;
}
this.base = this.statBaseValue;
} else {
this.base = 0;
}
constructor(){
this.base = undefined;
this.add = 0;
this.mul = 1;
this.min = Number.NEGATIVE_INFINITY;
@@ -45,10 +20,11 @@ export default class EffectAggregator{
switch(effect.operation){
case 'base':
// Take the largest base value
this.base = result > this.base ? result : this.base;
if (effect.statBase){
if (this.statBaseValue === undefined || result > this.statBaseValue){
this.statBaseValue = result;
if (Number.isFinite(result)){
if(Number.isFinite(this.base)){
this.base = Math.max(this.base, result);
} else {
this.base = result;
}
}
break;

View File

@@ -1,5 +1,5 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import { union } from 'lodash';
@@ -14,7 +14,15 @@ export default function combineStat(stat, aggregator, memo){
}
function getAggregatorResult(stat, aggregator){
let result = (aggregator.base + aggregator.add) * aggregator.mul;
let base;
if (!Number.isFinite(aggregator.base)){
base = stat.baseValue || 0;
} else if (!Number.isFinite(stat.baseValue)){
base = aggregator.base || 0;
} else {
base = Math.max(aggregator.base, stat.baseValue);
}
let result = (base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) {
result = aggregator.min;
}
@@ -32,8 +40,6 @@ function getAggregatorResult(stat, aggregator){
function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if (stat.attributeType === 'spellSlot'){
let {
result,
@@ -78,9 +84,7 @@ function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
}
computeStat(ability, memo);
stat.abilityMod = ability.modifier;
stat.dependencies = union(
stat.dependencies,
@@ -91,10 +95,10 @@ function combineSkill(stat, aggregator, memo){
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;
stat.proficiency = 0;
for (let i in stat.computationDetails.proficiencies){
let prof = stat.computationDetails.proficiencies[i];
applyToggles(prof, memo);
computeProficiency(prof, memo);
if (
!prof.deactivatedByToggle &&
prof.value > stat.proficiency
@@ -111,6 +115,14 @@ function combineSkill(stat, aggregator, memo){
let profBonusStat = memo.statsByVariableName['proficiencyBonus'];
let profBonus = profBonusStat && profBonusStat.value;
if (profBonusStat){
stat.dependencies = union(
stat.dependencies,
[profBonusStat._id],
profBonusStat.dependencies,
);
}
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let levelProp = memo.statsByVariableName['level'];
let level = levelProp.value;
@@ -121,20 +133,19 @@ function combineSkill(stat, aggregator, memo){
if (levelProp.dependencies){
stat.dependencies = union(stat.dependencies, levelProp.dependencies);
}
} else {
stat.dependencies = union(
stat.dependencies,
[profBonusStat._id],
profBonusStat.dependencies,
);
}
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;
// Base value
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if(stat.proficiency === 0.49){
// Round down proficiency bonus in the special case
profBonus = Math.floor(profBonus * 0.5);
} else {
profBonus = Math.ceil(profBonus * stat.proficiency);
}
// Combine everything to get the final result
let result = (aggregator.base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
let base = aggregator.base || 0;
let result = (base + stat.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
if (aggregator.set !== undefined) {

View File

@@ -0,0 +1,23 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeEffect(proficiency, memo){
if (proficiency.computationDetails.computed) return;
if (proficiency.computationDetails.busyComputing){
// Trying to compute this proficiency again while it is already computing.
// We must be in a dependency loop.
proficiency.computationDetails.computed = true;
proficiency.result = NaN;
proficiency.computationDetails.busyComputing = false;
proficiency.computationDetails.error = 'dependencyLoop';
if (Meteor.isClient) console.warn('dependencyLoop', proficiency);
return;
}
// Before doing any work, mark this proficiency as busy
proficiency.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(proficiency, memo);
proficiency.computationDetails.computed = true;
proficiency.computationDetails.busyComputing = false;
}

View File

@@ -1,8 +1,9 @@
import combineStat from '/imports/api/creature/computation/engine/combineStat.js';
import computeEffect from '/imports/api/creature/computation/engine/computeEffect.js';
import EffectAggregator from '/imports/api/creature/computation/engine/EffectAggregator.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { each, union } from 'lodash';
import { each, union, without } from 'lodash';
export default function computeStat(stat, memo){
// If the stat is already computed, skip it
@@ -19,29 +20,142 @@ export default function computeStat(stat, memo){
}
// Before doing any work, mark this stat as busy
stat.computationDetails.busyComputing = true;
// Apply any toggles
applyToggles(stat, memo);
let effects = stat.computationDetails.effects || [];
let proficiencies = stat.computationDetails.proficiencies || [];
// Get references to all the stats that share the variable name
let sameNameStats
if (stat.computationDetails.idsOfSameName){
sameNameStats = stat.computationDetails.idsOfSameName.map(
id => memo.propsById[id]
);
} else {
sameNameStats = [];
}
let allStats = [stat, ...sameNameStats];
// Decide which stat is the last active stat
// The last active stat is considered the cannonical stat
let lastActiveStat;
allStats.forEach(candidateStat => {
applyToggles(candidateStat, memo);
if (!candidateStat.inactive) lastActiveStat = candidateStat;
candidateStat.overridden = undefined;
});
if (!lastActiveStat){
delete memo.statsByVariableName[stat.variableName];
return;
}
// Make sure the active stat has all the effects and proficiencies
lastActiveStat.computationDetails.effects = effects;
lastActiveStat.computationDetails.proficiencies = proficiencies;
// Update the memo's stat with the chosen stat
memo.statsByVariableName[stat.variableName] = lastActiveStat;
// Recreate list of the non-cannonical stats
sameNameStats = without(allStats, lastActiveStat);
sameNameStats.forEach(statInstance => {
// Mark the non-cannonical stats as overridden
statInstance.overridden = true;
// Apply the cannonical damage
statInstance.damage = lastActiveStat.damage;
});
let baseDependencies = [];
allStats.forEach(statInstance => {
// Add this stat and its deps to the dependencies
baseDependencies = union(
baseDependencies,
[statInstance._id],
statInstance.dependencies,
);
// Apply all the base proficiencies
if (statInstance.baseProficiency && !statInstance.inactive){
proficiencies.push({
value: statInstance.baseProficiency,
stats: [statInstance.variableName],
type: 'proficiency',
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
}
});
}
// Compute each active stat's baseValue calculation and apply it
if (!statInstance.inactive) {
let {
result,
context,
dependencies
} = evaluateCalculation({
string: statInstance.baseValueCalculation,
prop: statInstance,
memo
});
result.value = +result.value;
if (!isNaN(result.value)){
statInstance.baseValue = result.value;
} else {
statInstance.baseValue = undefined;
}
statInstance.dependencies = union(statInstance.dependencies, dependencies);
if (context.errors.length){
statInstance.baseValueErrors = context.errors;
}
// Apply all the base values
if (Number.isFinite(statInstance.baseValue)){
effects.push({
operation: 'base',
calculation: statInstance.baseValueCalculation,
result: statInstance.baseValue,
stats: [statInstance.variableName],
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
},
});
}
}
});
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
let aggregator = new EffectAggregator();
let effectDeps = [];
each(effects, (effect) => {
// Compute
computeEffect(effect, memo);
if (effect.deactivatedByToggle) return;
if (effect._id){
stat.dependencies = union(
stat.dependencies,
[effect._id]
);
}
stat.dependencies = union(
stat.dependencies,
effect.dependencies
)
// dependencies
if (effect._id) effectDeps = union(effectDeps, [effect._id]);
effectDeps = union(effectDeps, effect.dependencies);
// Add computed effect to aggregator
aggregator.addEffect(effect);
});
// Conglomerate all the effects to compute the final stat values
combineStat(stat, aggregator, memo);
// Mark the attribute as computed
stat.computationDetails.computed = true;
stat.computationDetails.busyComputing = false;
// Combine the effects into the stats
allStats.forEach(statInstance => {
// Conglomerate all the effects to compute the final stat values
combineStat(statInstance, aggregator, memo);
// Mark the stats as computed
statInstance.computationDetails.computed = true;
statInstance.computationDetails.busyComputing = false;
// Only the active stat instance depeneds on the effects
if (!statInstance.overridden){
statInstance.dependencies = union(statInstance.dependencies, effectDeps);
}
});
}

View File

@@ -12,29 +12,22 @@ export default function writeAlteredProperties(memo){
console.warn('No schema for ' + changed.type);
return;
}
let extraIds = changed.computationDetails.idsOfSameName;
let ids;
if (extraIds && extraIds.length){
ids = [changed._id, ...extraIds];
} else {
ids = [changed._id];
let id = changed._id;
let op = undefined;
let original = memo.originalPropsById[id];
let keys = [
'dependencies',
'inactive',
'deactivatedBySelf',
'deactivatedByAncestor',
'deactivatedByToggle',
'damage',
...schema.objectKeys(),
];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);
}
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
let keys = [
'dependencies',
'inactive',
'deactivatedBySelf',
'deactivatedByAncestor',
'deactivatedByToggle',
...schema.objectKeys(),
];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);
}
});
});
writePropertiesSequentially(bulkWriteOperations);
}

View File

@@ -1,5 +1,5 @@
import { pick, forOwn } from 'lodash';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {

View File

@@ -1,7 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js';
import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js';
import computeMemo from '/imports/api/creature/computation/engine/computeMemo.js';
@@ -11,7 +11,7 @@ import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalis
import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
export const recomputeCreature = new ValidatedMethod({

View File

@@ -0,0 +1,37 @@
import SimpleSchema from 'simpl-schema';
let CreatureFolders = new Mongo.Collection('creatureFolders');
let creatureFolderSchema = new SimpleSchema({
name: {
type: String,
trim: false,
optional: true,
},
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,
},
order: {
type: Number,
defaultValue: 0,
},
});
CreatureFolders.attachSchema(creatureFolderSchema);
import '/imports/api/creature/creatureFolders/methods.js/index.js';
export default CreatureFolders;

View File

@@ -0,0 +1,4 @@
import '/imports/api/creature/creatureFolders/methods.js/insertCreatureFolder.js';
import '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js';
import '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js';
import '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js';

View File

@@ -0,0 +1,46 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const insertCreatureFolder = new ValidatedMethod({
name: 'creatureFolders.methods.insert',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.insert.denied',
'You need to be logged in to insert a folder');
}
// Limit folders to 50 per user
let existingFolders = CreatureFolders.find({
owner: userId
}, {
fields: {order: 1},
sort: {order :-1}
});
if (existingFolders.count() >= 50){
throw new Meteor.Error('creatureFolders.methods.insert.denied',
'You can not have more than 50 folders');
}
// Make the new folder the last in the order
let order = 0;
let lastFolder = existingFolders.fetch()[0];
if (lastFolder){
order = (lastFolder.order || 0) + 1;
}
// Insert
return CreatureFolders.insert({
name: 'Folder',
owner: userId,
order,
});
},
});
export default insertCreatureFolder;

View File

@@ -0,0 +1,45 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const moveCreatureToFolder = new ValidatedMethod({
name: 'creatureFolders.methods.moveCreatureToFolder',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, folderId}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'You need to be logged in to remove a folder');
}
// Check that this folder is owned by the user
if (folderId){
let existingFolder = CreatureFolders.findOne(folderId);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'This folder does not belong to you');
}
}
// Remove from other folders
CreatureFolders.update({
owner: userId
}, {
$pull: {creatures: creatureId},
}, {
multi: true,
});
if (folderId){
// Add to this folder
CreatureFolders.update(folderId, {
$addToSet: {creatures: creatureId},
});
}
},
});
export default moveCreatureToFolder;

View File

@@ -0,0 +1,31 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const removeCreatureFolder = new ValidatedMethod({
name: 'creatureFolders.methods.remove',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'You need to be logged in to remove a folder');
}
// Check that this folder is owned by the user
let existingFolder = CreatureFolders.findOne(_id);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'This folder does not belong to you');
}
// Remove
return CreatureFolders.remove(_id);
},
});
export default removeCreatureFolder;

View File

@@ -0,0 +1,43 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const reorderCreatureFolder = new ValidatedMethod({
name: 'creatureFolders.methods.reorder',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, order}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.reorder.denied',
'You need to be logged in to reorder a folder');
}
// Check that this folder is owned by the user
let existingFolder = CreatureFolders.findOne(_id);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.reorder.denied',
'This folder does not belong to you');
}
// First give it the new order, it should end in 0.5 putting it between two other docs
CreatureFolders.update(_id, {$set: {order}});
this.unblock();
// Reorder all the folders with integer numbers in this new order
CreatureFolders.find({
owner: userId
}, {
fields: {order: 1,},
sort: {order: -1}
}).forEach((folder, index) => {
if (folder.order !== index){
CreatureFolders.update(_id, {$set: {order: index}})
}
});
},
});
export default reorderCreatureFolder;

View File

@@ -0,0 +1,31 @@
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
const updateCreatureFolderName = new ValidatedMethod({
name: 'creatureFolders.methods.updateName',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, name}) {
// Ensure logged in
let userId = this.userId;
if (!userId) {
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'You need to be logged in to update a folder');
}
// Check that this folder is owned by the user
let existingFolder = CreatureFolders.findOne(_id);
if (existingFolder.owner !== userId){
throw new Meteor.Error('creatureFolders.methods.updateName.denied',
'This folder does not belong to you');
}
// Update
return CreatureFolders.update(_id, {$set: {name}});
},
});
export default updateCreatureFolderName;

View File

@@ -1,4 +1,4 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function getRootCreatureAncestor(property){
return Creatures.findOne(property.ancestors[0].id);

View File

@@ -2,7 +2,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';

View File

@@ -2,7 +2,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';

View File

@@ -7,23 +7,8 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
if (prop){
return {id: prop._id, collection: 'creatureProperties'};
} else {
return {id: creatureId, collection: 'creatures'};
}
}
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
@@ -50,8 +35,9 @@ const equipItem = new ValidatedMethod({
}, {
selector: {type: 'item'},
});
let tag = equipped ? INVENTORY_TAGS.equipment : INVENTORY_TAGS.carried;
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'};
organizeDoc.call({
docRef: {

View File

@@ -0,0 +1,13 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
return prop && {id: prop._id, collection: 'creatureProperties'};
}

View File

@@ -2,16 +2,26 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { getAncestry } from '/imports/api/parenting/parenting.js';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { getHighestOrder } from '/imports/api/parenting/order.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
validate: null,
validate: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
},
parentRef: RefSchema,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
@@ -42,6 +52,86 @@ const insertProperty = new ValidatedMethod({
},
});
const insertPropertyAsChildOfTag = new ValidatedMethod({
name: 'creatureProperties.insertAsChildOfTag',
validate: new SimpleSchema({
creatureProperty: {
type: Object,
blackbox: true,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
tag: {
type: String,
max: 20,
},
tagDefaultName: {
type: String,
max: 20,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty, creatureId, tag, tagDefaultName}) {
let parentRef = getParentRefByTag(creatureId, tag);
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'};
}
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
// Add the folder first if we need to
if (insertFolderFirst){
let order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
}) + 1;
let id = CreatureProperties.insert({
type: 'folder',
name: tagDefaultName || (tag.charAt(0).toUpperCase() + tag.slice(1)),
tags: [tag],
parent: parentRef,
ancestors: [parentRef],
order,
});
// Make the folder our new parent
let newParentRef = {id, collection: 'creatureProperties'};
ancestors = [parentRef, newParentRef];
parentRef = newParentRef;
creatureProperty.order = order + 1;
}
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
},
});
export function insertPropertyWork({property, creature}){
delete property._id;
let _id = CreatureProperties.insert(property);
@@ -63,3 +153,4 @@ export function insertPropertyWork({property, creature}){
}
export default insertProperty;
export { insertPropertyAsChildOfTag };

View File

@@ -16,6 +16,7 @@ import {
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
@@ -54,6 +55,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
// 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},
@@ -65,6 +67,9 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
removed: {$ne: true},
}).fetch();
// Convert all references into actual nodes
nodes = reifyNodeReferences(nodes);
// The root node is first in the array of nodes
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
@@ -115,4 +120,98 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
},
});
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
depth += 1;
// New nodes added this function
let newNodes = [];
// Filter out the reference nodes we replace
let resultingNodes = nodes.filter(node => {
// We have already visited this ref and replaced it
if (visitedRefs.has(node._id)) return false;
// Already replaced an ancestor node
for (let i; i < node.ancestors.length; i++){
if (visitedRefs.has(node.ancestors[i].id)) return false;
}
// This isn't a reference node, continue as normal
if (node.type !== 'reference') return true;
// We have gone too deep, keep the reference node as an error
if (depth > 10){
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
node.cache = {error: 'Reference depth limit exceeded'};
return true;
}
let referencedNode
try {
referencedNode = fetchDocByRef(node.ref);
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()};
return true;
}
// Get all the descendants of the referenced node
let descendents = LibraryNodes.find({
'ancestors.id': referencedNode._id,
removed: {$ne: true},
}, {
sort: {order: 1},
}).fetch();
// We are adding the referenced node and its descendants
let addedNodes = [referencedNode, ...descendents];
// 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,
});
// Remove all the looped references and descendents from the new nodes
// We can't rely on the reify recursion to do this, since the IDs are
// getting renewed before it is called
addedNodes = addedNodes.filter(node => {
// Exclude removed referenced
if (visitedRefs.has(node._id)) return false;
// Exclude descendants of removed references
for (let i; i < node.ancestors.length; i++){
if (visitedRefs.has(node.ancestors[i].id)) return false;
}
return true;
});
// TODO: Force the referencedNode to take the old id of the reference
// such that the reference's children can be kept
// Give the new referenced sub-tree new ids
renewDocIds({
docArray: addedNodes,
});
// Reify the subtree as well with recursion
addedNodes = reifyNodeReferences(addedNodes, visitedRefs, depth);
// Store the new nodes from this inner loop without altering the array
// we are looping over
newNodes.push(...addedNodes);
});
// We are done filtering the array, we can add the new nodes to it
resultingNodes.push(...newNodes);
return resultingNodes;
}
export default insertPropertyFromLibraryNode;

View File

@@ -1,15 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import '/imports/api/creature/removeCreature.js';
import '/imports/api/creature/restCreature.js';
//set up the collection for creatures
let Creatures = new Mongo.Collection('creatures');
@@ -175,87 +167,8 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
assertUserHasPaidBenefits(this.userId);
// Create the creature document
let creatureId = Creatures.insert({
owner: this.userId,
});
CreatureProperties.insert({
slotTags: ['base'],
quantityExpected: 1,
type: 'propertySlot',
name: 'Base',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
hideWhenFull: true,
parent: {collection: 'creatures', id: creatureId},
ancestors: [{collection: 'creatures', id: creatureId}],
order: 0,
tags: [],
spaceLeft: 1,
totalFilled: 0,
});
this.unblock();
return creatureId;
},
});
const updateCreature = new ValidatedMethod({
name: 'creatures.update',
validate({_id, path}){
if (!_id) return false;
// Allowed fields
let allowedFields = [
'name',
'alignment',
'gender',
'picture',
'avatarPicture',
'color',
'settings',
];
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);
assertEditPermission(creature, this.userId);
if (value === undefined || value === null){
Creatures.update(_id, {
$unset: {[path.join('.')]: 1},
});
} else {
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
}
},
});
import '/imports/api/creature/creatures/methods/index.js';
export default Creatures;
export { CreatureSchema, insertCreature, updateCreature };
export { CreatureSchema };

View File

@@ -1,4 +1,4 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {
assertEditPermission as editPermission,
assertViewPermission as viewPermission,

View File

@@ -0,0 +1,47 @@
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
export default function defaultCharacterProperties(creatureId){
if (!creatureId) throw 'creatureId is required';
const creatureRef = {collection: 'creatures', id: creatureId};
let randomSrc = DDP.randomStream('defaultProperties');
const inventoryId = randomSrc.id();
const inventoryRef = {collection: 'creatureProperties', id: inventoryId};
return [
{
type: 'propertySlot',
name: 'Ruleset',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
slotTags: ['base'],
tags: [],
quantityExpected: 1,
hideWhenFull: true,
spaceLeft: 1,
totalFilled: 0,
order: 0,
parent: creatureRef,
ancestors: [creatureRef],
}, {
_id: inventoryId,
type: 'folder',
name: 'Inventory',
tags: [BUILT_IN_TAGS.inventory],
order: 1,
parent: creatureRef,
ancestors: [creatureRef],
}, {
type: 'folder',
name: 'Equipment',
tags: [BUILT_IN_TAGS.equipment],
order: 2,
parent: inventoryRef,
ancestors: [creatureRef, inventoryRef],
}, {
type: 'folder',
name: 'Carried',
tags: [BUILT_IN_TAGS.carried],
order: 3,
parent: inventoryRef,
ancestors: [creatureRef, inventoryRef],
},
];
}

View File

@@ -0,0 +1,5 @@
import '/imports/api/creature/creatures/methods/insertCreature.js';
import '/imports/api/creature/creatures/methods/removeCreature.js';
import '/imports/api/creature/creatures/methods/restCreature.js';
import '/imports/api/creature/creatures/methods/transferCreatureOwnership.js';
import '/imports/api/creature/creatures/methods/updateCreature.js';

View File

@@ -0,0 +1,70 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
let currentCharacterCount = Creatures.find({
owner: this.userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.insert.denied',
`You are already at your limit of ${tier.characterSlots} characters`)
}
// Create the creature document
let creatureId = Creatures.insert({
owner: this.userId,
});
// Insert the default properties
// Not batchInsert because we want the properties cleaned by the schema
let baseId;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){
baseId = id;
}
});
if (Meteor.isServer){
// Insert the 5e ruleset as the default base
insertPropertyFromLibraryNode.call({
nodeId: 'iHbhfcg3AL5isSWbw',
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
}
return creatureId;
},
});
export default insertCreature;

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';

View File

@@ -1,9 +1,9 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const restCreature = new ValidatedMethod({

View File

@@ -0,0 +1,55 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
const transferCreatureOwnership = new ValidatedMethod({
name: 'creatures.methods.transferOwnership',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
userId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({creatureId, userId}) {
assertOwnership(creatureId, this.userId);
let tier = getUserTier(userId);
let currentCharacterCount = Creatures.find({
owner: userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.transferOwnership.denied',
'The new owner is already at their character limit')
}
Creatures.update(creatureId, {
$set: {owner: userId},
});
return creatureId;
},
});
export default transferCreatureOwnership;

View File

@@ -0,0 +1,45 @@
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';
const updateCreature = new ValidatedMethod({
name: 'creatures.update',
validate({_id, path}){
if (!_id) return false;
// Allowed fields
let allowedFields = [
'name',
'alignment',
'gender',
'picture',
'avatarPicture',
'color',
'settings',
];
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);
assertEditPermission(creature, this.userId);
if (value === undefined || value === null){
Creatures.update(_id, {
$unset: {[path.join('.')]: 1},
});
} else {
Creatures.update(_id, {
$set: {[path.join('.')]: value},
});
}
},
});
export default updateCreature;

View File

@@ -1,8 +1,8 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export const recomputeDamageMultipliers = new ValidatedMethod({

View File

@@ -1,5 +1,5 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
export default function recomputeInventory(creatureId){

View File

@@ -2,8 +2,8 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
let Experiences = new Mongo.Collection('experiences');

View File

@@ -1,9 +1,9 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
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/creaturePermissions.js';
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
import {
parse,
CompilationContext,

View File

@@ -2,7 +2,7 @@ import {
assertEditPermission,
assertViewPermission,
assertOwnership,
} from '/imports/api/creature/creaturePermissions.js';
} from '/imports/api/creature/creatures/creaturePermissions.js';
// Checks if the method has permission to run on the document. If the document
// has a charId, that creature is checked, otherwise if it has an _id and the

View File

@@ -12,6 +12,7 @@ import { softRemove } from '/imports/api/parenting/softRemove.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js';
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -76,7 +77,12 @@ const insertNode = new ValidatedMethod({
run(libraryNode) {
delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId);
return LibraryNodes.insert(libraryNode);
let nodeId = LibraryNodes.insert(libraryNode);
if (libraryNode.type == 'reference'){
libraryNode._id = nodeId;
updateReferenceNodeWork(libraryNode, this.userId);
}
return nodeId;
},
});
@@ -109,9 +115,14 @@ const updateLibraryNode = new ValidatedMethod({
} else {
modifier = {$set: {[pathString]: value}};
}
return LibraryNodes.update(_id, modifier, {
let numUpdated = LibraryNodes.update(_id, modifier, {
selector: {type: node.type},
});
if (node.type == 'reference'){
node = LibraryNodes.findOne(_id);
updateReferenceNodeWork(node, this.userId);
}
return numUpdated;
},
});

View File

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

View File

@@ -0,0 +1,67 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import {
assertDocEditPermission,
assertViewPermission,
} from '/imports/api/sharing/sharingPermissions.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const updateReferenceNode = new ValidatedMethod({
name: 'libraryNodes.updateReferenceNode',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let userId = this.userId;
let node = LibraryNodes.findOne(_id);
assertDocEditPermission(node, userId);
updateReferenceNodeWork(node, userId);
},
});
function writeCache(_id, cache){
LibraryNodes.update(_id, {$set: {cache}}, {
selector: {type: 'reference'},
});
}
function updateReferenceNodeWork(node, userId){
let cache = {}
if (!node.ref){
writeCache(node._id, cache);
return;
}
let doc, library;
try {
doc = fetchDocByRef(node.ref);
if (doc.removed) throw 'Property has been deleted';
if (doc.ancestors[0].id !== node.ancestors[0].id){
library = fetchDocByRef(doc.ancestors[0]);
assertViewPermission(library, userId)
}
} catch(e){
cache = {error: e.reason || e.message || e.toString()}
writeCache(node._id, cache);
return;
}
cache = {
node: {name: doc.name, type: doc.type},
};
if (library){
cache.library = {name: library.name};
}
writeCache(node._id, cache);
}
export default updateReferenceNode;
export { updateReferenceNodeWork }

View File

@@ -4,6 +4,11 @@ const RefSchema = new SimpleSchema({
id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
// TODO: Rather than indexing this field, index `ancestors.0.id` to only
// index the root of the ancestor heirarchy to significantly reduce
// index size and improve performance
// All queries on an ancestor document need to target `ancestors.0.id` first
// before targeting a younger ancestor
index: 1
},
collection: {

View File

@@ -133,6 +133,11 @@ let ComputedOnlyAttributeSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Denormalised tag if stat is overridden by one with the same variable name
overridden: {
type: Boolean,
optional: true,
},
});
const ComputedAttributeSchema = new SimpleSchema()

View File

@@ -14,9 +14,10 @@ let ProficiencySchema = new SimpleSchema({
type: String,
},
// 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.5, 1, 2],
allowedValues: [0.49, 0.5, 1, 2],
defaultValue: 1,
},
});

View File

@@ -0,0 +1,47 @@
import SimpleSchema from 'simpl-schema';
let ReferenceSchema = new SimpleSchema({
ref: {
type: Object,
defaultValue: {},
},
'ref.id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
'ref.collection': {
type: String,
optional: true,
},
// Denormalised store of referenced property's details
cache: {
type: Object,
defaultValue: {},
},
'cache.error': {
type: String,
optional: true,
},
'cache.node': {
type: Object,
optional: true,
},
'cache.node.name': {
type: String,
optional: true,
},
'cache.node.type': {
type: String,
},
'cache.library': {
type: Object,
optional: true,
},
'cache.library.name': {
type: String,
optional: true,
},
});
export { ReferenceSchema };

View File

@@ -121,6 +121,11 @@ let ComputedOnlySkillSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Denormalised tag if stat is overridden by one with the same variable name
overridden: {
type: Boolean,
optional: true,
},
})
const ComputedSkillSchema = new SimpleSchema()

View File

@@ -15,6 +15,7 @@ import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
@@ -42,6 +43,7 @@ const propertySchemasIndex = {
note: ComputedOnlyNoteSchema,
proficiency: ProficiencySchema,
propertySlot: ComputedOnlySlotSchema,
reference: ReferenceSchema,
roll: ComputedOnlyRollSchema,
savingThrow: ComputedOnlySavingThrowSchema,
skill: ComputedOnlySkillSchema,

View File

@@ -15,6 +15,7 @@ import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
@@ -40,6 +41,7 @@ const propertySchemasIndex = {
note: ComputedNoteSchema,
proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema,
reference: ReferenceSchema,
roll: ComputedRollSchema,
savingThrow: ComputedSavingThrowSchema,
skill: ComputedSkillSchema,

View File

@@ -13,6 +13,7 @@ import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js';
import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { SkillSchema } from '/imports/api/properties/Skills.js';
@@ -40,6 +41,7 @@ const propertySchemasIndex = {
note: NoteSchema,
proficiency: ProficiencySchema,
propertySlot: SlotSchema,
reference: ReferenceSchema,
roll: RollSchema,
savingThrow: SavingThrowSchema,
skill: SkillSchema,

View File

@@ -1,6 +1,5 @@
import { _ } from 'meteor/underscore';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
@@ -12,7 +11,7 @@ function assertIdValid(userId){
function assertdocExists(doc){
if (!doc){
throw new Meteor.Error('Permission denied',
'No such document exists');
'Permission denied: No such document exists');
}
}
@@ -48,13 +47,6 @@ export function assertEditPermission(doc, userId) {
return true;
}
// Ensure the user is of a tier with paid benefits
let tier = getUserTier(user);
if (!tier.paidBenefits){
throw new Meteor.Error('Edit permission denied',
`The ${tier.name} tier does not allow you to edit this document`);
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||

View File

@@ -1,7 +1,7 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import Tabletops, { assertUserInTabletop } from '/imports/api/tabletop/Tabletops.js';
let Messages = new Mongo.Collection('messages');

View File

@@ -2,7 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
let Tabletops = new Mongo.Collection('tabletops');

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
const userSchema = new SimpleSchema({
username: {
@@ -63,7 +64,7 @@ const userSchema = new SimpleSchema({
},
subscribedLibraries: {
type: Array,
defaultValue: [],
defaultValue: defaultLibraries,
max: 100,
},
'subscribedLibraries.$': {

View File

@@ -1,8 +1,8 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js';
import Creatures from '/imports/api/creature/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/removeCreature.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/creatures/methods/removeCreature.js';
Meteor.users.deleteMyAccount = new ValidatedMethod({
name: 'users.deleteMyAccount',

View File

@@ -1,58 +1,78 @@
import { findLast } from 'lodash';
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
import Invites from '/imports/api/users/Invites.js';
const patreonDisabled = !!process.env.DISABLE_PATREON;
const TIERS = [
const TIERS = Object.freeze([
{
name: 'Commoner',
minimumEntitledCents: 0,
invites: 0,
characterSlots: 0, //5,
paidBenefits: false,
}, {
name: 'Dreamer',
minimumEntitledCents: 100,
invites: 0,
characterSlots: 0, //5,
paidBenefits: false,
}, {
name: 'Wanderer',
minimumEntitledCents: 300,
invites: 0,
characterSlots: 0, //5,
paidBenefits: false,
}, {
//cost per user $5
name: 'Adventurer',
minimumEntitledCents: 500,
invites: 0,
characterSlots: -1, //20,
paidBenefits: true,
}, {
//cost per user $3.33
name: 'Hero',
minimumEntitledCents: 1000,
invites: 2,
characterSlots: -1, //50,
paidBenefits: true,
}, {
//cost per user $3.333
name: 'Legend',
minimumEntitledCents: 2000,
invites: 5,
characterSlots: -1, //120,
paidBenefits: true,
}, {
//cost per user $3.125
name: 'Paragon',
minimumEntitledCents: 5000,
invites: 15,
characterSlots: -1, // Unlimited characters
paidBenefits: true,
},
];
]);
const GUEST_TIER = {
// Companion tier should be equivalent to the Adventurer tier
const GUEST_TIER = Object.freeze({
name: 'Companion',
guest: true,
invites: 0,
characterSlots: 20,
paidBenefits: true,
}
});
// When patreon features are disabled, give all the users the same tier
// with no limitations
const PATREON_DISABLED_TIER = Object.freeze({
name: 'Outlander',
invites: 0,
characterSlots: -1, // Can have infinitely many characters
paidBenefits: true,
});
export function getTierByEntitledCents(entitledCents = 0){
if (patreonDisabled) return PATREON_DISABLED_TIER;
return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents);
}
@@ -66,6 +86,7 @@ export function getUserTier(user){
});
if (!user) throw 'User not found';
}
if (patreonDisabled) return PATREON_DISABLED_TIER;
const entitledCents = getEntitledCents(user);
const tier = getTierByEntitledCents(entitledCents);
if (tier.paidBenefits) return tier;

View File

@@ -1,7 +1,7 @@
const INVENTORY_TAGS = Object.freeze({
const BUILT_IN_TAGS = Object.freeze({
inventory: 'inventory',
equipment: 'equipment',
carried: 'carried',
});
export default INVENTORY_TAGS;
export default BUILT_IN_TAGS;

View File

@@ -24,11 +24,11 @@ const PROPERTIES = Object.freeze({
name: 'Class level'
},
constant: {
icon: 'anchor',
icon: 'mdi-anchor',
name: 'Constant'
},
container: {
icon: 'work',
icon: 'mdi-bag-personal-outline',
name: 'Container'
},
damage: {
@@ -44,29 +44,34 @@ const PROPERTIES = Object.freeze({
name: 'Effect'
},
feature: {
icon: 'subject',
icon: 'mdi-text-subject',
name: 'Feature'
},
folder: {
icon: 'folder',
icon: 'mdi-folder-outline',
name: 'Folder'
},
item: {
icon: '$vuetify.icons.item',
icon: 'mdi-cube-outline',
name: 'Item'
},
note: {
icon: 'note',
icon: 'mdi-note-outline',
name: 'Note'
},
proficiency: {
icon: 'radio_button_checked',
icon: 'mdi-brightness-1',
name: 'Proficiency'
},
roll: {
icon: '$vuetify.icons.roll',
name: 'Roll'
},
reference: {
icon: 'mdi-vector-link',
name: 'Reference',
libraryOnly: true,
},
savingThrow: {
icon: '$vuetify.icons.saving_throw',
name: 'Saving throw'
@@ -76,11 +81,11 @@ const PROPERTIES = Object.freeze({
name: 'Skill'
},
propertySlot: {
icon: 'tab_unselected',
icon: 'mdi-power-socket-eu',
name: 'Slot'
},
slotFiller: {
icon: 'picture_in_picture',
icon: 'mdi-power-plug-outline',
name: 'Slot filler'
},
spellList: {

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,6 @@ export default class CallNode extends ParseNode {
// Resolve the arguments
let resolvedArgs = this.args.map(node => node[fn](scope, context));
// Check that the arguments match what is expected
let checkFailed = this.checkArugments({
fn,
@@ -30,7 +29,7 @@ export default class CallNode extends ParseNode {
});
if (checkFailed){
if (fn !== 'reduce'){
if (fn === 'reduce'){
return new ErrorNode({
node: this,
error: `Invalid arguments to ${this.functionName} function`,

View File

@@ -0,0 +1,19 @@
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
Meteor.publish('archivedCreatures', function(){
this.autorun(function (){
var userId = this.userId;
if (!userId) {
return [];
}
return ArchivedCreatures.find({
owner: userId,
}, {
fields: {
creature: 1,
owner: 1,
}
}
);
});
});

View File

@@ -1,5 +1,5 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Parties from '/imports/api/creature/Parties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
Meteor.publish('characterList', function(){
this.autorun(function (){
@@ -36,7 +36,7 @@ Meteor.publish('characterList', function(){
}
}
),
Parties.find({owner: userId}),
CreatureFolders.find({owner: userId}),
];
});
});

View File

@@ -1,7 +1,7 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
let schema = new SimpleSchema({
creatureId: {

View File

@@ -6,5 +6,6 @@ import '/imports/server/publications/experiences.js';
import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'
import '/imports/server/publications/ownedDocuments.js'
import '/imports/server/publications/slotFillers.js';
import '/imports/server/publications/ownedDocuments.js';
import '/imports/server/publications/archivedCreatures.js';

View File

@@ -1,4 +1,4 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
Meteor.publish('ownedDocuments', function(){

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import VERSION from '/imports/constants/VERSION.js';

View File

@@ -1,5 +1,5 @@
import Tabletops from '/imports/api/tabletop/Tabletops.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import Messages from '/imports/api/tabletop/Messages.js';
Meteor.publish('tabletops', function(){

View File

@@ -10,7 +10,7 @@
icon
v-on="on"
>
<v-icon>format_paint</v-icon>
<v-icon>mdi-format-paint</v-icon>
</v-btn>
</template>
<v-card class="overflow-hidden">
@@ -30,7 +30,7 @@
v-if="kebabColor === colorOption"
:class="{dark: isDark(colorOption, shade)}"
>
check
mdi-check
</v-icon>
</v-scroll-y-transition>
</div>
@@ -58,7 +58,7 @@
v-if="kebabShade === shadeOption"
:class="isDark(color, shade) ? 'dark' : 'light'"
>
check
mdi-check
</v-icon>
</v-scroll-y-transition>
</div>

View File

@@ -14,7 +14,7 @@
@click.stop
>
<slot>
<v-icon>add</v-icon>
<v-icon>mdi-plus</v-icon>
</slot>
</v-btn>
</template>

View File

@@ -15,14 +15,14 @@
class="filled"
@click="toggleAdd(); $nextTick(() => $refs.editInput.focus())"
>
<v-icon>add</v-icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn
:disabled="context.editPermission === false"
class="filled"
@click="toggleSubtract(); $nextTick(() => $refs.editInput.focus())"
>
<v-icon>remove</v-icon>
<v-icon>mdi-minus</v-icon>
</v-btn>
</v-btn-toggle>
<v-text-field
@@ -48,7 +48,7 @@
class="mx-2 filled"
@click="commitEdit"
>
<v-icon>done</v-icon>
<v-icon>mdi-check</v-icon>
</v-btn>
<v-btn
:small="!flat"
@@ -58,7 +58,7 @@
class="filled"
@click="cancelEdit"
>
<v-icon>close</v-icon>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-spacer />
</v-layout>
@@ -117,11 +117,11 @@
operationIcon(operation) {
switch (operation) {
case 'set':
return 'forward';
return 'mdi-forward';
case 'add':
return 'add';
return 'mdi-plus';
case 'subtract':
return 'remove';
return 'mdi-minus';
}
},
toggleAdd(){

View File

@@ -0,0 +1,47 @@
<template lang="html">
<v-tooltip
v-if="accessRights === 'reader' || accessRights === 'writer'"
bottom
>
<template #activator="{ on }">
<v-icon
style="opacity: 0.4"
v-on="on"
>
{{ accessRights === 'reader' ? 'mdi-file-eye' : 'mdi-file-edit' }}
</v-icon>
</template>
<span>{{ accessText }}</span>
</v-tooltip>
</template>
<script lang="js">
export default {
props:{
model: {
type: Object,
required: true,
},
},
meteor:{
accessRights(){
let userId = Meteor.userId();
if (this.model.owner === userId) return 'owner'
else if (this.model.writers.includes(userId)) return 'writer';
else if (this.model.readers.includes(userId)) return 'reader';
else if (this.model.public) return 'public';
else return 'denied'
},
accessText(){
switch (this.accessRights){
case 'writer': return 'Shared with edit permission';
case 'reader': return 'Shared as view-only';
case 'public': return 'Shared as publicly viewable';
}
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -11,12 +11,12 @@
<v-text-field
:value="formattedSafeValue"
v-bind="$attrs"
prepend-icon="event"
prepend-icon="mdi-calendar"
readonly
:loading="loading"
:error-messages="errors"
:disabled="isDisabled"
filled
outlined
v-on="on"
@focus="focused = true"
@blur="focused = false"

View File

@@ -24,29 +24,29 @@
v-else
large
>
highlight_alt
mdi-select-search
</v-icon>
</v-btn>
</div>
</template>
<v-card>
<v-card-text>
<div class="layout">
<div class="layout row align-center">
<text-field
ref="iconSearchField"
label="Search icons"
append-icon="search"
append-icon="mdi-search"
clearable
hide-details
class="ma-2"
:value="searchString"
@change="search"
/>
<v-btn
icon
text
@click="select()"
>
<v-icon>
cancel
</v-icon>
clear
</v-btn>
</div>
<v-layout

View File

@@ -7,7 +7,7 @@
:menu-props="{auto: true, lazy: true}"
:search-input.sync="searchInput"
:disabled="isDisabled"
filled
outlined
@change="customChange"
@focus="focused = true"
@blur="focused = false"

View File

@@ -6,7 +6,7 @@
:value="safeValue"
:menu-props="{auto: true, lazy: true}"
:disabled="isDisabled"
filled
outlined
@change="change"
@focus="focused = true"
@blur="focused = false"

View File

@@ -6,7 +6,7 @@
:value="safeValue"
:disabled="isDisabled"
:auto-grow="autoGrow"
filled
outlined
@input="input"
@focus="focused = true"
@blur="focused = false"

View File

@@ -6,7 +6,7 @@
:error-messages="errors"
:value="safeValue"
:disabled="isDisabled"
:filled="!regular"
:outlined="!regular"
@input="input"
@focus="focused = true"
@blur="focused = false"

View File

@@ -5,6 +5,13 @@
:light="!isDark"
:flat="flat"
>
<v-btn
v-if="!embedded"
icon
@click="back"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<property-icon
:model="model"
class="mr-2"
@@ -43,7 +50,7 @@
data-id="property-toolbar-menu-button"
v-on="on"
>
<v-icon>more_vert</v-icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list>
@@ -57,7 +64,7 @@
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>file_copy</v-icon>
<v-icon>mdi-content-copy</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
@@ -70,7 +77,7 @@
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>send</v-icon>
<v-icon>mdi-send</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
@@ -83,7 +90,7 @@
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>delete</v-icon>
<v-icon>mdi-delete</v-icon>
</v-list-item-action>
</v-list-item>
</v-list>
@@ -105,13 +112,13 @@
v-if="editing"
key="doneIcon"
>
done
mdi-check
</v-icon>
<v-icon
v-else
key="createIcon"
>
create
mdi-pencil
</v-icon>
</v-slide-y-transition>
</v-btn>
@@ -137,6 +144,7 @@ export default {
},
flat: Boolean,
editing: Boolean,
embedded: Boolean,
},
computed: {
isDark(){
@@ -163,6 +171,9 @@ export default {
colorChanged(value){
this.$emit('color-changed', value);
},
back(){
this.$store.dispatch('popDialogStack');
},
}
}
</script>

View File

@@ -6,7 +6,7 @@ let lastSnackbarId = 0;
function snackbar(data) {
globalState.queue.push({
data,
data, //{text OR content, callback, callbackName} // content is logContent
id: ++lastSnackbarId,
enqueuedAt: new Date(),
shown: false,

View File

@@ -32,7 +32,7 @@
v-bind="attrs"
@click="closeSnackbar"
>
<v-icon>close</v-icon>
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>

View File

@@ -18,7 +18,7 @@
@click.stop="expanded = !expanded"
>
<v-icon v-if="canExpand && (hasChildren || organize)">
chevron_right
mdi-chevron-right
</v-icon>
</v-btn>
<div
@@ -31,7 +31,7 @@
:class="selected && 'primary--text'"
:disabled="expanded"
>
drag_handle
mdi-drag
</v-icon>
<!--{{node && node.order}}-->
<tree-node-view

View File

@@ -42,24 +42,12 @@
<v-switch
label="Show spells tab"
:input-value="!model.settings.hideSpellsTab"
@change="value => {
$emit('change', {path: ['settings','hideSpellsTab'], value: !value});
$store.commit(
'setTabForCharacterSheet',
{id: model._id, tab: 0}
);
}"
@change="changeHideSpellsTab"
/>
<v-switch
label="Show tree tab"
:input-value="model.settings.showTreeTab"
@change="value => {
$emit('change', {path: ['settings','showTreeTab'], value: !!value});
$store.commit(
'setTabForCharacterSheet',
{id: model._id, tab: 0}
);
}"
@change="changeShowTreeTab"
/>
<text-field
label="Hit Dice reset multiplier"
@@ -129,6 +117,34 @@ export default {
},
disabled: Boolean,
},
methods: {
changeShowTreeTab(value){
this.$emit('change', {
path: ['settings','showTreeTab'],
value: !!value
});
let currentTab = this.$store.getters.tabById(this.model._id);
if (!value && currentTab === 5){
this.$store.commit(
'setTabForCharacterSheet',
{id: this.model._id, tab: 4}
);
}
},
changeHideSpellsTab(value){
this.$emit('change', {
path: ['settings','hideSpellsTab'],
value: !value
});
let currentTab = this.$store.getters.tabById(this.model._id);
if (!value && currentTab === 3){
this.$store.commit(
'setTabForCharacterSheet',
{id: this.model._id, tab: 4}
);
}
},
},
};
</script>

View File

@@ -29,11 +29,11 @@
</template>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import {updateCreature} from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import CreatureForm from '/imports/ui/creature/CreatureForm.vue'
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
export default {

View File

@@ -1,47 +0,0 @@
<template
lang="html"
functional
>
<v-list-item v-bind="$attrs">
<v-list-item-avatar :color="model.color || 'grey'">
<img
v-if="model.avatarPicture"
:src="model.avatarPicture"
:alt="model.name"
>
<template v-else>
{{ model.initial }}
</template>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-item-title>
<v-list-item-subtitle>
{{ model.alignment }} {{ model.gender }} {{ model.race }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="selection">
<v-checkbox
:input-value="selected && selected.has(model._id)"
@change="$emit('select')"
/>
</v-list-item-action>
</v-list-item>
</template>
<script lang="js">
export default {
props: {
model: {
type: Object,
required: true,
},
selection: Boolean,
selected: {
type: Set,
default: () => new Set(),
},
}
}
</script>

View File

@@ -7,14 +7,14 @@
@click="rest"
>
<v-icon left>
{{ type === 'shortRest' ? 'snooze' : 'bedtime' }}
{{ type === 'shortRest' ? 'mdi-music-rest-quarter' : 'mdi-bed' }}
</v-icon>
{{ type === 'shortRest' ? 'Short Rest' : 'Long Rest' }}
</v-btn>
</template>
<script lang="js">
import restCreature from '/imports/api/creature/restCreature.js';
import restCreature from '/imports/api/creature/creatures/methods/restCreature.js';
export default {
inject: {

View File

@@ -0,0 +1,215 @@
<template lang="html">
<dialog-base>
<template #toolbar>
<v-toolbar-title>
{{ mode === 'archive' ? 'Archive' : 'Restore' }}
</v-toolbar-title>
<v-spacer />
<v-btn-toggle
v-model="mode"
mandatory
>
<v-btn value="archive">
<span>Archive</span>
<v-icon right>
mdi-archive-arrow-down
</v-icon>
</v-btn>
<v-btn value="restore">
<span>Restore</span>
<v-icon right>
mdi-archive-arrow-up-outline
</v-icon>
</v-btn>
</v-btn-toggle>
</template>
<creature-folder-list
selection
:creatures="mode === 'archive' ? CreaturesWithNoParty : archiveCreaturesWithNoParty"
:folders="mode === 'archive' ? folders : archivefolders"
:selected-creature="selectedCreature"
@creature-selected="id => selectedCreature = id"
/>
<v-spacer slot="actions" />
<v-btn
slot="actions"
text
:loading="archiveActionLoading"
:disabled="!numSelected"
color="primary"
@click="archiveAction"
>
{{ mode === 'archive' ? 'Archive' : 'Restore' }}
<template v-if="numSelected > 1">
{{ numSelected }} characters
</template>
<template v-else-if="numSelected === 1">
character
</template>
</v-btn>
<v-btn
slot="actions"
text
@click="$store.dispatch('popDialogStack')"
>
Close
</v-btn>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
import archiveCreatures from '/imports/api/creature/archive/methods/archiveCreatures.js';
import restoreCreatures from '/imports/api/creature/archive/methods/restoreCreatures.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`;
char.initial = char.name && char.name[0] || '?';
return char;
};
const creatureFields = {
'color': 1,
'avatarPicture': 1,
'name': 1,
'initial': 1,
'alignment': 1,
'gender': 1,
'race': 1,
'readers': 1,
'writers': 1,
'owner': 1,
};
export default {
components: {
DialogBase,
CreatureFolderList,
},
data(){return {
selectedCreature: null,
mode: 'archive',
archiveActionLoading: false,
}},
computed: {
numSelected(){
return this.selectedCreature ? 1 : 0;
},
},
watch: {
mode(){
this.selectedCreature = null;
},
},
methods: {
archiveAction(){
if (!this.selectedCreature) return;
this.archiveActionLoading = true;
if (this.mode === 'archive'){
archiveCreatures.call({
creatureIds: [this.selectedCreature],
}, error => {
this.archiveActionLoading = false;
if (!error) return;
console.error(error);
snackbar({text: error.reason});
});
} else if (this.mode === 'restore'){
let archiveId = ArchivedCreatures.findOne({
'creature._id': this.selectedCreature
})._id;
restoreCreatures.call({
archiveIds: [archiveId],
}, error => {
this.archiveActionLoading = false;
if (!error) return;
console.error(error);
snackbar({text: error.reason});
});
}
this.selectedCreature = null;
}
},
meteor: {
$subscribe: {
'archivedCreatures': [],
},
folders(){
const userId = Meteor.userId();
let folders = CreatureFolders.find(
{owner: userId, archived: {$ne: true}},
{sort: {order: 1}},
).map(folder => {
folder.creatures = Creatures.find(
{
_id: {$in: folder.creatures || []},
owner: userId,
}, {
sort: {name: 1},
fields: creatureFields,
}
).map(characterTransform);
return folder;
});
folders = folders.filter(folder => !!folder.creatures.length);
return folders;
},
CreaturesWithNoParty() {
var userId = Meteor.userId();
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
var folderChars = _.uniq(_.flatten(charArrays));
return Creatures.find(
{
_id: {$nin: folderChars},
owner: userId,
}, {
sort: {name: 1},
fields: creatureFields,
}
).map(characterTransform);
},
archivefolders(){
const userId = Meteor.userId();
let folders = CreatureFolders.find(
{owner: userId},
{sort: {order: 1}},
).map(folder => {
folder.creatures = ArchivedCreatures.find(
{
'creature._id': {$in: folder.creatures || []},
owner: userId,
}, {
sort: {'creature.name': 1},
fields: {creature: 1},
}
).map(arc => characterTransform(arc.creature));
return folder;
});
folders = folders.filter(folder => !!folder.creatures.length);
return folders;
},
archiveCreaturesWithNoParty() {
var userId = Meteor.userId();
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
var folderChars = _.uniq(_.flatten(charArrays));
return ArchivedCreatures.find(
{
'creature._id': {$nin: folderChars},
owner: userId,
}, {
sort: {'creature.name': 1},
fields: {creature: 1},
}
).map(arc => characterTransform(arc.creature));
},
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -31,9 +31,10 @@
</template>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import removeCreature from '/imports/api/creature/removeCreature.js';
import removeCreature from '/imports/api/creature/creatures/methods/removeCreature.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
@@ -61,12 +62,12 @@ export default {
},
methods: {
remove(){
this.$router.push('/characterList');
this.$store.dispatch('popDialogStack');
removeCreature.call({charId: this.id}, (error) => {
if (error) {
console.error(error);
} else {
this.$router.push('/characterList');
this.$store.dispatch('popDialogStack');
snackbar({text: error.message || error.toString()});
}
});
}

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