Compare commits

...

75 Commits

Author SHA1 Message Date
Stefan Zermatten
19a2798bf7 Fixed tree item search highlighting in dark mode 2021-08-02 01:13:15 +02:00
Stefan Zermatten
a5f2c2e0d2 Removed duplicate property button on tree tab 2021-08-02 00:54:59 +02:00
Stefan Zermatten
ee174210fd Added search to library tree views 2021-08-02 00:29:56 +02:00
Stefan Zermatten
1e38295164 All properties added to the sheet now use the type/library/create UX 2021-08-01 23:28:04 +02:00
Stefan Zermatten
758cb2f8bc Fixed search icons 2021-07-31 21:53:18 +02:00
Stefan Zermatten
36bb3c3181 New UX for inserting properties from libraries including text search and multi-add 2021-07-31 21:49:15 +02:00
Stefan Zermatten
02434de34c Drastically improved tree tab search UX for locating parts of the sheet 2021-07-31 15:19:54 +02:00
Stefan Zermatten
0dc0bea53e fixes #262, emails from DiceCloud should now be from no-reply@dicecloud.com 2021-07-27 16:59:09 +02:00
Stefan Zermatten
c392119430 Added suggested parents to PROPERTIES for use later with user guidance 2021-07-27 16:36:19 +02:00
Stefan Zermatten
4e2e0ca364 Improved display of referenced properties 2021-07-27 16:21:55 +02:00
Stefan Zermatten
4a8b72f163 Made sure the unsubscribe button always shows on libraries you are subscribed to 2021-07-27 15:51:09 +02:00
Stefan Zermatten
d916dc2b78 Removed add library node buttons from libraries the user deosn't have edit permissions for 2021-07-27 15:44:26 +02:00
Stefan Zermatten
56860ba96d Fixed proficiency Tree node view not showing the name if it is defined 2021-07-27 15:40:16 +02:00
Stefan Zermatten
b607755f9f Fixed attribute base value calculation errors not being cleared if no new errors were made 2021-07-27 15:37:57 +02:00
Stefan Zermatten
86d8fa4325 Fixed items not animating correctly on insert 2021-07-27 15:33:41 +02:00
Stefan Zermatten
2b08249e5e removed lazy from v-menu which caused a console warning 2021-07-27 15:29:05 +02:00
Stefan Zermatten
3133e664d5 Reduced stats computation precision to round off small decimal floating point errors 2021-07-27 15:28:44 +02:00
Stefan Zermatten
48f32e0a8d Removed floating point small decimal oddities in parts of inventory tab 2021-07-27 15:21:35 +02:00
Stefan Zermatten
c72785c9e7 Added components to spell viewer 2021-07-27 15:00:10 +02:00
Stefan Zermatten
421ff2aa7d Fixed DISABLE_PATREON not working, it's now a Meteor setting instead of an ENV variable 2021-07-27 14:31:54 +02:00
Stefan Zermatten
9a9e6491b9 Improved usability with better hints in property forms and property type selection 2021-07-26 18:19:29 +02:00
Stefan Zermatten
332258705c Added service worker 2021-07-21 13:44:55 +02:00
Stefan Zermatten
73ef109d4d Added calculation errors that were missing 2021-07-20 10:37:32 +02:00
Stefan Zermatten
fc240a34c4 Changed tiers to their open beta configuration 2021-07-17 12:50:38 +02:00
Stefan Zermatten
8ac4028f38 Removed limit from guest tiers for closed beta 2021-07-14 00:59:32 +02:00
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
217 changed files with 4937 additions and 1936 deletions

View File

@@ -66,5 +66,24 @@ You should see this:
=> App running at: http://localhost:3000/ => 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">
```
To disable Patreon features and unlock all paid restrictions for all users of your deployment, replace
`"patreon": { "clientId": ... }"` with `"disablePatreon": true` in the public key of the METEOR_SETTINGS environment variable.
Alternatively run `meteor run --settings exampleMeteorSettings.json` to start the app with the example settings that disable Patreon by default.
Now, visiting [](http://localhost:3000/) should show you an empty instance of Now, visiting [](http://localhost:3000/) should show you an empty instance of
DiceCloud running. DiceCloud running.

View File

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

View File

@@ -1 +1 @@
METEOR@2.2 METEOR@2.2.1

View File

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

View File

@@ -1,5 +1,6 @@
<head> <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="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">

View File

@@ -1,3 +1,4 @@
import '/imports/ui/vueSetup.js'; import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.js'; import '/imports/ui/styles/stylesIndex.js';
import '/imports/client/config.js'; import '/imports/client/config.js';
import '/imports/client/serviceWorker.js';

View File

@@ -0,0 +1,6 @@
{
"public": {
"environment": "production",
"disablePatreon": true
}
}

View File

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}){ export default function applyProperties({ forest, targets, ...options}){
forest.forEach(node => { forest.forEach(node => {
let prop = node.node; let prop = node.node;
options.actionContext[`#${prop.type}`] = prop;
let children = node.children; let children = node.children;
if (shouldSplit(prop) && targets.length){ if (shouldSplit(prop) && targets.length){
targets.forEach(target => { targets.forEach(target => {

View File

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

View File

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

View File

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

@@ -1,6 +1,6 @@
export default class EffectAggregator{ export default class EffectAggregator{
constructor(){ constructor(){
this.base = 0; this.base = undefined;
this.add = 0; this.add = 0;
this.mul = 1; this.mul = 1;
this.min = Number.NEGATIVE_INFINITY; this.min = Number.NEGATIVE_INFINITY;
@@ -20,7 +20,13 @@ export default class EffectAggregator{
switch(effect.operation){ switch(effect.operation){
case 'base': case 'base':
// Take the largest base value // Take the largest base value
this.base = result > this.base ? result : this.base; if (Number.isFinite(result)){
if(Number.isFinite(this.base)){
this.base = Math.max(this.base, result);
} else {
this.base = result;
}
}
break; break;
case 'add': case 'add':
// Add all adds together // Add all adds together

View File

@@ -1,6 +1,7 @@
import computeStat from '/imports/api/creature/computation/engine/computeStat.js'; import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js'; import computeProficiency from '/imports/api/creature/computation/engine/computeProficiency.js';
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js'; import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
import { union } from 'lodash'; import { union } from 'lodash';
export default function combineStat(stat, aggregator, memo){ export default function combineStat(stat, aggregator, memo){
@@ -14,7 +15,14 @@ export default function combineStat(stat, aggregator, memo){
} }
function getAggregatorResult(stat, aggregator){ function getAggregatorResult(stat, aggregator){
let base = Math.max(aggregator.base, stat.baseValue || 0); 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; let result = (base + aggregator.add) * aggregator.mul;
if (result < aggregator.min) { if (result < aggregator.min) {
result = aggregator.min; result = aggregator.min;
@@ -27,6 +35,8 @@ function getAggregatorResult(stat, aggregator){
} }
if (!stat.decimal && Number.isFinite(result)){ if (!stat.decimal && Number.isFinite(result)){
result = Math.floor(result); result = Math.floor(result);
} else if (Number.isFinite(result)){
result = stripFloatingPointOddities(result);
} }
return result; return result;
} }
@@ -137,7 +147,8 @@ function combineSkill(stat, aggregator, memo){
} }
// Combine everything to get the final result // 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.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max; if (result > aggregator.max) result = aggregator.max;
if (aggregator.set !== undefined) { if (aggregator.set !== undefined) {

View File

@@ -21,13 +21,19 @@ export default function computeStat(stat, memo){
// Before doing any work, mark this stat as busy // Before doing any work, mark this stat as busy
stat.computationDetails.busyComputing = true; stat.computationDetails.busyComputing = true;
let effects = stat.computationDetails.effects; let effects = stat.computationDetails.effects || [];
let proficiencies = stat.computationDetails.proficiencies; let proficiencies = stat.computationDetails.proficiencies || [];
// Get references to all the stats that share the variable name // Get references to all the stats that share the variable name
let sameNameStats = stat.computationDetails.idsOfSameName.map( let sameNameStats
id => memo.propsById[id]
); if (stat.computationDetails.idsOfSameName){
sameNameStats = stat.computationDetails.idsOfSameName.map(
id => memo.propsById[id]
);
} else {
sameNameStats = [];
}
let allStats = [stat, ...sameNameStats]; let allStats = [stat, ...sameNameStats];
@@ -76,7 +82,9 @@ export default function computeStat(stat, memo){
value: statInstance.baseProficiency, value: statInstance.baseProficiency,
stats: [statInstance.variableName], stats: [statInstance.variableName],
type: 'proficiency', type: 'proficiency',
dependencies: [], dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: { computationDetails: {
computed: true, computed: true,
} }
@@ -84,7 +92,8 @@ export default function computeStat(stat, memo){
} }
// Compute each active stat's baseValue calculation and apply it // Compute each active stat's baseValue calculation and apply it
if (statInstance.baseValueCalculation) { if (!statInstance.inactive) {
delete statInstance.baseValueErrors;
let { let {
result, result,
context, context,
@@ -94,19 +103,26 @@ export default function computeStat(stat, memo){
prop: statInstance, prop: statInstance,
memo memo
}); });
baseDependencies = union(baseDependencies, dependencies); result.value = +result.value;
statInstance.baseValue = +result.value; if (!isNaN(result.value)){
statInstance.baseValue = result.value;
} else {
statInstance.baseValue = undefined;
}
statInstance.dependencies = union(statInstance.dependencies, dependencies);
if (context.errors.length){ if (context.errors.length){
statInstance.baseValueErrors = context.errors; statInstance.baseValueErrors = context.errors;
} }
// Apply all the base values // Apply all the base values
if (!statInstance.inactive){ if (Number.isFinite(statInstance.baseValue)){
effects.push({ effects.push({
operation: 'base', operation: 'base',
calculation: statInstance.baseValueCalculation, calculation: statInstance.baseValueCalculation,
result: statInstance.baseValue, result: statInstance.baseValue,
stats: [statInstance.variableName], stats: [statInstance.variableName],
dependencies: [], dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: { computationDetails: {
computed: true, computed: true,
}, },
@@ -115,14 +131,6 @@ export default function computeStat(stat, memo){
} }
}); });
// Apply all the base baseDependencies
allStats.forEach(statInstance => {
statInstance.dependencies = union(
statInstance.dependencies,
without(baseDependencies, statInstance._id)
);
});
// Compute and aggregate all the effects // Compute and aggregate all the effects
let aggregator = new EffectAggregator(); let aggregator = new EffectAggregator();
let effectDeps = []; let effectDeps = [];
@@ -132,7 +140,7 @@ export default function computeStat(stat, memo){
if (effect.deactivatedByToggle) return; if (effect.deactivatedByToggle) return;
// dependencies // dependencies
if (effect._id) effectDeps = [effect._id]; if (effect._id) effectDeps = union(effectDeps, [effect._id]);
effectDeps = union(effectDeps, effect.dependencies); effectDeps = union(effectDeps, effect.dependencies);
// Add computed effect to aggregator // Add computed effect to aggregator
@@ -146,9 +154,9 @@ export default function computeStat(stat, memo){
// Mark the stats as computed // Mark the stats as computed
statInstance.computationDetails.computed = true; statInstance.computationDetails.computed = true;
statInstance.computationDetails.busyComputing = false; statInstance.computationDetails.busyComputing = false;
statInstance.dependencies = union( // Only the active stat instance depeneds on the effects
statInstance.dependencies, if (!statInstance.overridden){
effectDeps statInstance.dependencies = union(statInstance.dependencies, effectDeps);
); }
}); });
} }

View File

@@ -1,5 +1,5 @@
import { pick, forOwn } from 'lodash'; 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'; import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) { export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {

View File

@@ -1,7 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; 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 ComputationMemo from '/imports/api/creature/computation/engine/ComputationMemo.js';
import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js'; import getComputationProperties from '/imports/api/creature/computation/engine/getComputationProperties.js';
import computeMemo from '/imports/api/creature/computation/engine/computeMemo.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 recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js'; import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import getDependentProperties from '/imports/api/creature/computation/engine/getDependentProperties.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'; import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
export const recomputeCreature = new ValidatedMethod({ 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){ export default function getRootCreatureAncestor(property){
return Creatures.findOne(property.ancestors[0].id); 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 { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; 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 { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.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 { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; 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 { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { recomputeCreatureByDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';

View File

@@ -16,11 +16,16 @@ import {
import { reorderDocs } from '/imports/api/parenting/order.js'; import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js'; import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js'; import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({ const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode', name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({ validate: new SimpleSchema({
nodeId: { nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
}, },
@@ -37,7 +42,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
numRequests: 5, numRequests: 5,
timeInterval: 5000, timeInterval: 5000,
}, },
run({nodeId, parentRef, order}) { run({nodeIds, parentRef, order}) {
// get the new ancestry for the properties // get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef}); let {parentDoc, ancestors} = getAncestry({parentRef});
@@ -52,50 +57,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
} }
assertEditPermission(rootCreature, this.userId); assertEditPermission(rootCreature, this.userId);
// Fetch the library node and its decendents, provided they have not been // {libraryId: hasViewPermission}
// removed //let libraryPermissionMemoir = {};
let node = LibraryNodes.findOne({ let node;
_id: nodeId, nodeIds.forEach(nodeId => {
removed: {$ne: true}, // TODO: Check library view permission for each node before starting
}); node = insertPropertyFromNode(nodeId, ancestors, order);
if (!node) throw `Node not found for nodeId: ${nodeId}`; });
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
removed: {$ne: true},
}).fetch();
// The root node is first in the array of nodes // get one of the root inserted docs
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
// get the root inserted doc
let rootId = node._id; let rootId = node._id;
// Tree structure changed by inserts, reorder the tree // Tree structure changed by inserts, reorder the tree
@@ -105,7 +75,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}); });
// The library properties need to denormalise which of them are inactive // The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootId); recomputeInactiveProperties(rootCreature._id);
// Some of the library properties may be items or containers // Some of the library properties may be items or containers
recomputeInventory(rootCreature._id); recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute // Inserting a creature property invalidates dependencies: full recompute
@@ -115,4 +85,148 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}, },
}); });
function insertPropertyFromNode(nodeId, ancestors, order){
// Fetch the library node and its decendents, provided they have not been
// removed
// TODO: Check permission to read the library this node is in
let node = LibraryNodes.findOne({
_id: nodeId,
removed: {$ne: true},
});
if (!node) throw `Node not found for nodeId: ${nodeId}`;
let oldParent = node.parent;
let nodes = LibraryNodes.find({
'ancestors.id': nodeId,
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];
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry: ancestors,
oldParent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({
docArray: nodes,
collectionMap: {'libraryNodes': 'creatureProperties'}
});
// Order the root node
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
// 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; export default insertPropertyFromLibraryNode;

View File

@@ -1,16 +1,7 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js' import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js'; import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.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 defaultCharacterProperties from '/imports/api/creature/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import '/imports/api/creature/removeCreature.js';
import '/imports/api/creature/restCreature.js';
//set up the collection for creatures //set up the collection for creatures
let Creatures = new Mongo.Collection('creatures'); let Creatures = new Mongo.Collection('creatures');
@@ -176,92 +167,8 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema); Creatures.attachSchema(CreatureSchema);
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature', import '/imports/api/creature/creatures/methods/index.js';
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,
});
// 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,
});
}
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},
});
}
},
});
export default Creatures; 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 { import {
assertEditPermission as editPermission, assertEditPermission as editPermission,
assertViewPermission as viewPermission, assertViewPermission as viewPermission,

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({
nodeIds: ['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 SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js'; import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js'; import Experiences from '/imports/api/creature/experience/Experiences.js';

View File

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

View File

@@ -1,6 +1,6 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; 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'; import nodesToTree from '/imports/api/parenting/nodesToTree.js';
export default function recomputeInventory(creatureId){ export default function recomputeInventory(creatureId){
let inventoryForest = nodesToTree({ let inventoryForest = nodesToTree({

View File

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

View File

@@ -1,9 +1,9 @@
import SimpleSchema from 'simpl-schema'; 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 LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js'; import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
import { import {
parse, parse,
CompilationContext, CompilationContext,

View File

@@ -2,7 +2,7 @@ import {
assertEditPermission, assertEditPermission,
assertViewPermission, assertViewPermission,
assertOwnership, 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 // 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 // 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 SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js'; import '/imports/api/library/methods/index.js';
import { updateReferenceNodeWork } from '/imports/api/library/methods/updateReferenceNode.js';
let LibraryNodes = new Mongo.Collection('libraryNodes'); let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -76,7 +77,12 @@ const insertNode = new ValidatedMethod({
run(libraryNode) { run(libraryNode) {
delete libraryNode._id; delete libraryNode._id;
assertNodeEditPermission(libraryNode, this.userId); 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 { } else {
modifier = {$set: {[pathString]: value}}; modifier = {$set: {[pathString]: value}};
} }
return LibraryNodes.update(_id, modifier, { let numUpdated = LibraryNodes.update(_id, modifier, {
selector: {type: node.type}, 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/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: doc,
};
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: { id: {
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, 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 index: 1
}, },
collection: { collection: {

View File

@@ -1,4 +1,4 @@
import { nodesToTree } from '/imports/api/parenting/parenting.js'; import nodesToTree from '/imports/api/parenting/nodesToTree.js';
export default function getDescendantsInDepthFirstOrder({ export default function getDescendantsInDepthFirstOrder({
collection, collection,

View File

@@ -0,0 +1,115 @@
import { union, difference, sortBy, findLast } from 'lodash';
export function nodeArrayToTree(nodes){
// Store a dict and list of all the nodes
let nodeIndex = {};
let nodeList = [];
nodes.forEach( node => {
let treeNode = {
node: node,
children: [],
};
nodeIndex[node._id] = treeNode;
nodeList.push(treeNode);
});
// Create a forest of trees
let forest = [];
// Either the node is a child of its nearest found ancestor, or in the forest as a root
nodeList.forEach(treeNode => {
let ancestorInForest = findLast(
treeNode.node.ancestors,
ancestor => !!nodeIndex[ancestor.id]
);
if (ancestorInForest){
nodeIndex[ancestorInForest.id].children.push(treeNode);
} else {
forest.push(treeNode);
}
});
return forest;
}
// Fetch the documents from a collection, and return the tree of those documents
export default function nodesToTree({
collection, ancestorId, filter, options = {},
includeFilteredDocAncestors = false, includeFilteredDocDescendants = false
}){
// Setup the filter
let collectionFilter = {
'ancestors.id': ancestorId,
'removed': {$ne: true},
};
if (filter){
collectionFilter = {
...collectionFilter,
...filter,
}
}
// Set up the options
let collectionSort = {
order: 1
};
if (options && options.sort){
collectionSort = {
...collectionSort,
...options.sort,
}
}
let collectionOptions = {
sort: collectionSort,
}
if (options){
collectionOptions = {
...collectionOptions,
...options,
}
}
// Find all the nodes that match the filter
let docs = collection.find(collectionFilter, collectionOptions).map(doc => {
if (!filter) return doc;
// Mark the nodes that were found by the custom filter
doc._matchedDocumentFilter = true;
return doc;
});
let ancestors = [];
let ancestorIds = [];
let docIds = [];
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){
docIds = docs.map(doc => doc._id)
}
if (filter && includeFilteredDocAncestors){
// Add all ancestor ids to an array
docs.forEach(doc => {
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
});
// Remove the IDs of docs we have already found
ancestorIds = difference(ancestorIds, docIds);
// Get the docs from the collection, don't worry about `removed` docs,
// if their descendant was not removed, neither are they
ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => {
// Mark that the nodes are ancestors of the found nodes
doc._ancestorOfMatchedDocument = true;
return doc;
});
}
let descendants = [];
if (filter && includeFilteredDocDescendants){
let exludeIds = union(ancestorIds, docIds);
descendants = collection.find({
'_id': {$nin: exludeIds},
'ancestors.id': {$in: docIds},
'removed': {$ne: true},
}).map(doc => {
// Mark that the nodes are descendants of the found nodes
doc._descendantOfMatchedDocument = true;
return doc;
});
}
let nodes = sortBy([
...ancestors,
...docs,
...descendants
], 'order');
// Find all the nodes
return nodeArrayToTree(nodes);
}

View File

@@ -1,6 +1,6 @@
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js'; import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { flatten, findLast } from 'lodash'; import { flatten } from 'lodash';
const generalParents = [ const generalParents = [
'attribute', 'attribute',
@@ -217,41 +217,3 @@ export function getName(doc){
if (doc.ancestors[i].name) return doc.ancestors[i].name; if (doc.ancestors[i].name) return doc.ancestors[i].name;
} }
} }
export function nodeArrayToTree(nodes){
// Store a dict and list of all the nodes
let nodeIndex = {};
let nodeList = [];
nodes.forEach( node => {
let treeNode = {
node: node,
children: [],
};
nodeIndex[node._id] = treeNode;
nodeList.push(treeNode);
});
// Create a forest of trees
let forest = [];
// Either the node is a child of its nearest found ancestor, or in the forest as a root
nodeList.forEach(treeNode => {
let ancestorInForest = findLast(
treeNode.node.ancestors,
ancestor => !!nodeIndex[ancestor.id]
);
if (ancestorInForest){
nodeIndex[ancestorInForest.id].children.push(treeNode);
} else {
forest.push(treeNode);
}
});
return forest;
}
export function nodesToTree({collection, ancestorId, filter = {}, options = {}}){
if (!('ancestors.id' in filter)) filter['ancestors.id'] = ancestorId;
if (!('removed' in filter)) filter['removed'] = {$ne: true};
if (!options.sort) options.sort = {order: 1};
if (!('order' in options.sort)) options.sort.order = 1;
let nodes = collection.find(filter, options);
return nodeArrayToTree(nodes);
}

View File

@@ -0,0 +1,55 @@
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.node.level': {
type: Number,
optional: true,
},
'cache.node.value': {
type: Number,
optional: true,
},
'cache.library': {
type: Object,
optional: true,
},
'cache.library.name': {
type: String,
optional: true,
},
});
export { ReferenceSchema };

View File

@@ -1,6 +1,5 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js'; import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
let SpellListSchema = new SimpleSchema({ let SpellListSchema = new SimpleSchema({
@@ -12,12 +11,6 @@ let SpellListSchema = new SimpleSchema({
type: String, type: String,
optional: true, optional: true,
}, },
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
optional: true,
},
// Calculation of how many spells in this list can be prepared // Calculation of how many spells in this list can be prepared
maxPrepared: { maxPrepared: {
type: String, type: String,

View File

@@ -15,6 +15,7 @@ import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.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 { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js'; import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
@@ -42,6 +43,7 @@ const propertySchemasIndex = {
note: ComputedOnlyNoteSchema, note: ComputedOnlyNoteSchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: ComputedOnlySlotSchema, propertySlot: ComputedOnlySlotSchema,
reference: ReferenceSchema,
roll: ComputedOnlyRollSchema, roll: ComputedOnlyRollSchema,
savingThrow: ComputedOnlySavingThrowSchema, savingThrow: ComputedOnlySavingThrowSchema,
skill: ComputedOnlySkillSchema, 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 { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.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 { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js'; import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
@@ -40,6 +41,7 @@ const propertySchemasIndex = {
note: ComputedNoteSchema, note: ComputedNoteSchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema, propertySlot: ComputedSlotSchema,
reference: ReferenceSchema,
roll: ComputedRollSchema, roll: ComputedRollSchema,
savingThrow: ComputedSavingThrowSchema, savingThrow: ComputedSavingThrowSchema,
skill: ComputedSkillSchema, 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 { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js'; import { NoteSchema } from '/imports/api/properties/Notes.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.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 { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js'; import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { SkillSchema } from '/imports/api/properties/Skills.js'; import { SkillSchema } from '/imports/api/properties/Skills.js';
@@ -40,6 +41,7 @@ const propertySchemasIndex = {
note: NoteSchema, note: NoteSchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: SlotSchema, propertySlot: SlotSchema,
reference: ReferenceSchema,
roll: RollSchema, roll: RollSchema,
savingThrow: SavingThrowSchema, savingThrow: SavingThrowSchema,
skill: SkillSchema, skill: SkillSchema,

View File

@@ -1,6 +1,5 @@
import { _ } from 'meteor/underscore'; import { _ } from 'meteor/underscore';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js'; import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
function assertIdValid(userId){ function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){ if (!userId || typeof userId !== 'string'){
@@ -12,7 +11,7 @@ function assertIdValid(userId){
function assertdocExists(doc){ function assertdocExists(doc){
if (!doc){ if (!doc){
throw new Meteor.Error('Permission denied', 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; 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 // Ensure the user is authorized for this specific document
if ( if (
doc.owner === userId || doc.owner === userId ||

View File

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

View File

@@ -2,6 +2,8 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js'; import '/imports/api/users/deleteMyAccount.js';
import { some } from 'lodash';
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
const userSchema = new SimpleSchema({ const userSchema = new SimpleSchema({
username: { username: {
@@ -63,7 +65,7 @@ const userSchema = new SimpleSchema({
}, },
subscribedLibraries: { subscribedLibraries: {
type: Array, type: Array,
defaultValue: [], defaultValue: defaultLibraries,
max: 100, max: 100,
}, },
'subscribedLibraries.$': { 'subscribedLibraries.$': {
@@ -93,6 +95,10 @@ const userSchema = new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
'preferences.hidePropertySelectDialogHelp': {
type: Boolean,
optional: true,
},
}); });
Meteor.users.attachSchema(userSchema); Meteor.users.attachSchema(userSchema);
@@ -154,7 +160,7 @@ Meteor.users.sendVerificationEmail = new ValidatedMethod({
throw new Meteor.Error('User not found', throw new Meteor.Error('User not found',
'Can\'t send a validation email to a user that does not exist'); 'Can\'t send a validation email to a user that does not exist');
} }
if (!_.some(user.emails, email => email.address === address)) { if (!some(user.emails, email => email.address === address)) {
throw new Meteor.Error('Email address not found', throw new Meteor.Error('Email address not found',
'The specified email address wasn\'t found on this user account'); 'The specified email address wasn\'t found on this user account');
} }

View File

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

View File

@@ -1,58 +1,78 @@
import { findLast } from 'lodash'; import { findLast } from 'lodash';
import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js'; import getEntitledCents from '/imports/api/users/patreon/getEntitledCents.js';
import Invites from '/imports/api/users/Invites.js'; import Invites from '/imports/api/users/Invites.js';
const patreonDisabled = !!Meteor.settings?.public?.disablePatreon;
const TIERS = [ const TIERS = Object.freeze([
{ {
name: 'Commoner', name: 'Commoner',
minimumEntitledCents: 0, minimumEntitledCents: 0,
invites: 0, invites: 0,
characterSlots: 5,
paidBenefits: false, paidBenefits: false,
}, { }, {
name: 'Dreamer', name: 'Dreamer',
minimumEntitledCents: 100, minimumEntitledCents: 100,
invites: 0, invites: 0,
characterSlots: 5,
paidBenefits: false, paidBenefits: false,
}, { }, {
name: 'Wanderer', name: 'Wanderer',
minimumEntitledCents: 300, minimumEntitledCents: 300,
invites: 0, invites: 0,
characterSlots: 5,
paidBenefits: false, paidBenefits: false,
}, { }, {
//cost per user $5 //cost per user $5
name: 'Adventurer', name: 'Adventurer',
minimumEntitledCents: 500, minimumEntitledCents: 500,
invites: 0, invites: 0,
characterSlots: 20,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.33 //cost per user $3.33
name: 'Hero', name: 'Hero',
minimumEntitledCents: 1000, minimumEntitledCents: 1000,
invites: 2, invites: 2,
characterSlots: 50,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.333 //cost per user $3.333
name: 'Legend', name: 'Legend',
minimumEntitledCents: 2000, minimumEntitledCents: 2000,
invites: 5, invites: 5,
characterSlots: 120,
paidBenefits: true, paidBenefits: true,
}, { }, {
//cost per user $3.125 //cost per user $3.125
name: 'Paragon', name: 'Paragon',
minimumEntitledCents: 5000, minimumEntitledCents: 5000,
invites: 15, invites: 15,
characterSlots: -1, // Unlimited characters
paidBenefits: true, paidBenefits: true,
}, },
]; ]);
const GUEST_TIER = { // Companion tier should be equivalent to the Adventurer tier
const GUEST_TIER = Object.freeze({
name: 'Companion', name: 'Companion',
guest: true, guest: true,
invites: 0, invites: 0,
characterSlots: 20,
paidBenefits: true, 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){ export function getTierByEntitledCents(entitledCents = 0){
if (patreonDisabled) return PATREON_DISABLED_TIER;
return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents); return findLast(TIERS, tier => entitledCents >= tier.minimumEntitledCents);
} }
@@ -66,6 +86,7 @@ export function getUserTier(user){
}); });
if (!user) throw 'User not found'; if (!user) throw 'User not found';
} }
if (patreonDisabled) return PATREON_DISABLED_TIER;
const entitledCents = getEntitledCents(user); const entitledCents = getEntitledCents(user);
const tier = getTierByEntitledCents(entitledCents); const tier = getTierByEntitledCents(entitledCents);
if (tier.paidBenefits) return tier; if (tier.paidBenefits) return tier;

View File

@@ -0,0 +1,5 @@
Meteor.startup(() => {
navigator.serviceWorker.register('/sw.js')
.then()
.catch(error => console.log('ServiceWorker registration failed: ', error));
});

View File

@@ -1,99 +1,157 @@
const PROPERTIES = Object.freeze({ const PROPERTIES = Object.freeze({
action: { action: {
icon: '$vuetify.icons.action', icon: '$vuetify.icons.action',
name: 'Action' name: 'Action',
helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.',
suggestedParents: ['classLevel', 'feature', 'item'],
}, },
attack: { attack: {
icon: '$vuetify.icons.attack', icon: '$vuetify.icons.attack',
name: 'Attack' name: 'Attack',
helpText: 'Attacks are a special form of action that includes an attack roll. Attacks can critical hit, which doubles the number of damage dice in properties under the attack.',
suggestedParents: ['classLevel', 'feature', 'item'],
}, },
attribute: { attribute: {
icon: '$vuetify.icons.attribute', icon: '$vuetify.icons.attribute',
name: 'Attribute' name: 'Attribute',
helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.',
examples: 'Ability scores, speed, hit points, ki',
suggestedParents: ['classLevel', 'buff'],
}, },
adjustment: { adjustment: {
icon: '$vuetify.icons.attribute_damage', icon: '$vuetify.icons.attribute_damage',
name: 'Attribute damage' name: 'Attribute damage',
helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
buff: { buff: {
icon: '$vuetify.icons.buff', icon: '$vuetify.icons.buff',
name: 'Buff' name: 'Buff',
helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
classLevel: { classLevel: {
icon: '$vuetify.icons.class_level', icon: '$vuetify.icons.class_level',
name: 'Class level' name: 'Class level',
helpText: 'Class levels represent a single level gained in a class',
suggestedParents: ['class'],
}, },
constant: { constant: {
icon: 'anchor', icon: 'mdi-anchor',
name: 'Constant' name: 'Constant',
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
suggestedParents: [],
}, },
container: { container: {
icon: 'work', icon: 'mdi-bag-personal-outline',
name: 'Container' name: 'Container',
helpText: 'A container holds items in the inventory',
examples: 'Coin pouch, backpack',
suggestedParents: ['folder'],
}, },
damage: { damage: {
icon: '$vuetify.icons.damage', icon: '$vuetify.icons.damage',
name: 'Damage' name: 'Damage',
helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
damageMultiplier: { damageMultiplier: {
icon: '$vuetify.icons.damage_multiplier', icon: '$vuetify.icons.damage_multiplier',
name: 'Damage multiplier' name: 'Damage multiplier',
helpText: 'Resistance, vulnerability, and immunity.',
suggestedParents: ['classLevel', 'feature', 'item'],
}, },
effect: { effect: {
icon: '$vuetify.icons.effect', icon: '$vuetify.icons.effect',
name: 'Effect' name: 'Effect',
helpText: 'Effects change the value or state of attributes and skills.',
examples: '+2 Strength, Advantage on dexterity saving throws',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'],
}, },
feature: { feature: {
icon: 'subject', icon: 'mdi-text-subject',
name: 'Feature' name: 'Feature',
helpText: 'Descriptive or narrative features your character has access to',
suggestedParents: ['classLevel', 'folder'],
}, },
folder: { folder: {
icon: 'folder', icon: 'mdi-folder-outline',
name: 'Folder' name: 'Folder',
helpText: 'A way to organise other properties on the character',
suggestedParents: ['folder'],
}, },
item: { item: {
icon: '$vuetify.icons.item', icon: 'mdi-cube-outline',
name: 'Item' name: 'Item',
helpText: 'Objects and equipment your charcter finds on their adventures',
suggestedParents: ['container'],
}, },
note: { note: {
icon: 'note', icon: 'mdi-note-outline',
name: 'Note' name: 'Note',
helpText: 'Notes about your character and their adventures',
suggestedParents: ['folder'],
}, },
proficiency: { proficiency: {
icon: 'radio_button_checked', icon: 'mdi-brightness-1',
name: 'Proficiency' name: 'Proficiency',
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
}, },
roll: { roll: {
icon: '$vuetify.icons.roll', icon: '$vuetify.icons.roll',
name: 'Roll' name: 'Roll',
helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
},
reference: {
icon: 'mdi-vector-link',
name: 'Reference',
libraryOnly: true,
helpText: 'A reference is a link to a different property in a library. When a reference gets copied to a character sheet, it is replaced with the referenced property and all its children.',
suggestedParents: [],
}, },
savingThrow: { savingThrow: {
icon: '$vuetify.icons.saving_throw', icon: '$vuetify.icons.saving_throw',
name: 'Saving throw' name: 'Saving throw',
helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.',
suggestedParents: ['action', 'attack', 'spell'],
}, },
skill: { skill: {
icon: '$vuetify.icons.skill', icon: '$vuetify.icons.skill',
name: 'Skill' name: 'Skill',
helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.',
suggestedParents: ['classLevel', 'folder'],
}, },
propertySlot: { propertySlot: {
icon: 'tab_unselected', icon: 'mdi-power-socket-eu',
name: 'Slot' name: 'Slot',
helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot',
suggestedParents: [],
}, },
slotFiller: { slotFiller: {
icon: 'picture_in_picture', icon: 'mdi-power-plug-outline',
name: 'Slot filler' name: 'Slot filler',
helpText: 'A slot filler allows for more advanced logic when it attemptst to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
suggestedParents: ['propertySlot'],
}, },
spellList: { spellList: {
icon: '$vuetify.icons.spell_list', icon: '$vuetify.icons.spell_list',
name: 'Spell list' name: 'Spell list',
helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within',
suggestedParents: [],
}, },
spell: { spell: {
icon: '$vuetify.icons.spell', icon: '$vuetify.icons.spell',
name: 'Spell' name: 'Spell',
helpText: 'A spell your character can potentially cast',
suggestedParents: ['spellList'],
}, },
toggle: { toggle: {
icon: '$vuetify.icons.toggle', icon: '$vuetify.icons.toggle',
name: 'Toggle' name: 'Toggle',
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
suggestedParents: [],
}, },
}); });

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import { Accounts } from 'meteor/accounts-base'
Accounts.emailTemplates.from = 'no-reply@dicecloud.com';
Accounts.emailTemplates.siteName = 'DiceCloud';

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 Creatures from '/imports/api/creature/creatures/Creatures.js';
import Parties from '/imports/api/creature/Parties.js'; import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
Meteor.publish('characterList', function(){ Meteor.publish('characterList', function(){
this.autorun(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 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 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({ let schema = new SimpleSchema({
creatureId: { creatureId: {

View File

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

View File

@@ -2,13 +2,6 @@ import SimpleSchema from 'simpl-schema';
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js'; import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
const standardLibraryIds = [
'SRDLibraryGA3XWsd',
];
Meteor.publish('standardLibraries', function(){
return Libraries.find({_id: {$in: standardLibraryIds}});
});
Meteor.publish('libraries', function(){ Meteor.publish('libraries', function(){
this.autorun(function (){ this.autorun(function (){
@@ -75,3 +68,24 @@ Meteor.publish('libraryNodes', function(libraryId){
]; ];
}); });
}); });
Meteor.publish('descendantLibraryNodes', function(nodeId){
let node = LibraryNodes.findOne(nodeId);
let libraryId = node?.ancestors[0]?.id;
if (!libraryId) return [];
this.autorun(function (){
let userId = this.userId;
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){
return this.error(e);
}
return [
LibraryNodes.find({
'ancestors.id': nodeId,
}, {
sort: {order: 1},
}),
];
});
});

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'; import Libraries from '/imports/api/library/Libraries.js';
Meteor.publish('ownedDocuments', function(){ Meteor.publish('ownedDocuments', function(){

View File

@@ -0,0 +1,116 @@
import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
Meteor.publish('searchLibraryNodes', function(){
let self = this;
this.autorun(function (){
let type = self.data('type');
if (!type) return [];
let userId = this.userId;
if (!userId) {
return [];
}
// Get all the ids of libraries the user can access
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
if (!user) return [];
const subs = user.subscribedLibraries || [];
let libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
]
}, {
fields: {_id: 1, name: 1},
});
let libraryIds = libraries.map(lib => lib._id);
// Build a filter for nodes in those libraries that match the type
let filter = {
'ancestors.id': {$in: libraryIds},
removed: {$ne: true},
tags: {$ne: []}, // Only tagged library nodes are considered
};
if (type){
filter.$or = [{
type,
},{
type: 'slotFiller',
slotFillerType: type,
}];
}
this.autorun(function(){
// Get the limit of the documents the user can fetch
var limit = self.data('limit') || 32;
check(limit, Number);
// Get the search term
let searchTerm = self.data('searchTerm') || '';
check(searchTerm, String);
let options = undefined;
if (searchTerm){
filter.$text = {$search: searchTerm};
options = {
// relevant documents have a higher score.
fields: {
score: { $meta: 'textScore' }
},
sort: {
// `score` property specified in the projection fields above.
score: { $meta: 'textScore' },
'ancestors.0.id': 1,
name: 1,
order: 1,
}
}
} else {
delete filter.$text
options = {sort: {
'ancestors.0.id': 1,
name: 1,
order: 1,
}};
}
options.limit = limit;
this.autorun(function () {
self.setData('countAll', LibraryNodes.find(filter).count());
});
let cursor = LibraryNodes.find(filter, options);
Mongo.Collection._publishCursor(libraries, self, 'libraries');
let observeHandle = cursor.observeChanges({
added: function (id, fields) {
fields._searchResult = true;
self.added('libraryNodes', id, fields);
},
changed: function (id, fields) {
self.changed('libraryNodes', id, fields);
},
removed: function (id) {
self.removed('libraryNodes', id);
}
},
// Publications don't mutate the documents
{ nonMutatingCallbacks: true }
);
// register stop callback (expects lambda w/ no args).
this.onStop(function () {
observeHandle.stop();
});
// this.ready();
});
});
});

View File

@@ -1,8 +1,8 @@
import SimpleSchema from 'simpl-schema'; 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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.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 recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js'; import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import VERSION from '/imports/constants/VERSION.js'; import VERSION from '/imports/constants/VERSION.js';

View File

@@ -1,5 +1,5 @@
import Tabletops from '/imports/api/tabletop/Tabletops.js'; 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'; import Messages from '/imports/api/tabletop/Messages.js';
Meteor.publish('tabletops', function(){ Meteor.publish('tabletops', function(){

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@
icon icon
@click="back" @click="back"
> >
<v-icon>arrow_back</v-icon> <v-icon>mdi-arrow-left</v-icon>
</v-btn> </v-btn>
<property-icon <property-icon
:model="model" :model="model"
@@ -50,7 +50,7 @@
data-id="property-toolbar-menu-button" data-id="property-toolbar-menu-button"
v-on="on" v-on="on"
> >
<v-icon>more_vert</v-icon> <v-icon>mdi-dots-vertical</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
@@ -64,7 +64,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-icon>file_copy</v-icon> <v-icon>mdi-content-copy</v-icon>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
@@ -77,7 +77,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-icon>send</v-icon> <v-icon>mdi-send</v-icon>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
<v-list-item <v-list-item
@@ -90,7 +90,7 @@
</v-list-item-title> </v-list-item-title>
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-icon>delete</v-icon> <v-icon>mdi-delete</v-icon>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -112,13 +112,13 @@
v-if="editing" v-if="editing"
key="doneIcon" key="doneIcon"
> >
done mdi-check
</v-icon> </v-icon>
<v-icon <v-icon
v-else v-else
key="createIcon" key="createIcon"
> >
create mdi-pencil
</v-icon> </v-icon>
</v-slide-y-transition> </v-slide-y-transition>
</v-btn> </v-btn>

View File

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

View File

@@ -1,7 +1,10 @@
<template lang="html"> <template lang="html">
<v-sheet <v-sheet
class="tree-node" class="tree-node"
:class="!hasChildren ? 'empty' : null" :class="{
'empty': !hasChildren,
'found': node._matchedDocumentFilter,
}"
:data-id="`tree-node-${node._id}`" :data-id="`tree-node-${node._id}`"
> >
<div <div
@@ -18,7 +21,7 @@
@click.stop="expanded = !expanded" @click.stop="expanded = !expanded"
> >
<v-icon v-if="canExpand && (hasChildren || organize)"> <v-icon v-if="canExpand && (hasChildren || organize)">
chevron_right mdi-chevron-right
</v-icon> </v-icon>
</v-btn> </v-btn>
<div <div
@@ -31,7 +34,7 @@
:class="selected && 'primary--text'" :class="selected && 'primary--text'"
:disabled="expanded" :disabled="expanded"
> >
drag_handle mdi-drag
</v-icon> </v-icon>
<!--{{node && node.order}}--> <!--{{node && node.order}}-->
<tree-node-view <tree-node-view
@@ -52,7 +55,7 @@
:children="computedChildren" :children="computedChildren"
:group="group" :group="group"
:organize="organize" :organize="organize"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
@reordered="e => $emit('reordered', e)" @reordered="e => $emit('reordered', e)"
@reorganized="e => $emit('reorganized', e)" @reorganized="e => $emit('reorganized', e)"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@@ -80,6 +83,7 @@
import { canBeParent } from '/imports/api/parenting/parenting.js'; import { canBeParent } from '/imports/api/parenting/parenting.js';
import { getPropertyIcon } from '/imports/constants/PROPERTIES.js'; import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import { some } from 'lodash';
export default { export default {
name: 'TreeNode', name: 'TreeNode',
@@ -87,16 +91,33 @@
TreeNodeView, TreeNodeView,
}, },
props: { props: {
node: Object, node: {
group: String, type: Object,
required: true,
},
group: {
type: String,
required: true,
},
organize: Boolean, organize: Boolean,
children: Array, children: {
getChildren: Function, type: Array,
selectedNodeId: String, default: () => [],
},
getChildren: {
type: Function,
default: undefined,
},
selectedNode: {
type: Object,
default: undefined,
},
selected: Boolean, selected: Boolean,
}, },
data(){ return { data(){return {
expanded: false, expanded: this.node._ancestorOfMatchedDocument ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
false,
}}, }},
computed: { computed: {
hasChildren(){ hasChildren(){
@@ -119,6 +140,15 @@
return canBeParent(this.node.type); return canBeParent(this.node.type);
}, },
}, },
watch: {
'node._ancestorOfMatchedDocument'(value){
this.expanded = !!value ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id);
},
'selectedNode.ancestors'(value){
this.expanded = !!some(value, ref => ref.id === this.node._id) || this.expanded;
},
},
beforeCreate() { beforeCreate() {
this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default this.$options.components.TreeNodeList = require('./TreeNodeList.vue').default
}, },
@@ -148,9 +178,12 @@
.empty .v-btn { .empty .v-btn {
opacity: 0.4; opacity: 0.4;
} }
.found {
background: rgba(200, 0, 0, 0.1) !important;
}
.ghost { .ghost {
opacity: 0.5; opacity: 0.5;
background: #fbc8c8; background: rgba(251, 0, 0, 0.3);
} }
.v-icon.v-icon--disabled { .v-icon.v-icon--disabled {
opacity: 0; opacity: 0;

View File

@@ -18,8 +18,9 @@
:node="child.node" :node="child.node"
:children="child.children" :children="child.children"
:group="group" :group="group"
:selected-node-id="selectedNodeId" :selected-node="selectedNode"
:selected="selectedNodeId === child.node._id" :selected="selectedNode && selectedNode._id === child.node._id"
:ancestors-of-selected-node="ancestorsOfSelectedNode"
:organize="organize" :organize="organize"
:lazy="lazy" :lazy="lazy"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
@@ -49,7 +50,14 @@
type: Array, type: Array,
default: () => [], default: () => [],
}, },
selectedNodeId: String, selectedNode: {
type: Object,
default: undefined,
},
ancestorsOfSelectedNode: {
type: Array,
default: () => [],
},
}, },
data(){ return { data(){ return {
expanded: false, expanded: false,

View File

@@ -0,0 +1,81 @@
<template lang="html">
<v-combobox
v-model="filterTerms"
:items="filterOptions"
prepend-inner-icon="mdi-magnify"
hide-no-data
hide-selected
multiple
clearable
small-chips
deletable-chips
/>
</template>
<script lang="js">
export default {
props: {
value: {
type: Object,
default: undefined,
},
},
data(){return {
filterTerms: [],
filterOptions: [
{text: 'Actions', value: 'action'},
{text: 'Attacks', value: 'attack'},
{text: 'Attributes', value: 'attribute'},
{text: 'Buffs', value: 'buff'},
{text: 'Class Levels', value: 'classLevel'},
{text: 'Damage Multipliers', value: 'damageMultiplier'},
{text: 'Effects', value: 'effect'},
{text: 'Experiences', value: 'experience'},
{text: 'Features', value: 'feature'},
{text: 'Folders', value: 'folder'},
{text: 'Notes', value: 'note'},
{text: 'Proficiencies', value: 'proficiency'},
{text: 'Rolls', value: 'roll'},
{text: 'Saving Throws', value: 'savingThrow'},
{text: 'Skills', value: 'skill'},
{text: 'Spell Lists', value: 'spellList'},
{text: 'Spells', value: 'spell'},
{text: 'Containers', value: 'container'},
{text: 'Items', value: 'item'},
],
}},
computed: {
filter(){
if (!this.filterTerms.length) return;
let typeFilters = [];
let nameFilters = [];
this.filterTerms.forEach(filter => {
if (filter.value){
typeFilters.push(filter.value);
} else {
// escape string
let term = filter.replace( /[-/\\^$*+?.()|[\]{}]/g, '\\$&' );
var reg = new RegExp( '.*' + term + '.*', 'i' );
nameFilters.push(reg)
}
});
let filter = {};
if (typeFilters.length){
filter.type = {$in: typeFilters};
}
if (nameFilters.length){
filter.name = {$in: nameFilters};
}
return filter;
},
},
watch:{
filter(value){
this.$emit('input', value);
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -29,11 +29,11 @@
</template> </template>
<script lang="js"> <script lang="js">
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import {updateCreature} from '/imports/api/creature/Creatures.js'; import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import CreatureForm from '/imports/ui/creature/CreatureForm.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'; import ColorPicker from '/imports/ui/components/ColorPicker.vue';
export default { 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" @click="rest"
> >
<v-icon left> <v-icon left>
{{ type === 'shortRest' ? 'snooze' : 'bedtime' }} {{ type === 'shortRest' ? 'mdi-music-rest-quarter' : 'mdi-bed' }}
</v-icon> </v-icon>
{{ type === 'shortRest' ? 'Short Rest' : 'Long Rest' }} {{ type === 'shortRest' ? 'Short Rest' : 'Long Rest' }}
</v-btn> </v-btn>
</template> </template>
<script lang="js"> <script lang="js">
import restCreature from '/imports/api/creature/restCreature.js'; import restCreature from '/imports/api/creature/creatures/methods/restCreature.js';
export default { export default {
inject: { 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,9 @@
</template> </template>
<script lang="js"> <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 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'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {

View File

@@ -69,14 +69,14 @@
<script lang="js"> <script lang="js">
//TODO add a "no character found" screen if shown on a false address //TODO add a "no character found" screen if shown on a false address
// or on a character the user does not have permission to view // or on a character the user does not have permission to view
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue'; import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue';
import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue'; import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue'; import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue'; import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue'; import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue';
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue'; import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js'; import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';

View File

@@ -20,7 +20,7 @@
style="transition: transform 0.2s ease-in-out" style="transition: transform 0.2s ease-in-out"
:style="fab && 'transform: rotate(45deg)'" :style="fab && 'transform: rotate(45deg)'"
> >
add mdi-plus
</v-icon> </v-icon>
</transition> </transition>
</v-btn> </v-btn>
@@ -31,41 +31,22 @@
:key="type" :key="type"
color="primary" color="primary"
:data-id="`insert-creature-property-type-${type}`" :data-id="`insert-creature-property-type-${type}`"
:label="'New ' + properties[type].name" :label="type ? 'New ' + properties[type].name : 'New Property'"
:icon="properties[type].icon" :icon="type ? properties[type].icon : 'mdi-plus'"
:disabled="!editPermission" :disabled="!editPermission"
@click="insertPropertyOfType(type)" @click="addProperty(type)"
/> />
<template v-if="tabNumber === 5">
<labeled-fab
key="property"
color="primary"
data-id="insert-creature-property-btn"
label="New Property"
icon="create"
:disabled="!editPermission"
@click="insertTreeProperty"
/>
<labeled-fab
key="property"
color="primary"
data-id="insert-creature-property-from-library-btn"
label="Property From Library"
icon="book"
:disabled="!editPermission"
@click="propertyFromLibrary"
/>
</template>
</v-speed-dial> </v-speed-dial>
</template> </template>
<script lang="js"> <script lang="js">
import LabeledFab from '/imports/ui/components/LabeledFab.vue'; import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { getHighestOrder } from '/imports/api/parenting/order.js'; import { getHighestOrder } from '/imports/api/parenting/order.js';
import insertProperty, { insertPropertyAsChildOfTag } from '/imports/api/creature/creatureProperties/methods/insertProperty.js'; import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js'; import PROPERTIES from '/imports/constants/PROPERTIES.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function getParentAndOrderFromSelectedTreeNode(creatureId){ function getParentAndOrderFromSelectedTreeNode(creatureId){
// find the parent based on the currently selected property // find the parent based on the currently selected property
@@ -149,93 +130,50 @@
'inventory': ['item', 'container'], 'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'], 'spells': ['spellList', 'spell'],
'character': ['note'], 'character': ['note'],
'tree': [], 'tree': [null],
};}, };},
properties(){ properties(){
return PROPERTIES; return PROPERTIES;
}, },
}, },
methods: { methods: {
insertPropertyOfType(type){ addProperty(forcedType){
let creatureId = this.creatureId; let creatureId = this.creatureId;
let fab = hideFab(); let fab = hideFab();
// Open the dialog to insert the property let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
let parent;
try {
parent = fetchDocByRef(parentRef);
} catch (e) {
console.warn(e);
}
this.$store.commit('pushDialogStack', { this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog', component: 'add-creature-property-dialog',
elementId: 'insert-creature-property-type-' + type, elementId: 'insert-creature-property-type-' + forcedType,
data: { data: {
forcedType: type, parentDoc: forcedType ? undefined : parent,
forcedType,
}, },
callback(creatureProperty){ callback(result){
if (!creatureProperty) return 'insert-creature-property-fab'; if (!result){
revealFab(fab); return 'insert-creature-property-fab';
}
// Insert the property if (Array.isArray(result)){
creatureProperty.order = getHighestOrder({ revealFab(fab);
collection: CreatureProperties, let nodeIds = result;
ancestorId: creatureId let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
}) + 1; return forcedType ? id : `tree-node-${id}`;
} else {
let tagDetails; revealFab(fab);
switch (type){ let creatureProperty = result;
case 'item': // Get order and parent
tagDetails = {tag: 'carried', name: 'Carried'}; creatureProperty.order = order;
break; // Insert the property
case 'container': let id = insertProperty.call({creatureProperty, parentRef});
tagDetails = {tag: 'inventory', name: 'Inventory'}; return forcedType ? id : `tree-node-${id}`;
break;
default:
tagDetails = {tag: `${type}s`};
break;
} }
let id = insertPropertyAsChildOfTag.call({
creatureProperty,
creatureId,
tag: tagDetails.tag,
tagDefaultName: tagDetails.name,
});
return id;
}
});
},
insertTreeProperty(){
let creatureId = this.creatureId;
let fab = hideFab();
// Open the dialog to insert the property
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-btn',
callback(creatureProperty){
if (!creatureProperty) return 'insert-creature-property-fab';
revealFab(fab);
// Get order and parent
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
creatureProperty.order = order;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return `tree-node-${id}`;
}
});
},
propertyFromLibrary(){
let creatureId = this.creatureId;
let fab = hideFab();
this.$store.commit('pushDialogStack', {
component: 'creature-property-from-library-dialog',
elementId: 'insert-creature-property-from-library-btn',
callback(libraryNode){
if (!libraryNode) return 'insert-creature-property-fab';
revealFab(fab);
let nodeId = libraryNode._id;
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
let id = insertPropertyFromLibraryNode.call({nodeId, parentRef, order});
return `tree-node-${id}`;
} }
}); });
}, },

View File

@@ -26,6 +26,7 @@
> >
<div :key="$route.meta.title"> <div :key="$route.meta.title">
<template v-if="creature"> <template v-if="creature">
<shared-icon :model="creature" />
<v-menu <v-menu
bottom bottom
left left
@@ -37,30 +38,30 @@
icon icon
v-on="on" v-on="on"
> >
<v-icon>more_vert</v-icon> <v-icon>mdi-dots-vertical</v-icon>
</v-btn> </v-btn>
</template> </template>
<v-list v-if="editPermission"> <v-list v-if="editPermission">
<v-list-item @click="deleteCharacter"> <v-list-item @click="deleteCharacter">
<v-list-item-title> <v-list-item-title>
<v-icon>delete</v-icon> Delete <v-icon>mdi-delete</v-icon> Delete
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="showCharacterForm"> <v-list-item @click="showCharacterForm">
<v-list-item-title> <v-list-item-title>
<v-icon>create</v-icon> Edit details <v-icon>mdi-pencil</v-icon> Edit details
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
<v-list-item @click="showShareDialog"> <v-list-item @click="showShareDialog">
<v-list-item-title> <v-list-item-title>
<v-icon>share</v-icon> Sharing <v-icon>mdi-share-variant</v-icon> Sharing
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
<v-list v-else> <v-list v-else>
<v-list-item @click="unshareWithMe"> <v-list-item @click="unshareWithMe">
<v-list-item-title> <v-list-item-title>
<v-icon>delete</v-icon> Unshare with me <v-icon>mdi-delete</v-icon> Unshare with me
</v-list-item-title> </v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -122,14 +123,15 @@
</template> </template>
<script lang="js"> <script lang="js">
import Creatures from '/imports/api/creature/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import removeCreature from '/imports/api/creature/removeCreature.js'; import removeCreature from '/imports/api/creature/creatures/methods/removeCreature.js';
import { mapMutations } from 'vuex'; import { mapMutations } from 'vuex';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js'; import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js'; import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import CharacterSheetFab from '/imports/ui/creature/character/CharacterSheetFab.vue'; import CharacterSheetFab from '/imports/ui/creature/character/CharacterSheetFab.vue';
import getThemeColor from '/imports/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import SharedIcon from '/imports/ui/components/SharedIcon.vue';
export default { export default {
inject: { inject: {
@@ -137,6 +139,7 @@ export default {
}, },
components: { components: {
CharacterSheetFab, CharacterSheetFab,
SharedIcon,
}, },
computed: { computed: {
creatureId(){ creatureId(){

View File

@@ -73,7 +73,7 @@
data-id="experience-info-button" data-id="experience-info-button"
@click="showExperienceList" @click="showExperienceList"
> >
<v-icon>info</v-icon> <v-icon>mdi-information-outline</v-icon>
</v-btn> </v-btn>
</v-list-item-action> </v-list-item-action>
<v-list-item-action> <v-list-item-action>
@@ -82,7 +82,7 @@
data-id="experience-add-button" data-id="experience-add-button"
@click="addExperience" @click="addExperience"
> >
<v-icon>add</v-icon> <v-icon>mdi-plus</v-icon>
</v-btn> </v-btn>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
@@ -115,7 +115,7 @@
</template> </template>
<script lang="js"> <script lang="js">
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 CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue'; import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';

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