Merge branch 'version-2-dev' into version-2
This commit is contained in:
@@ -3,26 +3,25 @@
|
|||||||
# 'meteor add' and 'meteor remove' will edit this file for you,
|
# 'meteor add' and 'meteor remove' will edit this file for you,
|
||||||
# but you can also edit it by hand.
|
# but you can also edit it by hand.
|
||||||
|
|
||||||
accounts-password@2.2.0
|
accounts-password@2.3.1
|
||||||
random@1.2.0
|
random@1.2.0
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
dburles:mongo-collection-instances
|
dburles:mongo-collection-instances
|
||||||
accounts-google@1.4.0
|
accounts-google@1.4.0
|
||||||
email@2.2.0
|
email@2.2.1
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mongo@1.14.6
|
mongo@1.15.0
|
||||||
session@1.2.0
|
session@1.2.0
|
||||||
tracker@1.2.0
|
tracker@1.2.0
|
||||||
logging@1.3.1
|
logging@1.3.1
|
||||||
reload@1.3.1
|
reload@1.3.1
|
||||||
ejson@1.1.1
|
ejson@1.1.2
|
||||||
check@1.3.1
|
check@1.3.1
|
||||||
standard-minifier-js@2.8.0
|
standard-minifier-js@2.8.0
|
||||||
shell-server@0.5.0
|
shell-server@0.5.0
|
||||||
ecmascript@0.16.1
|
ecmascript@0.16.2
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
percolate:synced-cron
|
|
||||||
service-configuration@1.3.0
|
service-configuration@1.3.0
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.2
|
||||||
ddp-rate-limiter@1.1.0
|
ddp-rate-limiter@1.1.0
|
||||||
@@ -47,4 +46,5 @@ meteortesting:mocha
|
|||||||
ostrio:files
|
ostrio:files
|
||||||
simple:rest-bearer-token-parser
|
simple:rest-bearer-token-parser
|
||||||
simple:rest-json-error-handler
|
simple:rest-json-error-handler
|
||||||
|
littledata:synced-cron
|
||||||
mdg:meteor-apm-agent
|
mdg:meteor-apm-agent
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
METEOR@2.6.1
|
METEOR@2.7.3
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
accounts-base@2.2.1
|
accounts-base@2.2.3
|
||||||
accounts-google@1.4.0
|
accounts-google@1.4.0
|
||||||
accounts-oauth@1.4.0
|
accounts-oauth@1.4.1
|
||||||
accounts-password@2.2.0
|
accounts-password@2.3.1
|
||||||
accounts-patreon@0.1.0
|
accounts-patreon@0.1.0
|
||||||
akryum:npm-check@0.1.2
|
akryum:npm-check@0.1.2
|
||||||
akryum:vue-component@0.15.2
|
akryum:vue-component@0.15.2
|
||||||
@@ -12,13 +12,13 @@ aldeed:collection2@3.5.0
|
|||||||
aldeed:schema-index@3.0.0
|
aldeed:schema-index@3.0.0
|
||||||
allow-deny@1.1.1
|
allow-deny@1.1.1
|
||||||
autoupdate@1.8.0
|
autoupdate@1.8.0
|
||||||
babel-compiler@7.8.1
|
babel-compiler@7.9.0
|
||||||
babel-runtime@1.5.0
|
babel-runtime@1.5.1
|
||||||
base64@1.0.12
|
base64@1.0.12
|
||||||
binary-heap@1.0.11
|
binary-heap@1.0.11
|
||||||
blaze-tools@1.1.2
|
blaze-tools@1.1.3
|
||||||
boilerplate-generator@1.7.1
|
boilerplate-generator@1.7.1
|
||||||
bozhao:link-accounts@2.4.0
|
bozhao:link-accounts@2.6.1
|
||||||
caching-compiler@1.2.2
|
caching-compiler@1.2.2
|
||||||
caching-html-compiler@1.2.1
|
caching-html-compiler@1.2.1
|
||||||
callback-hook@1.4.0
|
callback-hook@1.4.0
|
||||||
@@ -33,24 +33,25 @@ ddp-rate-limiter@1.1.0
|
|||||||
ddp-server@2.5.0
|
ddp-server@2.5.0
|
||||||
diff-sequence@1.1.1
|
diff-sequence@1.1.1
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.2
|
||||||
ecmascript@0.16.1
|
ecmascript@0.16.2
|
||||||
ecmascript-runtime@0.8.0
|
ecmascript-runtime@0.8.0
|
||||||
ecmascript-runtime-client@0.12.1
|
ecmascript-runtime-client@0.12.1
|
||||||
ecmascript-runtime-server@0.11.0
|
ecmascript-runtime-server@0.11.0
|
||||||
ejson@1.1.1
|
ejson@1.1.2
|
||||||
email@2.2.0
|
email@2.2.1
|
||||||
es5-shim@4.8.0
|
es5-shim@4.8.0
|
||||||
fetch@0.1.1
|
fetch@0.1.1
|
||||||
geojson-utils@1.0.10
|
geojson-utils@1.0.10
|
||||||
google-oauth@1.4.1
|
google-oauth@1.4.2
|
||||||
hot-code-push@1.0.4
|
hot-code-push@1.0.4
|
||||||
html-tools@1.1.2
|
html-tools@1.1.3
|
||||||
htmljs@1.1.1
|
htmljs@1.1.1
|
||||||
http@2.0.0
|
http@2.0.0
|
||||||
id-map@1.1.1
|
id-map@1.1.1
|
||||||
inter-process-messaging@0.1.1
|
inter-process-messaging@0.1.1
|
||||||
lai:collection-extensions@0.3.0
|
lai:collection-extensions@0.3.0
|
||||||
launch-screen@1.3.0
|
launch-screen@1.3.0
|
||||||
|
littledata:synced-cron@1.5.1
|
||||||
livedata@1.0.18
|
livedata@1.0.18
|
||||||
localstorage@1.2.0
|
localstorage@1.2.0
|
||||||
logging@1.3.1
|
logging@1.3.1
|
||||||
@@ -63,23 +64,23 @@ meteortesting:mocha@2.0.3
|
|||||||
meteortesting:mocha-core@8.1.2
|
meteortesting:mocha-core@8.1.2
|
||||||
mikowals:batch-insert@1.3.0
|
mikowals:batch-insert@1.3.0
|
||||||
minifier-css@1.6.0
|
minifier-css@1.6.0
|
||||||
minifier-js@2.7.3
|
minifier-js@2.7.4
|
||||||
minimongo@1.8.0
|
minimongo@1.8.0
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mobile-status-bar@1.1.0
|
mobile-status-bar@1.1.0
|
||||||
modern-browsers@0.1.7
|
modern-browsers@0.1.8
|
||||||
modules@0.18.0
|
modules@0.18.0
|
||||||
modules-runtime@0.12.0
|
modules-runtime@0.13.0
|
||||||
mongo@1.14.6
|
mongo@1.15.0
|
||||||
mongo-decimal@0.1.2
|
mongo-decimal@0.1.3
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
npm-mongo@4.3.1
|
npm-mongo@4.3.1
|
||||||
oauth@2.1.1
|
oauth@2.1.2
|
||||||
oauth2@1.3.1
|
oauth2@1.3.1
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
ostrio:cookies@2.7.0
|
ostrio:cookies@2.7.2
|
||||||
ostrio:files@2.0.1
|
ostrio:files@2.0.1
|
||||||
patreon-oauth@0.1.0
|
patreon-oauth@0.1.0
|
||||||
peerlibrary:assert@0.3.0
|
peerlibrary:assert@0.3.0
|
||||||
@@ -93,12 +94,11 @@ peerlibrary:reactive-publish@0.10.0
|
|||||||
peerlibrary:server-autorun@0.8.0
|
peerlibrary:server-autorun@0.8.0
|
||||||
peerlibrary:subscription-data@0.8.0
|
peerlibrary:subscription-data@0.8.0
|
||||||
percolate:migrations@1.0.3
|
percolate:migrations@1.0.3
|
||||||
percolate:synced-cron@1.3.2
|
|
||||||
promise@0.12.0
|
promise@0.12.0
|
||||||
raix:eventemitter@1.0.0
|
raix:eventemitter@1.0.0
|
||||||
random@1.2.0
|
random@1.2.0
|
||||||
rate-limit@1.0.9
|
rate-limit@1.0.9
|
||||||
react-fast-refresh@0.2.2
|
react-fast-refresh@0.2.3
|
||||||
reactive-dict@1.3.0
|
reactive-dict@1.3.0
|
||||||
reactive-var@1.0.11
|
reactive-var@1.0.11
|
||||||
reload@1.3.1
|
reload@1.3.1
|
||||||
@@ -114,14 +114,14 @@ simple:rest@1.2.1
|
|||||||
simple:rest-bearer-token-parser@1.1.1
|
simple:rest-bearer-token-parser@1.1.1
|
||||||
simple:rest-json-error-handler@1.1.1
|
simple:rest-json-error-handler@1.1.1
|
||||||
simple:rest-method-mixin@1.1.0
|
simple:rest-method-mixin@1.1.0
|
||||||
socket-stream-client@0.4.0
|
socket-stream-client@0.5.0
|
||||||
spacebars-compiler@1.3.0
|
spacebars-compiler@1.3.1
|
||||||
standard-minifier-js@2.8.0
|
standard-minifier-js@2.8.0
|
||||||
static-html@1.3.2
|
static-html@1.3.2
|
||||||
templating-tools@1.2.1
|
templating-tools@1.2.2
|
||||||
tmeasday:check-npm-versions@1.0.2
|
tmeasday:check-npm-versions@1.0.2
|
||||||
tracker@1.2.0
|
tracker@1.2.0
|
||||||
typescript@4.4.1
|
typescript@4.5.4
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
url@1.3.2
|
url@1.3.2
|
||||||
webapp@1.13.1
|
webapp@1.13.1
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||||
|
import { CreaturePropertySchema } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
|
||||||
const ArchiveCreatureFiles = createS3FilesCollection({
|
const ArchiveCreatureFiles = createS3FilesCollection({
|
||||||
collectionName: 'archiveCreatureFiles',
|
collectionName: 'archiveCreatureFiles',
|
||||||
@@ -11,7 +15,38 @@ const ArchiveCreatureFiles = createS3FilesCollection({
|
|||||||
if (!/json/i.test(file.extension)){
|
if (!/json/i.test(file.extension)){
|
||||||
return 'Please upload only a JSON file';
|
return 'Please upload only a JSON file';
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onAfterUpload(file) {
|
||||||
|
if (Meteor.isServer) incrementFileStorageUsed(file.userId, file.size);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let archiveSchema = new SimpleSchema({
|
||||||
|
meta: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
creature: CreatureSchema,
|
||||||
|
properties: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
'properties.$': CreaturePropertySchema,
|
||||||
|
experiences: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
'experiences.$': {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
logs: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
|
'logs.$': {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default ArchiveCreatureFiles;
|
export default ArchiveCreatureFiles;
|
||||||
|
export { archiveSchema };
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -68,7 +68,7 @@ const archiveCreatureToFile = new ValidatedMethod({
|
|||||||
async run({creatureId}) {
|
async run({creatureId}) {
|
||||||
assertOwnership(creatureId, this.userId);
|
assertOwnership(creatureId, this.userId);
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer){
|
||||||
archiveCreature(creatureId);
|
archiveCreature(creatureId, this.userId);
|
||||||
} else {
|
} else {
|
||||||
removeCreatureWork(creatureId);
|
removeCreatureWork(creatureId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// import '/imports/api/creature/archive/methods/archiveCreatures.js';
|
|
||||||
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
||||||
import '/imports/api/creature/archive/methods/restoreCreatures.js';
|
|
||||||
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
||||||
|
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||||
|
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||||
|
|
||||||
|
const removeArchiveCreature = new ValidatedMethod({
|
||||||
|
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
'fileId': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
async run({ fileId }) {
|
||||||
|
// fetch the file
|
||||||
|
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
|
||||||
|
if (!file) {
|
||||||
|
throw new Meteor.Error('File not found',
|
||||||
|
'The requested creature archive does not exist');
|
||||||
|
}
|
||||||
|
// Assert ownership
|
||||||
|
const userId = file?.userId;
|
||||||
|
if (!userId || userId !== this.userId) {
|
||||||
|
throw new Meteor.Error('Permission denied',
|
||||||
|
'You can only restore creatures you own');
|
||||||
|
}
|
||||||
|
//Remove the archive once the restore succeeded
|
||||||
|
ArchiveCreatureFiles.remove({ _id: fileId });
|
||||||
|
// Update the user's file storage limits
|
||||||
|
incrementFileStorageUsed(userId, -file.size);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default removeArchiveCreature;
|
||||||
@@ -8,12 +8,16 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
|||||||
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
||||||
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
|
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||||
|
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
|
||||||
|
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||||
|
import verifyArchiveSafety from '/imports/api/creature/archive/methods/verifyArchiveSafety.js';
|
||||||
|
|
||||||
let migrateArchive;
|
let migrateArchive;
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer){
|
||||||
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
|
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreCreature(archive){
|
function restoreCreature(archive, userId){
|
||||||
if (SCHEMA_VERSION < archive.meta.schemaVersion){
|
if (SCHEMA_VERSION < archive.meta.schemaVersion){
|
||||||
throw new Meteor.Error('Incompatible',
|
throw new Meteor.Error('Incompatible',
|
||||||
'The archive file is from a newer version. Update required to read.')
|
'The archive file is from a newer version. Update required to read.')
|
||||||
@@ -22,6 +26,19 @@ function restoreCreature(archive){
|
|||||||
// Migrate and verify the archive meets the current schema
|
// Migrate and verify the archive meets the current schema
|
||||||
migrateArchive(archive);
|
migrateArchive(archive);
|
||||||
|
|
||||||
|
// Asset that the archive is safe
|
||||||
|
verifyArchiveSafety(archive);
|
||||||
|
|
||||||
|
// Don't upload creatures twice
|
||||||
|
const existingCreature = Creatures.findOne(archive.creature._id, {
|
||||||
|
fields: { _id: 1 }
|
||||||
|
});
|
||||||
|
if (existingCreature) throw new Meteor.Error('Already exists',
|
||||||
|
'The creature you are trying to restore already exists.')
|
||||||
|
|
||||||
|
// Ensure the user owns the restored creature
|
||||||
|
archive.creature.owner = userId;
|
||||||
|
|
||||||
// Insert the creature sub documents
|
// Insert the creature sub documents
|
||||||
// They still have their original _id's
|
// They still have their original _id's
|
||||||
Creatures.insert(archive.creature);
|
Creatures.insert(archive.creature);
|
||||||
@@ -69,13 +86,18 @@ const restoreCreaturefromFile = new ValidatedMethod({
|
|||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
'You can only restore creatures you own');
|
'You can only restore creatures you own');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertHasCharactersSlots(this.userId);
|
||||||
|
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer){
|
||||||
// Read the file data
|
// Read the file data
|
||||||
const archive = await ArchiveCreatureFiles.readJSONFile(file);
|
const archive = await ArchiveCreatureFiles.readJSONFile(file);
|
||||||
restoreCreature(archive);
|
restoreCreature(archive, this.userId);
|
||||||
}
|
}
|
||||||
//Remove the archive once the restore succeeded
|
//Remove the archive once the restore succeeded
|
||||||
ArchiveCreatureFiles.remove({_id: fileId});
|
ArchiveCreatureFiles.remove({ _id: fileId });
|
||||||
|
// Update the user's file storage limits
|
||||||
|
incrementFileStorageUsed(userId, -file.size);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
export 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;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { slice } from 'lodash';
|
||||||
|
import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
|
|
||||||
|
export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }){
|
||||||
|
const creatureId = creature._id;
|
||||||
|
|
||||||
|
// Check lengths of arrays
|
||||||
|
if (logs.length > PER_CREATURE_LOG_LIMIT) {
|
||||||
|
logs = slice(logs, 0, PER_CREATURE_LOG_LIMIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that everything belongs to the right creature
|
||||||
|
logs.forEach(log => {
|
||||||
|
if (log.creatureId !== creatureId) {
|
||||||
|
throw new Meteor.Error('Malicious log', 'Log contains an entry for the wrong creature');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
experiences.forEach(experience => {
|
||||||
|
if (experience.creatureId !== creatureId) {
|
||||||
|
throw new Meteor.Error('Malicious experience', 'Experiences contains an entry for the wrong creature');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
properties.forEach(prop => {
|
||||||
|
if (prop.ancestors[0].id !== creatureId) {
|
||||||
|
throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -82,6 +82,13 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
|||||||
index: 1,
|
index: 1,
|
||||||
removeBeforeCompute: true,
|
removeBeforeCompute: true,
|
||||||
},
|
},
|
||||||
|
// When this is true on any property, the creature needs to be recomputed
|
||||||
|
dirty: {
|
||||||
|
type: Boolean,
|
||||||
|
// Default to true because new properties cause a recomputation
|
||||||
|
defaultValue: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
|
CreaturePropertySchema.extend(DenormalisedOnlyCreaturePropertySchema);
|
||||||
@@ -98,10 +105,6 @@ for (let key in propertySchemasIndex){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
import '/imports/api/creature/creatureProperties/methods/index.js';
|
|
||||||
//import '/imports/api/creature/actions/doAction.js';
|
|
||||||
//import '/imports/api/creature/actions/castSpellWithSlot.js';
|
|
||||||
|
|
||||||
export default CreatureProperties;
|
export default CreatureProperties;
|
||||||
export {
|
export {
|
||||||
DenormalisedOnlyCreaturePropertySchema,
|
DenormalisedOnlyCreaturePropertySchema,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const adjustQuantity = new ValidatedMethod({
|
const adjustQuantity = new ValidatedMethod({
|
||||||
name: 'creatureProperties.adjustQuantity',
|
name: 'creatureProperties.adjustQuantity',
|
||||||
@@ -29,10 +28,6 @@ const adjustQuantity = new ValidatedMethod({
|
|||||||
|
|
||||||
// Do work
|
// Do work
|
||||||
adjustQuantityWork({property, operation, value});
|
adjustQuantityWork({property, operation, value});
|
||||||
|
|
||||||
// Changing quantity does not change dependencies, but recomputing the
|
|
||||||
// inventory changes many deps at once, so recompute fully
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,7 +42,7 @@ export function adjustQuantityWork({property, operation, value}){
|
|||||||
}
|
}
|
||||||
if (operation === 'set'){
|
if (operation === 'set'){
|
||||||
CreatureProperties.update(property._id, {
|
CreatureProperties.update(property._id, {
|
||||||
$set: {quantity: value}
|
$set: {quantity: value, dirty: true}
|
||||||
}, {
|
}, {
|
||||||
selector: property
|
selector: property
|
||||||
});
|
});
|
||||||
@@ -57,7 +52,8 @@ export function adjustQuantityWork({property, operation, value}){
|
|||||||
let currentQuantity = property.quantity;
|
let currentQuantity = property.quantity;
|
||||||
if (currentQuantity + value < 0) value = -currentQuantity;
|
if (currentQuantity + value < 0) value = -currentQuantity;
|
||||||
CreatureProperties.update(property._id, {
|
CreatureProperties.update(property._id, {
|
||||||
$inc: {quantity: value}
|
$inc: { quantity: value },
|
||||||
|
$set: { dirty: true }
|
||||||
}, {
|
}, {
|
||||||
selector: property
|
selector: property
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const damageProperty = new ValidatedMethod({
|
const damageProperty = new ValidatedMethod({
|
||||||
name: 'creatureProperties.damage',
|
name: 'creatureProperties.damage',
|
||||||
@@ -37,9 +36,7 @@ const damageProperty = new ValidatedMethod({
|
|||||||
`Property of type "${property.type}" can't be damaged`
|
`Property of type "${property.type}" can't be damaged`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let result = damagePropertyWork({property, operation, value});
|
let result = damagePropertyWork({ property, operation, value });
|
||||||
// Dependencies can't be changed through damage, only recompute deps
|
|
||||||
computeCreatureDependencyGroup(property);
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -70,7 +67,7 @@ export function damagePropertyWork({property, operation, value}){
|
|||||||
|
|
||||||
// Write the results
|
// Write the results
|
||||||
CreatureProperties.update(property._id, {
|
CreatureProperties.update(property._id, {
|
||||||
$set: {damage, value: newValue}
|
$set: {damage, value: newValue, dirty: true}
|
||||||
}, {
|
}, {
|
||||||
selector: property
|
selector: property
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const dealDamage = new ValidatedMethod({
|
const dealDamage = new ValidatedMethod({
|
||||||
name: 'creatureProperties.dealDamage',
|
name: 'creatureProperties.dealDamage',
|
||||||
@@ -33,7 +32,6 @@ const dealDamage = new ValidatedMethod({
|
|||||||
assertEditPermission(creature, this.userId);
|
assertEditPermission(creature, this.userId);
|
||||||
|
|
||||||
const totalDamage = dealDamageWork({creature, damageType, amount})
|
const totalDamage = dealDamageWork({creature, damageType, amount})
|
||||||
computeCreature(creatureId);
|
|
||||||
return totalDamage;
|
return totalDamage;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
renewDocIds
|
renewDocIds
|
||||||
} from '/imports/api/parenting/parenting.js';
|
} from '/imports/api/parenting/parenting.js';
|
||||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
var snackbar;
|
var snackbar;
|
||||||
if (Meteor.isClient){
|
if (Meteor.isClient){
|
||||||
snackbar = require(
|
snackbar = require(
|
||||||
@@ -77,6 +76,9 @@ const duplicateProperty = new ValidatedMethod({
|
|||||||
|
|
||||||
// Order the root node
|
// Order the root node
|
||||||
property.order += 0.5;
|
property.order += 0.5;
|
||||||
|
|
||||||
|
// Mark the sheet as needing recompute
|
||||||
|
property.dirty = true;
|
||||||
|
|
||||||
// Insert the properties
|
// Insert the properties
|
||||||
CreatureProperties.batchInsert([property, ...nodes]);
|
CreatureProperties.batchInsert([property, ...nodes]);
|
||||||
@@ -87,9 +89,6 @@ const duplicateProperty = new ValidatedMethod({
|
|||||||
ancestorId: property.ancestors[0].id,
|
ancestorId: property.ancestors[0].id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inserting a creature property invalidates dependencies: full recompute
|
|
||||||
computeCreature(creature._id);
|
|
||||||
|
|
||||||
return propertyId;
|
return propertyId;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
|
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
|
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
|
||||||
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
|
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
|
||||||
|
|
||||||
@@ -29,7 +28,7 @@ const equipItem = new ValidatedMethod({
|
|||||||
let creature = getRootCreatureAncestor(item);
|
let creature = getRootCreatureAncestor(item);
|
||||||
assertEditPermission(creature, this.userId);
|
assertEditPermission(creature, this.userId);
|
||||||
CreatureProperties.update(_id, {
|
CreatureProperties.update(_id, {
|
||||||
$set: {equipped},
|
$set: { equipped, dirty: true },
|
||||||
}, {
|
}, {
|
||||||
selector: {type: 'item'},
|
selector: {type: 'item'},
|
||||||
});
|
});
|
||||||
@@ -46,8 +45,6 @@ const equipItem = new ValidatedMethod({
|
|||||||
order: Number.MAX_SAFE_INTEGER,
|
order: Number.MAX_SAFE_INTEGER,
|
||||||
skipRecompute: true,
|
skipRecompute: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
computeCreature(creature._id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const flipToggle = new ValidatedMethod({
|
const flipToggle = new ValidatedMethod({
|
||||||
name: 'creatureProperties.flipToggle',
|
name: 'creatureProperties.flipToggle',
|
||||||
@@ -36,12 +35,10 @@ const flipToggle = new ValidatedMethod({
|
|||||||
CreatureProperties.update(_id, {$set: {
|
CreatureProperties.update(_id, {$set: {
|
||||||
enabled: !currentValue,
|
enabled: !currentValue,
|
||||||
disabled: currentValue,
|
disabled: currentValue,
|
||||||
|
dirty: true,
|
||||||
}}, {
|
}}, {
|
||||||
selector: {type: 'toggle'},
|
selector: {type: 'toggle'},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Updating a toggle is likely to change the whole tree, do a full recompute
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,26 @@ export default function getSlotFillFilter({slot, libraryIds}){
|
|||||||
slotFillerType: slot.slotType,
|
slotFillerType: slot.slotType,
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
} else if (slot.type === 'class') {
|
||||||
|
filter.$and.push({
|
||||||
|
$or: [{
|
||||||
|
type: 'classLevel',
|
||||||
|
},{
|
||||||
|
type: 'slotFiller',
|
||||||
|
slotFillerType: 'classLevel',
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
filter.variableName = slot.variableName;
|
||||||
|
|
||||||
|
// Only search for levels the class needs
|
||||||
|
if (slot.missingLevels && slot.missingLevels.length) {
|
||||||
|
filter.level = {$in: slot.missingLevels};
|
||||||
|
} else {
|
||||||
|
filter.level = (slot.level || 0) + 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let tagsOr = [];
|
let tagsOr = [];
|
||||||
let tagsNor = [];
|
let tagsNin = [];
|
||||||
if (slot.slotTags && slot.slotTags.length){
|
if (slot.slotTags && slot.slotTags.length){
|
||||||
tagsOr.push({tags: {$all: slot.slotTags}});
|
tagsOr.push({tags: {$all: slot.slotTags}});
|
||||||
}
|
}
|
||||||
@@ -27,15 +44,15 @@ export default function getSlotFillFilter({slot, libraryIds}){
|
|||||||
if (extra.operation === 'OR'){
|
if (extra.operation === 'OR'){
|
||||||
tagsOr.push({tags: {$all: extra.tags}});
|
tagsOr.push({tags: {$all: extra.tags}});
|
||||||
} else if (extra.operation === 'NOT'){
|
} else if (extra.operation === 'NOT'){
|
||||||
tagsNor.push({tags: {$all: extra.tags}});
|
tagsNin.push(...extra.tags);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (tagsOr.length){
|
if (tagsOr.length){
|
||||||
filter.$and.push({$or: tagsOr});
|
filter.$or = tagsOr;
|
||||||
}
|
}
|
||||||
if (tagsNor.length){
|
if (tagsNin.length){
|
||||||
filter.$and.push({$nor: tagsNor});
|
filter.$and.push({tags: {$nin: tagsNin}});
|
||||||
}
|
}
|
||||||
if (!filter.$and.length){
|
if (!filter.$and.length){
|
||||||
delete filter.$and;
|
delete filter.$and;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import { getAncestry } from '/imports/api/parenting/parenting.js';
|
import { getAncestry } from '/imports/api/parenting/parenting.js';
|
||||||
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
|
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
|
||||||
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||||
@@ -132,14 +131,13 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
|||||||
|
|
||||||
export function insertPropertyWork({property, creature}){
|
export function insertPropertyWork({property, creature}){
|
||||||
delete property._id;
|
delete property._id;
|
||||||
|
property.dirty = true;
|
||||||
let _id = CreatureProperties.insert(property);
|
let _id = CreatureProperties.insert(property);
|
||||||
// Tree structure changed by insert, reorder the tree
|
// Tree structure changed by insert, reorder the tree
|
||||||
reorderDocs({
|
reorderDocs({
|
||||||
collection: CreatureProperties,
|
collection: CreatureProperties,
|
||||||
ancestorId: creature._id,
|
ancestorId: creature._id,
|
||||||
});
|
});
|
||||||
// Inserting a creature property invalidates dependencies: full recompute
|
|
||||||
computeCreature(creature._id);
|
|
||||||
return _id;
|
return _id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import {
|
import {
|
||||||
setLineageOfDocs,
|
setLineageOfDocs,
|
||||||
@@ -71,9 +70,6 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
|
|||||||
collection: CreatureProperties,
|
collection: CreatureProperties,
|
||||||
ancestorId: rootCreature._id,
|
ancestorId: rootCreature._id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inserting a creature property invalidates dependencies: full recompute
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
// Return the docId of the last property, the inserted root property
|
// Return the docId of the last property, the inserted root property
|
||||||
return rootId;
|
return rootId;
|
||||||
},
|
},
|
||||||
@@ -135,12 +131,14 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
|||||||
node.order = order;
|
node.order = order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark all nodes as dirty
|
||||||
|
dirtyNodes(nodes);
|
||||||
|
|
||||||
// Insert the creature properties
|
// Insert the creature properties
|
||||||
CreatureProperties.batchInsert(nodes);
|
CreatureProperties.batchInsert(nodes);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function storeLibraryNodeReferences(nodes){
|
function storeLibraryNodeReferences(nodes){
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
if (node.libraryNodeId) return;
|
if (node.libraryNodeId) return;
|
||||||
@@ -148,6 +146,12 @@ function storeLibraryNodeReferences(nodes){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dirtyNodes(nodes) {
|
||||||
|
nodes.forEach(node => {
|
||||||
|
node.dirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Covert node references into actual nodes
|
// Covert node references into actual nodes
|
||||||
// TODO: check permissions for each library a reference node references
|
// TODO: check permissions for each library a reference node references
|
||||||
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const pullFromProperty = new ValidatedMethod({
|
const pullFromProperty = new ValidatedMethod({
|
||||||
name: 'creatureProperties.pull',
|
name: 'creatureProperties.pull',
|
||||||
@@ -21,15 +20,12 @@ const pullFromProperty = new ValidatedMethod({
|
|||||||
|
|
||||||
// Do work
|
// Do work
|
||||||
CreatureProperties.update(_id, {
|
CreatureProperties.update(_id, {
|
||||||
$pull: {[path.join('.')]: {_id: itemId}},
|
$pull: { [path.join('.')]: { _id: itemId } },
|
||||||
|
$set: { dirty: true }
|
||||||
}, {
|
}, {
|
||||||
selector: {type: property.type},
|
selector: {type: property.type},
|
||||||
getAutoValues: false,
|
getAutoValues: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO figure out if this method can change deps or not
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
// recomputePropertyDependencies(property);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
|
|
||||||
const pushToProperty = new ValidatedMethod({
|
const pushToProperty = new ValidatedMethod({
|
||||||
@@ -39,13 +38,11 @@ const pushToProperty = new ValidatedMethod({
|
|||||||
|
|
||||||
// Do work
|
// Do work
|
||||||
CreatureProperties.update(_id, {
|
CreatureProperties.update(_id, {
|
||||||
$push: {[joinedPath]: value},
|
$push: { [joinedPath]: value },
|
||||||
|
$set: { dirty: true },
|
||||||
}, {
|
}, {
|
||||||
selector: {type: property.type},
|
selector: {type: property.type},
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO figure out if this method can change deps or not
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { restore } from '/imports/api/parenting/softRemove.js';
|
import { restore } from '/imports/api/parenting/softRemove.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const restoreProperty = new ValidatedMethod({
|
const restoreProperty = new ValidatedMethod({
|
||||||
name: 'creatureProperties.restore',
|
name: 'creatureProperties.restore',
|
||||||
@@ -24,10 +23,13 @@ const restoreProperty = new ValidatedMethod({
|
|||||||
assertEditPermission(rootCreature, this.userId);
|
assertEditPermission(rootCreature, this.userId);
|
||||||
|
|
||||||
// Do work
|
// Do work
|
||||||
restore({_id, collection: CreatureProperties});
|
restore({
|
||||||
|
_id,
|
||||||
// Changes dependency tree by restoring children
|
collection: CreatureProperties,
|
||||||
computeCreature(rootCreature._id);
|
extraUpdates: {
|
||||||
|
$set: { dirty: true }
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const selectAmmoItem = new ValidatedMethod({
|
const selectAmmoItem = new ValidatedMethod({
|
||||||
name: 'creatureProperties.selectAmmoItem',
|
name: 'creatureProperties.selectAmmoItem',
|
||||||
@@ -37,15 +36,10 @@ const selectAmmoItem = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
let path = `resources.itemsConsumed.${itemConsumedIndex}.itemId`;
|
let path = `resources.itemsConsumed.${itemConsumedIndex}.itemId`;
|
||||||
CreatureProperties.update(actionId, {
|
CreatureProperties.update(actionId, {
|
||||||
$set: {[path]: itemId}
|
$set: { [path]: itemId, dirty: true }
|
||||||
}, {
|
}, {
|
||||||
selector: action,
|
selector: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Changing the linked item does change the dependency tree
|
|
||||||
// TODO: We can predict exactly which deps will be affected instead of
|
|
||||||
// recomputing the entire creature
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const softRemoveProperty = new ValidatedMethod({
|
const softRemoveProperty = new ValidatedMethod({
|
||||||
name: 'creatureProperties.softRemove',
|
name: 'creatureProperties.softRemove',
|
||||||
@@ -25,9 +24,6 @@ const softRemoveProperty = new ValidatedMethod({
|
|||||||
|
|
||||||
// Do work
|
// Do work
|
||||||
softRemove({_id, collection: CreatureProperties});
|
softRemove({_id, collection: CreatureProperties});
|
||||||
|
|
||||||
// Changes dependency tree by removing children
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
|
|
||||||
const updateCreatureProperty = new ValidatedMethod({
|
const updateCreatureProperty = new ValidatedMethod({
|
||||||
name: 'creatureProperties.update',
|
name: 'creatureProperties.update',
|
||||||
@@ -37,17 +36,13 @@ const updateCreatureProperty = new ValidatedMethod({
|
|||||||
let modifier;
|
let modifier;
|
||||||
// unset empty values
|
// unset empty values
|
||||||
if (value === null || value === undefined){
|
if (value === null || value === undefined){
|
||||||
modifier = {$unset: {[pathString]: 1}};
|
modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } };
|
||||||
} else {
|
} else {
|
||||||
modifier = {$set: {[pathString]: value}};
|
modifier = { $set: {[pathString]: value, dirty: true } };
|
||||||
}
|
}
|
||||||
CreatureProperties.update(_id, modifier, {
|
CreatureProperties.update(_id, modifier, {
|
||||||
selector: {type: property.type},
|
selector: {type: property.type},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Updating a property is likely to change dependencies, do a full recompute
|
|
||||||
// denormalised stats might change, so fetch the creature again
|
|
||||||
computeCreature(rootCreature._id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
21
app/imports/api/creature/creatures/CreatureVariables.js
Normal file
21
app/imports/api/creature/creatures/CreatureVariables.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//set up the collection for creature variables
|
||||||
|
let CreatureVariables = new Mongo.Collection('creatureVariables');
|
||||||
|
|
||||||
|
// Unique index on _creatureId
|
||||||
|
if (Meteor.isServer) {
|
||||||
|
CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** No schema because the structure isn't known until compute time
|
||||||
|
* Expect documents to looke like:
|
||||||
|
* {
|
||||||
|
* _id: "nE8Ngd6K4L4jSxLY2",
|
||||||
|
* _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature
|
||||||
|
* explicitlyDefinedVariableName: {...some creatureProperty}
|
||||||
|
* implicitVariableName: {value: 10},
|
||||||
|
* undefinedVariableName: {},
|
||||||
|
* }
|
||||||
|
* Where top level fields that don't start with `_` are variables on the sheet
|
||||||
|
**/
|
||||||
|
|
||||||
|
export default CreatureVariables;
|
||||||
@@ -80,6 +80,27 @@ let CreatureSchema = new SimpleSchema({
|
|||||||
optional: true,
|
optional: true,
|
||||||
max: STORAGE_LIMITS.url,
|
max: STORAGE_LIMITS.url,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Libraries
|
||||||
|
allowedLibraries: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: 100,
|
||||||
|
},
|
||||||
|
'allowedLibraries.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
allowedLibraryCollections: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: 100,
|
||||||
|
},
|
||||||
|
'allowedLibraryCollections.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
|
||||||
// Mechanics
|
// Mechanics
|
||||||
deathSave: {
|
deathSave: {
|
||||||
type: deathSaveSchema,
|
type: deathSaveSchema,
|
||||||
@@ -100,6 +121,11 @@ let CreatureSchema = new SimpleSchema({
|
|||||||
type: SimpleSchema.Integer,
|
type: SimpleSchema.Integer,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
},
|
},
|
||||||
|
// Does the character need a recompute?
|
||||||
|
dirty: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
// Version of computation engine that was last used to compute this creature
|
// Version of computation engine that was last used to compute this creature
|
||||||
computeVersion: {
|
computeVersion: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -160,8 +186,8 @@ CreatureSchema.extend(SharingSchema);
|
|||||||
Creatures.attachSchema(CreatureSchema);
|
Creatures.attachSchema(CreatureSchema);
|
||||||
|
|
||||||
|
|
||||||
import '/imports/api/creature/creatures/methods/index.js';
|
|
||||||
import '/imports/api/engine/actions/doAction.js';
|
|
||||||
|
|
||||||
export default Creatures;
|
export default Creatures;
|
||||||
export { CreatureSchema };
|
export { CreatureSchema };
|
||||||
|
|
||||||
|
import '/imports/api/engine/actions/doAction.js';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export default function defaultCharacterProperties(creatureId){
|
|||||||
{
|
{
|
||||||
type: 'propertySlot',
|
type: 'propertySlot',
|
||||||
name: 'Ruleset',
|
name: 'Ruleset',
|
||||||
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'},
|
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.'},
|
||||||
slotTags: ['base'],
|
slotTags: ['base'],
|
||||||
tags: [],
|
tags: [],
|
||||||
quantityExpected: {calculation: '1'},
|
quantityExpected: {calculation: '1'},
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
|
||||||
|
export default function assertHasCharactersSlots(userId) {
|
||||||
|
if (characterSlotsRemaining(userId) <= 0) {
|
||||||
|
throw new Meteor.Error('characterSlotLimit',
|
||||||
|
'No character slots left')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function characterSlotsRemaining(userId) {
|
||||||
|
let tier = getUserTier(userId);
|
||||||
|
const currentCharacterCount = Creatures.find({
|
||||||
|
owner: userId,
|
||||||
|
}, {
|
||||||
|
fields: { _id: 1 },
|
||||||
|
}).count();
|
||||||
|
if (tier.characterSlots === -1) {
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
return tier.characterSlots - currentCharacterCount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||||
|
|
||||||
|
const changeAllowedLibraries = new ValidatedMethod({
|
||||||
|
name: 'creatures.changeAllowedLibraries',
|
||||||
|
mixins: [RateLimiterMixin, simpleSchemaMixin],
|
||||||
|
schema: new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
allowedLibraries: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: 100,
|
||||||
|
},
|
||||||
|
'allowedLibraries.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
allowedLibraryCollections: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: 100,
|
||||||
|
},
|
||||||
|
'allowedLibraryCollections.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 10,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({_id, allowedLibraries, allowedLibraryCollections}) {
|
||||||
|
let creature = Creatures.findOne(_id);
|
||||||
|
assertEditPermission(creature, this.userId);
|
||||||
|
let $set;
|
||||||
|
if (allowedLibraries) {
|
||||||
|
$set = { allowedLibraries }
|
||||||
|
}
|
||||||
|
if (allowedLibraryCollections) {
|
||||||
|
if (!$set) $set = {};
|
||||||
|
$set.allowedLibraryCollections = allowedLibraryCollections;
|
||||||
|
}
|
||||||
|
if (!$set) return;
|
||||||
|
Creatures.update(_id, {$set});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAllUserLibraries = new ValidatedMethod({
|
||||||
|
name: 'creatures.removeLibraryLimits',
|
||||||
|
mixins: [RateLimiterMixin, simpleSchemaMixin],
|
||||||
|
schema: new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 10,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({_id, value}) {
|
||||||
|
if (value) {
|
||||||
|
Creatures.update(_id, {
|
||||||
|
$unset: {
|
||||||
|
allowedLibraryCollections: 1,
|
||||||
|
allowedLibraries: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Creatures.update(_id, {
|
||||||
|
$set: {
|
||||||
|
allowedLibraryCollections: [],
|
||||||
|
allowedLibraries: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export {changeAllowedLibraries, toggleAllUserLibraries};
|
||||||
@@ -2,3 +2,4 @@ import '/imports/api/creature/creatures/methods/insertCreature.js';
|
|||||||
import '/imports/api/creature/creatures/methods/removeCreature.js';
|
import '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||||
import '/imports/api/creature/creatures/methods/restCreature.js';
|
import '/imports/api/creature/creatures/methods/restCreature.js';
|
||||||
import '/imports/api/creature/creatures/methods/updateCreature.js';
|
import '/imports/api/creature/creatures/methods/updateCreature.js';
|
||||||
|
import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
|
||||||
|
|||||||
@@ -1,70 +1,104 @@
|
|||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||||
|
import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
|
||||||
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
|
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
|
||||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||||
|
import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
|
||||||
|
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js';
|
||||||
|
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
|
||||||
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
|
import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
|
||||||
const insertCreature = new ValidatedMethod({
|
const insertCreature = new ValidatedMethod({
|
||||||
|
|
||||||
name: 'creatures.insertCreature',
|
name: 'creatures.insertCreature',
|
||||||
|
mixins: [RateLimiterMixin, simpleSchemaMixin],
|
||||||
validate: null,
|
schema: CreatureSchema.pick(
|
||||||
|
'name',
|
||||||
mixins: [RateLimiterMixin],
|
'gender',
|
||||||
|
'alignment',
|
||||||
|
'allowedLibraries',
|
||||||
|
'allowedLibraryCollections',
|
||||||
|
).extend({
|
||||||
|
'startingLevel': {
|
||||||
|
type: SimpleSchema.Integer,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 5,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
|
|
||||||
run() {
|
run({ name, gender, alignment, startingLevel,
|
||||||
if (!this.userId) {
|
allowedLibraries, allowedLibraryCollections }) {
|
||||||
|
const userId = this.userId
|
||||||
|
if (!userId) {
|
||||||
throw new Meteor.Error('Creatures.methods.insert.denied',
|
throw new Meteor.Error('Creatures.methods.insert.denied',
|
||||||
'You need to be logged in to insert a creature');
|
'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
|
assertHasCharactersSlots(userId);
|
||||||
|
|
||||||
|
// Create the creature document
|
||||||
let creatureId = Creatures.insert({
|
let creatureId = Creatures.insert({
|
||||||
owner: this.userId,
|
owner: userId,
|
||||||
});
|
name,
|
||||||
|
gender,
|
||||||
|
alignment,
|
||||||
|
allowedLibraries,
|
||||||
|
allowedLibraryCollections,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert experience to get character to starting level
|
||||||
|
if (startingLevel) {
|
||||||
|
insertExperienceForCreature({
|
||||||
|
experience: {
|
||||||
|
name: 'Starting level',
|
||||||
|
levels: startingLevel,
|
||||||
|
creatureId
|
||||||
|
},
|
||||||
|
creatureId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Insert the default properties
|
// Insert the default properties
|
||||||
// Not batchInsert because we want the properties cleaned by the schema
|
// Not batchInsert because we want the properties cleaned by the schema
|
||||||
let baseId;
|
let baseId, rulesetSlot;
|
||||||
defaultCharacterProperties(creatureId).forEach(prop => {
|
defaultCharacterProperties(creatureId).forEach(prop => {
|
||||||
let id = CreatureProperties.insert(prop);
|
let id = CreatureProperties.insert(prop);
|
||||||
if (prop.name === 'Ruleset'){
|
if (prop.name === 'Ruleset'){
|
||||||
baseId = id;
|
baseId = id;
|
||||||
|
rulesetSlot = prop;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Meteor.isServer){
|
// If the user only has a single ruleset subscribed, use it by default
|
||||||
// Insert the 5e ruleset as the default base
|
if (Meteor.isServer) {
|
||||||
insertPropertyFromLibraryNode.call({
|
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
|
||||||
nodeIds: ['iHbhfcg3AL5isSWbw'],
|
|
||||||
parentRef: {id: baseId, collection: 'creatureProperties'},
|
|
||||||
order: 0.5,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return creatureId;
|
return creatureId;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// If the user only has a single ruleset subscribed, insert it by default
|
||||||
|
function insertDefaultRuleset(creatureId, baseId, userId, slot) {
|
||||||
|
const libraryIds = getCreatureLibraryIds(creatureId, userId);
|
||||||
|
const filter = getSlotFillFilter({ slot, libraryIds });
|
||||||
|
const fillCursor = LibraryNodes.find(filter, { fields: { _id: 1 } });
|
||||||
|
const numRulesets = fillCursor.count();
|
||||||
|
if (numRulesets === 1) {
|
||||||
|
const ruleset = fillCursor.fetch()[0]
|
||||||
|
insertPropertyFromLibraryNode.call({
|
||||||
|
nodeIds: [ruleset._id],
|
||||||
|
parentRef: {id: baseId, collection: 'creatureProperties'},
|
||||||
|
order: 0.5,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default insertCreature;
|
export default insertCreature;
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
||||||
|
|
||||||
function removeRelatedDocuments(creatureId){
|
function removeRelatedDocuments(creatureId){
|
||||||
|
CreatureVariables.remove({_creatureId: creatureId});
|
||||||
CreatureProperties.remove({'ancestors.id': creatureId});
|
CreatureProperties.remove({'ancestors.id': creatureId});
|
||||||
CreatureLogs.remove({creatureId});
|
CreatureLogs.remove({creatureId});
|
||||||
Experiences.remove({creatureId});
|
Experiences.remove({creatureId});
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
import { groupBy, remove, rest, union } from 'lodash';
|
||||||
|
import {
|
||||||
|
getCreature, getVariables, getPropertiesOfType
|
||||||
|
} from '/imports/api/engine/loadCreatures.js';
|
||||||
|
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
|
import { applyTrigger } from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
|
|
||||||
const restCreature = new ValidatedMethod({
|
const restCreature = new ValidatedMethod({
|
||||||
name: 'creature.methods.longRest',
|
name: 'creature.methods.rest',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
creatureId: {
|
creatureId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -24,93 +28,142 @@ const restCreature = new ValidatedMethod({
|
|||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({creatureId, restType}) {
|
run({creatureId, restType}) {
|
||||||
let creature = Creatures.findOne(creatureId, {
|
// Check permissions
|
||||||
fields: {
|
let creature = getCreature(creatureId);
|
||||||
owner: 1,
|
|
||||||
writers: 1,
|
|
||||||
settings: 1,
|
|
||||||
}
|
|
||||||
}) ;
|
|
||||||
// Need edit permissions
|
|
||||||
assertEditPermission(creature, this.userId);
|
assertEditPermission(creature, this.userId);
|
||||||
|
|
||||||
// Long rests reset short rest properties as well
|
// Add the variables to the creature document
|
||||||
let resetFilter;
|
const variables = getVariables(creatureId);
|
||||||
if (restType === 'shortRest'){
|
delete variables._id;
|
||||||
resetFilter = 'shortRest'
|
delete variables._creatureId;
|
||||||
} else {
|
creature.variables = variables;
|
||||||
resetFilter = {$in: ['shortRest', 'longRest']}
|
const scope = creature.variables;
|
||||||
|
|
||||||
|
// Get the triggers
|
||||||
|
let triggers = getPropertiesOfType(creatureId, 'trigger');
|
||||||
|
remove(triggers, trigger =>
|
||||||
|
trigger.event !== 'anyRest' &&
|
||||||
|
trigger.event !== 'longRest' &&
|
||||||
|
trigger.event !== 'shortRest'
|
||||||
|
);
|
||||||
|
triggers = groupBy(triggers, 'event');
|
||||||
|
for (let type in triggers) {
|
||||||
|
triggers[type] = groupBy(triggers[type], 'timing')
|
||||||
}
|
}
|
||||||
// Only apply to active properties
|
|
||||||
let filter = {
|
// Create the log
|
||||||
'ancestors.id': creatureId,
|
const log = CreatureLogSchema.clean({
|
||||||
reset: resetFilter,
|
creatureId: creature._id,
|
||||||
removed: {$ne: true},
|
creatureName: creature.name,
|
||||||
inactive: {$ne: true},
|
|
||||||
};
|
|
||||||
// update all attribute's damage
|
|
||||||
filter.type = 'attribute';
|
|
||||||
CreatureProperties.update(filter, {
|
|
||||||
$set: {damage: 0}
|
|
||||||
}, {
|
|
||||||
selector: {type: 'attribute'},
|
|
||||||
multi: true,
|
|
||||||
});
|
});
|
||||||
// Update all action-like properties' usesUsed
|
|
||||||
filter.type = {$in: [
|
const targets = [creature];
|
||||||
'action',
|
|
||||||
'attack',
|
applyTriggers(triggers, restType, 'before', { creature, targets, scope, log });
|
||||||
'spell'
|
doRestWork(creature, restType);
|
||||||
]};
|
applyTriggers(triggers, restType, 'after', { creature, targets, scope, log });
|
||||||
CreatureProperties.update(filter, {
|
|
||||||
$set: {usesUsed: 0}
|
insertCreatureLogWork({log, creature, method: this});
|
||||||
}, {
|
|
||||||
selector: {type: 'action'},
|
|
||||||
multi: true,
|
|
||||||
});
|
|
||||||
// Reset half hit dice on a long rest, starting with the highest dice
|
|
||||||
if (restType === 'longRest'){
|
|
||||||
let hitDice = CreatureProperties.find({
|
|
||||||
'ancestors.id': creatureId,
|
|
||||||
type: 'attribute',
|
|
||||||
attributeType: 'hitDice',
|
|
||||||
removed: {$ne: true},
|
|
||||||
inactive: {$ne: true},
|
|
||||||
}, {
|
|
||||||
fields: {
|
|
||||||
hitDiceSize: 1,
|
|
||||||
damage: 1,
|
|
||||||
value: 1,
|
|
||||||
}
|
|
||||||
}).fetch();
|
|
||||||
// Use a collator to do sorting in natural order
|
|
||||||
let collator = new Intl.Collator('en', {
|
|
||||||
numeric: true, sensitivity: 'base'
|
|
||||||
});
|
|
||||||
// Get the hit dice in decending order of hitDiceSize
|
|
||||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
|
||||||
hitDice.sort(compare);
|
|
||||||
// Get the total number of hit dice that can be recovered this rest
|
|
||||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0);
|
|
||||||
let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
|
|
||||||
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
|
||||||
// recover each hit dice in turn until the recoverable amount is used up
|
|
||||||
let amountToRecover, resultingDamage;
|
|
||||||
hitDice.forEach(hd => {
|
|
||||||
if (!recoverableHd) return;
|
|
||||||
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
|
||||||
if (!amountToRecover) return;
|
|
||||||
recoverableHd -= amountToRecover;
|
|
||||||
resultingDamage = hd.damage - amountToRecover;
|
|
||||||
CreatureProperties.update(hd._id, {
|
|
||||||
$set: {damage: resultingDamage}
|
|
||||||
}, {
|
|
||||||
selector: {type: 'attribute'},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
computeCreature(creatureId);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function applyTriggers(triggers, restType, timing, opts) {
|
||||||
|
// Get matching triggers
|
||||||
|
let selectedTriggers = triggers[restType]?.[timing] || [];
|
||||||
|
// Get any rest triggers as well
|
||||||
|
selectedTriggers = union(selectedTriggers, triggers['anyRest']?.[timing]);
|
||||||
|
selectedTriggers.sort((a, b) => a.order - b.order);
|
||||||
|
// Apply the triggers
|
||||||
|
selectedTriggers.forEach(trigger => {
|
||||||
|
applyTrigger(trigger, opts)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRestWork(creature, restType) {
|
||||||
|
// Long rests reset short rest properties as well
|
||||||
|
let resetFilter;
|
||||||
|
if (restType === 'shortRest'){
|
||||||
|
resetFilter = 'shortRest'
|
||||||
|
} else {
|
||||||
|
resetFilter = {$in: ['shortRest', 'longRest']}
|
||||||
|
}
|
||||||
|
// Only apply to active properties
|
||||||
|
let filter = {
|
||||||
|
'ancestors.id': creature._id,
|
||||||
|
reset: resetFilter,
|
||||||
|
removed: { $ne: true },
|
||||||
|
inactive: { $ne: true },
|
||||||
|
};
|
||||||
|
// update all attribute's damage
|
||||||
|
filter.type = 'attribute';
|
||||||
|
CreatureProperties.update(filter, {
|
||||||
|
$set: {
|
||||||
|
damage: 0,
|
||||||
|
dirty: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
selector: {type: 'attribute'},
|
||||||
|
multi: true,
|
||||||
|
});
|
||||||
|
// Update all action-like properties' usesUsed
|
||||||
|
filter.type = {$in: [
|
||||||
|
'action',
|
||||||
|
'attack',
|
||||||
|
'spell'
|
||||||
|
]};
|
||||||
|
CreatureProperties.update(filter, {
|
||||||
|
$set: {
|
||||||
|
usesUsed: 0,
|
||||||
|
dirty: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
selector: {type: 'action'},
|
||||||
|
multi: true,
|
||||||
|
});
|
||||||
|
// Reset half hit dice on a long rest, starting with the highest dice
|
||||||
|
if (restType === 'longRest'){
|
||||||
|
let hitDice = CreatureProperties.find({
|
||||||
|
'ancestors.id': creature._id,
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'hitDice',
|
||||||
|
removed: {$ne: true},
|
||||||
|
inactive: {$ne: true},
|
||||||
|
}, {
|
||||||
|
fields: {
|
||||||
|
hitDiceSize: 1,
|
||||||
|
damage: 1,
|
||||||
|
value: 1,
|
||||||
|
}
|
||||||
|
}).fetch();
|
||||||
|
// Use a collator to do sorting in natural order
|
||||||
|
let collator = new Intl.Collator('en', {
|
||||||
|
numeric: true, sensitivity: 'base'
|
||||||
|
});
|
||||||
|
// Get the hit dice in decending order of hitDiceSize
|
||||||
|
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||||
|
hitDice.sort(compare);
|
||||||
|
// Get the total number of hit dice that can be recovered this rest
|
||||||
|
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.value || 0), 0);
|
||||||
|
let resetMultiplier = creature.settings.hitDiceResetMultiplier || 0.5;
|
||||||
|
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
||||||
|
// recover each hit dice in turn until the recoverable amount is used up
|
||||||
|
let amountToRecover, resultingDamage;
|
||||||
|
hitDice.forEach(hd => {
|
||||||
|
if (!recoverableHd) return;
|
||||||
|
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
||||||
|
if (!amountToRecover) return;
|
||||||
|
recoverableHd -= amountToRecover;
|
||||||
|
resultingDamage = hd.damage - amountToRecover;
|
||||||
|
CreatureProperties.update(hd._id, {
|
||||||
|
$set: {
|
||||||
|
damage: resultingDamage,
|
||||||
|
dirty: true,
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
selector: {type: 'attribute'},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default restCreature;
|
export default restCreature;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
|
||||||
let Experiences = new Mongo.Collection('experiences');
|
let Experiences = new Mongo.Collection('experiences');
|
||||||
@@ -48,20 +47,20 @@ let ExperienceSchema = new SimpleSchema({
|
|||||||
Experiences.attachSchema(ExperienceSchema);
|
Experiences.attachSchema(ExperienceSchema);
|
||||||
|
|
||||||
const insertExperienceForCreature = function({experience, creatureId, userId}){
|
const insertExperienceForCreature = function({experience, creatureId, userId}){
|
||||||
assertEditPermission(creatureId, userId);
|
|
||||||
if (experience.xp){
|
if (experience.xp){
|
||||||
Creatures.update(creatureId, {$inc: {
|
Creatures.update(creatureId, {
|
||||||
'denormalizedStats.xp': experience.xp
|
$inc: { 'denormalizedStats.xp': experience.xp },
|
||||||
}});
|
$set: { dirty: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (experience.levels) {
|
if (experience.levels) {
|
||||||
Creatures.update(creatureId, {$inc: {
|
Creatures.update(creatureId, {
|
||||||
'denormalizedStats.milestoneLevels': experience.levels
|
$inc: { 'denormalizedStats.milestoneLevels': experience.levels },
|
||||||
}});
|
$set: { dirty: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
experience.creatureId = creatureId;
|
experience.creatureId = creatureId;
|
||||||
let id = Experiences.insert(experience);
|
let id = Experiences.insert(experience);
|
||||||
computeCreature(creatureId);
|
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,6 +92,7 @@ const insertExperience = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
let insertedIds = [];
|
let insertedIds = [];
|
||||||
creatureIds.forEach(creatureId => {
|
creatureIds.forEach(creatureId => {
|
||||||
|
assertEditPermission(creatureId, userId);
|
||||||
let id = insertExperienceForCreature({experience, creatureId, userId});
|
let id = insertExperienceForCreature({experience, creatureId, userId});
|
||||||
insertedIds.push(id);
|
insertedIds.push(id);
|
||||||
});
|
});
|
||||||
@@ -124,18 +124,19 @@ const removeExperience = new ValidatedMethod({
|
|||||||
let creatureId = experience.creatureId
|
let creatureId = experience.creatureId
|
||||||
assertEditPermission(creatureId, userId);
|
assertEditPermission(creatureId, userId);
|
||||||
if (experience.xp){
|
if (experience.xp){
|
||||||
Creatures.update(creatureId, {$inc: {
|
Creatures.update(creatureId, {
|
||||||
'denormalizedStats.xp': -experience.xp
|
$inc: { 'denormalizedStats.xp': -experience.xp },
|
||||||
}});
|
$set: { dirty: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (experience.levels) {
|
if (experience.levels) {
|
||||||
Creatures.update(creatureId, {$inc: {
|
Creatures.update(creatureId, {
|
||||||
'denormalizedStats.milestoneLevels': -experience.levels
|
$inc: { 'denormalizedStats.milestoneLevels': -experience.levels },
|
||||||
}});
|
$set: { dirty: true },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
experience.creatureId = creatureId;
|
experience.creatureId = creatureId;
|
||||||
let numRemoved = Experiences.remove(experienceId);
|
let numRemoved = Experiences.remove(experienceId);
|
||||||
computeCreature(creatureId);
|
|
||||||
return numRemoved;
|
return numRemoved;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -173,11 +174,11 @@ const recomputeExperiences = new ValidatedMethod({
|
|||||||
});
|
});
|
||||||
Creatures.update(creatureId, {$set: {
|
Creatures.update(creatureId, {$set: {
|
||||||
'denormalizedStats.xp': xp,
|
'denormalizedStats.xp': xp,
|
||||||
'denormalizedStats.milestoneLevels': milestoneLevels
|
'denormalizedStats.milestoneLevels': milestoneLevels,
|
||||||
|
dirty: true,
|
||||||
}});
|
}});
|
||||||
computeCreature(creatureId);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Experiences;
|
export default Experiences;
|
||||||
export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences };
|
export { ExperienceSchema, insertExperience, insertExperienceForCreature, removeExperience, recomputeExperiences };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||||
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
|
import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
@@ -154,7 +155,6 @@ const logRoll = new ValidatedMethod({
|
|||||||
}).validator(),
|
}).validator(),
|
||||||
run({roll, creatureId}){
|
run({roll, creatureId}){
|
||||||
const creature = Creatures.findOne(creatureId, {fields: {
|
const creature = Creatures.findOne(creatureId, {fields: {
|
||||||
variables: 1,
|
|
||||||
readers: 1,
|
readers: 1,
|
||||||
writers: 1,
|
writers: 1,
|
||||||
owner: 1,
|
owner: 1,
|
||||||
@@ -163,6 +163,7 @@ const logRoll = new ValidatedMethod({
|
|||||||
avatarPicture: 1,
|
avatarPicture: 1,
|
||||||
}});
|
}});
|
||||||
assertEditPermission(creature, this.userId);
|
assertEditPermission(creature, this.userId);
|
||||||
|
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
||||||
let logContent = []
|
let logContent = []
|
||||||
let parsedResult = undefined;
|
let parsedResult = undefined;
|
||||||
try {
|
try {
|
||||||
@@ -175,7 +176,7 @@ const logRoll = new ValidatedMethod({
|
|||||||
let {
|
let {
|
||||||
result: compiled,
|
result: compiled,
|
||||||
context
|
context
|
||||||
} = resolve('compile', parsedResult, creature.variables);
|
} = resolve('compile', parsedResult, variables);
|
||||||
const compiledString = toString(compiled);
|
const compiledString = toString(compiled);
|
||||||
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
|
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
|
||||||
value: roll
|
value: roll
|
||||||
@@ -183,12 +184,12 @@ const logRoll = new ValidatedMethod({
|
|||||||
logContent.push({
|
logContent.push({
|
||||||
value: compiledString
|
value: compiledString
|
||||||
});
|
});
|
||||||
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
|
let {result: rolled} = resolve('roll', compiled, variables, context);
|
||||||
let rolledString = toString(rolled);
|
let rolledString = toString(rolled);
|
||||||
if (rolledString !== compiledString) logContent.push({
|
if (rolledString !== compiledString) logContent.push({
|
||||||
value: rolledString
|
value: rolledString
|
||||||
});
|
});
|
||||||
let {result} = resolve('reduce', rolled, creature.variables, context);
|
let {result} = resolve('reduce', rolled, variables, context);
|
||||||
let resultString = toString(result);
|
let resultString = toString(result);
|
||||||
if (resultString !== rolledString) logContent.push({
|
if (resultString !== rolledString) logContent.push({
|
||||||
value: resultString
|
value: resultString
|
||||||
@@ -210,4 +211,4 @@ const logRoll = new ValidatedMethod({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default CreatureLogs;
|
export default CreatureLogs;
|
||||||
export { CreatureLogSchema, insertCreatureLog, logRoll};
|
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import note from './applyPropertyByType/applyNote.js';
|
|||||||
import roll from './applyPropertyByType/applyRoll.js';
|
import roll from './applyPropertyByType/applyRoll.js';
|
||||||
import savingThrow from './applyPropertyByType/applySavingThrow.js';
|
import savingThrow from './applyPropertyByType/applySavingThrow.js';
|
||||||
import toggle from './applyPropertyByType/applyToggle.js';
|
import toggle from './applyPropertyByType/applyToggle.js';
|
||||||
|
import applyTriggers from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
|
|
||||||
const applyPropertyByType = {
|
const applyPropertyByType = {
|
||||||
action,
|
action,
|
||||||
@@ -21,7 +22,9 @@ const applyPropertyByType = {
|
|||||||
toggle,
|
toggle,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function applyProperty(node, opts, ...rest){
|
export default function applyProperty(node, opts, ...rest) {
|
||||||
|
applyTriggers(node, opts, 'before');
|
||||||
opts.scope[`#${node.node.type}`] = node.node;
|
opts.scope[`#${node.node.type}`] = node.node;
|
||||||
return applyPropertyByType[node.node.type]?.(node, opts, ...rest);
|
applyPropertyByType[node.node.type]?.(node, opts, ...rest);
|
||||||
|
applyTriggers(node, opts, 'after');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ export default function applyDamage(node, {
|
|||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
|
|
||||||
// Skip if there is no parse node to work with
|
// Skip if there is no parse node to work with
|
||||||
if (!prop.amount.parseNode) return;
|
if (!prop.amount?.parseNode) return;
|
||||||
|
|
||||||
// Choose target
|
// Choose target
|
||||||
|
|
||||||
let damageTargets = prop.target === 'self' ? [creature] : targets;
|
let damageTargets = prop.target === 'self' ? [creature] : targets;
|
||||||
// Determine if the hit is critical
|
// Determine if the hit is critical
|
||||||
let criticalHit = scope['$criticalHit']?.value &&
|
let criticalHit = scope['$criticalHit']?.value &&
|
||||||
@@ -69,6 +68,15 @@ export default function applyDamage(node, {
|
|||||||
// Round the damage to a whole number
|
// Round the damage to a whole number
|
||||||
damage = Math.floor(damage);
|
damage = Math.floor(damage);
|
||||||
|
|
||||||
|
// Convert extra damage into the stored type
|
||||||
|
if (prop.damageType === 'extra' && scope['$lastDamageType']) {
|
||||||
|
prop.damageType = scope['$lastDamageType'];
|
||||||
|
}
|
||||||
|
// Store current damage type
|
||||||
|
if (prop.damageType !== 'healing') {
|
||||||
|
scope['$lastDamageType'] = prop.damageType;
|
||||||
|
}
|
||||||
|
|
||||||
// Memoise the damage suffix for the log
|
// Memoise the damage suffix for the log
|
||||||
let suffix = (criticalHit ? ' critical ' : ' ') +
|
let suffix = (criticalHit ? ' critical ' : ' ') +
|
||||||
prop.damageType +
|
prop.damageType +
|
||||||
|
|||||||
98
app/imports/api/engine/actions/applyTriggers.js
Normal file
98
app/imports/api/engine/actions/applyTriggers.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import recalculateCalculation from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateCalculation.js';
|
||||||
|
import recalculateInlineCalculations from '/imports/api/engine/actions/applyPropertyByType/shared/recalculateInlineCalculations.js';
|
||||||
|
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js';
|
||||||
|
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||||
|
import applyProperty from '/imports/api/engine/actions/applyProperty.js';
|
||||||
|
import { difference, intersection } from 'lodash';
|
||||||
|
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||||
|
|
||||||
|
export default function applyTriggers(node, { creature, targets, scope, log }, timing) {
|
||||||
|
const prop = node.node;
|
||||||
|
const type = prop.type;
|
||||||
|
if (creature.triggers?.[type]?.[timing]) {
|
||||||
|
creature.triggers[type][timing].forEach(trigger => {
|
||||||
|
// Tags
|
||||||
|
if (!triggerMatchTags(trigger, prop)) return;
|
||||||
|
// Condition
|
||||||
|
if (trigger.condition?.parseNode) {
|
||||||
|
recalculateCalculation(trigger.condition, scope, log);
|
||||||
|
if (!trigger.condition.value) return;
|
||||||
|
}
|
||||||
|
// Apply
|
||||||
|
applyTrigger(trigger, { creature, targets, scope, log });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerMatchTags(trigger, prop) {
|
||||||
|
let matched = false;
|
||||||
|
const propTags = getEffectivePropTags(prop);
|
||||||
|
// Check the target tags
|
||||||
|
if (
|
||||||
|
!trigger.targetTags?.length ||
|
||||||
|
difference(trigger.targetTags, propTags).length === 0
|
||||||
|
) {
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
// Check the extra tags
|
||||||
|
trigger.extraTags?.forEach(extra => {
|
||||||
|
if (extra.operation === 'OR') {
|
||||||
|
if (matched) return;
|
||||||
|
if (
|
||||||
|
!extra.tags.length ||
|
||||||
|
difference(extra.tags, propTags).length === 0
|
||||||
|
) {
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
} else if (extra.operation === 'NOT') {
|
||||||
|
if (
|
||||||
|
extra.tags.length &&
|
||||||
|
intersection(extra.tags, propTags)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTrigger(trigger, { creature, targets, scope, log }) {
|
||||||
|
if (trigger.firing) {
|
||||||
|
/*
|
||||||
|
log.content.push({
|
||||||
|
name: trigger.name || 'Trigger',
|
||||||
|
value: 'Trigger can\'t fire itself',
|
||||||
|
inline: true,
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
trigger.firing = true;
|
||||||
|
|
||||||
|
// Fire the trigger
|
||||||
|
const content = {
|
||||||
|
name: trigger.name || 'Trigger',
|
||||||
|
value: trigger.summary,
|
||||||
|
inline: false,
|
||||||
|
}
|
||||||
|
if (trigger.summary?.text){
|
||||||
|
recalculateInlineCalculations(trigger.summary, scope, log);
|
||||||
|
content.value = trigger.summary.value;
|
||||||
|
}
|
||||||
|
log.content.push(content);
|
||||||
|
|
||||||
|
// Get all the trigger's properties and apply them
|
||||||
|
const properties = getPropertyDecendants(creature._id, trigger._id);
|
||||||
|
properties.sort((a, b) => a.order - b.order);
|
||||||
|
const propertyForest = nodeArrayToTree(properties);
|
||||||
|
propertyForest.forEach(node => {
|
||||||
|
applyProperty(node, {
|
||||||
|
creature,
|
||||||
|
targets,
|
||||||
|
scope,
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
trigger.firing = false;
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
|
||||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||||
|
import {
|
||||||
|
getCreature, getVariables, getProperyAncestors, getPropertyDecendants, getPropertiesOfType
|
||||||
|
} from '/imports/api/engine/loadCreatures.js';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import applyProperty from './applyProperty.js';
|
import applyProperty from './applyProperty.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
import { groupBy, remove } from 'lodash';
|
||||||
|
|
||||||
const doAction = new ValidatedMethod({
|
const doAction = new ValidatedMethod({
|
||||||
name: 'creatureProperties.doAction',
|
name: 'creatureProperties.doAction',
|
||||||
@@ -38,48 +40,46 @@ const doAction = new ValidatedMethod({
|
|||||||
run({actionId, targetIds = [], scope}) {
|
run({actionId, targetIds = [], scope}) {
|
||||||
let action = CreatureProperties.findOne(actionId);
|
let action = CreatureProperties.findOne(actionId);
|
||||||
// Check permissions
|
// Check permissions
|
||||||
let creature = getRootCreatureAncestor(action);
|
const creatureId = action.ancestors[0].id;
|
||||||
|
let creature = getCreature(action.ancestors[0].id);
|
||||||
assertEditPermission(creature, this.userId);
|
assertEditPermission(creature, this.userId);
|
||||||
|
|
||||||
|
// Add the variables to the creature document
|
||||||
|
const variables = getVariables(creatureId);
|
||||||
|
delete variables._id;
|
||||||
|
delete variables._creatureId;
|
||||||
|
creature.variables = variables;
|
||||||
|
|
||||||
// Get all the targets and make sure we can edit them
|
// Get all the targets and make sure we can edit them
|
||||||
let targets = [];
|
let targets = [];
|
||||||
targetIds.forEach(targetId => {
|
targetIds.forEach(targetId => {
|
||||||
let target = Creatures.findOne(targetId);
|
let target = getCreature(targetId);
|
||||||
assertEditPermission(target, this.userId);
|
assertEditPermission(target, this.userId);
|
||||||
|
|
||||||
|
// add the variables to the target documents
|
||||||
|
const variables = getVariables(creatureId);
|
||||||
|
delete variables._id;
|
||||||
|
delete variables._creatureId;
|
||||||
|
target.variables = variables;
|
||||||
|
|
||||||
targets.push(target);
|
targets.push(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch all the action's ancestor creatureProperties
|
const ancestors = getProperyAncestors(creatureId, action._id);
|
||||||
const ancestorIds = [];
|
ancestors.sort((a, b) => a.order - b.order);
|
||||||
action.ancestors.forEach(ref => {
|
|
||||||
if (ref.collection === 'creatureProperties') {
|
|
||||||
ancestorIds.push(ref.id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get cursor of ancestors
|
const properties = getPropertyDecendants(creatureId, action._id);
|
||||||
const ancestors = CreatureProperties.find({
|
properties.push(action);
|
||||||
_id: {$in: ancestorIds},
|
properties.sort((a, b) => a.order - b.order);
|
||||||
}, {
|
|
||||||
sort: {order: 1},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get cursor of the properties
|
|
||||||
const properties = CreatureProperties.find({
|
|
||||||
$or: [{_id: action._id}, {'ancestors.id': action._id}],
|
|
||||||
removed: {$ne: true},
|
|
||||||
}, {
|
|
||||||
sort: {order: 1},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Do the action
|
// Do the action
|
||||||
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
|
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
|
||||||
|
|
||||||
// Recompute all involved creatures
|
// Recompute all involved creatures
|
||||||
computeCreature(creature._id);
|
Creatures.update({
|
||||||
targets.forEach(target => {
|
_id: { $in: [creature._id, ...targetIds] }
|
||||||
computeCreature(target._id);
|
}, {
|
||||||
|
$set: {dirty: true},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -96,6 +96,14 @@ export function doActionWork({
|
|||||||
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
|
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the triggers
|
||||||
|
const triggers = getPropertiesOfType(creature._id, 'trigger');
|
||||||
|
remove(triggers, trigger => trigger.event !== 'doActionProperty');
|
||||||
|
creature.triggers = groupBy(triggers, 'actionPropertyType');
|
||||||
|
for (let type in creature.triggers) {
|
||||||
|
creature.triggers[type] = groupBy(creature.triggers[type], 'timing')
|
||||||
|
}
|
||||||
|
|
||||||
// Create the log
|
// Create the log
|
||||||
if (!log) log = CreatureLogSchema.clean({
|
if (!log) log = CreatureLogSchema.clean({
|
||||||
creatureId: creature._id,
|
creatureId: creature._id,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
|
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
|
|
||||||
const doAction = new ValidatedMethod({
|
const doAction = new ValidatedMethod({
|
||||||
@@ -129,12 +128,12 @@ const doAction = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Do the action
|
// Do the action
|
||||||
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope, log});
|
doActionWork({ creature, targets, properties, ancestors, method: this, methodScope: scope, log });
|
||||||
|
|
||||||
// Recompute all involved creatures
|
Creatures.update({
|
||||||
computeCreature(creature._id);
|
_id: { $in: [creature._id, ...targetIds] }
|
||||||
targets.forEach(target => {
|
}, {
|
||||||
computeCreature(target._id);
|
$set: { dirty: true },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import rollDice from '/imports/parser/rollDice.js';
|
import rollDice from '/imports/parser/rollDice.js';
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
|
|
||||||
@@ -32,9 +31,6 @@ const doCheck = new ValidatedMethod({
|
|||||||
|
|
||||||
// Do the check
|
// Do the check
|
||||||
doCheckWork({creature, prop, method: this, methodScope: scope});
|
doCheckWork({creature, prop, method: this, methodScope: scope});
|
||||||
|
|
||||||
// Recompute all involved creatures
|
|
||||||
computeCreature(creature._id);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { EJSON } from 'meteor/ejson';
|
import { EJSON } from 'meteor/ejson';
|
||||||
import createGraph from 'ngraph.graph';
|
import createGraph from 'ngraph.graph';
|
||||||
|
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||||
|
|
||||||
export default class CreatureComputation {
|
export default class CreatureComputation {
|
||||||
constructor(properties, creature){
|
constructor(properties, creature, variables){
|
||||||
// Set up fields
|
// Set up fields
|
||||||
this.originalPropsById = {};
|
this.originalPropsById = {};
|
||||||
this.propsById = {};
|
this.propsById = {};
|
||||||
@@ -12,6 +13,7 @@ export default class CreatureComputation {
|
|||||||
this.dependencyGraph = createGraph();
|
this.dependencyGraph = createGraph();
|
||||||
this.errors = [];
|
this.errors = [];
|
||||||
this.creature = creature;
|
this.creature = creature;
|
||||||
|
this.variables = variables;
|
||||||
|
|
||||||
// Store properties for easy access later
|
// Store properties for easy access later
|
||||||
properties.forEach(prop => {
|
properties.forEach(prop => {
|
||||||
@@ -21,28 +23,15 @@ export default class CreatureComputation {
|
|||||||
// Store by id
|
// Store by id
|
||||||
this.propsById[prop._id] = prop;
|
this.propsById[prop._id] = prop;
|
||||||
|
|
||||||
// Store tags
|
// Store sets of ids in each tag
|
||||||
const storePropOnTag = (prop, tag) => {
|
getEffectivePropTags(prop).forEach(tag => {
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
if (this.propsWithTag[tag]){
|
if (this.propsWithTag[tag]) {
|
||||||
this.propsWithTag[tag].push(prop._id);
|
this.propsWithTag[tag].push(prop._id);
|
||||||
} else {
|
} else {
|
||||||
this.propsWithTag[tag] = [prop._id];
|
this.propsWithTag[tag] = [prop._id];
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
// Store sets of ids in each tag
|
|
||||||
if (prop.tags){
|
|
||||||
prop.tags.forEach(tag => {
|
|
||||||
storePropOnTag(prop, tag);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Store tags for the property type
|
|
||||||
storePropOnTag(prop, `#${prop.type}`);
|
|
||||||
// Store tags for some string properties
|
|
||||||
storePropOnTag(prop, prop.damageType);
|
|
||||||
storePropOnTag(prop, prop.skillType);
|
|
||||||
storePropOnTag(prop, prop.attributeType);
|
|
||||||
storePropOnTag(prop, prop.reset);
|
|
||||||
|
|
||||||
// Store the prop in the dependency graph
|
// Store the prop in the dependency graph
|
||||||
this.dependencyGraph.addNode(prop._id, prop);
|
this.dependencyGraph.addNode(prop._id, prop);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { get, intersection, difference } from 'lodash';
|
import { get, intersection, difference, union } from 'lodash';
|
||||||
|
|
||||||
const linkDependenciesByType = {
|
const linkDependenciesByType = {
|
||||||
action: linkAction,
|
action: linkAction,
|
||||||
@@ -128,10 +128,21 @@ function linkEffects(dependencyGraph, prop, computation){
|
|||||||
if (prop.targetByTags){
|
if (prop.targetByTags){
|
||||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||||
const targetProp = computation.propsById[targetId];
|
const targetProp = computation.propsById[targetId];
|
||||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
if (
|
||||||
const calcObj = get(targetProp, key);
|
(targetProp.type === 'attribute' || targetProp.type === 'skill')
|
||||||
if (calcObj && calcObj.calculation){
|
&& targetProp.variableName
|
||||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
|
&& !prop.targetField
|
||||||
|
) {
|
||||||
|
// If the field wasn't specified and we're targeting an attribute or
|
||||||
|
// skill, just treat it like a normal effect on its variable name
|
||||||
|
dependencyGraph.addLink(targetProp.variableName, prop._id, 'effect');
|
||||||
|
} else {
|
||||||
|
// Otherwise target a field on that property
|
||||||
|
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||||
|
const calcObj = get(targetProp, key);
|
||||||
|
if (calcObj && calcObj.calculation){
|
||||||
|
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -144,16 +155,18 @@ function linkEffects(dependencyGraph, prop, computation){
|
|||||||
|
|
||||||
// Returns an array of IDs of the properties the effect targets
|
// Returns an array of IDs of the properties the effect targets
|
||||||
function getEffectTagTargets(effect, computation){
|
function getEffectTagTargets(effect, computation){
|
||||||
const targets = getTargetListFromTags(effect.targetTags, computation);
|
let targets = getTargetListFromTags(effect.targetTags, computation);
|
||||||
const notIds = [];
|
let notIds = [];
|
||||||
if (effect.extraTags){
|
if (effect.extraTags){
|
||||||
effect.extraTags.forEach(ex => {
|
effect.extraTags.forEach(ex => {
|
||||||
if (ex.operation === 'OR'){
|
if (ex.operation === 'OR') {
|
||||||
targets.push(...getTargetListFromTags(ex.tags, computation));
|
targets = union(targets, getTargetListFromTags(ex.tags, computation));
|
||||||
} else if (ex.operation === 'NOT'){
|
} else if (ex.operation === 'NOT'){
|
||||||
ex.tags.forEach(tag => {
|
ex.tags.forEach(tag => {
|
||||||
const idList = computation.propsWithTag[tag];
|
const idList = computation.propsWithTag[tag];
|
||||||
if (idList) notIds.push(...computation.propsWithTag[tag])
|
if (idList) {
|
||||||
|
notIds = union(notIds, computation.propsWithTag[tag]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||||
import CreatureProperties,
|
import { DenormalisedOnlyCreaturePropertySchema as denormSchema }
|
||||||
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
|
|
||||||
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import { getProperties, getCreature, getVariables } from '/imports/api/engine/loadCreatures.js';
|
||||||
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
||||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
||||||
import linkInventory from './buildComputation/linkInventory.js';
|
import linkInventory from './buildComputation/linkInventory.js';
|
||||||
@@ -32,29 +31,15 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
|||||||
|
|
||||||
export default function buildCreatureComputation(creatureId){
|
export default function buildCreatureComputation(creatureId){
|
||||||
const creature = getCreature(creatureId);
|
const creature = getCreature(creatureId);
|
||||||
|
const variables = getVariables(creatureId);
|
||||||
const properties = getProperties(creatureId);
|
const properties = getProperties(creatureId);
|
||||||
const computation = buildComputationFromProps(properties, creature);
|
const computation = buildComputationFromProps(properties, creature, variables);
|
||||||
return computation;
|
return computation;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProperties(creatureId){
|
export function buildComputationFromProps(properties, creature, variables){
|
||||||
return CreatureProperties.find({
|
|
||||||
'ancestors.id': creatureId,
|
|
||||||
'removed': {$ne: true},
|
|
||||||
}, {
|
|
||||||
sort: {order: 1}
|
|
||||||
}).fetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCreature(creatureId){
|
const computation = new CreatureComputation(properties, creature, variables);
|
||||||
return Creatures.findOne(creatureId, {
|
|
||||||
denormalizedStats: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildComputationFromProps(properties, creature){
|
|
||||||
|
|
||||||
const computation = new CreatureComputation(properties, creature);
|
|
||||||
// Dependency graph where edge(a, b) means a depends on b
|
// Dependency graph where edge(a, b) means a depends on b
|
||||||
// The graph includes all dependencies even of inactive properties
|
// The graph includes all dependencies even of inactive properties
|
||||||
// such that any properties changing without changing their dependencies
|
// such that any properties changing without changing their dependencies
|
||||||
@@ -81,8 +66,10 @@ export function buildComputationFromProps(properties, creature){
|
|||||||
|
|
||||||
// Process the properties one by one
|
// Process the properties one by one
|
||||||
properties.forEach(prop => {
|
properties.forEach(prop => {
|
||||||
|
// The prop has been processed, it's no longer dirty
|
||||||
|
delete prop.dirty;
|
||||||
|
|
||||||
let computedSchema = computedOnlySchemas[prop.type];
|
const computedSchema = computedOnlySchemas[prop.type];
|
||||||
removeSchemaFields([computedSchema, denormSchema], prop);
|
removeSchemaFields([computedSchema, denormSchema], prop);
|
||||||
|
|
||||||
// Add a place to store all the computation details
|
// Add a place to store all the computation details
|
||||||
@@ -114,5 +101,6 @@ export function buildComputationFromProps(properties, creature){
|
|||||||
linkTypeDependencies(dependencyGraph, prop, computation);
|
linkTypeDependencies(dependencyGraph, prop, computation);
|
||||||
linkCalculationDependencies(dependencyGraph, prop, computation);
|
linkCalculationDependencies(dependencyGraph, prop, computation);
|
||||||
});
|
});
|
||||||
|
|
||||||
return computation;
|
return computation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export default function computeVariableAsSkill(computation, node, prop){
|
|||||||
prop.fail = aggregator.fail;
|
prop.fail = aggregator.fail;
|
||||||
// Rollbonus
|
// Rollbonus
|
||||||
prop.rollBonuses = aggregator.rollBonus;
|
prop.rollBonuses = aggregator.rollBonus;
|
||||||
|
// Store effects
|
||||||
|
prop.effects = node.data.effects;
|
||||||
}
|
}
|
||||||
|
|
||||||
function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default function getEffectivePropTags(prop) {
|
||||||
|
if (!prop.tags) return [];
|
||||||
|
const tags = [...prop.tags];
|
||||||
|
// Tags for the property type, separate #damage from #healing
|
||||||
|
if (prop.type === 'damage' && prop.damageType === 'healing') {
|
||||||
|
tags.push('#healing');
|
||||||
|
} else {
|
||||||
|
tags.push(`#${prop.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags for some string properties
|
||||||
|
if (prop.damageType) tags.push(prop.damageType);
|
||||||
|
if (prop.skillType) tags.push(prop.skillType);
|
||||||
|
if (prop.attributeType) tags.push(prop.attributeType);
|
||||||
|
if (prop.reset) tags.push(prop.reset);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ export default function walkDown(tree, callback){
|
|||||||
let stack = [...tree];
|
let stack = [...tree];
|
||||||
while(stack.length){
|
while(stack.length){
|
||||||
let node = stack.pop();
|
let node = stack.pop();
|
||||||
callback(node);
|
callback(node, stack);
|
||||||
stack.push(...node.children);
|
stack.push(...node.children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default function writeAlteredProperties(computation){
|
|||||||
'deactivatedByAncestor',
|
'deactivatedByAncestor',
|
||||||
'deactivatedByToggle',
|
'deactivatedByToggle',
|
||||||
'damage',
|
'damage',
|
||||||
|
'dirty',
|
||||||
...schema.objectKeys(),
|
...schema.objectKeys(),
|
||||||
];
|
];
|
||||||
op = addChangedKeysToOp(op, keys, original, changed);
|
op = addChangedKeysToOp(op, keys, original, changed);
|
||||||
@@ -28,13 +29,14 @@ export default function writeAlteredProperties(computation){
|
|||||||
bulkWriteOperations.push(op);
|
bulkWriteOperations.push(op);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
writePropertiesSequentially(bulkWriteOperations);
|
bulkWriteProperties(bulkWriteOperations);
|
||||||
|
//if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addChangedKeysToOp(op, keys, original, changed) {
|
function addChangedKeysToOp(op, keys, original, changed) {
|
||||||
// Loop through all keys that can be changed by computation
|
// Loop through all keys that can be changed by computation
|
||||||
// and compile an operation that sets all those keys
|
// and compile an operation that sets all those keys
|
||||||
for (let key of keys){
|
for (let key of keys) {
|
||||||
if (!EJSON.equals(original[key], changed[key])){
|
if (!EJSON.equals(original[key], changed[key])){
|
||||||
if (!op) op = newOperation(original._id, changed.type);
|
if (!op) op = newOperation(original._id, changed.type);
|
||||||
let value = changed[key];
|
let value = changed[key];
|
||||||
@@ -79,10 +81,10 @@ function addUnsetOp(op, key){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use this instead of bulkWriteProperties because it functions with latency
|
// If we re-enable client-side sheet recalculation, this needs to be run on
|
||||||
// compensation without needing to roll back changes, which causes multiple
|
// both client and server to preserve latency compensation. Bulkwrite breaks
|
||||||
// expensive redraws of the character sheet
|
// latency compensation and causes flickering
|
||||||
function writePropertiesSequentially(bulkWriteOps){
|
function writePropertiesSequentially(bulkWriteOps) {
|
||||||
bulkWriteOps.forEach(op => {
|
bulkWriteOps.forEach(op => {
|
||||||
let updateOneOrMany = op.updateOne || op.updateMany;
|
let updateOneOrMany = op.updateOne || op.updateMany;
|
||||||
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
|
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
|
||||||
@@ -101,7 +103,7 @@ function writePropertiesSequentially(bulkWriteOps){
|
|||||||
function bulkWriteProperties(bulkWriteOps){
|
function bulkWriteProperties(bulkWriteOps){
|
||||||
if (!bulkWriteOps.length) return;
|
if (!bulkWriteOps.length) return;
|
||||||
// bulkWrite is only available on the server
|
// bulkWrite is only available on the server
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer) {
|
||||||
CreatureProperties.rawCollection().bulkWrite(
|
CreatureProperties.rawCollection().bulkWrite(
|
||||||
bulkWriteOps,
|
bulkWriteOps,
|
||||||
{ordered : false},
|
{ordered : false},
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import { EJSON } from 'meteor/ejson';
|
import { EJSON } from 'meteor/ejson';
|
||||||
|
|
||||||
export default function writeScope(creatureId, computation) {
|
export default function writeScope(creatureId, computation) {
|
||||||
|
if (!creatureId) throw 'creatureId is required';
|
||||||
const scope = computation.scope;
|
const scope = computation.scope;
|
||||||
const variables = computation.creature.variables || {};
|
let variables = computation.variables;
|
||||||
let $set;
|
if (!variables) {
|
||||||
|
CreatureVariables.insert({ _creatureId: creatureId });
|
||||||
|
variables = {};
|
||||||
|
}
|
||||||
|
delete variables._id;
|
||||||
|
delete variables._creatureId;
|
||||||
|
|
||||||
|
let $set, $unset;
|
||||||
|
|
||||||
for (const key in scope){
|
for (const key in scope){
|
||||||
// Remove large properties that aren't likely to be accessed
|
// Remove large properties that aren't likely to be accessed
|
||||||
delete scope[key].parent;
|
delete scope[key].parent;
|
||||||
@@ -20,12 +30,32 @@ export default function writeScope(creatureId, computation) {
|
|||||||
// Only update changed fields
|
// Only update changed fields
|
||||||
if (!EJSON.equals(variables[key], scope[key])) {
|
if (!EJSON.equals(variables[key], scope[key])) {
|
||||||
if (!$set) $set = {};
|
if (!$set) $set = {};
|
||||||
|
/* Log detailed diffs
|
||||||
|
const diff = omitBy(variables[key], (v, k) => EJSON.equals(scope[key][k], v));
|
||||||
|
for (let subkey in diff) {
|
||||||
|
console.log(`${key}.${subkey}: ${variables[key][subkey]} => ${scope[key][subkey]}`)
|
||||||
|
}
|
||||||
|
*/
|
||||||
// Set the changed key in the creature variables
|
// Set the changed key in the creature variables
|
||||||
$set[`variables.${key}`] = scope[key];
|
$set[key] = scope[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($set) {
|
|
||||||
Creatures.update(creatureId, {$set});
|
// Remove all the keys that no longer exist in scope
|
||||||
|
for (const key in variables) {
|
||||||
|
if (!scope[key]) {
|
||||||
|
if (!$unset) $unset = {};
|
||||||
|
$unset[key] = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($set || $unset) {
|
||||||
|
const update = {};
|
||||||
|
if ($set) update.$set = $set;
|
||||||
|
if ($unset) update.$unset = $unset;
|
||||||
|
CreatureVariables.update({_creatureId: creatureId}, update);
|
||||||
|
}
|
||||||
|
if (computation.creature?.dirty) {
|
||||||
|
Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import writeErrors from './computation/writeComputation/writeErrors.js';
|
|||||||
|
|
||||||
export default function computeCreature(creatureId){
|
export default function computeCreature(creatureId){
|
||||||
if (Meteor.isClient) return;
|
if (Meteor.isClient) return;
|
||||||
|
// console.log('compute ' + creatureId);
|
||||||
const computation = buildCreatureComputation(creatureId);
|
const computation = buildCreatureComputation(creatureId);
|
||||||
|
computeComputation(computation, creatureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeComputation(computation, creatureId) {
|
||||||
try {
|
try {
|
||||||
computeCreatureComputation(computation);
|
computeCreatureComputation(computation);
|
||||||
writeAlteredProperties(computation);
|
writeAlteredProperties(computation);
|
||||||
@@ -15,24 +20,18 @@ export default function computeCreature(creatureId){
|
|||||||
const errorText = e.reason || e.message || e.toString();
|
const errorText = e.reason || e.message || e.toString();
|
||||||
computation.errors.push({
|
computation.errors.push({
|
||||||
type: 'crash',
|
type: 'crash',
|
||||||
details: {error: errorText},
|
details: { error: errorText },
|
||||||
});
|
});
|
||||||
const logError = {
|
const logError = {
|
||||||
creatureId,
|
creatureId,
|
||||||
computeError: errorText,
|
computeError: errorText,
|
||||||
};
|
};
|
||||||
if (e.stack){
|
if (e.stack) {
|
||||||
logError.location = e.stack.split('\n')[1];
|
logError.location = e.stack.split('\n')[1];
|
||||||
}
|
}
|
||||||
console.error(logError);
|
console.error(logError);
|
||||||
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
writeErrors(creatureId, computation.errors);
|
writeErrors(creatureId, computation.errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now just recompute the whole creature, TODO only recompute a single
|
|
||||||
// connected section of the depdendency graph
|
|
||||||
export function computeCreatureDependencyGroup(property){
|
|
||||||
let creatureId = property.ancestors[0].id;
|
|
||||||
computeCreature(creatureId);
|
|
||||||
}
|
|
||||||
|
|||||||
302
app/imports/api/engine/loadCreatures.js
Normal file
302
app/imports/api/engine/loadCreatures.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import { debounce } from 'lodash';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import computeCreature from './computeCreature';
|
||||||
|
|
||||||
|
const COMPUTE_DEBOUNCE_TIME = 100; // ms
|
||||||
|
export const loadedCreatures = new Map(); // creatureId => {creature, properties, etc.}
|
||||||
|
|
||||||
|
export function loadCreature(creatureId, subscription) {
|
||||||
|
if (!creatureId) throw 'creatureId is required';
|
||||||
|
let creature = loadedCreatures.get(creatureId);
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
creature.subs.add(subscription);
|
||||||
|
} else {
|
||||||
|
creature = new LoadedCreature(subscription, creatureId);
|
||||||
|
loadedCreatures.set(creatureId, creature);
|
||||||
|
}
|
||||||
|
subscription.onStop(() => {
|
||||||
|
unloadCreature(creatureId, subscription);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function unloadCreature(creatureId, subscription) {
|
||||||
|
if (!creatureId) throw 'creatureId is required';
|
||||||
|
const creature = loadedCreatures.get(creatureId);
|
||||||
|
if (!creature) return;
|
||||||
|
creature.subs.delete(subscription);
|
||||||
|
if (creature.subs.size === 0) {
|
||||||
|
creature.stop();
|
||||||
|
loadedCreatures.delete(creatureId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSingleProperty(creatureId, propertyId) {
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
const creature = loadedCreatures.get(creatureId);
|
||||||
|
const property = creature.properties.get(propertyId);
|
||||||
|
const cloneProp = EJSON.clone(property);
|
||||||
|
return cloneProp;
|
||||||
|
}
|
||||||
|
// console.time(`Cache miss on creature properties: ${creatureId}`)
|
||||||
|
const prop = CreatureProperties.findOne({
|
||||||
|
_id: propertyId,
|
||||||
|
'ancestors.id': creatureId,
|
||||||
|
'removed': {$ne: true},
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 },
|
||||||
|
fields: { icon: 0 },
|
||||||
|
});
|
||||||
|
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProperties(creatureId) {
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
const creature = loadedCreatures.get(creatureId);
|
||||||
|
const props = Array.from(creature.properties.values());
|
||||||
|
const cloneProps = EJSON.clone(props);
|
||||||
|
return cloneProps
|
||||||
|
}
|
||||||
|
// console.time(`Cache miss on creature properties: ${creatureId}`)
|
||||||
|
const props = CreatureProperties.find({
|
||||||
|
'ancestors.id': creatureId,
|
||||||
|
'removed': {$ne: true},
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 },
|
||||||
|
fields: { icon: 0 },
|
||||||
|
}).fetch();
|
||||||
|
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPropertiesOfType(creatureId, propType) {
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
const creature = loadedCreatures.get(creatureId);
|
||||||
|
const props = []
|
||||||
|
for (const prop of creature.properties.values()){
|
||||||
|
if (prop.type === propType) {
|
||||||
|
props.push(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cloneProps = EJSON.clone(props);
|
||||||
|
return cloneProps
|
||||||
|
}
|
||||||
|
// console.time(`Cache miss on creature properties: ${creatureId}`)
|
||||||
|
const props = CreatureProperties.find({
|
||||||
|
'ancestors.id': creatureId,
|
||||||
|
'removed': { $ne: true },
|
||||||
|
'type': propType,
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 },
|
||||||
|
fields: { icon: 0 },
|
||||||
|
}).fetch();
|
||||||
|
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCreature(creatureId) {
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
const loadedCreature = loadedCreatures.get(creatureId);
|
||||||
|
const creature = loadedCreature.creature;
|
||||||
|
if (creature) return creature;
|
||||||
|
}
|
||||||
|
// console.time(`Cache miss on Creature: ${creatureId}`);
|
||||||
|
const creature = Creatures.findOne(creatureId, {
|
||||||
|
denormalizedStats: 1,
|
||||||
|
variables: 1,
|
||||||
|
dirty: 1,
|
||||||
|
});
|
||||||
|
// console.timeEnd(`Cache miss on Creature: ${creatureId}`);
|
||||||
|
return creature;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getVariables(creatureId) {
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
const loadedCreature = loadedCreatures.get(creatureId);
|
||||||
|
const variables = loadedCreature.variables;
|
||||||
|
if (variables) return variables;
|
||||||
|
}
|
||||||
|
// console.time(`Cache miss on variables: ${creatureId}`);
|
||||||
|
const variables = CreatureVariables.findOne({_creatureId: creatureId});
|
||||||
|
// console.timeEnd(`Cache miss on variables: ${creatureId}`);
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProperyAncestors(creatureId, propertyId) {
|
||||||
|
const prop = getSingleProperty(creatureId, propertyId);
|
||||||
|
if (!prop) return [];
|
||||||
|
const ancestorIds = [];
|
||||||
|
prop.ancestors.forEach(ref => {
|
||||||
|
if (ref.collection === 'creatureProperties') {
|
||||||
|
ancestorIds.push(ref.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
// Get the ancestor properties from the cache
|
||||||
|
const creature = loadedCreatures.get(creatureId);
|
||||||
|
const props = [];
|
||||||
|
ancestorIds.forEach(id => {
|
||||||
|
const prop = creature.properties.get(id);
|
||||||
|
if (prop) {
|
||||||
|
props.push(prop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const cloneProps = EJSON.clone(props);
|
||||||
|
return cloneProps
|
||||||
|
} else {
|
||||||
|
// Fetch from database
|
||||||
|
return CreatureProperties.find({
|
||||||
|
_id: { $in: ancestorIds },
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 },
|
||||||
|
}).fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPropertyDecendants(creatureId, propertyId) {
|
||||||
|
const property = getSingleProperty(creatureId, propertyId);
|
||||||
|
if (!property) return [];
|
||||||
|
// This prop will always appear at the same position in the ancestor array
|
||||||
|
// of its decendants, so only check there
|
||||||
|
const expectedAncestorPostition = property.ancestors.length;
|
||||||
|
if (loadedCreatures.has(creatureId)) {
|
||||||
|
const creature = loadedCreatures.get(creatureId);
|
||||||
|
const props = [];
|
||||||
|
for(const prop of creature.properties.values()){
|
||||||
|
if (prop.ancestors[expectedAncestorPostition]?.id === propertyId) {
|
||||||
|
props.push(prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cloneProps = EJSON.clone(props);
|
||||||
|
return cloneProps
|
||||||
|
} else {
|
||||||
|
return CreatureProperties.find({
|
||||||
|
'ancestors.id': propertyId,
|
||||||
|
removed: { $ne: true },
|
||||||
|
}).fetch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadedCreature {
|
||||||
|
constructor(sub, creatureId) {
|
||||||
|
// This may be called from a subscription, but we don't want the observers
|
||||||
|
// to be destroyed with it, so use a non-reactive context to observe
|
||||||
|
// the required documents
|
||||||
|
const self = this;
|
||||||
|
Tracker.nonreactive(() => {
|
||||||
|
self.subs = new Set([sub]);
|
||||||
|
|
||||||
|
const compute = debounce(Meteor.bindEnvironment(() => {
|
||||||
|
computeCreature(creatureId);
|
||||||
|
}), COMPUTE_DEBOUNCE_TIME);
|
||||||
|
|
||||||
|
self.properties = new Map();
|
||||||
|
// Observe all creature properties which are needed for computation
|
||||||
|
self.propertyObserver = CreatureProperties.find({
|
||||||
|
'ancestors.id': creatureId,
|
||||||
|
removed: { $ne: true },
|
||||||
|
}, {
|
||||||
|
sort: { order: 1 },
|
||||||
|
fields: { icon: 0 },
|
||||||
|
}).observeChanges({
|
||||||
|
added(id, fields) {
|
||||||
|
fields._id = id;
|
||||||
|
self.addProperty(fields);
|
||||||
|
if (fields.dirty) compute();
|
||||||
|
},
|
||||||
|
changed(id, fields) {
|
||||||
|
self.changeProperty(id, fields);
|
||||||
|
if (fields.dirty) compute();
|
||||||
|
},
|
||||||
|
removed(id) {
|
||||||
|
self.removeProperty(id);
|
||||||
|
compute();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe the creature itself
|
||||||
|
self.creatureObserver = Creatures.find({
|
||||||
|
_id: creatureId,
|
||||||
|
}).observeChanges({
|
||||||
|
added(id, fields) {
|
||||||
|
fields._id = id;
|
||||||
|
self.addCreature(fields)
|
||||||
|
if (fields.dirty) compute();
|
||||||
|
},
|
||||||
|
changed(id, fields) {
|
||||||
|
self.changeCreature(id, fields);
|
||||||
|
if (fields.dirty) compute();
|
||||||
|
},
|
||||||
|
removed(id) {
|
||||||
|
self.removeCreature(id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Observe the creature's variables
|
||||||
|
self.variablesObserver = CreatureVariables.find({
|
||||||
|
_creatureId: creatureId,
|
||||||
|
}, {
|
||||||
|
fields: { _creatureId: 0},
|
||||||
|
}).observeChanges({
|
||||||
|
added(id, fields) {
|
||||||
|
fields._id = id;
|
||||||
|
self.addVariables(fields)
|
||||||
|
},
|
||||||
|
changed(id, fields) {
|
||||||
|
self.changeVariables(id, fields);
|
||||||
|
},
|
||||||
|
removed(id) {
|
||||||
|
self.removeVariables(id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.propertyObserver.stop();
|
||||||
|
this.creatureObserver.stop();
|
||||||
|
this.variablesObserver.stop();
|
||||||
|
}
|
||||||
|
addProperty(prop) {
|
||||||
|
this.properties.set(prop._id, prop);
|
||||||
|
}
|
||||||
|
changeProperty(id, fields) {
|
||||||
|
LoadedCreature.changeMap(id, fields, this.properties);
|
||||||
|
}
|
||||||
|
removeProperty(id) {
|
||||||
|
this.properties.delete(id)
|
||||||
|
}
|
||||||
|
addCreature(creature) {
|
||||||
|
this.creature = creature;
|
||||||
|
}
|
||||||
|
changeCreature(id, fields) {
|
||||||
|
LoadedCreature.changeDoc(this.creature, fields);
|
||||||
|
}
|
||||||
|
removeCreature() {
|
||||||
|
delete this.creature;
|
||||||
|
}
|
||||||
|
addVariables(variables) {
|
||||||
|
this.variables = variables;
|
||||||
|
}
|
||||||
|
changeVariables(id, fields) {
|
||||||
|
LoadedCreature.changeDoc(this.variables, fields);
|
||||||
|
}
|
||||||
|
removeVariables() {
|
||||||
|
delete this.variables;
|
||||||
|
}
|
||||||
|
static changeMap(id, fields, map) {
|
||||||
|
const doc = map.get(id);
|
||||||
|
LoadedCreature.changeDoc(doc, fields);
|
||||||
|
}
|
||||||
|
static changeDoc(doc, fields) {
|
||||||
|
if (!doc) return;
|
||||||
|
for (let key in fields) {
|
||||||
|
if (key === undefined) {
|
||||||
|
delete doc[key];
|
||||||
|
} else {
|
||||||
|
doc[key] = fields[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/imports/api/files/UserImages.js
Normal file
19
app/imports/api/files/UserImages.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
||||||
|
|
||||||
|
const UserImages = createS3FilesCollection({
|
||||||
|
collectionName: 'userImages',
|
||||||
|
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
|
||||||
|
onBeforeUpload(file) {
|
||||||
|
// Allow upload files under 10MB
|
||||||
|
if (file.size > 10485760) {
|
||||||
|
return 'Please upload with size equal or less than 10MB';
|
||||||
|
}
|
||||||
|
// Allow common image extensions
|
||||||
|
if (!/gif|png|jpe?g|webp/i.test(file.extension || '')) {
|
||||||
|
return 'Please upload an image file only';
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UserImages;
|
||||||
@@ -47,6 +47,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
|||||||
collectionName,
|
collectionName,
|
||||||
storagePath,
|
storagePath,
|
||||||
onBeforeUpload,
|
onBeforeUpload,
|
||||||
|
onAfterUpload,
|
||||||
debug = Meteor.isProduction,
|
debug = Meteor.isProduction,
|
||||||
allowClientCode = false,
|
allowClientCode = false,
|
||||||
}){
|
}){
|
||||||
@@ -54,7 +55,10 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
|||||||
collectionName,
|
collectionName,
|
||||||
storagePath,
|
storagePath,
|
||||||
onBeforeUpload,
|
onBeforeUpload,
|
||||||
onAfterUpload(fileRef){
|
onAfterUpload(fileRef) {
|
||||||
|
// Call the provided afterUpload hook first
|
||||||
|
onAfterUpload?.(fileRef);
|
||||||
|
|
||||||
// Start moving files to AWS:S3
|
// Start moving files to AWS:S3
|
||||||
// after fully received by the Meteor server
|
// after fully received by the Meteor server
|
||||||
|
|
||||||
@@ -217,6 +221,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
|||||||
collectionName,
|
collectionName,
|
||||||
storagePath,
|
storagePath,
|
||||||
onBeforeUpload,
|
onBeforeUpload,
|
||||||
|
onAfterUpload,
|
||||||
debug = Meteor.isProduction,
|
debug = Meteor.isProduction,
|
||||||
allowClientCode = false,
|
allowClientCode = false,
|
||||||
}){
|
}){
|
||||||
@@ -224,11 +229,12 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
|||||||
collectionName,
|
collectionName,
|
||||||
storagePath,
|
storagePath,
|
||||||
onBeforeUpload,
|
onBeforeUpload,
|
||||||
|
onAfterUpload,
|
||||||
debug,
|
debug,
|
||||||
allowClientCode,
|
allowClientCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer) {
|
||||||
// Use the normal file system to read files
|
// Use the normal file system to read files
|
||||||
collection.readJSONFile = async function(file){
|
collection.readJSONFile = async function(file){
|
||||||
const fileString = await fsp.readFile(file.path, 'utf8');
|
const fileString = await fsp.readFile(file.path, 'utf8');
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ let LibrarySchema = new SimpleSchema({
|
|||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.name,
|
max: STORAGE_LIMITS.name,
|
||||||
},
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.summary,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
LibrarySchema.extend(SharingSchema);
|
LibrarySchema.extend(SharingSchema);
|
||||||
@@ -76,6 +81,29 @@ const updateLibraryName = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateLibraryDescription = new ValidatedMethod({
|
||||||
|
name: 'libraries.updateDescription',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.id
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({_id, description}){
|
||||||
|
let library = Libraries.findOne(_id);
|
||||||
|
assertEditPermission(library, this.userId);
|
||||||
|
Libraries.update(_id, {$set: {description}});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const removeLibrary = new ValidatedMethod({
|
const removeLibrary = new ValidatedMethod({
|
||||||
name: 'libraries.remove',
|
name: 'libraries.remove',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
@@ -102,4 +130,4 @@ export function removeLibaryWork(libraryId){
|
|||||||
LibraryNodes.remove({'ancestors.id': libraryId});
|
LibraryNodes.remove({'ancestors.id': libraryId});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary };
|
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };
|
||||||
|
|||||||
135
app/imports/api/library/LibraryCollections.js
Normal file
135
app/imports/api/library/LibraryCollections.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
|
||||||
|
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||||
|
import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
|
import { getUserTier } from '/imports/api/users/patreon/tiers.js'
|
||||||
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LibraryCollections are groups of libraries that are subscribed together at once
|
||||||
|
*/
|
||||||
|
const LibraryCollections = new Mongo.Collection('libraryCollections');
|
||||||
|
|
||||||
|
const LibraryCollectionSchema = new SimpleSchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.name,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.summary,
|
||||||
|
},
|
||||||
|
libraries: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: STORAGE_LIMITS.libraryCollectionCount,
|
||||||
|
},
|
||||||
|
'libraries.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
LibraryCollectionSchema.extend(SharingSchema);
|
||||||
|
LibraryCollections.attachSchema(LibraryCollectionSchema);
|
||||||
|
|
||||||
|
export default LibraryCollections;
|
||||||
|
|
||||||
|
const insertLibraryCollection = new ValidatedMethod({
|
||||||
|
name: 'libraryCollections.insert',
|
||||||
|
mixins: [
|
||||||
|
simpleSchemaMixin,
|
||||||
|
],
|
||||||
|
schema: LibraryCollectionSchema.omit('owner'),
|
||||||
|
run(libraryCollection) {
|
||||||
|
if (!this.userId) {
|
||||||
|
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
||||||
|
'You need to be logged in to insert a library');
|
||||||
|
}
|
||||||
|
let tier = getUserTier(this.userId);
|
||||||
|
if (!tier.paidBenefits){
|
||||||
|
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
||||||
|
`The ${tier.name} tier does not allow you to insert a library collection`);
|
||||||
|
}
|
||||||
|
libraryCollection.owner = this.userId;
|
||||||
|
return LibraryCollections.insert(libraryCollection);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateLibraryCollection = new ValidatedMethod({
|
||||||
|
name: 'libraryCollections.update',
|
||||||
|
mixins: [
|
||||||
|
simpleSchemaMixin,
|
||||||
|
],
|
||||||
|
schema: {
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
type: LibraryCollectionSchema
|
||||||
|
.pick('name', 'description', 'libraries')
|
||||||
|
.extend({ //make libraries optional
|
||||||
|
libraries: {
|
||||||
|
optional: true,
|
||||||
|
defaultValue: undefined,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({_id, update}){
|
||||||
|
const libraryCollection = LibraryCollections.findOne(_id, {
|
||||||
|
fields: {
|
||||||
|
owner: 1,
|
||||||
|
writers: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertEditPermission(libraryCollection, this.userId);
|
||||||
|
return LibraryCollections.update(_id, {$set: update});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeLibraryCollection = new ValidatedMethod({
|
||||||
|
name: 'libraryCollections.remove',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.id
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({_id}){
|
||||||
|
const libraryCollection = LibraryCollections.findOne(_id, {
|
||||||
|
fields: {
|
||||||
|
owner: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assertOwnership(libraryCollection, this.userId);
|
||||||
|
return LibraryCollections.remove(_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getLibraryIdsByCollectionId(libraryCollectionId) {
|
||||||
|
const libraryCollection = LibraryCollections.findOne(libraryCollectionId)
|
||||||
|
return libraryCollection?.libraries || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
LibraryCollectionSchema,
|
||||||
|
insertLibraryCollection,
|
||||||
|
updateLibraryCollection,
|
||||||
|
removeLibraryCollection,
|
||||||
|
getLibraryIdsByCollectionId,
|
||||||
|
};
|
||||||
39
app/imports/api/library/getCreatureLibraryIds.js
Normal file
39
app/imports/api/library/getCreatureLibraryIds.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import getUserLibraryIds from './getUserLibraryIds';
|
||||||
|
import { intersection, union } from 'lodash';
|
||||||
|
|
||||||
|
export default function getCreatureLibraryIds(creature, userId) {
|
||||||
|
if (!userId) return [];
|
||||||
|
|
||||||
|
// Get the ids of libraries the user is permitted to view
|
||||||
|
const userLibIds = getUserLibraryIds(userId);
|
||||||
|
|
||||||
|
// If given a creature Id, get the creature document
|
||||||
|
if (typeof creature === 'string') {
|
||||||
|
creature = Creatures.findOne(creature, {
|
||||||
|
fields: {
|
||||||
|
allowedLibraries: 1,
|
||||||
|
allowedLibraryCollections: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!creature) return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the creature does not restrict the libraries, let it use them all
|
||||||
|
if (!creature.allowedLibraryCollections && !creature.allowedLibraries) {
|
||||||
|
return userLibIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ids of the libraries that the creature allows
|
||||||
|
const allowedCollections = creature.allowedLibraryCollections || [];
|
||||||
|
let creatureLibIds = creature.allowedLibraries || [];
|
||||||
|
LibraryCollections.find({
|
||||||
|
_id: { $in: allowedCollections }
|
||||||
|
}, { fields: { libraries: 1 } }).forEach(collection => {
|
||||||
|
creatureLibIds = union(creatureLibIds, collection.libraries);
|
||||||
|
});
|
||||||
|
|
||||||
|
// return all the ids that the creature allows and the user can view
|
||||||
|
return intersection(userLibIds, creatureLibIds);
|
||||||
|
}
|
||||||
31
app/imports/api/library/getUserLibraryIds.js
Normal file
31
app/imports/api/library/getUserLibraryIds.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||||
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
|
import { union } from 'lodash';
|
||||||
|
|
||||||
|
export default function getUserLibraryIds(userId) {
|
||||||
|
if (!userId) return [];
|
||||||
|
const user = Meteor.users.findOne(userId);
|
||||||
|
let subbedIds = user?.subscribedLibraries || [];
|
||||||
|
const subCollections = user?.subscribedLibraryCollections || [];
|
||||||
|
LibraryCollections.find({
|
||||||
|
$or: [
|
||||||
|
{ owner: userId },
|
||||||
|
{ writers: userId },
|
||||||
|
{ readers: userId },
|
||||||
|
{ _id: { $in: subCollections }, public: true },
|
||||||
|
]
|
||||||
|
}, { fields: { libraries: 1 } }).forEach(collection => {
|
||||||
|
subbedIds = union(subbedIds, collection.libraries);
|
||||||
|
});
|
||||||
|
const libraryIds = Libraries.find({
|
||||||
|
$or: [
|
||||||
|
{ owner: userId },
|
||||||
|
{ writers: userId },
|
||||||
|
{ readers: userId },
|
||||||
|
{ _id: { $in: subbedIds }, public: true },
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
fields: { _id: 1 }
|
||||||
|
}).map(lib => lib._id);
|
||||||
|
return libraryIds;
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
|
|||||||
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||||
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
|
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
|
||||||
const organizeDoc = new ValidatedMethod({
|
const organizeDoc = new ValidatedMethod({
|
||||||
name: 'organize.organizeDoc',
|
name: 'organize.organizeDoc',
|
||||||
@@ -57,10 +57,11 @@ const organizeDoc = new ValidatedMethod({
|
|||||||
let parentCreatures = getCreatureAncestors(parent);
|
let parentCreatures = getCreatureAncestors(parent);
|
||||||
if (!skipRecompute){
|
if (!skipRecompute){
|
||||||
let creaturesToRecompute = union(docCreatures, parentCreatures);
|
let creaturesToRecompute = union(docCreatures, parentCreatures);
|
||||||
// Recompute the creatures
|
// Mark the creatures for recompute
|
||||||
creaturesToRecompute.forEach(id => {
|
Creatures.update({
|
||||||
// Some Dependencies depend on ancestry, so a full recompute is needed
|
_id: { $in: creaturesToRecompute }
|
||||||
computeCreature(id);
|
}, {
|
||||||
|
$set: { dirty: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -85,9 +86,14 @@ const reorderDoc = new ValidatedMethod({
|
|||||||
assertDocEditPermission(doc, this.userId);
|
assertDocEditPermission(doc, this.userId);
|
||||||
safeUpdateDocOrder({docRef, order});
|
safeUpdateDocOrder({docRef, order});
|
||||||
// Recompute the affected creatures
|
// Recompute the affected creatures
|
||||||
getCreatureAncestors(doc).forEach(id => {
|
const ancestors = getCreatureAncestors(doc);
|
||||||
computeCreature(id);
|
if (ancestors.length) {
|
||||||
});
|
Creatures.update({
|
||||||
|
_id: { $in: ancestors }
|
||||||
|
}, {
|
||||||
|
$set: { dirty: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,17 +40,22 @@ const restoreError = function(){
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function restore({_id, collection}){
|
export function restore({ _id, collection, extraUpdates}){
|
||||||
if (typeof collection === 'string') {
|
if (typeof collection === 'string') {
|
||||||
collection = getCollectionByName(collection);
|
collection = getCollectionByName(collection);
|
||||||
}
|
}
|
||||||
|
const update = {
|
||||||
|
$unset: {
|
||||||
|
removed: 1,
|
||||||
|
removedAt: 1,
|
||||||
|
},
|
||||||
|
...extraUpdates
|
||||||
|
}
|
||||||
|
|
||||||
let numUpdated = collection.update({
|
let numUpdated = collection.update({
|
||||||
_id,
|
_id,
|
||||||
removedWith: {$exists: false}
|
removedWith: {$exists: false}
|
||||||
}, { $unset: {
|
}, update , {
|
||||||
removed: 1,
|
|
||||||
removedAt: 1,
|
|
||||||
}}, {
|
|
||||||
selector: {type: 'any'},
|
selector: {type: 'any'},
|
||||||
},);
|
},);
|
||||||
if (numUpdated === 0) restoreError();
|
if (numUpdated === 0) restoreError();
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Actions are things a character can do
|
* Actions are things a character can do
|
||||||
* Any rolls that are children of actions will be rolled when taking the action
|
|
||||||
* Any actions that are children of this action will be considered alternatives
|
|
||||||
* to this action
|
|
||||||
*/
|
*/
|
||||||
let ActionSchema = createPropertySchema({
|
let ActionSchema = createPropertySchema({
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -145,6 +145,15 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
|
|||||||
optional: true,
|
optional: true,
|
||||||
removeBeforeCompute: true,
|
removeBeforeCompute: true,
|
||||||
},
|
},
|
||||||
|
// A list of effect ids targeting this attribute
|
||||||
|
effects: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'effects.$': {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedAttributeSchema = new SimpleSchema()
|
const ComputedAttributeSchema = new SimpleSchema()
|
||||||
|
|||||||
@@ -1,34 +1,95 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
import { SlotSchema, ComputedOnlySlotSchema } from './Slots.js';
|
|
||||||
|
|
||||||
// Classes are like slots, except they only take class levels and enforce that
|
// Classes are like slots, except they only take class levels and enforce that
|
||||||
// lower levels are taken before higher levels
|
// lower levels are taken before higher levels
|
||||||
let ClassSchema = createPropertySchema({
|
let ClassSchema = createPropertySchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.name,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'inlineCalculationFieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
// Only `classLevel`s with the same variable name can fill the class
|
// Only `classLevel`s with the same variable name can fill the class
|
||||||
variableName: {
|
variableName: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
max: STORAGE_LIMITS.variableName,
|
max: STORAGE_LIMITS.variableName,
|
||||||
},
|
},
|
||||||
}).extend(SlotSchema);
|
slotTags: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
|
},
|
||||||
|
'slotTags.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.tagLength,
|
||||||
|
},
|
||||||
|
extraTags: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: STORAGE_LIMITS.extraTagsCount,
|
||||||
|
},
|
||||||
|
'extraTags.$': {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
'extraTags.$._id': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
autoValue(){
|
||||||
|
if (!this.isSet) return Random.id();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'extraTags.$.operation': {
|
||||||
|
type: String,
|
||||||
|
allowedValues: ['OR', 'NOT'],
|
||||||
|
defaultValue: 'OR',
|
||||||
|
},
|
||||||
|
'extraTags.$.tags': {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
|
},
|
||||||
|
'extraTags.$.tags.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.tagLength,
|
||||||
|
},
|
||||||
|
slotCondition: {
|
||||||
|
type: 'fieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ComputedOnlyClassSchema = createPropertySchema({
|
const ComputedOnlyClassSchema = createPropertySchema({
|
||||||
level: {
|
// Computed fields
|
||||||
type: SimpleSchema.Integer,
|
description: {
|
||||||
optional: true,
|
type: 'computedOnlyInlineCalculationField',
|
||||||
removeBeforeCompute: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
missingLevels: {
|
slotCondition: {
|
||||||
type: Array,
|
type: 'computedOnlyField',
|
||||||
optional: true,
|
optional: true,
|
||||||
removeBeforeCompute: true,
|
},
|
||||||
},
|
|
||||||
'missingLevels.$': {
|
// Denormalised fields
|
||||||
type: SimpleSchema.Integer,
|
level: {
|
||||||
},
|
type: SimpleSchema.Integer,
|
||||||
}).extend(ComputedOnlySlotSchema);
|
optional: true,
|
||||||
|
removeBeforeCompute: true,
|
||||||
|
},
|
||||||
|
missingLevels: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
removeBeforeCompute: true,
|
||||||
|
},
|
||||||
|
'missingLevels.$': {
|
||||||
|
type: SimpleSchema.Integer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const ComputedClassSchema = new SimpleSchema()
|
const ComputedClassSchema = new SimpleSchema()
|
||||||
.extend(ClassSchema)
|
.extend(ClassSchema)
|
||||||
|
|||||||
96
app/imports/api/properties/PointBuys.js
Normal file
96
app/imports/api/properties/PointBuys.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||||
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PointBuys are reason-value attached to skills and abilities
|
||||||
|
* that modify their final value or presentation in some way
|
||||||
|
*/
|
||||||
|
let PointBuySchema = createPropertySchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.name,
|
||||||
|
},
|
||||||
|
variableName: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
regEx: VARIABLE_NAME_REGEX,
|
||||||
|
min: 2,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
|
},
|
||||||
|
ignored: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'values': {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
},
|
||||||
|
'values.$': {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
'values.$.name': {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.name,
|
||||||
|
},
|
||||||
|
'values.$.variableName': {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
regEx: VARIABLE_NAME_REGEX,
|
||||||
|
min: 2,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
|
},
|
||||||
|
'values.$.value': {
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
min: {
|
||||||
|
type: 'fieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: 'fieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: 'fieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
type: 'fieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ComputedOnlyPointBuySchema = createPropertySchema({
|
||||||
|
min: {
|
||||||
|
type: 'computedOnlyField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: 'computedOnlyField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: 'computedOnlyField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
cost: {
|
||||||
|
type: 'computedOnlyField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
spent: {
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
removeBeforeCompute: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ComputedPointBuySchema = new SimpleSchema()
|
||||||
|
.extend(ComputedOnlyPointBuySchema)
|
||||||
|
.extend(PointBuySchema);
|
||||||
|
|
||||||
|
export { PointBuySchema, ComputedPointBuySchema, ComputedOnlyPointBuySchema };
|
||||||
@@ -131,6 +131,15 @@ let ComputedOnlySkillSchema = createPropertySchema({
|
|||||||
optional: true,
|
optional: true,
|
||||||
removeBeforeCompute: true,
|
removeBeforeCompute: true,
|
||||||
},
|
},
|
||||||
|
// A list of effect ids targeting this skill
|
||||||
|
effects: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'effects.$': {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const ComputedSkillSchema = new SimpleSchema()
|
const ComputedSkillSchema = new SimpleSchema()
|
||||||
|
|||||||
136
app/imports/api/properties/Triggers.js
Normal file
136
app/imports/api/properties/Triggers.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
|
||||||
|
const eventOptions = {
|
||||||
|
doActionProperty: 'Do action',
|
||||||
|
// receiveActionProperty: 'Receiving action property',
|
||||||
|
// flipToggle: 'Toggle changed',
|
||||||
|
// adjustProperty: 'Attribute adjusted',
|
||||||
|
anyRest: 'Short or long rest',
|
||||||
|
longRest: 'Long rest',
|
||||||
|
shortRest: 'Short rest',
|
||||||
|
}
|
||||||
|
|
||||||
|
const timingOptions = {
|
||||||
|
before: 'Before',
|
||||||
|
after: 'After',
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionPropertyTypeOptions = {
|
||||||
|
action: 'Action',
|
||||||
|
adjustment: 'Attribute damage',
|
||||||
|
branch: 'Branch',
|
||||||
|
buff: 'Buff',
|
||||||
|
damage: 'Damage',
|
||||||
|
note: 'Note',
|
||||||
|
roll: 'Roll',
|
||||||
|
savingThrow: 'Saving throw',
|
||||||
|
toggle: 'Toggle',
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Triggers are like actions that fire themselves when certain things happen on
|
||||||
|
* the sheet. Either during another action or as its own action after a sheet
|
||||||
|
* event. The same trigger can't fire twice in the same action step.
|
||||||
|
*/
|
||||||
|
let TriggerSchema = createPropertySchema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.name,
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: 'inlineCalculationFieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'inlineCalculationFieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
event: {
|
||||||
|
type: String,
|
||||||
|
allowedValues: Object.keys(eventOptions),
|
||||||
|
defaultValue: 'doActionProperty',
|
||||||
|
},
|
||||||
|
// Action type
|
||||||
|
actionPropertyType: {
|
||||||
|
type: String,
|
||||||
|
allowedValues: Object.keys(actionPropertyTypeOptions),
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
timing: {
|
||||||
|
type: String,
|
||||||
|
allowedValues: Object.keys(timingOptions),
|
||||||
|
defaultValue: 'after',
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
type: 'fieldToCompute',
|
||||||
|
optional: true,
|
||||||
|
parseLevel: 'compile',
|
||||||
|
},
|
||||||
|
// Which tags the trigger is applied to
|
||||||
|
targetTags: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
|
},
|
||||||
|
'targetTags.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.tagLength,
|
||||||
|
},
|
||||||
|
extraTags: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
maxCount: STORAGE_LIMITS.extraTagsCount,
|
||||||
|
},
|
||||||
|
'extraTags.$': {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
'extraTags.$._id': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
autoValue(){
|
||||||
|
if (!this.isSet) return Random.id();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'extraTags.$.operation': {
|
||||||
|
type: String,
|
||||||
|
allowedValues: ['OR', 'NOT'],
|
||||||
|
defaultValue: 'OR',
|
||||||
|
},
|
||||||
|
'extraTags.$.tags': {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: STORAGE_LIMITS.tagCount,
|
||||||
|
},
|
||||||
|
'extraTags.$.tags.$': {
|
||||||
|
type: String,
|
||||||
|
max: STORAGE_LIMITS.tagLength,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ComputedOnlyTriggerSchema = createPropertySchema({
|
||||||
|
summary: {
|
||||||
|
type: 'computedOnlyInlineCalculationField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: 'computedOnlyInlineCalculationField',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
type: 'computedOnlyField',
|
||||||
|
optional: true,
|
||||||
|
parseLevel: 'compile',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ComputedTriggerSchema = new SimpleSchema()
|
||||||
|
.extend(TriggerSchema)
|
||||||
|
.extend(ComputedOnlyTriggerSchema);
|
||||||
|
|
||||||
|
export {
|
||||||
|
TriggerSchema, ComputedOnlyTriggerSchema, ComputedTriggerSchema,
|
||||||
|
eventOptions, timingOptions, actionPropertyTypeOptions
|
||||||
|
};
|
||||||
@@ -25,6 +25,7 @@ import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFiller
|
|||||||
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
|
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
|
||||||
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
|
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
|
||||||
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
|
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
|
||||||
|
import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers.js';
|
||||||
|
|
||||||
const propertySchemasIndex = {
|
const propertySchemasIndex = {
|
||||||
action: ComputedOnlyActionSchema,
|
action: ComputedOnlyActionSchema,
|
||||||
@@ -53,6 +54,7 @@ const propertySchemasIndex = {
|
|||||||
spellList: ComputedOnlySpellListSchema,
|
spellList: ComputedOnlySpellListSchema,
|
||||||
spell: ComputedOnlySpellSchema,
|
spell: ComputedOnlySpellSchema,
|
||||||
toggle: ComputedOnlyToggleSchema,
|
toggle: ComputedOnlyToggleSchema,
|
||||||
|
trigger: ComputedOnlyTriggerSchema,
|
||||||
any: new SimpleSchema({}),
|
any: new SimpleSchema({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
|
|||||||
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
|
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
|
||||||
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
|
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
|
||||||
import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js';
|
import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js';
|
||||||
|
import { ComputedTriggerSchema } from '/imports/api/properties/Triggers.js';
|
||||||
|
|
||||||
const propertySchemasIndex = {
|
const propertySchemasIndex = {
|
||||||
action: ComputedActionSchema,
|
action: ComputedActionSchema,
|
||||||
@@ -51,6 +52,7 @@ const propertySchemasIndex = {
|
|||||||
spellList: ComputedSpellListSchema,
|
spellList: ComputedSpellListSchema,
|
||||||
spell: ComputedSpellSchema,
|
spell: ComputedSpellSchema,
|
||||||
toggle: ComputedToggleSchema,
|
toggle: ComputedToggleSchema,
|
||||||
|
trigger: ComputedTriggerSchema,
|
||||||
container: ComputedContainerSchema,
|
container: ComputedContainerSchema,
|
||||||
item: ComputedItemSchema,
|
item: ComputedItemSchema,
|
||||||
any: new SimpleSchema({}),
|
any: new SimpleSchema({}),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
|
|||||||
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
|
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
|
||||||
import { SpellSchema } from '/imports/api/properties/Spells.js';
|
import { SpellSchema } from '/imports/api/properties/Spells.js';
|
||||||
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
|
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
|
||||||
|
import { TriggerSchema } from '/imports/api/properties/Triggers.js';
|
||||||
import { ContainerSchema } from '/imports/api/properties/Containers.js';
|
import { ContainerSchema } from '/imports/api/properties/Containers.js';
|
||||||
import { ItemSchema } from '/imports/api/properties/Items.js';
|
import { ItemSchema } from '/imports/api/properties/Items.js';
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ const propertySchemasIndex = {
|
|||||||
spellList: SpellListSchema,
|
spellList: SpellListSchema,
|
||||||
spell: SpellSchema,
|
spell: SpellSchema,
|
||||||
toggle: ToggleSchema,
|
toggle: ToggleSchema,
|
||||||
|
trigger: TriggerSchema,
|
||||||
container: ContainerSchema,
|
container: ContainerSchema,
|
||||||
item: ItemSchema,
|
item: ItemSchema,
|
||||||
any: new SimpleSchema({}),
|
any: new SimpleSchema({}),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function assertOwnership(doc, userId){
|
|||||||
export function assertEditPermission(doc, userId) {
|
export function assertEditPermission(doc, userId) {
|
||||||
assertIdValid(userId);
|
assertIdValid(userId);
|
||||||
assertdocExists(doc);
|
assertdocExists(doc);
|
||||||
let user = Meteor.users.findOne(userId, {
|
const user = Meteor.users.findOne(userId, {
|
||||||
fields: {
|
fields: {
|
||||||
'services.patreon': 1,
|
'services.patreon': 1,
|
||||||
'roles': 1,
|
'roles': 1,
|
||||||
@@ -83,6 +83,7 @@ export function assertViewPermission(doc, userId) {
|
|||||||
assertdocExists(doc);
|
assertdocExists(doc);
|
||||||
if (doc.public) return true;
|
if (doc.public) return true;
|
||||||
assertIdValid(userId);
|
assertIdValid(userId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
doc.owner === userId ||
|
doc.owner === userId ||
|
||||||
_.contains(doc.readers, userId) ||
|
_.contains(doc.readers, userId) ||
|
||||||
@@ -90,6 +91,17 @@ export function assertViewPermission(doc, userId) {
|
|||||||
){
|
){
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
// Admin override
|
||||||
|
const user = Meteor.users.findOne(userId, {
|
||||||
|
fields: {
|
||||||
|
'roles': 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (user.roles && user.roles.includes('admin')){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
throw new Meteor.Error('View permission denied',
|
throw new Meteor.Error('View permission denied',
|
||||||
'You do not have permission to view this document');
|
'You do not have permission to view this document');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|||||||
import '/imports/api/users/methods/deleteMyAccount.js';
|
import '/imports/api/users/methods/deleteMyAccount.js';
|
||||||
import '/imports/api/users/methods/addEmail.js';
|
import '/imports/api/users/methods/addEmail.js';
|
||||||
import '/imports/api/users/methods/removeEmail.js';
|
import '/imports/api/users/methods/removeEmail.js';
|
||||||
|
import '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||||
|
|
||||||
import { some } from 'lodash';
|
import { some } from 'lodash';
|
||||||
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
|
const defaultLibraries = process.env.DEFAULT_LIBRARIES && process.env.DEFAULT_LIBRARIES.split(',') || [];
|
||||||
|
const defaultLibraryCollections = process.env.DEFAULT_LIBRARY_COLLECTIONS && process.env.DEFAULT_LIBRARY_COLLECTIONS.split(',') || [];
|
||||||
|
|
||||||
const userSchema = new SimpleSchema({
|
const userSchema = new SimpleSchema({
|
||||||
username: {
|
username: {
|
||||||
@@ -69,10 +71,19 @@ const userSchema = new SimpleSchema({
|
|||||||
subscribedLibraries: {
|
subscribedLibraries: {
|
||||||
type: Array,
|
type: Array,
|
||||||
defaultValue: defaultLibraries,
|
defaultValue: defaultLibraries,
|
||||||
max: 100,
|
maxCount: 100,
|
||||||
},
|
},
|
||||||
'subscribedLibraries.$': {
|
'subscribedLibraries.$': {
|
||||||
type: String,
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
subscribedLibraryCollections: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: defaultLibraryCollections,
|
||||||
|
maxCount: 100,
|
||||||
|
},
|
||||||
|
'subscribedLibraryCollections.$': {
|
||||||
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
},
|
},
|
||||||
subscribedCharacters: {
|
subscribedCharacters: {
|
||||||
@@ -84,6 +95,10 @@ const userSchema = new SimpleSchema({
|
|||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
},
|
},
|
||||||
|
fileStorageUsed: {
|
||||||
|
type: Number,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
profile: {
|
profile: {
|
||||||
type: Object,
|
type: Object,
|
||||||
blackbox: true,
|
blackbox: true,
|
||||||
@@ -265,6 +280,36 @@ Meteor.users.subscribeToLibrary = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Meteor.users.subscribeToLibraryCollection = new ValidatedMethod({
|
||||||
|
name: 'users.subscribeToLibraryCollection',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
libraryCollectionId:{
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
subscribe: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({libraryCollectionId, subscribe}){
|
||||||
|
if (!this.userId) throw 'Can only subscribe if logged in';
|
||||||
|
if (subscribe){
|
||||||
|
return Meteor.users.update(this.userId, {
|
||||||
|
$addToSet: {subscribedLibraryCollections: libraryCollectionId},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Meteor.users.update(this.userId, {
|
||||||
|
$pullAll: {subscribedLibraryCollections: libraryCollectionId},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
|
Meteor.users.findUserByUsernameOrEmail = new ValidatedMethod({
|
||||||
name: 'users.findUserByUsernameOrEmail',
|
name: 'users.findUserByUsernameOrEmail',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
|
|||||||
71
app/imports/api/users/methods/updateFileStorageUsed.js
Normal file
71
app/imports/api/users/methods/updateFileStorageUsed.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||||
|
import UserImages from '/imports/api/files/UserImages.js';
|
||||||
|
const fileCollections = [ArchiveCreatureFiles, UserImages];
|
||||||
|
|
||||||
|
const updateFileStorageUsed = new ValidatedMethod({
|
||||||
|
name: 'users.recalculateFileStorageUsed',
|
||||||
|
validate: null,
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run() {
|
||||||
|
const userId = Meteor.userId();
|
||||||
|
if (!userId) throw new Meteor.Error('No user',
|
||||||
|
'You must be logged in to recalculate your file use');
|
||||||
|
const user = Meteor.users.findOne(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Meteor.Error('noUser', 'User not found');
|
||||||
|
}
|
||||||
|
updateFileStorageUsedWork(userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updateFileStorageUsed;
|
||||||
|
|
||||||
|
export function updateFileStorageUsedWork(userId) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Meteor.Error('idRequired',
|
||||||
|
'No user ID was provided to update file storage used')
|
||||||
|
}
|
||||||
|
|
||||||
|
let sum = 0;
|
||||||
|
fileCollections.forEach(collection => {
|
||||||
|
collection.find({ userId }, { fields: { size: 1 } }).forEach(file => {
|
||||||
|
sum += file.size;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Meteor.users.update(userId, {
|
||||||
|
$set: {
|
||||||
|
fileStorageUsed: sum,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementFileStorageUsed(userId, amount) {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Meteor.Error('idRequired',
|
||||||
|
'No user ID was provided to update file storage used')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = Meteor.users.findOne(userId);
|
||||||
|
if (!user) {
|
||||||
|
throw new Meteor.Error('noUser', 'User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.fileStorageUsed === undefined) {
|
||||||
|
// The user doesn't have a current value for storage used, calculate it
|
||||||
|
// from scratch
|
||||||
|
updateFileStorageUsedWork(userId);
|
||||||
|
} else {
|
||||||
|
Meteor.users.update(userId, {
|
||||||
|
$inc: {
|
||||||
|
fileStorageUsed: amount,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,18 +9,21 @@ const TIERS = Object.freeze([
|
|||||||
minimumEntitledCents: 0,
|
minimumEntitledCents: 0,
|
||||||
invites: 0,
|
invites: 0,
|
||||||
characterSlots: 5,
|
characterSlots: 5,
|
||||||
|
fileStorage: 50,
|
||||||
paidBenefits: false,
|
paidBenefits: false,
|
||||||
}, {
|
}, {
|
||||||
name: 'Dreamer',
|
name: 'Dreamer',
|
||||||
minimumEntitledCents: 100,
|
minimumEntitledCents: 100,
|
||||||
invites: 0,
|
invites: 0,
|
||||||
characterSlots: 5,
|
characterSlots: 5,
|
||||||
|
fileStorage: 50,
|
||||||
paidBenefits: false,
|
paidBenefits: false,
|
||||||
}, {
|
}, {
|
||||||
name: 'Wanderer',
|
name: 'Wanderer',
|
||||||
minimumEntitledCents: 300,
|
minimumEntitledCents: 300,
|
||||||
invites: 0,
|
invites: 0,
|
||||||
characterSlots: 5,
|
characterSlots: 5,
|
||||||
|
fileStorage: 50,
|
||||||
paidBenefits: false,
|
paidBenefits: false,
|
||||||
}, {
|
}, {
|
||||||
//cost per user $5
|
//cost per user $5
|
||||||
@@ -28,6 +31,7 @@ const TIERS = Object.freeze([
|
|||||||
minimumEntitledCents: 500,
|
minimumEntitledCents: 500,
|
||||||
invites: 0,
|
invites: 0,
|
||||||
characterSlots: 20,
|
characterSlots: 20,
|
||||||
|
fileStorage: 200,
|
||||||
paidBenefits: true,
|
paidBenefits: true,
|
||||||
}, {
|
}, {
|
||||||
//cost per user $3.33
|
//cost per user $3.33
|
||||||
@@ -35,6 +39,7 @@ const TIERS = Object.freeze([
|
|||||||
minimumEntitledCents: 1000,
|
minimumEntitledCents: 1000,
|
||||||
invites: 2,
|
invites: 2,
|
||||||
characterSlots: 50,
|
characterSlots: 50,
|
||||||
|
fileStorage: 500,
|
||||||
paidBenefits: true,
|
paidBenefits: true,
|
||||||
}, {
|
}, {
|
||||||
//cost per user $3.333
|
//cost per user $3.333
|
||||||
@@ -42,6 +47,7 @@ const TIERS = Object.freeze([
|
|||||||
minimumEntitledCents: 2000,
|
minimumEntitledCents: 2000,
|
||||||
invites: 5,
|
invites: 5,
|
||||||
characterSlots: 120,
|
characterSlots: 120,
|
||||||
|
fileStorage: 1000,
|
||||||
paidBenefits: true,
|
paidBenefits: true,
|
||||||
}, {
|
}, {
|
||||||
//cost per user $3.125
|
//cost per user $3.125
|
||||||
@@ -49,6 +55,7 @@ const TIERS = Object.freeze([
|
|||||||
minimumEntitledCents: 5000,
|
minimumEntitledCents: 5000,
|
||||||
invites: 15,
|
invites: 15,
|
||||||
characterSlots: -1, // Unlimited characters
|
characterSlots: -1, // Unlimited characters
|
||||||
|
fileStorage: 2000,
|
||||||
paidBenefits: true,
|
paidBenefits: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -59,6 +66,7 @@ const GUEST_TIER = Object.freeze({
|
|||||||
guest: true,
|
guest: true,
|
||||||
invites: 0,
|
invites: 0,
|
||||||
characterSlots: 20,
|
characterSlots: 20,
|
||||||
|
fileStorage: 200,
|
||||||
paidBenefits: true,
|
paidBenefits: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,6 +76,7 @@ const PATREON_DISABLED_TIER = Object.freeze({
|
|||||||
name: 'Outlander',
|
name: 'Outlander',
|
||||||
invites: 0,
|
invites: 0,
|
||||||
characterSlots: -1, // Can have infinitely many characters
|
characterSlots: -1, // Can have infinitely many characters
|
||||||
|
fileStorage: 1000000, // 1TB file storage
|
||||||
paidBenefits: true,
|
paidBenefits: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const DAMAGE_TYPES = Object.freeze([
|
|||||||
'psychic',
|
'psychic',
|
||||||
'radiant',
|
'radiant',
|
||||||
'thunder',
|
'thunder',
|
||||||
|
'extra',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default DAMAGE_TYPES;
|
export default DAMAGE_TYPES;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ if (Meteor.isServer){
|
|||||||
const dbVersion = Migrations.getVersion();
|
const dbVersion = Migrations.getVersion();
|
||||||
if (
|
if (
|
||||||
!Meteor.settings.public.maintenanceMode &&
|
!Meteor.settings.public.maintenanceMode &&
|
||||||
|
dbVersion !== undefined &&
|
||||||
SCHEMA_VERSION !== dbVersion
|
SCHEMA_VERSION !== dbVersion
|
||||||
){
|
){
|
||||||
Meteor.settings.public.maintenanceMode = {
|
Meteor.settings.public.maintenanceMode = {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const PROPERTIES = Object.freeze({
|
|||||||
slotFiller: {
|
slotFiller: {
|
||||||
icon: 'mdi-power-plug-outline',
|
icon: 'mdi-power-plug-outline',
|
||||||
name: 'Slot filler',
|
name: 'Slot filler',
|
||||||
helpText: 'A slot filler allows for more advanced logic when it attemptst to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
|
helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
|
||||||
suggestedParents: ['propertySlot'],
|
suggestedParents: ['propertySlot'],
|
||||||
},
|
},
|
||||||
spellList: {
|
spellList: {
|
||||||
@@ -159,6 +159,12 @@ const PROPERTIES = Object.freeze({
|
|||||||
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
|
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
|
||||||
suggestedParents: [],
|
suggestedParents: [],
|
||||||
},
|
},
|
||||||
|
trigger: {
|
||||||
|
icon: 'mdi-electric-switch',
|
||||||
|
name: 'Trigger',
|
||||||
|
helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage',
|
||||||
|
suggestedParents: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PROPERTIES;
|
export default PROPERTIES;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const STORAGE_LIMITS = Object.freeze({
|
|||||||
statsToTarget: 64,
|
statsToTarget: 64,
|
||||||
tagCount: 64,
|
tagCount: 64,
|
||||||
writersCount: 20,
|
writersCount: 20,
|
||||||
|
libraryCollectionCount: 32,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default STORAGE_LIMITS;
|
export default STORAGE_LIMITS;
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Migrations } from 'meteor/percolate:migrations';
|
import { Migrations } from 'meteor/percolate:migrations';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
|
|
||||||
import { restoreCreature } from '/imports/api/creature/archive/methods/restoreCreatures.js';
|
|
||||||
import { archiveCreature } from '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
|
||||||
import transformFields from '/imports/migrations/server/transformFields.js';
|
import transformFields from '/imports/migrations/server/transformFields.js';
|
||||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
@@ -22,34 +19,11 @@ Migrations.add({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function migrate({reversed} = {}){
|
function migrate({reversed} = {}){
|
||||||
console.log('restoring all characters from database archive');
|
|
||||||
const restoredIds = restoreAllCreatures();
|
|
||||||
|
|
||||||
console.log('migrating creature properties');
|
console.log('migrating creature properties');
|
||||||
migrateCollection({collection: CreatureProperties, reversed});
|
migrateCollection({collection: CreatureProperties, reversed});
|
||||||
|
|
||||||
console.log('migrating library nodes')
|
console.log('migrating library nodes')
|
||||||
migrateCollection({collection: LibraryNodes, reversed});
|
migrateCollection({collection: LibraryNodes, reversed});
|
||||||
|
|
||||||
console.log('archiving characters to file system archive');
|
|
||||||
rearchiveAllCreatures(restoredIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreAllCreatures(){
|
|
||||||
const ids = [];
|
|
||||||
ArchivedCreatures.find({}, {
|
|
||||||
fields: {_id: 1}
|
|
||||||
}).forEach(archive => {
|
|
||||||
const id = restoreCreature(archive._id);
|
|
||||||
ids.push(id);
|
|
||||||
});
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rearchiveAllCreatures(ids){
|
|
||||||
ids.forEach(id => {
|
|
||||||
archiveCreature(id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateCollection({collection, reversed}){
|
function migrateCollection({collection, reversed}){
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { fetch } from 'meteor/fetch'
|
|
||||||
|
|
||||||
export default function importCharacter(url){
|
|
||||||
// Using v1's JSON API to fetch the character data in a usable format
|
|
||||||
// url -> https://dicecloud.com/character/<id>/json?key=<key>
|
|
||||||
fetch(url)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
let character = data.characters[0];
|
|
||||||
console.log(character.name + ' fetched successfuly')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<template lang="html">
|
|
||||||
<v-text-field
|
|
||||||
ref="input"
|
|
||||||
v-bind="$attrs"
|
|
||||||
class="dc-text-field"
|
|
||||||
:loading="loading"
|
|
||||||
:error-messages="errors"
|
|
||||||
:value="safeValue"
|
|
||||||
:disabled="isDisabled"
|
|
||||||
:outlined="!regular"
|
|
||||||
@input="input"
|
|
||||||
@focus="focused = true"
|
|
||||||
@blur="focused = false"
|
|
||||||
@keyup="e => $emit('keyup', e)"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<slot name="value" />
|
|
||||||
</template>
|
|
||||||
</v-text-field>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="js">
|
|
||||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [SmartInput],
|
|
||||||
props: {
|
|
||||||
regular: Boolean,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="css">
|
|
||||||
.dc-text-field .v-input__append-inner{
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 36px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import constant from './constant.js';
|
import constant from './constant.js';
|
||||||
|
// import array from './array.js';
|
||||||
import { toString } from '../resolve.js';
|
import { toString } from '../resolve.js';
|
||||||
|
|
||||||
const accessor = {
|
const accessor = {
|
||||||
@@ -9,18 +10,18 @@ const accessor = {
|
|||||||
name,
|
name,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
compile(node, scope, context){
|
compile(node, scope, context) {
|
||||||
let value = scope && scope[node.name];
|
let value = scope && scope[node.name];
|
||||||
// For objects, get their value
|
// For objects, get their value
|
||||||
node.path.forEach(name => {
|
node.path.forEach(name => {
|
||||||
if (value === undefined) return;
|
if (value === undefined) return;
|
||||||
value = value[name];
|
value = value[name];
|
||||||
});
|
});
|
||||||
let valueType = typeof value;
|
let valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||||
// If the accessor returns an objet, get the object's value instead
|
// If the accessor returns an objet, get the object's value instead
|
||||||
while (valueType === 'object'){
|
while (valueType === 'object'){
|
||||||
value = value.value;
|
value = value.value;
|
||||||
valueType = typeof value;
|
valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||||
}
|
}
|
||||||
// Return a parse node based on the type returned
|
// Return a parse node based on the type returned
|
||||||
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){
|
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){
|
||||||
@@ -31,7 +32,21 @@ const accessor = {
|
|||||||
}),
|
}),
|
||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
} else if (valueType === 'undefined'){
|
}
|
||||||
|
/* Can't access #object.tags until this is fixed
|
||||||
|
* If we activate this, the array node expects values to be an array of
|
||||||
|
* parse nodes, so it will break unless the values are coerced here or at
|
||||||
|
* in the array node's code to be parse nodes, not raw js
|
||||||
|
else if (valueType === 'array') {
|
||||||
|
return {
|
||||||
|
result: array.create({
|
||||||
|
values: value,
|
||||||
|
}),
|
||||||
|
context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
else if (valueType === 'undefined') {
|
||||||
return {
|
return {
|
||||||
result: accessor.create({
|
result: accessor.create({
|
||||||
name: node.name,
|
name: node.name,
|
||||||
@@ -40,8 +55,7 @@ const accessor = {
|
|||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
context.error(`${node.name} returned an unexpected type`);
|
context.error(`Accessing ${accessor.toString(node)} is not supported yet`);
|
||||||
context.error(JSON.stringify(value, null, 2));
|
|
||||||
return {
|
return {
|
||||||
result: accessor.create({
|
result: accessor.create({
|
||||||
name: node.name,
|
name: node.name,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { SyncedCron } from 'meteor/percolate:synced-cron';
|
import { SyncedCron } from 'meteor/littledata:synced-cron';
|
||||||
|
|
||||||
SyncedCron.config({
|
SyncedCron.config({
|
||||||
// Log job run details to console
|
// Log job run details to console
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
import { SyncedCron } from 'meteor/percolate:synced-cron';
|
import { SyncedCron } from 'meteor/littledata:synced-cron';
|
||||||
|
|
||||||
Meteor.startup(() => {
|
Meteor.startup(() => {
|
||||||
const collections = [
|
const collections = [
|
||||||
|
|||||||
8
app/imports/server/imageProcessing/createThumbnail.js
Normal file
8
app/imports/server/imageProcessing/createThumbnail.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as sharp from 'sharp';
|
||||||
|
|
||||||
|
export default async function createThumbnail(image) {
|
||||||
|
await sharp(image)
|
||||||
|
.resize(320, 240)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -8,6 +8,6 @@ import '/imports/server/publications/icons.js';
|
|||||||
import '/imports/server/publications/tabletops.js';
|
import '/imports/server/publications/tabletops.js';
|
||||||
import '/imports/server/publications/slotFillers.js';
|
import '/imports/server/publications/slotFillers.js';
|
||||||
import '/imports/server/publications/ownedDocuments.js';
|
import '/imports/server/publications/ownedDocuments.js';
|
||||||
import '/imports/server/publications/archivedCreatures.js';
|
|
||||||
import '/imports/server/publications/searchLibraryNodes.js';
|
import '/imports/server/publications/searchLibraryNodes.js';
|
||||||
import '/imports/server/publications/archiveFiles.js';
|
import '/imports/server/publications/archiveFiles.js';
|
||||||
|
import '/imports/server/publications/userImages.js';
|
||||||
|
|||||||
@@ -1,27 +1,133 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import Libraries from '/imports/api/library/Libraries.js';
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
|
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertViewPermission, assertDocViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
|
import { union } from 'lodash';
|
||||||
|
|
||||||
Meteor.publish('libraries', function(){
|
const LIBRARY_NODE_TREE_FIELDS = {
|
||||||
this.autorun(function (){
|
_id: 1,
|
||||||
|
name: 1,
|
||||||
|
type: 1,
|
||||||
|
icon: 1,
|
||||||
|
color: 1,
|
||||||
|
order: 1,
|
||||||
|
parent: 1,
|
||||||
|
ancestors: 1,
|
||||||
|
tags: 1,
|
||||||
|
slotFillerCondition: 1,
|
||||||
|
// SlotFillers
|
||||||
|
slotQuantityFilled: 1,
|
||||||
|
// Effect
|
||||||
|
operation: 1,
|
||||||
|
targetTags: 1,
|
||||||
|
stats: 1,
|
||||||
|
// Item
|
||||||
|
quantity: 1,
|
||||||
|
plural: 1,
|
||||||
|
equipped: 1,
|
||||||
|
// Branch
|
||||||
|
branchType: 1,
|
||||||
|
// Damage:
|
||||||
|
damageType: 1,
|
||||||
|
stat: 1,
|
||||||
|
amount: 1,
|
||||||
|
// Class level
|
||||||
|
level: 1,
|
||||||
|
variableName: 1,
|
||||||
|
// Proficiency
|
||||||
|
value: 1,
|
||||||
|
// Reference
|
||||||
|
cache: 1,
|
||||||
|
// Saving throw
|
||||||
|
dc: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LIBRARY_NODE_TREE_FIELDS };
|
||||||
|
|
||||||
|
Meteor.publish('libraryCollection', function (libraryCollectionId) {
|
||||||
|
this.autorun(function () {
|
||||||
|
let userId = this.userId;
|
||||||
|
if (!userId) return [];
|
||||||
|
this.autorun(function () {
|
||||||
|
const libraryCollectionCursor = LibraryCollections.find({
|
||||||
|
_id: libraryCollectionId,
|
||||||
|
$or: [
|
||||||
|
{ owner: userId },
|
||||||
|
{ writers: userId },
|
||||||
|
{ readers: userId },
|
||||||
|
{ public: true },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const libraryCollection = libraryCollectionCursor.fetch()[0];
|
||||||
|
if (!libraryCollection) return [ libraryCollectionCursor ];
|
||||||
|
this.autorun(function () {
|
||||||
|
const libraryCursor = Libraries.find({
|
||||||
|
_id: {$in: libraryCollection.libraries},
|
||||||
|
$or: [
|
||||||
|
{ owner: userId },
|
||||||
|
{ writers: userId },
|
||||||
|
{ readers: userId },
|
||||||
|
{ public: true },
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
sort: { name: 1 }
|
||||||
|
});
|
||||||
|
return [ libraryCollectionCursor, libraryCursor ];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Meteor.publish('libraries', function () {
|
||||||
|
this.autorun(function () {
|
||||||
let userId = this.userId;
|
let userId = this.userId;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const user = Meteor.users.findOne(userId, {
|
const user = Meteor.users.findOne(userId, {
|
||||||
fields: {subscribedLibraries: 1}
|
fields: { subscribedLibraries: 1, subscribedLibraryCollections: 1 }
|
||||||
});
|
});
|
||||||
const subs = user && user.subscribedLibraries || [];
|
|
||||||
return Libraries.find({
|
this.autorun(function () {
|
||||||
$or: [
|
// Get the collections the user is subscribed to
|
||||||
{owner: this.userId},
|
const subCollections = user && user.subscribedLibraryCollections || [];
|
||||||
{writers: this.userId},
|
const libraryCollectionsCursor = LibraryCollections.find({
|
||||||
{readers: this.userId},
|
$or: [
|
||||||
{ _id: {$in: subs}, public: true },
|
{ owner: userId },
|
||||||
]
|
{ writers: userId },
|
||||||
}, {
|
{ readers: userId },
|
||||||
sort: {name: 1}
|
{ _id: { $in: subCollections }, public: true },
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
sort: { name: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collate all the libraryIds in those collections
|
||||||
|
let collectionLibIds = [];
|
||||||
|
libraryCollectionsCursor.forEach(libCollection => {
|
||||||
|
collectionLibIds = union(collectionLibIds, libCollection.libraries);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get the libraries the user is subscribed to directly
|
||||||
|
const subs = user && user.subscribedLibraries || [];
|
||||||
|
|
||||||
|
// Combine all the library Ids
|
||||||
|
const libIds = union(collectionLibIds, subs);
|
||||||
|
|
||||||
|
this.autorun(function () {
|
||||||
|
const librariesCursor = Libraries.find({
|
||||||
|
$or: [
|
||||||
|
{ owner: userId },
|
||||||
|
{ writers: userId },
|
||||||
|
{ readers: userId },
|
||||||
|
{ _id: { $in: libIds }, public: true },
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
sort: { name: 1 }
|
||||||
|
});
|
||||||
|
return [librariesCursor, libraryCollectionsCursor];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -64,37 +170,7 @@ Meteor.publish('libraryNodes', function(libraryId){
|
|||||||
'ancestors.id': libraryId,
|
'ancestors.id': libraryId,
|
||||||
}, {
|
}, {
|
||||||
sort: { order: 1 },
|
sort: { order: 1 },
|
||||||
fields: {
|
fields: LIBRARY_NODE_TREE_FIELDS,
|
||||||
_id: 1,
|
|
||||||
name: 1,
|
|
||||||
type: 1,
|
|
||||||
icon: 1,
|
|
||||||
color: 1,
|
|
||||||
order: 1,
|
|
||||||
parent: 1,
|
|
||||||
ancestors: 1,
|
|
||||||
removed: 1,
|
|
||||||
// Effect
|
|
||||||
operation: 1,
|
|
||||||
targetTags: 1,
|
|
||||||
stats: 1,
|
|
||||||
// Item
|
|
||||||
quantity: 1,
|
|
||||||
plural: 1,
|
|
||||||
equipped: 1,
|
|
||||||
// Branch
|
|
||||||
branchType: 1,
|
|
||||||
// Damage:
|
|
||||||
damageType: 1,
|
|
||||||
stat: 1,
|
|
||||||
amount: 1,
|
|
||||||
// Class level
|
|
||||||
level: 1,
|
|
||||||
// Proficiency
|
|
||||||
value: 1,
|
|
||||||
// Reference
|
|
||||||
cache: 1,
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
import Libraries from '/imports/api/library/Libraries.js';
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
|
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
|
||||||
|
import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js';
|
||||||
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||||
|
|
||||||
Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
|
Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
|
||||||
@@ -37,7 +39,7 @@ Meteor.publish('selectedLibraryNodes', function(selectedNodeIds){
|
|||||||
})];
|
})];
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publish('searchLibraryNodes', function(){
|
Meteor.publish('searchLibraryNodes', function(creatureId){
|
||||||
let self = this;
|
let self = this;
|
||||||
this.autorun(function (){
|
this.autorun(function (){
|
||||||
let type = self.data('type');
|
let type = self.data('type');
|
||||||
@@ -49,23 +51,12 @@ Meteor.publish('searchLibraryNodes', function(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all the ids of libraries the user can access
|
// Get all the ids of libraries the user can access
|
||||||
const user = Meteor.users.findOne(userId, {
|
let libraryIds;
|
||||||
fields: {subscribedLibraries: 1}
|
if (creatureId) {
|
||||||
});
|
libraryIds = getCreatureLibraryIds(creatureId, userId)
|
||||||
if (!user) return [];
|
} else {
|
||||||
|
libraryIds = getUserLibraryIds(userId)
|
||||||
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
|
// Build a filter for nodes in those libraries that match the type
|
||||||
let filter = {
|
let filter = {
|
||||||
@@ -122,6 +113,7 @@ Meteor.publish('searchLibraryNodes', function(){
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cursor = LibraryNodes.find(filter, options);
|
let cursor = LibraryNodes.find(filter, options);
|
||||||
|
const libraries = Libraries.find({ _id: { $in: libraryIds } });
|
||||||
|
|
||||||
Mongo.Collection._publishCursor(libraries, self, 'libraries');
|
Mongo.Collection._publishCursor(libraries, self, 'libraries');
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
import SimpleSchema from 'simpl-schema';
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||||
import VERSION from '/imports/constants/VERSION.js';
|
import VERSION from '/imports/constants/VERSION.js';
|
||||||
|
import { loadCreature } from '/imports/api/engine/loadCreatures.js';
|
||||||
|
|
||||||
let schema = new SimpleSchema({
|
let schema = new SimpleSchema({
|
||||||
creatureId: {
|
creatureId: {
|
||||||
@@ -13,28 +15,24 @@ let schema = new SimpleSchema({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.publish('singleCharacter', function(creatureId){
|
Meteor.publish('singleCharacter', function (creatureId) {
|
||||||
|
const self = this;
|
||||||
try {
|
try {
|
||||||
schema.validate({ creatureId });
|
schema.validate({ creatureId });
|
||||||
} catch (e){
|
} catch (e){
|
||||||
this.error(e);
|
this.error(e);
|
||||||
}
|
}
|
||||||
this.autorun(function (computation){
|
this.autorun(function (computation){
|
||||||
const userId = this.userId;
|
let userId = this.userId;
|
||||||
const creature = Creatures.findOne({
|
let permissionCreature = Creatures.findOne({
|
||||||
_id: creatureId,
|
_id: creatureId,
|
||||||
}, {
|
}, {
|
||||||
fields: {
|
fields: { owner: 1, readers: 1, writers: 1, public: 1, computeVersion: 1 }
|
||||||
owner: 1,
|
|
||||||
readers: 1,
|
|
||||||
writers: 1,
|
|
||||||
public: 1,
|
|
||||||
computeVersion: 1,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
try { assertViewPermission(creature, userId) }
|
try { assertViewPermission(permissionCreature, userId) }
|
||||||
catch(e){ return [] }
|
catch (e) { return [] }
|
||||||
if (creature.computeVersion !== VERSION && computation.firstRun){
|
loadCreature(creatureId, self);
|
||||||
|
if (permissionCreature.computeVersion !== VERSION && computation.firstRun){
|
||||||
try {
|
try {
|
||||||
computeCreature(creatureId)
|
computeCreature(creatureId)
|
||||||
}
|
}
|
||||||
@@ -44,6 +42,9 @@ Meteor.publish('singleCharacter', function(creatureId){
|
|||||||
Creatures.find({
|
Creatures.find({
|
||||||
_id: creatureId,
|
_id: creatureId,
|
||||||
}),
|
}),
|
||||||
|
CreatureVariables.find({
|
||||||
|
_creatureId: creatureId,
|
||||||
|
}),
|
||||||
CreatureProperties.find({
|
CreatureProperties.find({
|
||||||
'ancestors.id': creatureId,
|
'ancestors.id': creatureId,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import Libraries from '/imports/api/library/Libraries.js';
|
|||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
||||||
|
import getCreatureLibraryIds from '/imports/api/library/getCreatureLibraryIds.js';
|
||||||
|
import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js';
|
||||||
|
|
||||||
Meteor.publish('slotFillers', function(slotId, searchTerm){
|
Meteor.publish('slotFillers', function(slotId, searchTerm){
|
||||||
if (searchTerm) check(searchTerm, String);
|
if (searchTerm) check(searchTerm, String);
|
||||||
@@ -20,21 +22,18 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all the ids of libraries the user can access
|
// Get all the ids of libraries the user can access
|
||||||
const user = Meteor.users.findOne(userId, {
|
const creatureId = slot.ancestors[0].id;
|
||||||
fields: {subscribedLibraries: 1}
|
const libraryIds = getCreatureLibraryIds(creatureId, userId);
|
||||||
});
|
const libraries = Libraries.find({
|
||||||
const subs = user && user.subscribedLibraries || [];
|
|
||||||
let libraries = Libraries.find({
|
|
||||||
$or: [
|
$or: [
|
||||||
{owner: this.userId},
|
{ owner: userId },
|
||||||
{writers: this.userId},
|
{ writers: userId },
|
||||||
{readers: this.userId},
|
{ readers: userId },
|
||||||
{_id: {$in: subs}},
|
{ _id: { $in: libraryIds }, public: true },
|
||||||
]
|
]
|
||||||
}, {
|
}, {
|
||||||
fields: {_id: 1, name: 1},
|
sort: { name: 1 }
|
||||||
});
|
});
|
||||||
let libraryIds = libraries.map(lib => lib._id);
|
|
||||||
|
|
||||||
// Build a filter for nodes in those libraries that match the slot
|
// Build a filter for nodes in those libraries that match the slot
|
||||||
let filter = getSlotFillFilter({slot, libraryIds});
|
let filter = getSlotFillFilter({slot, libraryIds});
|
||||||
@@ -50,7 +49,8 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
|
|||||||
options = {
|
options = {
|
||||||
// relevant documents have a higher score.
|
// relevant documents have a higher score.
|
||||||
fields: {
|
fields: {
|
||||||
_score: { $meta: 'textScore' }
|
_score: { $meta: 'textScore' },
|
||||||
|
...LIBRARY_NODE_TREE_FIELDS,
|
||||||
},
|
},
|
||||||
sort: {
|
sort: {
|
||||||
// `score` property specified in the projection fields above.
|
// `score` property specified in the projection fields above.
|
||||||
@@ -61,10 +61,13 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete filter.$text
|
delete filter.$text
|
||||||
options = {sort: {
|
options = {
|
||||||
name: 1,
|
sort: {
|
||||||
order: 1,
|
name: 1,
|
||||||
}};
|
order: 1,
|
||||||
|
},
|
||||||
|
fields: LIBRARY_NODE_TREE_FIELDS,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
options.limit = limit;
|
options.limit = limit;
|
||||||
|
|
||||||
@@ -72,7 +75,68 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){
|
|||||||
self.setData('countAll', LibraryNodes.find(filter).count());
|
self.setData('countAll', LibraryNodes.find(filter).count());
|
||||||
});
|
});
|
||||||
self.autorun(function () {
|
self.autorun(function () {
|
||||||
Meteor._sleepForMs(1000);
|
return [
|
||||||
|
LibraryNodes.find(filter, options),
|
||||||
|
libraries
|
||||||
|
];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Meteor.publish('classFillers', function(classId){
|
||||||
|
let self = this;
|
||||||
|
if (!classId) return [];
|
||||||
|
|
||||||
|
this.autorun(function (){
|
||||||
|
let userId = this.userId;
|
||||||
|
if (!userId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
// Get the class
|
||||||
|
let classProp = CreatureProperties.findOne(classId);
|
||||||
|
if (!classProp){
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the ids of libraries the user can access
|
||||||
|
const user = Meteor.users.findOne(userId, {
|
||||||
|
fields: {subscribedLibraries: 1}
|
||||||
|
});
|
||||||
|
const subs = user && user.subscribedLibraries || [];
|
||||||
|
let libraries = Libraries.find({
|
||||||
|
$or: [
|
||||||
|
{owner: userId},
|
||||||
|
{writers: userId},
|
||||||
|
{readers: 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 slot
|
||||||
|
let filter = getSlotFillFilter({slot: classProp, libraryIds});
|
||||||
|
|
||||||
|
this.autorun(function(){
|
||||||
|
// Get the limit of the documents the user can fetch
|
||||||
|
var limit = self.data('limit') || 50;
|
||||||
|
check(limit, Number);
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
sort: {
|
||||||
|
name: 1,
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
fields: LIBRARY_NODE_TREE_FIELDS,
|
||||||
|
limit,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.autorun(function () {
|
||||||
|
self.setData('countAll', LibraryNodes.find(filter).count());
|
||||||
|
});
|
||||||
|
self.autorun(function () {
|
||||||
return [LibraryNodes.find(filter, options), libraries];
|
return [LibraryNodes.find(filter, options), libraries];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
7
app/imports/server/publications/userImages.js
Normal file
7
app/imports/server/publications/userImages.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import UserImages from '/imports/api/files/UserImages.js';
|
||||||
|
|
||||||
|
Meteor.publish('userImages', function () {
|
||||||
|
return UserImages.find({
|
||||||
|
userId: this.userId,
|
||||||
|
}).cursor;
|
||||||
|
});
|
||||||
@@ -10,6 +10,8 @@ Meteor.publish('user', function(){
|
|||||||
apiKey: 1,
|
apiKey: 1,
|
||||||
darkMode: 1,
|
darkMode: 1,
|
||||||
subscribedLibraries: 1,
|
subscribedLibraries: 1,
|
||||||
|
subscribedLibraryCollections: 1,
|
||||||
|
fileStorageUsed: 1,
|
||||||
profile: 1,
|
profile: 1,
|
||||||
preferences: 1,
|
preferences: 1,
|
||||||
'services.patreon.id': 1,
|
'services.patreon.id': 1,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
props: {
|
props: {
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
77
app/imports/ui/components/ImageUploadInput.vue
Normal file
77
app/imports/ui/components/ImageUploadInput.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-upload-input">
|
||||||
|
<v-file-input
|
||||||
|
v-model="file"
|
||||||
|
v-bind="$attrs"
|
||||||
|
accept="image/*"
|
||||||
|
/>
|
||||||
|
<v-progress-linear
|
||||||
|
:progress="progress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
import UserImages from '/imports/api/files/UserImages.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data(){return {
|
||||||
|
progress: 0,
|
||||||
|
file: undefined,
|
||||||
|
uploadingInProgress: false,
|
||||||
|
}},
|
||||||
|
watch: {
|
||||||
|
file(file){
|
||||||
|
if (!file) return;
|
||||||
|
let self = this;
|
||||||
|
let uploadInstance = UserImages.insert({
|
||||||
|
file: file,
|
||||||
|
/*meta: {
|
||||||
|
userId: Meteor.userId() // Optional, used to check on server for file tampering
|
||||||
|
},*/
|
||||||
|
chunkSize: 'dynamic',
|
||||||
|
allowWebWorkers: true // If you see issues with uploads, change this to false
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// These are the event functions, don't need most of them, it shows where we are in the process
|
||||||
|
uploadInstance.on('start', function () {
|
||||||
|
console.log('Starting');
|
||||||
|
this.uploadingInProgress = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadInstance.on('end', function (error, fileObj) {
|
||||||
|
console.log('On end File Object: ', fileObj);
|
||||||
|
this.uploadingInProgress = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadInstance.on('uploaded', function (error, fileObj) {
|
||||||
|
console.log('uploaded: ', fileObj);
|
||||||
|
|
||||||
|
// Remove the file from the input box
|
||||||
|
self.file = undefined;
|
||||||
|
|
||||||
|
// Reset our state for the next file
|
||||||
|
self.uploadingInProgress = false;
|
||||||
|
self.progress = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadInstance.on('error', function (error, fileObj) {
|
||||||
|
console.log('Error during upload: ' + error, fileObj)
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadInstance.on('progress', function (progress, fileObj) {
|
||||||
|
console.log('Upload Percentage: ' + progress, fileObj)
|
||||||
|
// Update our progress bar
|
||||||
|
self.progress = progress;
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadInstance.start(); // Must manually start the upload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="accessRights === 'reader' || accessRights === 'writer'"
|
v-if="accessRights === 'reader' || accessRights === 'writer' || accessRights === 'public'"
|
||||||
bottom
|
bottom
|
||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
style="opacity: 0.4"
|
style="opacity: 0.4"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
{{ accessRights === 'reader' ? 'mdi-file-eye' : 'mdi-file-edit' }}
|
{{ accessIcon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<span>{{ accessText }}</span>
|
<span>{{ accessText }}</span>
|
||||||
@@ -32,13 +32,24 @@ export default {
|
|||||||
else if (this.model.public) return 'public';
|
else if (this.model.public) return 'public';
|
||||||
else return 'denied'
|
else return 'denied'
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
accessIcon() {
|
||||||
|
switch (this.accessRights){
|
||||||
|
case 'writer': return 'mdi-file-edit';
|
||||||
|
case 'reader': return 'mdi-file-eye';
|
||||||
|
case 'public': return 'mdi-cloud';
|
||||||
|
default: return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
accessText(){
|
accessText(){
|
||||||
switch (this.accessRights){
|
switch (this.accessRights){
|
||||||
case 'writer': return 'Shared with edit permission';
|
case 'writer': return 'Shared with edit permission';
|
||||||
case 'reader': return 'Shared as view-only';
|
case 'reader': return 'Shared as view-only';
|
||||||
case 'public': return 'Shared as publicly viewable';
|
case 'public': return 'Shared publically';
|
||||||
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,28 +7,28 @@
|
|||||||
style="overflow-y: auto;"
|
style="overflow-y: auto;"
|
||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<div class="layout align-center">
|
<v-btn
|
||||||
<v-btn
|
:loading="loading"
|
||||||
:loading="loading"
|
outlined
|
||||||
outlined
|
:min-width="108"
|
||||||
:min-width="108"
|
v-bind="$attrs"
|
||||||
v-on="on"
|
:style="buttonStyle"
|
||||||
|
v-on="on"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
<svg-icon
|
||||||
|
v-if="safeValue && safeValue.shape"
|
||||||
|
right
|
||||||
|
class="ml-2"
|
||||||
|
:shape="safeValue.shape"
|
||||||
|
/>
|
||||||
|
<v-icon
|
||||||
|
v-else
|
||||||
|
right
|
||||||
>
|
>
|
||||||
{{ label }}
|
mdi-select-search
|
||||||
<svg-icon
|
</v-icon>
|
||||||
v-if="safeValue && safeValue.shape"
|
</v-btn>
|
||||||
right
|
|
||||||
class="ml-2"
|
|
||||||
:shape="safeValue.shape"
|
|
||||||
/>
|
|
||||||
<v-icon
|
|
||||||
v-else
|
|
||||||
right
|
|
||||||
>
|
|
||||||
mdi-select-search
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
@@ -91,6 +91,10 @@ export default {
|
|||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'Icon',
|
default: 'Icon',
|
||||||
|
},
|
||||||
|
buttonStyle: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
@click.stop="$emit('selected', node._id)"
|
@click.stop="$emit('selected', node._id)"
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-if="!startExpanded"
|
||||||
small
|
small
|
||||||
icon
|
icon
|
||||||
:class="showExpanded ? 'rotate-90' : null"
|
:class="showExpanded ? 'rotate-90' : null"
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<div
|
<div
|
||||||
class="layout align-center justify-start pr-1"
|
class="layout align-center justify-start pr-1"
|
||||||
|
:class="{'ml-4': startExpanded}"
|
||||||
style="flex-grow: 0;"
|
style="flex-grow: 0;"
|
||||||
>
|
>
|
||||||
<v-icon
|
<v-icon
|
||||||
@@ -56,6 +58,7 @@
|
|||||||
:group="group"
|
:group="group"
|
||||||
:organize="organize"
|
:organize="organize"
|
||||||
:selected-node="selectedNode"
|
:selected-node="selectedNode"
|
||||||
|
:start-expanded="startExpanded"
|
||||||
@reordered="e => $emit('reordered', e)"
|
@reordered="e => $emit('reordered', e)"
|
||||||
@reorganized="e => $emit('reorganized', e)"
|
@reorganized="e => $emit('reorganized', e)"
|
||||||
@selected="e => $emit('selected', e)"
|
@selected="e => $emit('selected', e)"
|
||||||
@@ -112,10 +115,11 @@
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
|
startExpanded: Boolean,
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
expanded: this.node._ancestorOfMatchedDocument ||
|
expanded: this.startExpanded || this.node._ancestorOfMatchedDocument ||
|
||||||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
|
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
|
||||||
false,
|
false,
|
||||||
}},
|
}},
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
:ancestors-of-selected-node="ancestorsOfSelectedNode"
|
:ancestors-of-selected-node="ancestorsOfSelectedNode"
|
||||||
:organize="organize"
|
:organize="organize"
|
||||||
:lazy="lazy"
|
:lazy="lazy"
|
||||||
|
:start-expanded="startExpanded"
|
||||||
@selected="e => $emit('selected', e)"
|
@selected="e => $emit('selected', e)"
|
||||||
@reordered="e => $emit('reordered', e)"
|
@reordered="e => $emit('reordered', e)"
|
||||||
@reorganized="e => $emit('reorganized', e)"
|
@reorganized="e => $emit('reorganized', e)"
|
||||||
@@ -58,9 +59,10 @@
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
startExpanded: Boolean,
|
||||||
},
|
},
|
||||||
data(){ return {
|
data(){ return {
|
||||||
expanded: false,
|
expanded: this.startExpanded || false,
|
||||||
displayedChildren: [],
|
displayedChildren: [],
|
||||||
}},
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user