Merge branch 'version-2-dev' into version-2

This commit is contained in:
Stefan Zermatten
2021-07-13 12:45:59 +02:00
151 changed files with 2175 additions and 808 deletions

View File

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

View File

@@ -3,42 +3,42 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.7.0
random@1.2.0
accounts-password
random
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
reactive-var
underscore
momentjs:moment
dburles:mongo-collection-instances
accounts-google@1.3.3
email@2.0.0
accounts-google
email
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
mobile-experience@1.1.0
mongo@1.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,28 +0,0 @@
import SimpleSchema from 'simpl-schema';
let Parties = new Mongo.Collection('parties');
let partySchema = new SimpleSchema({
name: {
type: String,
defaultValue: 'New Party',
trim: false,
optional: true,
},
creatures: {
type: Array,
defaultValue: [],
},
'creatures.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
owner: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Parties.attachSchema(partySchema);
export default Parties;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

@@ -14,7 +14,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;
@@ -137,7 +144,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

@@ -102,24 +102,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

@@ -192,6 +192,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({
nodeId: 'iHbhfcg3AL5isSWbw',
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
}
return creatureId;
},
});
export default insertCreature;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,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-search"
clearable
hide-details
class="ma-2"
:value="searchString"
@change="search"
/>
<v-btn
icon
text
@click="select()"
>
<v-icon>
cancel
</v-icon>
clear
</v-btn>
</div>
<v-layout

View File

@@ -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

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

View File

@@ -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>
@@ -42,7 +42,7 @@
color="primary"
data-id="insert-creature-property-btn"
label="New Property"
icon="create"
icon="mdi-pencil"
:disabled="!editPermission"
@click="insertTreeProperty"
/>
@@ -51,7 +51,7 @@
color="primary"
data-id="insert-creature-property-from-library-btn"
label="Property From Library"
icon="book"
icon="mdi-library-shelves"
:disabled="!editPermission"
@click="propertyFromLibrary"
/>

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

@@ -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';
@@ -183,10 +183,24 @@ 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: {

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

@@ -28,7 +28,7 @@
slot="extension"
v-model="filterString"
:items="filterOptions"
prepend-inner-icon="search"
prepend-inner-icon="mdi-search"
class="mx-4"
hide-no-data
hide-selected

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>

View File

@@ -0,0 +1,70 @@
<template lang="html">
<v-list
expand
>
<creature-list
:creatures="creatures"
:selection="selection"
:selected-creature="selectedCreature"
:dense="dense"
@creature-selected="id => $emit('creature-selected', id)"
/>
<v-list-group
v-for="folder in folders"
:key="folder._id"
v-model="openFolders[folder._id]"
group="folder"
>
<template #activator>
<creature-folder-header
:open="openFolders[folder._id]"
:model="folder"
:selection="selection"
:dense="dense"
/>
</template>
<creature-list
:creatures="folder.creatures"
:folder-id="folder._id"
:selection="selection"
:selected-creature="selectedCreature"
:dense="dense"
@creature-selected="id => $emit('creature-selected', id)"
/>
</v-list-group>
</v-list>
</template>
<script lang="js">
import CreatureFolderHeader from '/imports/ui/creature/creatureList/CreatureFolderHeader.vue';
import CreatureList from '/imports/ui/creature/creatureList/CreatureList.vue';
export default {
components: {
CreatureFolderHeader,
CreatureList,
},
props:{
creatures: {
type: Array,
default: () => [],
},
folders: {
type: Array,
default: () => [],
},
selection: Boolean,
selectedCreature: {
type: String,
default: undefined,
},
dense: Boolean,
},
data(){return{
openFolders: {},
}},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,103 @@
<template lang="html">
<draggable
v-model="dataCreatures"
style="min-height: 24px;"
:sort="false"
:group="`creature-list`"
ghost-class="ghost"
draggable=".creature"
handle=".handle"
:animation="200"
@change="draggableChange"
>
<creature-list-tile
v-for="creature in dataCreatures"
:key="creature._id"
class="creature"
:model="creature"
:selection="selection"
:is-selected="selectedCreature === creature._id"
v-bind="selection ? {} : {to: creature.url}"
:dense="dense"
@click="$emit('creature-selected', creature._id)"
/>
</draggable>
</template>
<script lang="js">
import CreatureListTile from '/imports/ui/creature/creatureList/CreatureListTile.vue';
import draggable from 'vuedraggable';
import moveCreatureToFolder from '/imports/api/creature/creatureFolders/methods.js/moveCreatureToFolder.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
CreatureListTile,
draggable,
},
props: {
creatures: {
type: Array,
required: true,
},
folderId: {
type: String,
default: null,
},
selection: Boolean,
selectedCreature: {
type: String,
default: undefined,
},
dense: Boolean,
},
data(){return {
dataCreatures: [],
}},
watch:{
creatures(newValue){
this.dataCreatures = newValue;
},
},
mounted(){
this.dataCreatures = this.creatures;
},
methods: {
draggableChange({added, moved}){
let event = added || moved;
if (event){
/*
// If this item is now adjacent to another, set the order accordingly
let order;
let before = this.dataCreatures[event.newIndex - 1];
let after = this.dataCreatures[event.newIndex + 1];
if (before && before._id){
order = before.order + 0.5;
} else if (after && after._id) {
order = after.order - 0.5;
} else {
order = -0.5;
}
*/
let doc = event.element;
moveCreatureToFolder.call({
creatureId: doc._id,
folderId: this.folderId
}, error => {
if (!error) return;
console.error(error);
snackbar({
text: error.reason,
});
});
}
},
selectionChange(index){
this.$emit('creatureSelected', this.dataCreatures[index]._id)
},
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,70 @@
<template
lang="html"
functional
>
<v-list-item
v-bind="$attrs"
:class="isSelected && 'primary--text v-list-item--active'"
v-on="selection ? { click() {$emit('click')} } : {}"
>
<v-list-item-avatar
:color="isSelected ? 'red darken-1' : model.color || 'grey'"
class="white--text"
style="transition: background 0.3s;"
>
<v-fade-transition leave-absolute>
<v-icon v-if="isSelected">
mdi-check
</v-icon>
<img
v-else-if="model.avatarPicture"
:src="model.avatarPicture"
:alt="model.name"
>
<template v-else>
<span>
{{ model.initial }}
</span>
</template>
</v-fade-transition>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-item-title>
<v-list-item-subtitle v-if="!dense">
{{ model.alignment }} {{ model.gender }} {{ model.race }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="!dense">
<shared-icon :model="model" />
</v-list-item-action>
<v-list-item-action v-if="!selection && !dense">
<v-icon
style="height: 100%; width: 40px; cursor: move;"
class="handle"
>
mdi-drag
</v-icon>
</v-list-item-action>
</v-list-item>
</template>
<script lang="js">
import SharedIcon from '/imports/ui/components/SharedIcon.vue';
export default {
components: {
SharedIcon,
},
props: {
model: {
type: Object,
required: true,
},
selection: Boolean,
isSelected: Boolean,
dense: Boolean,
}
}
</script>

View File

@@ -8,7 +8,7 @@
v-if="index !== 0"
:key="index"
>
chevron_right
mdi-chevron-right
</v-icon>
<span
v-if="noLinks"

View File

@@ -53,7 +53,7 @@
data-id="insert-creature-property-btn"
@click="addProperty"
>
<v-icon>add</v-icon>
<v-icon>mdi-plus</v-icon>
Property
</v-btn>
</template>
@@ -82,7 +82,7 @@ import softRemoveProperty from '/imports/api/creature/creatureProperties/methods
import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
import duplicateProperty from '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
import Creatures from '/imports/api/creature/Creatures.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import PropertyToolbar from '/imports/ui/components/propertyToolbar.vue';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
@@ -91,7 +91,7 @@ import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormI
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { get, findLast } from 'lodash';
import equipItem from '/imports/api/creature/creatureProperties/methods/equipItem.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';

View File

@@ -10,13 +10,13 @@
data-id="experience-add-button"
@click="addExperience"
>
<v-icon>add</v-icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
<v-btn
icon
@click="recompute"
>
<v-icon>refresh</v-icon>
<v-icon>mdi-refresh</v-icon>
</v-btn>
</template>
<div
@@ -72,11 +72,10 @@
<v-list-item-action>
<v-btn
icon
flat
:loading="experiencesRemovalLoading.has(experience._id)"
@click="removeExperience(experience._id)"
>
<v-icon>delete</v-icon>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>

View File

@@ -9,7 +9,7 @@
</v-toolbar-title>
<v-spacer />
<text-field
prepend-inner-icon="search"
prepend-inner-icon="mdi-search"
regular
hide-details
:value="searchValue"
@@ -153,7 +153,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 LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';

View File

@@ -31,7 +31,7 @@
small
@click.stop="remove(child)"
>
<v-icon>delete</v-icon>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
@@ -44,7 +44,7 @@
style="background-color: inherit;"
@click="fillSlot(slot)"
>
<v-icon>add</v-icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</div>

View File

@@ -18,7 +18,7 @@
icon
@click="back"
>
<v-icon>arrow_back</v-icon>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<slot name="toolbar" />
</v-toolbar>

View File

@@ -1,3 +1,4 @@
import ArchiveDialog from '/imports/ui/creature/archive/ArchiveDialog.vue';
import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue';
import CreaturePropertyCreationDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue';
@@ -22,6 +23,7 @@ import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue';
import UsernameDialog from '/imports/ui/user/UsernameDialog.vue';
export default {
ArchiveDialog,
CastSpellWithSlotDialog,
CreatureFormDialog,
CreaturePropertyCreationDialog,

View File

@@ -55,6 +55,9 @@
VLayout,
...DialogComponentIndex,
},
data(){return {
hiddenElements: [],
}},
computed: {
dialogs(){
return this.$store.state.dialogStack.dialogs;
@@ -130,7 +133,7 @@
// hide the source
source.style.transition = 'none';
source.style.opacity = '0';
this.hiddenElement = source;
this.hiddenElements.push(source);
// Instantly mock the source
target.style.transition = 'none';
@@ -153,6 +156,7 @@
},
doLeave(target, done){
let elementId;
let hiddenElement = this.hiddenElements.pop();
let returnElementId = this.$store.state.dialogStack.currentReturnElement;
if (returnElementId) {
elementId = returnElementId;
@@ -166,7 +170,7 @@
let source = this.getTopElementByDataId(elementId);
if (!source){
console.warn(`Can't find source for ${elementId}`);
if (this.hiddenElement) this.hiddenElement.style.opacity = null;
if (hiddenElement) hiddenElement.style.opacity = null;
done();
return;
}
@@ -180,10 +184,10 @@
// If the source and the hidden Element are different
// hide the source and reveal the hidden element
let originalSourceTransition = source.style.transition;
if (this.hiddenElement !== source){
if (hiddenElement !== source){
source.style.transition = 'none';
source.style.opacity = '0';
this.hiddenElement.style.opacity = null;
hiddenElement.style.opacity = null;
}
setTimeout(() => {
source.style.opacity = null;

View File

@@ -18,7 +18,7 @@
<code>{{ example.input }}</code>
</td>
<td>
<v-icon>arrow_right_alt</v-icon>
<v-icon>mdi-arrow-right-thick</v-icon>
</td>
<td>
<code>{{ example.result }}</code>

View File

@@ -72,6 +72,7 @@
import DialogStack from '/imports/ui/dialogStack/DialogStack.vue';
import { mapMutations } from 'vuex';
import SnackbarQueue from '/imports/ui/components/snackbars/SnackbarQueue.vue';
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
export default {
components: {
@@ -96,7 +97,9 @@
meteor: {
darkMode(){
let user = Meteor.user();
return user && user.darkMode;
if (!user) return;
let tier = getUserTier(user);
return tier.paidBenefits && user.darkMode;
},
},
watch: {

View File

@@ -28,7 +28,7 @@
to="/account"
v-on="on"
>
<v-icon>settings</v-icon>
<v-icon>mdi-cog</v-icon>
</v-btn>
</template>
<span>Account Settings</span>
@@ -48,69 +48,34 @@
<v-list-item-title>
{{ link.title }}
</v-list-item-title>
<v-icon v-if="link.href">
mdi-open-in-new
</v-icon>
</v-list-item>
<v-divider />
</v-list>
<v-list
avatar
>
<v-list-item
v-for="character in CreaturesWithNoParty"
:key="character._id"
:to="character.url"
>
<v-list-item-avatar :color="character.color || 'grey'">
<img
v-if="character.avatarPicture"
:src="character.avatarPicture"
:alt="character.name"
>
<template v-else>
{{ character.initial }}
</template>
</v-list-item-avatar>
<v-list-item-title>
{{ character.name }}
</v-list-item-title>
</v-list-item>
<v-list-group
v-for="party in parties"
:key="party._id"
>
<v-list-item slot="activator">
<v-list-item-title>
{{ party.name }}
</v-list-item-title>
</v-list-item>
<v-list-item
v-for="character in characterDocs"
:key="character._id"
:to="character.url"
>
<v-list-item-avatar :color="character.color || 'grey'">
<img
v-if="character.avatarPicture"
:src="character.avatarPicture"
:alt="character.name"
>
<template v-else>
{{ character.initial }}
</template>
</v-list-item-avatar>
<v-list-item-title>
{{ character.name }}
</v-list-item-title>
</v-list-item>
</v-list-group>
</v-list>
<creature-folder-list
dense
:creatures="CreaturesWithNoParty"
:folders="folders"
/>
</div>
</template>
<script lang="js">
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';
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
const characterTransform = function(char){
char.url = `/character/${char._id}/${char.urlName || '-'}`;
char.initial = char.name && char.name[0] || '?';
return char;
};
export default {
components: {
CreatureFolderList
},
meteor: {
$subscribe: {
'characterList': [],
@@ -125,55 +90,48 @@
links(){
let isLoggedIn = !!Meteor.userId();
let links = [
{title: 'Home', icon: 'home', to: '/'},
{title: 'Characters', icon: 'portrait', to: '/characterList', requireLogin: true},
{title: 'Library', icon: 'book', to: '/library', requireLogin: true},
{title: 'Home', icon: 'mdi-home', to: '/'},
{title: 'Characters', icon: 'mdi-account-group', to: '/characterList', requireLogin: true},
{title: 'Library', icon: 'mdi-library-shelves', to: '/library', requireLogin: true},
//{title: 'Tabletops', icon: 'api', to: '/tabletops', requireLogin: true},
//{title: 'Friends', icon: 'people', to: '/friends', requireLogin: true},
{title: 'Feedback', icon: 'bug_report', to: '/feedback'},
{title: 'About', icon: 'subject', to: '/about'},
{title: 'Patreon', icon: '', href: 'https://www.patreon.com/dicecloud'},
{title: 'Github', icon: '', href: 'https://github.com/ThaumRystra/DiceCloud/tree/version-2'},
{title: 'Feedback', icon: 'mdi-bug', to: '/feedback'},
{title: 'About', icon: 'mdi-sign-text', to: '/about'},
{title: 'Patreon', icon: 'mdi-patreon', href: 'https://www.patreon.com/dicecloud'},
{title: 'Github', icon: 'mdi-github', href: 'https://github.com/ThaumRystra/DiceCloud/tree/version-2'},
];
return links.filter(link => !link.requireLogin || isLoggedIn);
},
parties(){
folders(){
const userId = Meteor.userId();
return Parties.find(
{owner: userId},
{sort: {name: 1}},
).map(party => {
party.characterDocs = Creatures.find(
let folders = CreatureFolders.find(
{owner: userId, archived: {$ne: true}},
{sort: {order: 1}},
).map(folder => {
folder.creatures = Creatures.find(
{
_id: {$in: party.Creatures},
_id: {$in: folder.creatures || []},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
}, {
sort: {name: 1},
fields: {name: 1, urlName: 1},
}
).map(char => {
char.url = `/character/${char._id}/${char.urlName || '-'}`;
char.initial = char.name && char.name[0] || '?';
return char;
});
return party;
).map(characterTransform);
return folder;
});
folders = folders.filter(folder => !!folder.creatures.length);
return folders;
},
CreaturesWithNoParty() {
var userId = Meteor.userId();
var charArrays = Parties.find({owner: userId}).map(p => p.Creatures);
var partyChars = _.uniq(_.flatten(charArrays));
var charArrays = CreatureFolders.find({owner: userId}).map(p => p.creatures);
var folderChars = _.uniq(_.flatten(charArrays));
return Creatures.find(
{
_id: {$nin: partyChars},
_id: {$nin: folderChars},
$or: [{readers: userId}, {writers: userId}, {owner: userId}],
},
{sort: {name: 1}}
).map(char => {
char.url = `/character/${char._id}/${char.urlName || '-'}`;
char.initial = char.name && char.name[0] || '?';
return char;
});
).map(characterTransform);
},
},
};

View File

@@ -7,7 +7,7 @@
data-id="insert-library-node-button"
@click="insertLibraryNode"
>
<v-icon>add</v-icon>
<v-icon>mdi-plus</v-icon>
<slot />
</v-btn>
</template>

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