Compare commits

...

68 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
207 changed files with 4528 additions and 1950 deletions

View File

@@ -66,5 +66,24 @@ You should see this:
=> App running at: http://localhost:3000/
```
Environmental Variables
-----------------------
```
MAIL_URL=smtp://<your smtp mail url>
METEOR_SETTINGS={ "public": { "environment": "production", "patreon": { "clientId": "<your patreon client ID>", "campaignId": "<your campaign id>" } }, "patreon": { "clientSecret": "<your client secret>", "creatorAccessToken": "<your creator access token>" } }
MONGO_OPLOG_URL=mongodb+srv://<your url for the oplog account of your mongo database>
MONGO_URL=mongodb+srv://<your url for the read/write account of your mongo database>
NPM_CONFIG_PRODUCTION=true
PROJECT_DIR=app
ROOT_URL=https://<url of your DiceCloud instance>
DEFAULT_LIBRARIES=<comma separated list of library ids that will be subscribed by default: "abc123,def456">
```
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
DiceCloud running.

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
<head>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
<meta name="viewport" content="width=device-width initial-scale=1.0, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">

View File

@@ -1,3 +1,4 @@
import '/imports/ui/vueSetup.js';
import '/imports/ui/styles/stylesIndex.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}){
forest.forEach(node => {
let prop = node.node;
options.actionContext[`#${prop.type}`] = prop;
let children = node.children;
if (shouldSplit(prop) && targets.length){
targets.forEach(target => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
export default class EffectAggregator{
constructor(){
this.base = 0;
this.base = undefined;
this.add = 0;
this.mul = 1;
this.min = Number.NEGATIVE_INFINITY;
@@ -20,7 +20,13 @@ export default class EffectAggregator{
switch(effect.operation){
case 'base':
// 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;
case 'add':
// Add all adds together

View File

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

View File

@@ -93,6 +93,7 @@ export default function computeStat(stat, memo){
// Compute each active stat's baseValue calculation and apply it
if (!statInstance.inactive) {
delete statInstance.baseValueErrors;
let {
result,
context,
@@ -102,24 +103,31 @@ export default function computeStat(stat, memo){
prop: statInstance,
memo
});
statInstance.baseValue = +result.value;
result.value = +result.value;
if (!isNaN(result.value)){
statInstance.baseValue = result.value;
} else {
statInstance.baseValue = undefined;
}
statInstance.dependencies = union(statInstance.dependencies, dependencies);
if (context.errors.length){
statInstance.baseValueErrors = context.errors;
}
// Apply all the base values
effects.push({
operation: 'base',
calculation: statInstance.baseValueCalculation,
result: statInstance.baseValue,
stats: [statInstance.variableName],
dependencies: statInstance.overridden ?
if (Number.isFinite(statInstance.baseValue)){
effects.push({
operation: 'base',
calculation: statInstance.baseValueCalculation,
result: statInstance.baseValue,
stats: [statInstance.variableName],
dependencies: statInstance.overridden ?
union(statInstance.dependencies, [statInstance._id]) :
[],
computationDetails: {
computed: true,
},
});
computationDetails: {
computed: true,
},
});
}
}
});

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
import SimpleSchema from 'simpl-schema';
let CreatureFolders = new Mongo.Collection('creatureFolders');
let creatureFolderSchema = new SimpleSchema({
name: {
type: String,
trim: false,
optional: true,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
archived: {
type: Boolean,
optional: true,
},
order: {
type: Number,
defaultValue: 0,
},
});
CreatureFolders.attachSchema(creatureFolderSchema);
import '/imports/api/creature/creatureFolders/methods.js/index.js';
export default CreatureFolders;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,11 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
validate: new SimpleSchema({
nodeId: {
nodeIds: {
type: Array,
max: 20,
},
'nodeIds.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
@@ -38,7 +42,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({nodeId, parentRef, order}) {
run({nodeIds, parentRef, order}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
@@ -53,54 +57,15 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
}
assertEditPermission(rootCreature, this.userId);
// 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();
// {libraryId: hasViewPermission}
//let libraryPermissionMemoir = {};
let node;
nodeIds.forEach(nodeId => {
// TODO: Check library view permission for each node before starting
node = insertPropertyFromNode(nodeId, ancestors, order);
});
// 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);
// get the root inserted doc
// get one of the root inserted docs
let rootId = node._id;
// Tree structure changed by inserts, reorder the tree
@@ -110,7 +75,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
});
// 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
recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute
@@ -120,6 +85,56 @@ 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){
@@ -192,6 +207,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
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,

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 deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
import 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
let Creatures = new Mongo.Collection('creatures');
@@ -176,92 +167,8 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
const insertCreature = new ValidatedMethod({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
assertUserHasPaidBenefits(this.userId);
// Create the creature document
let creatureId = Creatures.insert({
owner: this.userId,
});
// 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},
});
}
},
});
import '/imports/api/creature/creatures/methods/index.js';
export default Creatures;
export { CreatureSchema, insertCreature, updateCreature };
export { CreatureSchema };

View File

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

View File

@@ -0,0 +1,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 { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ function updateReferenceNodeWork(node, userId){
return;
}
cache = {
node: {name: doc.name, type: doc.type},
node: doc,
};
if (library){
cache.library = {name: library.name};

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({
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 getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { flatten, findLast } from 'lodash';
import { flatten } from 'lodash';
const generalParents = [
'attribute',
@@ -217,41 +217,3 @@ export function getName(doc){
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

@@ -34,6 +34,14 @@ let ReferenceSchema = new SimpleSchema({
'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,

View File

@@ -1,6 +1,5 @@
import SimpleSchema from 'simpl-schema';
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';
let SpellListSchema = new SimpleSchema({
@@ -12,12 +11,6 @@ let SpellListSchema = new SimpleSchema({
type: String,
optional: true,
},
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
optional: true,
},
// Calculation of how many spells in this list can be prepared
maxPrepared: {
type: String,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,104 +1,157 @@
const PROPERTIES = Object.freeze({
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: {
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: {
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: {
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: {
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: {
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: {
icon: 'anchor',
name: 'Constant'
icon: 'mdi-anchor',
name: 'Constant',
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
suggestedParents: [],
},
container: {
icon: 'work',
name: 'Container'
icon: 'mdi-bag-personal-outline',
name: 'Container',
helpText: 'A container holds items in the inventory',
examples: 'Coin pouch, backpack',
suggestedParents: ['folder'],
},
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: {
icon: '$vuetify.icons.damage_multiplier',
name: 'Damage multiplier'
name: 'Damage multiplier',
helpText: 'Resistance, vulnerability, and immunity.',
suggestedParents: ['classLevel', 'feature', 'item'],
},
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: {
icon: 'subject',
name: 'Feature'
icon: 'mdi-text-subject',
name: 'Feature',
helpText: 'Descriptive or narrative features your character has access to',
suggestedParents: ['classLevel', 'folder'],
},
folder: {
icon: 'folder',
name: 'Folder'
icon: 'mdi-folder-outline',
name: 'Folder',
helpText: 'A way to organise other properties on the character',
suggestedParents: ['folder'],
},
item: {
icon: '$vuetify.icons.item',
name: 'Item'
icon: 'mdi-cube-outline',
name: 'Item',
helpText: 'Objects and equipment your charcter finds on their adventures',
suggestedParents: ['container'],
},
note: {
icon: 'note',
name: 'Note'
icon: 'mdi-note-outline',
name: 'Note',
helpText: 'Notes about your character and their adventures',
suggestedParents: ['folder'],
},
proficiency: {
icon: 'radio_button_checked',
name: 'Proficiency'
icon: 'mdi-brightness-1',
name: 'Proficiency',
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
},
roll: {
icon: '$vuetify.icons.roll',
name: 'Roll'
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: 'link',
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: {
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: {
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: {
icon: 'tab_unselected',
name: 'Slot'
icon: 'mdi-power-socket-eu',
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: {
icon: 'picture_in_picture',
name: 'Slot filler'
icon: 'mdi-power-plug-outline',
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: {
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: {
icon: '$vuetify.icons.spell',
name: 'Spell'
name: 'Spell',
helpText: 'A spell your character can potentially cast',
suggestedParents: ['spellList'],
},
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 Parties from '/imports/api/creature/Parties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
Meteor.publish('characterList', function(){
this.autorun(function (){
@@ -36,7 +36,7 @@ Meteor.publish('characterList', function(){
}
}
),
Parties.find({owner: userId}),
CreatureFolders.find({owner: userId}),
];
});
});

View File

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

View File

@@ -6,5 +6,7 @@ import '/imports/server/publications/experiences.js';
import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'
import '/imports/server/publications/ownedDocuments.js'
import '/imports/server/publications/slotFillers.js';
import '/imports/server/publications/ownedDocuments.js';
import '/imports/server/publications/archivedCreatures.js';
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 LibraryNodes from '/imports/api/library/LibraryNodes.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(){
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';
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 Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import VERSION from '/imports/constants/VERSION.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
<v-text-field
:value="formattedSafeValue"
v-bind="$attrs"
prepend-icon="event"
prepend-icon="mdi-calendar"
readonly
:loading="loading"
:error-messages="errors"

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,8 +18,9 @@
:node="child.node"
:children="child.children"
:group="group"
:selected-node-id="selectedNodeId"
:selected="selectedNodeId === child.node._id"
:selected-node="selectedNode"
:selected="selectedNode && selectedNode._id === child.node._id"
:ancestors-of-selected-node="ancestorsOfSelectedNode"
:organize="organize"
:lazy="lazy"
@selected="e => $emit('selected', e)"
@@ -49,7 +50,14 @@
type: Array,
default: () => [],
},
selectedNodeId: String,
selectedNode: {
type: Object,
default: undefined,
},
ancestorsOfSelectedNode: {
type: Array,
default: () => [],
},
},
data(){ return {
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>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import {updateCreature} from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import CreatureForm from '/imports/ui/creature/CreatureForm.vue'
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
export default {

View File

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

View File

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

View File

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

View File

@@ -31,9 +31,9 @@
</template>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import removeCreature from '/imports/api/creature/removeCreature.js';
import removeCreature from '/imports/api/creature/creatures/methods/removeCreature.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {

View File

@@ -69,14 +69,14 @@
<script lang="js">
//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
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 FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/CharacterTab.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 { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';

View File

@@ -20,7 +20,7 @@
style="transition: transform 0.2s ease-in-out"
:style="fab && 'transform: rotate(45deg)'"
>
add
mdi-plus
</v-icon>
</transition>
</v-btn>
@@ -31,41 +31,22 @@
:key="type"
color="primary"
:data-id="`insert-creature-property-type-${type}`"
:label="'New ' + properties[type].name"
:icon="properties[type].icon"
:label="type ? 'New ' + properties[type].name : 'New Property'"
:icon="type ? properties[type].icon : 'mdi-plus'"
: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>
</template>
<script lang="js">
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
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 PROPERTIES from '/imports/constants/PROPERTIES.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function getParentAndOrderFromSelectedTreeNode(creatureId){
// find the parent based on the currently selected property
@@ -149,93 +130,50 @@
'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'],
'character': ['note'],
'tree': [],
'tree': [null],
};},
properties(){
return PROPERTIES;
},
},
methods: {
insertPropertyOfType(type){
addProperty(forcedType){
let creatureId = this.creatureId;
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', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-type-' + type,
component: 'add-creature-property-dialog',
elementId: 'insert-creature-property-type-' + forcedType,
data: {
forcedType: type,
parentDoc: forcedType ? undefined : parent,
forcedType,
},
callback(creatureProperty){
if (!creatureProperty) return 'insert-creature-property-fab';
revealFab(fab);
// Insert the property
creatureProperty.order = getHighestOrder({
collection: CreatureProperties,
ancestorId: creatureId
}) + 1;
let tagDetails;
switch (type){
case 'item':
tagDetails = {tag: 'carried', name: 'Carried'};
break;
case 'container':
tagDetails = {tag: 'inventory', name: 'Inventory'};
break;
default:
tagDetails = {tag: `${type}s`};
break;
callback(result){
if (!result){
return 'insert-creature-property-fab';
}
if (Array.isArray(result)){
revealFab(fab);
let nodeIds = result;
let id = insertPropertyFromLibraryNode.call({nodeIds, parentRef, order});
return forcedType ? id : `tree-node-${id}`;
} else {
revealFab(fab);
let creatureProperty = result;
// Get order and parent
creatureProperty.order = order;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return forcedType ? id : `tree-node-${id}`;
}
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">
<template v-if="creature">
<shared-icon :model="creature" />
<v-menu
bottom
left
@@ -37,30 +38,30 @@
icon
v-on="on"
>
<v-icon>more_vert</v-icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list v-if="editPermission">
<v-list-item @click="deleteCharacter">
<v-list-item-title>
<v-icon>delete</v-icon> Delete
<v-icon>mdi-delete</v-icon> Delete
</v-list-item-title>
</v-list-item>
<v-list-item @click="showCharacterForm">
<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>
<v-list-item @click="showShareDialog">
<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>
</v-list>
<v-list v-else>
<v-list-item @click="unshareWithMe">
<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>
</v-list>
@@ -122,14 +123,15 @@
</template>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import removeCreature from '/imports/api/creature/removeCreature.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import removeCreature from '/imports/api/creature/creatures/methods/removeCreature.js';
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 isDarkColor from '/imports/ui/utility/isDarkColor.js';
import CharacterSheetFab from '/imports/ui/creature/character/CharacterSheetFab.vue';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import SharedIcon from '/imports/ui/components/SharedIcon.vue';
export default {
inject: {
@@ -137,6 +139,7 @@ export default {
},
components: {
CharacterSheetFab,
SharedIcon,
},
computed: {
creatureId(){

View File

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

View File

@@ -15,7 +15,7 @@
</v-list-item-content>
<v-list-item-action>
<v-list-item-title>
{{ creature.denormalizedStats.weightCarried || 0 }} lb
{{ weightCarried }} lb
</v-list-item-title>
</v-list-item-action>
</v-list-item>
@@ -99,7 +99,7 @@
<script lang="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 ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import ContainerCard from '/imports/ui/properties/components/inventory/ContainerCard.vue';
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
@@ -107,6 +107,7 @@ import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import stripFloatingPointOddities from '/imports/ui/utility/stripFloatingPointOddities.js';
export default {
components: {
@@ -183,16 +184,35 @@ export default {
});
},
equipmentParentRef(){
return getParentRefByTag(this.creatureId, BUILT_IN_TAGS.equipment);
return getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.equipment
) || getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.inventory
) || {
id: this.creatureId,
collection: 'creatures'
};
},
carriedParentRef(){
return getParentRefByTag(this.creatureId, BUILT_IN_TAGS.carried);
return getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.carried
) || getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.inventory
) || {
id: this.creatureId,
collection: 'creatures'
};
},
},
computed: {
containerIds(){
return this.containers.map(container => container._id);
},
weightCarried(){
return stripFloatingPointOddities(
this.creature.denormalizedStats.weightCarried || 0
);
},
},
methods: {
clickProperty(_id){

View File

@@ -45,7 +45,7 @@
icon
@click.stop="softRemove(buff._id)"
>
<v-icon>delete</v-icon>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
@@ -320,7 +320,7 @@
</template>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';

View File

@@ -23,19 +23,11 @@
:disabled="organizeDisabled"
style="flex-grow: 0; height: 32px;"
/>
<v-combobox
<tree-search-input
ref="searchBox"
slot="extension"
v-model="filterString"
:items="filterOptions"
prepend-inner-icon="search"
v-model="filter"
class="mx-4"
hide-no-data
hide-selected
multiple
clearable
small-chips
deletable-chips
/>
</v-toolbar>
<creature-properties-tree
@@ -43,7 +35,7 @@
style="overflow-y: auto;"
:root="{collection: 'creatures', id: creatureId}"
:organize="organize"
:selected-node-id="selected"
:selected-node="selectedNode"
:filter="filter"
@selected="clickNode"
/>
@@ -51,9 +43,9 @@
<template slot="detail">
<creature-property-dialog
embedded
:_id="selected"
@removed="selected = undefined"
@duplicated="id => selected = id"
:_id="selectedNodeId"
@removed="selectedNodeId = undefined"
@duplicated="id => selectedNodeId = id"
/>
</template>
</tree-detail-layout>
@@ -65,13 +57,14 @@
import TreeDetailLayout from '/imports/ui/components/TreeDetailLayout.vue';
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
import TreeSearchInput from '/imports/ui/components/tree/TreeSearchInput.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
export default {
components: {
TreeDetailLayout,
TreeSearchInput,
CreaturePropertiesTree,
CreaturePropertyDialog,
},
@@ -87,52 +80,10 @@
data(){ return {
organize: false,
organizeDisabled: false,
selected: undefined,
selectedNodeId: undefined,
fab: false,
filterString: '',
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'},
],
filter: undefined,
};},
computed: {
filter(){
if (!this.filterString.length) return;
let typeFilters = [];
let nameFilters = [];
this.filterString.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)
}
});
return {$or: [
{type: {$in: typeFilters}},
{name: {$in: nameFilters}},
]};
},
},
watch: {
filter(filter){
if (filter) {
@@ -144,14 +95,14 @@
},
'$vuetify.breakpoint.mdAndUp'(mdAndUp){
if (!mdAndUp){
this.selected = undefined;
this.selectedNodeId = undefined;
}
},
},
methods: {
clickNode(id){
if (this.$vuetify.breakpoint.mdAndUp){
this.selected = id;
this.selectedNodeId = id;
} else {
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
@@ -167,7 +118,7 @@
component: 'creature-property-dialog',
elementId: 'selected-node-card',
data: {
_id: this.selected,
_id: this.selectedNodeId,
startInEditTab: true,
},
});
@@ -175,9 +126,9 @@
getPropertyName,
},
meteor: {
selectedProperty(){
selectedNode(){
return CreatureProperties.findOne({
_id: this.selected,
_id: this.selectedNodeId,
removed: {$ne: true}
});
}

View File

@@ -0,0 +1,38 @@
<template lang="html">
<v-btn
:icon="!text"
:text="text"
:data-id="randomId"
v-bind="$attrs"
@click="openArchive"
>
<template v-if="text">
Archive Characters
</template>
<v-icon :right="text">
mdi-archive
</v-icon>
</v-btn>
</template>
<script lang="js">
export default {
props: {
text: Boolean,
},
data(){return {
randomId: Random.id(),
}},
methods: {
openArchive(){
this.$store.commit('pushDialogStack', {
component: 'archive-dialog',
elementId: this.randomId,
});
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,35 @@
<template lang="html">
<div>
{{ creatureCount }} /
<v-icon v-if="characterSlots === -1">
mdi-infinity
</v-icon>
<template v-else>
{{ characterSlots }}
</template>
<archive-button />
</div>
</template>
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
import ArchiveButton from '/imports/ui/creature/creatureList/ArchiveButton.vue';
export default {
components: {
ArchiveButton,
},
meteor: {
creatureCount(){
return Creatures.find({owner: Meteor.userId()}).count();
},
characterSlots(){
return getUserTier(Meteor.userId()).characterSlots;
}
},
}
</script>
<style lang="css">
</style>

View File

@@ -0,0 +1,108 @@
<template lang="html">
<v-list-item style="min-height: 60px;">
<v-list-item-content>
<v-list-item-title>
<template v-if="!renaming">
{{ model.name }}
</template>
<text-field
v-if="renaming"
ref="name-input"
regular
hide-details
dense
:value="model.name"
@change="renameFolder"
@click.native.stop=""
@input.native.stop=""
@keydown.native.stop=""
@keyup.native.stop=""
/>
</v-list-item-title>
</v-list-item-content>
<template v-if="!selection && !dense">
<v-list-item-action v-if="renaming || open">
<v-btn
icon
style="flex-grow: 0"
@click.stop="renaming = !renaming"
>
<v-icon v-if="renaming">
mdi-check
</v-icon>
<v-icon v-else>
mdi-pencil
</v-icon>
</v-btn>
</v-list-item-action>
<v-list-item-action v-if="open">
<v-btn
icon
style="flex-grow: 0"
@click.stop="removeFolder"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</template>
</v-list-item>
</template>
<script lang="js">
import Vue from 'vue';
import updateCreatureFolderName from '/imports/api/creature/creatureFolders/methods.js/updateCreatureFolderName.js';
import removeCreatureFolder from '/imports/api/creature/creatureFolders/methods.js/removeCreatureFolder.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
props: {
model: {
type: Object,
required: true,
},
open: Boolean,
selection: Boolean,
dense: Boolean,
},
data(){return {
renaming: false,
}},
watch: {
renaming(value){
if (!value) return;
Vue.nextTick(() => {
this.$refs['name-input'].focus();
});
},
},
methods:{
renameFolder(name, ack){
updateCreatureFolderName.call({
_id: this.model._id,
name
}, error => {
ack(error);
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
removeFolder(){
removeCreatureFolder.call({
_id: this.model._id
}, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
},
}
}
</script>
<style lang="css" scoped>
</style>

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