Compare commits

...

88 Commits

Author SHA1 Message Date
Stefan Zermatten
bec65be170 Merge branch 'version-2-dev' into version-2 2022-07-25 09:40:31 +02:00
Stefan Zermatten
0483a7effc Tag targeting attributes and skills with effects
now works like normal effects would
2022-07-24 22:32:40 +02:00
Stefan Zermatten
4c5c537f29 Improved slot tag handling 2022-07-24 21:50:31 +02:00
Stefan Zermatten
a0c2822dac Added "extra" damage type
Takes on the same damage type as the last damage applied during the
same action, otherwise deals "extra" damage
2022-07-24 20:38:42 +02:00
Stefan Zermatten
27a21aed59 Added error alert to stat tab for dependency loops 2022-07-24 19:59:43 +02:00
Stefan Zermatten
1da2d319fb Improved handling of tag targeting 2022-07-24 15:22:07 +02:00
Stefan Zermatten
82879aaa60 Added admin override to view permission 2022-07-24 15:12:12 +02:00
Stefan Zermatten
be654d5d45 Added tree root dialog to breadcrumbs 2022-07-24 14:46:27 +02:00
Stefan Zermatten
f88ffcf0c3 Hardened archive upload slightly 2022-07-24 14:17:39 +02:00
Stefan Zermatten
8b62bac83f Added classes and level up button to build tab 2022-07-24 14:17:27 +02:00
Stefan Zermatten
e698b4b838 Added Dai as Paragon 2022-07-21 09:05:27 +02:00
Stefan Zermatten
566d6a4fef Triggers 🤫 2022-07-20 15:57:38 +02:00
Stefan Zermatten
6f7e742eb9 More of the sheet conforms to library allowances 2022-07-20 00:09:58 +02:00
Stefan Zermatten
0c06f60b7e Fixed typo "Transfer Onwership" 2022-07-19 19:25:12 +02:00
Stefan Zermatten
f8e9131bdd Vastly improved new character UX
Characters now can limit which libraries they allow
2022-07-18 13:45:14 +02:00
Stefan Zermatten
bf9639ae59 Library Collections UI built 2022-07-17 22:48:48 +02:00
Stefan Zermatten
ee89a052bc Added libraryCollections on server 2022-07-13 23:16:25 +02:00
Stefan Zermatten
59ef7527b7 Fixed some errors with character insertion/deletion 2022-07-09 12:53:44 +02:00
Stefan Zermatten
b8a03245ea Level up dialog now working 2022-07-05 15:40:55 +02:00
Stefan Zermatten
1a71c2cfa7 Began implementing class level up UI 2022-07-04 13:55:41 +02:00
Stefan Zermatten
292388dead Iterated on class UI 2022-06-29 15:35:12 +02:00
Stefan Zermatten
00272e7b55 Action cards now show their decendants 2022-06-29 15:27:12 +02:00
Stefan Zermatten
f07f05ca2c Moved creature variables to their own collection
Another big change to the engine, expect bugs
2022-06-29 14:54:25 +02:00
Stefan Zermatten
9dd84a83d2 Started removing scope from creature doc 2022-06-24 10:50:35 +02:00
Stefan Zermatten
b2f89eceee Fixed some bugs with characters not recomputing
TODO: remove .variables cache from creature document, it's not viable
2022-06-23 08:39:48 +02:00
Stefan Zermatten
b484a27637 iterated on class form to match new schema 2022-06-23 08:39:15 +02:00
Stefan Zermatten
da5143693f Iterated on class UI 2022-06-21 11:08:45 +02:00
Stefan Zermatten
9cc4186171 Reduced data load in slot fill dialog 2022-06-21 11:00:50 +02:00
Stefan Zermatten
9f59a6cf86 Fixed long folder names not truncating correctly 2022-06-20 15:11:39 +02:00
Stefan Zermatten
fdaa035bfb Fixed Shakespearean typo in slot filer description 2022-06-20 14:50:04 +02:00
Stefan Zermatten
b31760af0c Don't show children expansion panel if not needed 2022-06-20 14:22:13 +02:00
Stefan Zermatten
a8ffa2f786 Fixed denormalized creature variables not removed 2022-06-20 13:05:38 +02:00
Stefan Zermatten
9b1ec46064 Added children properties to prop edit dialog 2022-06-08 12:22:51 +02:00
Stefan Zermatten
11f373ddd8 Improved slotCard UI 2022-06-07 23:27:14 +02:00
Stefan Zermatten
e7e8f938ed Improved archive storage calculations and errors 2022-06-07 23:21:48 +02:00
Stefan Zermatten
28934baac9 Implemented archive upload handling 2022-06-07 23:01:06 +02:00
Stefan Zermatten
385ac17812 Added big slot cards to build tab, improved build tab 2022-06-07 21:43:35 +02:00
Stefan Zermatten
a04935c5b4 Updated packages 2022-06-07 18:51:07 +02:00
Stefan Zermatten
ccc861b6fa Improved character subscription performance
By limiting fields that can trigger an autorun
2022-05-11 15:52:44 +02:00
Stefan Zermatten
6702f431d0 Fixed bug where removed library nodes kept showing 2022-05-11 15:52:02 +02:00
Stefan Zermatten
1b3efae81a Replaced manual recompute calls with dirty flag settings 2022-05-11 15:42:29 +02:00
Stefan Zermatten
7a35c66904 Removed performance logging from loading creatures 2022-05-11 13:33:56 +02:00
Stefan Zermatten
78cd8ffc8d Creatures are now cached in memory for computation
Also removed dependency group calculation because the optimisation isn't
as useful to reduce DB calls if the creature is in memory anyway
2022-05-11 13:30:33 +02:00
Stefan Zermatten
23fa6fe634 Progress on dependency updates 2022-05-09 16:32:15 +02:00
Stefan Zermatten
caf50d1578 Merge branch 'version-2' into version-2-dev 2022-05-09 12:46:09 +02:00
Stefan Zermatten
df7889edd9 Reduced fields loaded by library tree view
This should improve performance a little for large libraries,
at the expense of loading when a property is selected
2022-05-09 11:23:46 +02:00
Stefan Zermatten
ddc7f87a4a Replaced unsupported cron manager with new one 2022-05-03 11:29:27 +02:00
Stefan Zermatten
33fa22c187 Removed stray log 2022-05-02 23:31:52 +02:00
Stefan Zermatten
2e3f0320f3 Added dependency grouping, but commented out for now until it's needed 2022-05-02 23:31:10 +02:00
Stefan Zermatten
0b7c20e616 Added APM monitoring 2022-05-02 18:48:41 +02:00
Stefan Zermatten
abb8890070 Build card being converted into build tree
Still need to be able to delete fillers
2022-05-02 12:32:14 +02:00
Stefan Zermatten
8dbcae1060 Fixed FAB appearing on wrong tabs 2022-05-01 23:43:31 +02:00
Stefan Zermatten
3a18bce7e6 Added error message for unsupported accessors 2022-05-01 22:54:30 +02:00
Stefan Zermatten
3e97baaaaa Progress on storing user images 2022-04-25 16:16:17 +02:00
Stefan Zermatten
ea32c54f57 Fixed massive writes to creature.variables on calc
Now only writes changed variables, preventing oplog from being
polluted with massive updates
2022-04-25 13:57:39 +02:00
Stefan Zermatten
6b724cf365 Dicecloud instances without db version numbers won't go into migration mode 2022-04-25 11:16:02 +02:00
Stefan Zermatten
8b44c83741 Added archive upload UI 2022-04-23 11:35:11 +02:00
Stefan Zermatten
2ca9ac5342 Added storage stats to the account page 2022-04-23 09:52:20 +02:00
Stefan Zermatten
7609e916c6 Implemented remove button on archive files 2022-04-23 09:35:29 +02:00
Stefan Zermatten
f440e030cf Fixed bug in generating error messages when out of character slots 2022-04-22 11:31:59 +02:00
Stefan Zermatten
13b6689ba4 Progress on file system UI 2022-04-22 11:30:59 +02:00
Stefan Zermatten
b28bcbe079 Work on general UI for user files 2022-04-21 22:08:18 +02:00
Stefan Zermatten
ffa6353a3f Merge branch 'version-2' into version-2-dev 2022-04-21 20:25:23 +02:00
Stefan Zermatten
33f60c2c6d Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2022-04-21 10:59:22 +02:00
Stefan Zermatten
dcc95486b3 fixed parser error when using incorrect call args 2022-04-21 10:59:19 +02:00
Stefan Zermatten
88bc223daa Began working on build tab 2022-04-20 13:56:06 +02:00
Stefan Zermatten
cbc42f8500 fix: buff descriptions aren't being calculated 2022-04-15 18:13:03 +02:00
Stefan Zermatten
cc24690a67 Fixed bug where children of rolls would be applied 2022-04-09 15:02:44 +02:00
Stefan Zermatten
4d5cb3ed50 Merge branch 'version-2-dev' into version-2 2022-04-08 18:41:48 +02:00
Stefan Zermatten
c3d9ee7589 Default health bars are now the app's primary color 2022-04-08 18:40:53 +02:00
Stefan Zermatten
d9f572504d Merge branch 'version-2' into version-2-dev 2022-04-08 17:59:20 +02:00
Stefan Zermatten
cb80f3a6da Custom health bar colors. Also works for setting mid and low health colors 2022-04-08 17:58:45 +02:00
Stefan Zermatten
e89d415e7e Fixed error handling of compute engine crashes 2022-03-09 08:24:55 +02:00
Stefan Zermatten
ac8f19bcfb Hotfix for skills without effects breaking calculations 2022-03-09 08:09:16 +02:00
Stefan Zermatten
788cbb182d Action system improvements
- Actions/spells now display their summary, not their description
- All save branches and attack branches run when there are no targets
- Improved action logging
- Index branch lets you customise a choice of children to run
2022-03-09 01:31:09 +02:00
Stefan Zermatten
c68667be9c Added data validation diagnostics for offline use 2022-03-08 15:04:51 +02:00
Stefan Zermatten
fada07e048 Improved handling of poorly migrated archive creatures 2022-03-08 14:12:11 +02:00
Stefan Zermatten
12fc9b1be3 Added summary field back to spell form 2022-03-08 13:17:39 +02:00
Stefan Zermatten
e7f718c785 Prevented updates from running on the server if they fail client validation 2022-03-08 13:15:48 +02:00
Stefan Zermatten
9732db8d67 Hid damage multiplier card if there are no multipliers 2022-03-05 18:56:34 +02:00
Stefan Zermatten
73ca6dc364 Damage multipliers are now applied to damage dealt 2022-03-05 18:40:18 +02:00
Stefan Zermatten
10242b596f Updated test cases to account for new damage multipliers 2022-03-05 17:59:50 +02:00
Stefan Zermatten
782f2cdc73 Added default tags to properties
#type, damageType, skillType, attributeType, reset
2022-03-05 17:52:15 +02:00
Stefan Zermatten
a8ebf6a1de Tags now wrap in damage multiplier viewer 2022-03-05 17:48:52 +02:00
Stefan Zermatten
7dcd0aeff2 Fixed single-select combobox not showing rules errors 2022-03-05 16:30:53 +02:00
Stefan Zermatten
a19e7d0514 Prevented errors from crashing archive restoration 2022-03-05 16:24:42 +02:00
Stefan Zermatten
2442ae4fa0 Overhauled damage multipliers UX
Form and viewer revamp
custom damage types
Variables: `bludgeoning.resistance`
2022-03-05 16:23:21 +02:00
Stefan Zermatten
545050cfa3 Fixed attack disadvantage being treated as advantage 2022-03-04 16:09:00 +02:00
251 changed files with 9808 additions and 2664 deletions

View File

@@ -3,26 +3,25 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@2.2.0
accounts-password@2.3.1
random@1.2.0
underscore@1.0.10
dburles:mongo-collection-instances
accounts-google@1.4.0
email@2.2.0
email@2.2.1
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.14.6
mongo@1.15.0
session@1.2.0
tracker@1.2.0
logging@1.3.1
reload@1.3.1
ejson@1.1.1
ejson@1.1.2
check@1.3.1
standard-minifier-js@2.8.0
shell-server@0.5.0
ecmascript@0.16.1
ecmascript@0.16.2
es5-shim@4.8.0
percolate:synced-cron
service-configuration@1.3.0
dynamic-import@0.7.2
ddp-rate-limiter@1.1.0
@@ -47,3 +46,5 @@ meteortesting:mocha
ostrio:files
simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
mdg:meteor-apm-agent

View File

@@ -1 +1 @@
METEOR@2.6.1
METEOR@2.7.3

View File

@@ -1,7 +1,7 @@
accounts-base@2.2.1
accounts-base@2.2.3
accounts-google@1.4.0
accounts-oauth@1.4.0
accounts-password@2.2.0
accounts-oauth@1.4.1
accounts-password@2.3.1
accounts-patreon@0.1.0
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
@@ -12,13 +12,13 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.8.1
babel-runtime@1.5.0
babel-compiler@7.9.0
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
blaze-tools@1.1.2
blaze-tools@1.1.3
boilerplate-generator@1.7.1
bozhao:link-accounts@2.4.0
bozhao:link-accounts@2.6.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
@@ -33,26 +33,29 @@ ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.1
ecmascript@0.16.2
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.1
email@2.2.0
ejson@1.1.2
email@2.2.1
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-oauth@1.4.1
google-oauth@1.4.2
hot-code-push@1.0.4
html-tools@1.1.2
html-tools@1.1.3
htmljs@1.1.1
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
lai:collection-extensions@0.3.0
launch-screen@1.3.0
littledata:synced-cron@1.5.1
livedata@1.0.18
localstorage@1.2.0
logging@1.3.1
mdg:meteor-apm-agent@3.5.0
mdg:validated-method@1.2.0
meteor@1.10.0
meteor-base@1.5.1
@@ -61,22 +64,23 @@ meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.0
minifier-js@2.7.3
minifier-js@2.7.4
minimongo@1.8.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.7
modern-browsers@0.1.8
modules@0.18.0
modules-runtime@0.12.0
mongo@1.14.6
mongo-decimal@0.1.2
modules-runtime@0.13.0
mongo@1.15.0
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
npm-mongo@4.3.1
oauth@2.1.1
oauth@2.1.2
oauth2@1.3.1
ordered-dict@1.1.0
ostrio:cookies@2.7.0
ostrio:cookies@2.7.2
ostrio:files@2.0.1
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
@@ -90,12 +94,11 @@ peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@1.0.3
percolate:synced-cron@1.3.2
promise@0.12.0
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.2.2
react-fast-refresh@0.2.3
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.1
@@ -111,14 +114,14 @@ simple:rest@1.2.1
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0
socket-stream-client@0.4.0
spacebars-compiler@1.3.0
socket-stream-client@0.5.0
spacebars-compiler@1.3.1
standard-minifier-js@2.8.0
static-html@1.3.2
templating-tools@1.2.1
templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2
tracker@1.2.0
typescript@4.4.1
typescript@4.5.4
underscore@1.0.10
url@1.3.2
webapp@1.13.1

View File

@@ -1,4 +1,8 @@
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({
collectionName: 'archiveCreatureFiles',
@@ -11,7 +15,38 @@ const ArchiveCreatureFiles = createS3FilesCollection({
if (!/json/i.test(file.extension)){
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 { archiveSchema };

View File

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

View File

@@ -68,7 +68,7 @@ const archiveCreatureToFile = new ValidatedMethod({
async run({creatureId}) {
assertOwnership(creatureId, this.userId);
if (Meteor.isServer){
archiveCreature(creatureId);
archiveCreature(creatureId, this.userId);
} else {
removeCreatureWork(creatureId);
}

View File

@@ -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/restoreCreatures.js';
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';

View File

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

View File

@@ -8,12 +8,16 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
import 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;
if (Meteor.isServer){
migrateArchive = require('/imports/migrations/server/migrateArchive.js').default;
}
function restoreCreature(archive){
function restoreCreature(archive, userId){
if (SCHEMA_VERSION < archive.meta.schemaVersion){
throw new Meteor.Error('Incompatible',
'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
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
// They still have their original _id's
Creatures.insert(archive.creature);
@@ -69,13 +86,18 @@ const restoreCreaturefromFile = new ValidatedMethod({
throw new Meteor.Error('Permission denied',
'You can only restore creatures you own');
}
assertHasCharactersSlots(this.userId);
if (Meteor.isServer){
// Read the file data
const archive = await ArchiveCreatureFiles.readJSONFile(file);
restoreCreature(archive);
restoreCreature(archive, this.userId);
}
//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);
},
});

View File

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

View File

@@ -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');
}
});
}

View File

@@ -82,6 +82,13 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
index: 1,
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);
@@ -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 {
DenormalisedOnlyCreaturePropertySchema,

View File

@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const adjustQuantity = new ValidatedMethod({
name: 'creatureProperties.adjustQuantity',
@@ -29,10 +28,6 @@ const adjustQuantity = new ValidatedMethod({
// Do work
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'){
CreatureProperties.update(property._id, {
$set: {quantity: value}
$set: {quantity: value, dirty: true}
}, {
selector: property
});
@@ -57,7 +52,8 @@ export function adjustQuantityWork({property, operation, value}){
let currentQuantity = property.quantity;
if (currentQuantity + value < 0) value = -currentQuantity;
CreatureProperties.update(property._id, {
$inc: {quantity: value}
$inc: { quantity: value },
$set: { dirty: true }
}, {
selector: property
});

View File

@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js';
const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage',
@@ -37,9 +36,7 @@ const damageProperty = new ValidatedMethod({
`Property of type "${property.type}" can't be damaged`
);
}
let result = damagePropertyWork({property, operation, value});
// Dependencies can't be changed through damage, only recompute deps
computeCreatureDependencyGroup(property);
let result = damagePropertyWork({ property, operation, value });
return result;
},
});
@@ -70,7 +67,7 @@ export function damagePropertyWork({property, operation, value}){
// Write the results
CreatureProperties.update(property._id, {
$set: {damage, value: newValue}
$set: {damage, value: newValue, dirty: true}
}, {
selector: property
});

View File

@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const dealDamage = new ValidatedMethod({
name: 'creatureProperties.dealDamage',
@@ -33,7 +32,6 @@ const dealDamage = new ValidatedMethod({
assertEditPermission(creature, this.userId);
const totalDamage = dealDamageWork({creature, damageType, amount})
computeCreature(creatureId);
return totalDamage;
},
});

View File

@@ -9,7 +9,6 @@ import {
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
@@ -77,6 +76,9 @@ const duplicateProperty = new ValidatedMethod({
// Order the root node
property.order += 0.5;
// Mark the sheet as needing recompute
property.dirty = true;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
@@ -87,9 +89,6 @@ const duplicateProperty = new ValidatedMethod({
ancestorId: property.ancestors[0].id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(creature._id);
return propertyId;
},
});

View File

@@ -4,7 +4,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.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 getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
@@ -29,7 +28,7 @@ const equipItem = new ValidatedMethod({
let creature = getRootCreatureAncestor(item);
assertEditPermission(creature, this.userId);
CreatureProperties.update(_id, {
$set: {equipped},
$set: { equipped, dirty: true },
}, {
selector: {type: 'item'},
});
@@ -46,8 +45,6 @@ const equipItem = new ValidatedMethod({
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
computeCreature(creature._id);
},
});

View File

@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const flipToggle = new ValidatedMethod({
name: 'creatureProperties.flipToggle',
@@ -36,12 +35,10 @@ const flipToggle = new ValidatedMethod({
CreatureProperties.update(_id, {$set: {
enabled: !currentValue,
disabled: currentValue,
dirty: true,
}}, {
selector: {type: 'toggle'},
});
// Updating a toggle is likely to change the whole tree, do a full recompute
computeCreature(rootCreature._id);
},
});

View File

@@ -15,9 +15,26 @@ export default function getSlotFillFilter({slot, libraryIds}){
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 tagsNor = [];
let tagsNin = [];
if (slot.slotTags && slot.slotTags.length){
tagsOr.push({tags: {$all: slot.slotTags}});
}
@@ -27,15 +44,15 @@ export default function getSlotFillFilter({slot, libraryIds}){
if (extra.operation === 'OR'){
tagsOr.push({tags: {$all: extra.tags}});
} else if (extra.operation === 'NOT'){
tagsNor.push({tags: {$all: extra.tags}});
tagsNin.push(...extra.tags);
}
});
}
if (tagsOr.length){
filter.$and.push({$or: tagsOr});
filter.$or = tagsOr;
}
if (tagsNor.length){
filter.$and.push({$nor: tagsNor});
if (tagsNin.length){
filter.$and.push({tags: {$nin: tagsNin}});
}
if (!filter.$and.length){
delete filter.$and;

View File

@@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.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 getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
@@ -132,14 +131,13 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
export function insertPropertyWork({property, creature}){
delete property._id;
property.dirty = true;
let _id = CreatureProperties.insert(property);
// Tree structure changed by insert, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: creature._id,
});
// Inserting a creature property invalidates dependencies: full recompute
computeCreature(creature._id);
return _id;
}

View File

@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { RefSchema } from '/imports/api/parenting/ChildSchema.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 {
setLineageOfDocs,
@@ -71,9 +70,6 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
collection: CreatureProperties,
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 rootId;
},
@@ -135,12 +131,14 @@ function insertPropertyFromNode(nodeId, ancestors, order){
node.order = order;
}
// Mark all nodes as dirty
dirtyNodes(nodes);
// Insert the creature properties
CreatureProperties.batchInsert(nodes);
return node;
}
function storeLibraryNodeReferences(nodes){
nodes.forEach(node => {
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
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){

View File

@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const pullFromProperty = new ValidatedMethod({
name: 'creatureProperties.pull',
@@ -21,15 +20,12 @@ const pullFromProperty = new ValidatedMethod({
// Do work
CreatureProperties.update(_id, {
$pull: {[path.join('.')]: {_id: itemId}},
$pull: { [path.join('.')]: { _id: itemId } },
$set: { dirty: true }
}, {
selector: {type: property.type},
getAutoValues: false,
});
// TODO figure out if this method can change deps or not
computeCreature(rootCreature._id);
// recomputePropertyDependencies(property);
}
});

View File

@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
@@ -39,13 +38,11 @@ const pushToProperty = new ValidatedMethod({
// Do work
CreatureProperties.update(_id, {
$push: {[joinedPath]: value},
$push: { [joinedPath]: value },
$set: { dirty: true },
}, {
selector: {type: property.type},
});
// TODO figure out if this method can change deps or not
computeCreature(rootCreature._id);
}
});

View File

@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
@@ -24,10 +23,13 @@ const restoreProperty = new ValidatedMethod({
assertEditPermission(rootCreature, this.userId);
// Do work
restore({_id, collection: CreatureProperties});
// Changes dependency tree by restoring children
computeCreature(rootCreature._id);
restore({
_id,
collection: CreatureProperties,
extraUpdates: {
$set: { dirty: true }
},
});
}
});

View File

@@ -4,7 +4,6 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const selectAmmoItem = new ValidatedMethod({
name: 'creatureProperties.selectAmmoItem',
@@ -37,15 +36,10 @@ const selectAmmoItem = new ValidatedMethod({
}
let path = `resources.itemsConsumed.${itemConsumedIndex}.itemId`;
CreatureProperties.update(actionId, {
$set: {[path]: itemId}
$set: { [path]: itemId, dirty: true }
}, {
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);
},
});

View File

@@ -5,7 +5,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
@@ -25,9 +24,6 @@ const softRemoveProperty = new ValidatedMethod({
// Do work
softRemove({_id, collection: CreatureProperties});
// Changes dependency tree by removing children
computeCreature(rootCreature._id);
}
});

View File

@@ -3,7 +3,6 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import computeCreature from '/imports/api/engine/computeCreature.js';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
@@ -37,17 +36,13 @@ const updateCreatureProperty = new ValidatedMethod({
let modifier;
// unset empty values
if (value === null || value === undefined){
modifier = {$unset: {[pathString]: 1}};
modifier = { $unset: {[pathString]: 1}, $set: { dirty: true } };
} else {
modifier = {$set: {[pathString]: value}};
modifier = { $set: {[pathString]: value, dirty: true } };
}
CreatureProperties.update(_id, modifier, {
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);
},
});

View 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;

View File

@@ -80,6 +80,27 @@ let CreatureSchema = new SimpleSchema({
optional: true,
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
deathSave: {
type: deathSaveSchema,
@@ -100,6 +121,11 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer,
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
computeVersion: {
type: String,
@@ -133,6 +159,7 @@ let CreatureSchema = new SimpleSchema({
'computeErrors.$.details' : {
type: Object,
blackbox: true,
optional: true,
},
// Tabletop
@@ -159,8 +186,8 @@ CreatureSchema.extend(SharingSchema);
Creatures.attachSchema(CreatureSchema);
import '/imports/api/creature/creatures/methods/index.js';
import '/imports/api/engine/actions/doAction.js';
export default Creatures;
export { CreatureSchema };
import '/imports/api/engine/actions/doAction.js';

View File

@@ -10,7 +10,7 @@ export default function defaultCharacterProperties(creatureId){
{
type: 'propertySlot',
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'],
tags: [],
quantityExpected: {calculation: '1'},

View File

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

View File

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

View File

@@ -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/restCreature.js';
import '/imports/api/creature/creatures/methods/updateCreature.js';
import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';

View File

@@ -1,70 +1,104 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
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 { getUserTier } from '/imports/api/users/patreon/tiers.js';
import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
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({
name: 'creatures.insertCreature',
validate: null,
mixins: [RateLimiterMixin],
mixins: [RateLimiterMixin, simpleSchemaMixin],
schema: CreatureSchema.pick(
'name',
'gender',
'alignment',
'allowedLibraries',
'allowedLibraryCollections',
).extend({
'startingLevel': {
type: SimpleSchema.Integer,
min: 0,
},
}),
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run() {
if (!this.userId) {
run({ name, gender, alignment, startingLevel,
allowedLibraries, allowedLibraryCollections }) {
const userId = this.userId
if (!userId) {
throw new Meteor.Error('Creatures.methods.insert.denied',
'You need to be logged in to insert a creature');
}
let tier = getUserTier(this.userId);
let currentCharacterCount = Creatures.find({
owner: this.userId,
}, {
fields: {_id: 1},
}).count();
if (
tier.characterSlots !== -1 &&
currentCharacterCount >= tier.characterSlots
){
throw new Meteor.Error('Creatures.methods.insert.denied',
`You are already at your limit of ${tier.characterSlots} characters`)
'You need to be logged in to insert a creature');
}
// Create the creature document
assertHasCharactersSlots(userId);
// Create the creature document
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
// Not batchInsert because we want the properties cleaned by the schema
let baseId;
let baseId, rulesetSlot;
defaultCharacterProperties(creatureId).forEach(prop => {
let id = CreatureProperties.insert(prop);
if (prop.name === 'Ruleset'){
baseId = id;
rulesetSlot = prop;
}
});
if (Meteor.isServer){
// Insert the 5e ruleset as the default base
insertPropertyFromLibraryNode.call({
nodeIds: ['iHbhfcg3AL5isSWbw'],
parentRef: {id: baseId, collection: 'creatureProperties'},
order: 0.5,
});
// If the user only has a single ruleset subscribed, use it by default
if (Meteor.isServer) {
insertDefaultRuleset(creatureId, baseId, userId, rulesetSlot);
}
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;

View File

@@ -3,11 +3,13 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creatures/creaturePermissions.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.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';
function removeRelatedDocuments(creatureId){
CreatureVariables.remove({_creatureId: creatureId});
CreatureProperties.remove({'ancestors.id': creatureId});
CreatureLogs.remove({creatureId});
Experiences.remove({creatureId});

View File

@@ -1,13 +1,17 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.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({
name: 'creature.methods.longRest',
name: 'creature.methods.rest',
validate: new SimpleSchema({
creatureId: {
type: String,
@@ -24,93 +28,142 @@ const restCreature = new ValidatedMethod({
timeInterval: 5000,
},
run({creatureId, restType}) {
let creature = Creatures.findOne(creatureId, {
fields: {
owner: 1,
writers: 1,
settings: 1,
}
}) ;
// Need edit permissions
// Check permissions
let creature = getCreature(creatureId);
assertEditPermission(creature, this.userId);
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
resetFilter = 'shortRest'
} else {
resetFilter = {$in: ['shortRest', 'longRest']}
// Add the variables to the creature document
const variables = getVariables(creatureId);
delete variables._id;
delete variables._creatureId;
creature.variables = variables;
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 = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: {$ne: true},
inactive: {$ne: true},
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
$set: {damage: 0}
}, {
selector: {type: 'attribute'},
multi: true,
// Create the log
const log = CreatureLogSchema.clean({
creatureId: creature._id,
creatureName: creature.name,
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
$set: {usesUsed: 0}
}, {
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);
const targets = [creature];
applyTriggers(triggers, restType, 'before', { creature, targets, scope, log });
doRestWork(creature, restType);
applyTriggers(triggers, restType, 'after', { creature, targets, scope, log });
insertCreatureLogWork({log, creature, method: this});
},
});
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;

View File

@@ -3,7 +3,6 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.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';
let Experiences = new Mongo.Collection('experiences');
@@ -48,20 +47,20 @@ let ExperienceSchema = new SimpleSchema({
Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': experience.xp
}});
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': experience.xp },
$set: { dirty: true },
});
}
if (experience.levels) {
Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': experience.levels
}});
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.milestoneLevels': experience.levels },
$set: { dirty: true },
});
}
experience.creatureId = creatureId;
let id = Experiences.insert(experience);
computeCreature(creatureId);
return id;
};
@@ -93,6 +92,7 @@ const insertExperience = new ValidatedMethod({
}
let insertedIds = [];
creatureIds.forEach(creatureId => {
assertEditPermission(creatureId, userId);
let id = insertExperienceForCreature({experience, creatureId, userId});
insertedIds.push(id);
});
@@ -124,18 +124,19 @@ const removeExperience = new ValidatedMethod({
let creatureId = experience.creatureId
assertEditPermission(creatureId, userId);
if (experience.xp){
Creatures.update(creatureId, {$inc: {
'denormalizedStats.xp': -experience.xp
}});
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': -experience.xp },
$set: { dirty: true },
});
}
if (experience.levels) {
Creatures.update(creatureId, {$inc: {
'denormalizedStats.milestoneLevels': -experience.levels
}});
Creatures.update(creatureId, {
$inc: { 'denormalizedStats.milestoneLevels': -experience.levels },
$set: { dirty: true },
});
}
experience.creatureId = creatureId;
let numRemoved = Experiences.remove(experienceId);
computeCreature(creatureId);
return numRemoved;
},
});
@@ -173,11 +174,11 @@ const recomputeExperiences = new ValidatedMethod({
});
Creatures.update(creatureId, {$set: {
'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels
'denormalizedStats.milestoneLevels': milestoneLevels,
dirty: true,
}});
computeCreature(creatureId);
},
});
export default Experiences;
export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences };
export { ExperienceSchema, insertExperience, insertExperienceForCreature, removeExperience, recomputeExperiences };

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
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 { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
@@ -154,7 +155,6 @@ const logRoll = new ValidatedMethod({
}).validator(),
run({roll, creatureId}){
const creature = Creatures.findOne(creatureId, {fields: {
variables: 1,
readers: 1,
writers: 1,
owner: 1,
@@ -163,6 +163,7 @@ const logRoll = new ValidatedMethod({
avatarPicture: 1,
}});
assertEditPermission(creature, this.userId);
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
let logContent = []
let parsedResult = undefined;
try {
@@ -175,7 +176,7 @@ const logRoll = new ValidatedMethod({
let {
result: compiled,
context
} = resolve('compile', parsedResult, creature.variables);
} = resolve('compile', parsedResult, variables);
const compiledString = toString(compiled);
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
value: roll
@@ -183,12 +184,12 @@ const logRoll = new ValidatedMethod({
logContent.push({
value: compiledString
});
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
let {result: rolled} = resolve('roll', compiled, variables, context);
let rolledString = toString(rolled);
if (rolledString !== compiledString) logContent.push({
value: rolledString
});
let {result} = resolve('reduce', rolled, creature.variables, context);
let {result} = resolve('reduce', rolled, variables, context);
let resultString = toString(result);
if (resultString !== rolledString) logContent.push({
value: resultString
@@ -210,4 +211,4 @@ const logRoll = new ValidatedMethod({
});
export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll};
export { CreatureLogSchema, insertCreatureLog, logRoll, PER_CREATURE_LOG_LIMIT};

View File

@@ -7,6 +7,7 @@ import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
import toggle from './applyPropertyByType/applyToggle.js';
import applyTriggers from '/imports/api/engine/actions/applyTriggers.js';
const applyPropertyByType = {
action,
@@ -21,7 +22,9 @@ const applyPropertyByType = {
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;
return applyPropertyByType[node.node.type]?.(node, opts, ...rest);
applyPropertyByType[node.node.type]?.(node, opts, ...rest);
applyTriggers(node, opts, 'after');
}

View File

@@ -11,11 +11,11 @@ export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
// Log the name and description
// Log the name and summary
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
@@ -33,7 +33,7 @@ export default function applyAction(node, {creature, targets, scope, log}){
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
applyChildren(node, {creature, targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({attack, scope, log});
@@ -65,6 +65,13 @@ function applyAttackWithoutTarget({attack, scope, log}){
} else if(scope['$attackAdvantage'] === -1){
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
}
log.content.push({
name,
value: `${resultPrefix}\n**${result}**`,
@@ -106,10 +113,10 @@ function applyAttackToTarget({attack, target, scope, log}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
} else {
scope['$attackMiss'] = true;
scope['$attackHit'] = {value: true};
}
} else {
log.content.push({
@@ -127,7 +134,7 @@ function applyAttackToTarget({attack, target, scope, log}){
function rollAttack(attack, scope){
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (attack.advantage === 1 || scope['$attackAdvantage']){
if (scope['$attackAdvantage'] === 1){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -136,7 +143,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (attack.advantage === -1 || scope['$attackDisadvantage']){
} else if (scope['$attackAdvantage'] === -1){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -161,15 +168,10 @@ function applyCrits(value, scope){
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
scope['$criticalMiss'] = {value: true};
}
}
return {criticalHit, criticalMiss};

View File

@@ -16,17 +16,47 @@ export default function applyBranch(node, {
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) applyChildren();
break;
case 'index':
if (node.children.length){
recalculateCalculation(prop.condition, scope, log);
if (!isFinite(prop.condition?.value)) {
log.content.push({
name: 'Branch Error',
value: 'Index did not resolve into a valid number'
});
break;
}
let index = Math.floor(prop.condition?.value);
if (index < 1) index = 1;
if (index > node.children.length) index = node.children.length;
applyProperty(node.children[index - 1], {
creature, targets, scope, log
});
}
break;
case 'hit':
if (scope['$attackHit']?.value) applyChildren();
if (scope['$attackHit']?.value){
if (!targets.length) log.content.push({value: '**On hit**'});
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value) applyChildren();
if (scope['$attackMiss']?.value){
if (!targets.length) log.content.push({value: '**On miss**'});
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value) applyChildren();
if (scope['$saveFailed']?.value){
if (!targets.length) log.content.push({value: '**On failed save**'});
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value) applyChildren();
if (scope['$saveSucceeded']?.value){
if (!targets.length) log.content.push({value: '**On save**',});
applyChildren();
}
break;
case 'random':
if (node.children.length){

View File

@@ -10,6 +10,7 @@ import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve.js';
import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function applyBuff(node, {creature, targets, scope, log}){
@@ -32,7 +33,30 @@ export default function applyBuff(node, {creature, targets, scope, log}){
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
// Apply the buff
copyNodeListToTarget(propList, target, oldParent);
//Log the buff
if (prop.name || prop.description?.value){
if (target._id === creature._id){
// Targeting self
log.content.push({
name: prop.name,
value: prop.description?.value,
});
} else {
// Targeting other
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name: prop.name,
value: prop.description?.value,
}],
}
});
}
}
});
// Don't apply the children of the buff, they get copied to the target instead

View File

@@ -1,3 +1,4 @@
import { some, intersection, difference } from 'lodash';
import applyProperty from '../applyProperty.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
@@ -17,10 +18,9 @@ export default function applyDamage(node, {
const prop = node.node;
// Skip if there is no parse node to work with
if (!prop.amount.parseNode) return;
if (!prop.amount?.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets;
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
@@ -35,15 +35,12 @@ export default function applyDamage(node, {
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
// logValue.push(toString(compiled));
// logErrors(context.errors, log);
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
@@ -61,14 +58,25 @@ export default function applyDamage(node, {
} else {
prop.amount.value = toString(reduced);
}
const damage = +reduced.value;
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
return applyChildren();
}
// Round the damage to a whole number
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
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
@@ -78,6 +86,14 @@ export default function applyDamage(node, {
// Iterate through all the targets
damageTargets.forEach(target => {
// Apply weaknesses/resistances/immunities
damage = applyDamageMultipliers({
target,
damage,
damageProp: prop,
logValue
});
// Deal the damage to the target
let damageDealt = dealDamageWork({
creature: target,
@@ -114,3 +130,51 @@ export default function applyDamage(node, {
});
return applyChildren();
}
function applyDamageMultipliers({target, damage, damageProp, logValue}){
const damageType = damageProp?.damageType;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
if (
multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp))
){
logValue.push(`Immune to ${damageTypeText}`);
return 0;
} else {
if (
multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp))
){
logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2);
}
if (
multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp))
){
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
}
return damage;
}
function multiplierAppliesTo(damageProp){
return multiplier => {
const hasRequiredTags = difference(
multiplier.includeTags, damageProp.tags
).length === 0;
const hasNoExcludedTags = intersection(
multiplier.excludeTags, damageProp.tags
).length === 0;
return hasRequiredTags && hasNoExcludedTags;
}
}

View File

@@ -1,22 +1,60 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js';
export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log);
const applyChildren = function(){
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
};
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
if (prop.roll?.calculation){
const logValue = [];
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, log);
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
// Store the result
if (reduced.parseType === 'constant'){
prop.roll.value = reduced.value;
} else if (reduced.parseType === 'error'){
prop.roll.value = null;
} else {
prop.roll.value = toString(reduced);
}
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
return applyChildren();
}
const value = reduced.value;
scope[prop.variableName] = value;
logValue.push(`**${value}**`);
if (!prop.silent){
log.content.push({
name: prop.name,
value: logValue.join('\n'),
inline: true,
});
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
inline: true,
});
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
return applyChildren();
}

View File

@@ -22,10 +22,21 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
}
log.content.push({
name: prop.name,
value: ' DC ' + dc,
value: `DC **${dc}**`,
inline: true,
});
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length){
scope['$saveFailed'] = {value: true};
scope['$saveSucceeded'] = {value: true};
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}
// Each target makes the saving throw
saveTargets.forEach(target => {
delete scope['$saveFailed'];
delete scope['$saveSucceeded'];
@@ -55,24 +66,24 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
@@ -84,8 +95,8 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
scope['$saveFailed'] = {value: true};
}
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**',
inline: true,
});
return applyChildren();

View 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;
}

View File

@@ -1,14 +1,16 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
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 { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.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 computeCreature from '/imports/api/engine/computeCreature.js';
import { groupBy, remove } from 'lodash';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -38,48 +40,46 @@ const doAction = new ValidatedMethod({
run({actionId, targetIds = [], scope}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
const creatureId = action.ancestors[0].id;
let creature = getCreature(action.ancestors[0].id);
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
let targets = [];
targetIds.forEach(targetId => {
let target = Creatures.findOne(targetId);
let target = getCreature(targetId);
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);
});
// Fetch all the action's ancestor creatureProperties
const ancestorIds = [];
action.ancestors.forEach(ref => {
if (ref.collection === 'creatureProperties') {
ancestorIds.push(ref.id);
}
});
const ancestors = getProperyAncestors(creatureId, action._id);
ancestors.sort((a, b) => a.order - b.order);
// Get cursor of ancestors
const ancestors = CreatureProperties.find({
_id: {$in: ancestorIds},
}, {
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},
});
const properties = getPropertyDecendants(creatureId, action._id);
properties.push(action);
properties.sort((a, b) => a.order - b.order);
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
// Recompute all involved creatures
computeCreature(creature._id);
targets.forEach(target => {
computeCreature(target._id);
Creatures.update({
_id: { $in: [creature._id, ...targetIds] }
}, {
$set: {dirty: true},
});
},
});
@@ -96,6 +96,14 @@ export function doActionWork({
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
if (!log) log = CreatureLogSchema.clean({
creatureId: creature._id,

View File

@@ -7,7 +7,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.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';
const doAction = new ValidatedMethod({
@@ -129,12 +128,12 @@ const doAction = new ValidatedMethod({
}
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope, log});
// Recompute all involved creatures
computeCreature(creature._id);
targets.forEach(target => {
computeCreature(target._id);
doActionWork({ creature, targets, properties, ancestors, method: this, methodScope: scope, log });
Creatures.update({
_id: { $in: [creature._id, ...targetIds] }
}, {
$set: { dirty: true },
});
},
});

View File

@@ -5,7 +5,6 @@ import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/ge
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.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 numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
@@ -32,9 +31,6 @@ const doCheck = new ValidatedMethod({
// Do the check
doCheckWork({creature, prop, method: this, methodScope: scope});
// Recompute all involved creatures
computeCreature(creature._id);
},
});

View File

@@ -1,8 +1,9 @@
import { EJSON } from 'meteor/ejson';
import createGraph from 'ngraph.graph';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
export default class CreatureComputation {
constructor(properties){
constructor(properties, creature, variables){
// Set up fields
this.originalPropsById = {};
this.propsById = {};
@@ -11,6 +12,8 @@ export default class CreatureComputation {
this.props = properties;
this.dependencyGraph = createGraph();
this.errors = [];
this.creature = creature;
this.variables = variables;
// Store properties for easy access later
properties.forEach(prop => {
@@ -21,15 +24,14 @@ export default class CreatureComputation {
this.propsById[prop._id] = prop;
// Store sets of ids in each tag
if (prop.tags){
prop.tags.forEach(tag => {
if (this.propsWithTag[tag]){
this.propsWithTag[tag].push(prop._id);
} else {
this.propsWithTag[tag] = [prop._id];
}
});
}
getEffectivePropTags(prop).forEach(tag => {
if (!tag) return;
if (this.propsWithTag[tag]) {
this.propsWithTag[tag].push(prop._id);
} else {
this.propsWithTag[tag] = [prop._id];
}
});
// Store the prop in the dependency graph
this.dependencyGraph.addNode(prop._id, prop);

View File

@@ -1,4 +1,4 @@
import { get, intersection, difference } from 'lodash';
import { get, intersection, difference, union } from 'lodash';
const linkDependenciesByType = {
action: linkAction,
@@ -128,10 +128,21 @@ function linkEffects(dependencyGraph, prop, computation){
if (prop.targetByTags){
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation){
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
if (
(targetProp.type === 'attribute' || targetProp.type === 'skill')
&& targetProp.variableName
&& !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 {
@@ -144,16 +155,18 @@ function linkEffects(dependencyGraph, prop, computation){
// Returns an array of IDs of the properties the effect targets
function getEffectTagTargets(effect, computation){
const targets = getTargetListFromTags(effect.targetTags, computation);
const notIds = [];
let targets = getTargetListFromTags(effect.targetTags, computation);
let notIds = [];
if (effect.extraTags){
effect.extraTags.forEach(ex => {
if (ex.operation === 'OR'){
targets.push(...getTargetListFromTags(ex.tags, computation));
if (ex.operation === 'OR') {
targets = union(targets, getTargetListFromTags(ex.tags, computation));
} else if (ex.operation === 'NOT'){
ex.tags.forEach(tag => {
const idList = computation.propsWithTag[tag];
if (idList) notIds.push(...computation.propsWithTag[tag])
if (idList) {
notIds = union(notIds, computation.propsWithTag[tag]);
}
});
}
});
@@ -218,7 +231,7 @@ function linkDamageMultiplier(dependencyGraph, prop){
prop.damageTypes.forEach(damageType => {
// Remove all non-letter characters from the damage name
const damageName = damageType.replace(/[^a-z]/gi, '')
dependencyGraph.addLink(`${damageName}Multiplier`, prop._id, prop.type);
dependencyGraph.addLink(damageName, prop._id, prop.type);
});
}
@@ -242,7 +255,7 @@ function linkSkill(dependencyGraph, prop){
}
// Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
// Depends on base value
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
}

View File

@@ -1,8 +1,7 @@
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties,
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
import { DenormalisedOnlyCreaturePropertySchema as denormSchema }
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 computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
import linkInventory from './buildComputation/linkInventory.js';
@@ -32,29 +31,15 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
export default function buildCreatureComputation(creatureId){
const creature = getCreature(creatureId);
const variables = getVariables(creatureId);
const properties = getProperties(creatureId);
const computation = buildComputationFromProps(properties, creature);
const computation = buildComputationFromProps(properties, creature, variables);
return computation;
}
function getProperties(creatureId){
return CreatureProperties.find({
'ancestors.id': creatureId,
'removed': {$ne: true},
}, {
sort: {order: 1}
}).fetch();
}
export function buildComputationFromProps(properties, creature, variables){
function getCreature(creatureId){
return Creatures.findOne(creatureId, {
denormalizedStats: 1,
});
}
export function buildComputationFromProps(properties, creature){
const computation = new CreatureComputation(properties);
const computation = new CreatureComputation(properties, creature, variables);
// Dependency graph where edge(a, b) means a depends on b
// The graph includes all dependencies even of inactive properties
// such that any properties changing without changing their dependencies
@@ -81,8 +66,10 @@ export function buildComputationFromProps(properties, creature){
// Process the properties one by one
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);
// Add a place to store all the computation details
@@ -114,5 +101,6 @@ export function buildComputationFromProps(properties, creature){
linkTypeDependencies(dependencyGraph, prop, computation);
linkCalculationDependencies(dependencyGraph, prop, computation);
});
return computation;
}

View File

@@ -54,6 +54,21 @@ function combineAggregations(computation, node){
function computeVariableProp(computation, node, prop){
if (!prop) return;
// Combine damage multipliers in all props so that they can't be overridden
if (node.data.immunity){
prop.immunity = node.data.immunity;
prop.immunities = node.data.immunities;
}
if (node.data.resistance){
prop.resistance = node.data.resistance;
prop.resistances = node.data.resistances;
}
if (node.data.vulnerability){
prop.vulnerability = node.data.vulnerability;
prop.vulnerabilities = node.data.vulnerabilities;
}
if (prop.type === 'attribute'){
computeVariableAsAttribute(computation, node, prop);
} else if (prop.type === 'skill'){
@@ -73,21 +88,16 @@ function combineMultiplierAggregator(node){
if (!aggregator) return;
// Combine
let value;
if (aggregator.immunityCount){
value = 0;
} else if (
aggregator.resistanceCount &&
!aggregator.vulnerabilityCount
){
value = 0.5;
} else if (
!aggregator.resistanceCount &&
aggregator.vulnerabilityCount
){
value = 2;
} else {
value = 1;
if (aggregator.immunities?.length){
node.data.immunity = true;
node.data.immunities = aggregator.immunities;
}
if (aggregator.resistances?.length){
node.data.resistance = true;
node.data.resistances = aggregator.resistances;
}
if (aggregator.vulnerabilities?.length){
node.data.vulnerability = true;
node.data.vulnerabilities = aggregator.vulnerabilities;
}
node.data.damageMultiplyValue = value;
}

View File

@@ -1,22 +1,36 @@
import { pick } from 'lodash';
export default function aggregateDamageMultipliers({node, linkedNode, link}){
if (link.data !== 'damageMultiplier') return;
const multiplierValue = linkedNode.data.value;
if (multiplierValue === undefined) return;
// Store an aggregator, its presence indicates damage multipliers target this
// variable
if (!node.data.multiplierAggregator) node.data.multiplierAggregator = {
immunityCount: 0,
resistanceCount: 0,
vulnerabilityCount: 0,
immunities: [],
resistances: [],
vulnerabilities: [],
}
// Store a short reference to the aggregator
const aggregator = node.data.multiplierAggregator;
// Sum the counts of each type of multiplier
// Make a stripped down copy of the multiplier to store in the aggregator
const keysToStore = ['_id', 'name'];
if (linkedNode.data.excludeTags?.length){
keysToStore.push('excludeTags');
}
if (linkedNode.data.includeTags?.length){
keysToStore.push('includeTags');
}
const storedMultiplier = pick(linkedNode.data, keysToStore);
// Store the multiplier in the appropriate field
if (multiplierValue === 0){
aggregator.immunityCount += 1;
aggregator.immunities.push(storedMultiplier);
} else if (multiplierValue === 0.5){
aggregator.resistanceCount += 1;
aggregator.resistances.push(storedMultiplier);
} else if (multiplierValue === 2){
aggregator.vulnerabilityCount += 1;
aggregator.vulnerabilities.push(storedMultiplier);
}
}

View File

@@ -6,6 +6,21 @@ import getAggregatorResult from './getAggregatorResult.js';
*/
export default function computeImplicitVariable(node){
const prop = {};
// Combine damage multipliers
if (node.data.immunity){
prop.immunity = node.data.immunity;
prop.immunities = node.data.immunities;
}
if (node.data.resistance){
prop.resistance = node.data.resistance;
prop.resistances = node.data.resistances;
}
if (node.data.vulnerability){
prop.vulnerability = node.data.vulnerability;
prop.vulnerabilities = node.data.vulnerabilities;
}
const result = getAggregatorResult(node);
if (result !== undefined){
prop.value = result;

View File

@@ -1,7 +1,7 @@
import getAggregatorResult from './getAggregatorResult.js';
export default function computeVariableAsAttribute(computation, node, prop){
let result = getAggregatorResult(node, prop) || 0;
let result = getAggregatorResult(node) || 0;
prop.total = result;
prop.value = prop.total - (prop.damage || 0);

View File

@@ -29,8 +29,9 @@ export default function computeVariableAsSkill(computation, node, prop){
}
// Combine everything to get the final result
const statBase = node.data.baseValue;
const statBase = node.data.baseValue || 0;
const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator?.base || 0;
// If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){
@@ -41,7 +42,7 @@ export default function computeVariableAsSkill(computation, node, prop){
return;
}
// Combine aggregator
const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0;
const base = statBase > aggregatorBase ? statBase : aggregatorBase;
let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;
@@ -70,6 +71,8 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.fail = aggregator.fail;
// Rollbonus
prop.rollBonuses = aggregator.rollBonus;
// Store effects
prop.effects = node.data.effects;
}
function aggregateAbilityEffects({computation, skillNode, abilityNode}){

View File

@@ -1,15 +1,10 @@
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default function getAggregatorResult(node){
// Work out the base value as the greater of the deining stat value or
// the damage multiplier value
// Work out the base value as the greater of the deining stat value
// This baseValue comes from aggregating definitions
let statBase = node.data.baseValue;
const damageMultiplyValue = node.data.damageMultiplyValue;
if (statBase === undefined || damageMultiplyValue > statBase){
statBase = damageMultiplyValue;
}
// get a reference to the aggregator
const aggregator = node.data.effectAggregator;

View File

@@ -7,9 +7,11 @@ export default function(){
const computation = buildComputationFromProps(testProperties);
computeCreatureComputation(computation);
const scope = id => computation.scope[id];
assert.equal(scope('blugeoningMultiplier').value, 1);
assert.equal(scope('customDamageMultiplier').value, 0.5);
assert.equal(scope('slashingMultiplier').value, 0);
assert.isTrue(scope('blugeoning').vulnerability);
assert.isTrue(scope('customDamage').resistance);
assert.isNotTrue(scope('customDamage').immunity);
assert.isNotTrue(scope('customDamage').vulnerability);
assert.isTrue(scope('slashing').immunity);
}
var testProperties = [

View File

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

View File

@@ -2,7 +2,7 @@ export default function walkDown(tree, callback){
let stack = [...tree];
while(stack.length){
let node = stack.pop();
callback(node);
callback(node, stack);
stack.push(...node.children);
}
}

View File

@@ -21,6 +21,7 @@ export default function writeAlteredProperties(computation){
'deactivatedByAncestor',
'deactivatedByToggle',
'damage',
'dirty',
...schema.objectKeys(),
];
op = addChangedKeysToOp(op, keys, original, changed);
@@ -28,13 +29,14 @@ export default function writeAlteredProperties(computation){
bulkWriteOperations.push(op);
}
});
writePropertiesSequentially(bulkWriteOperations);
bulkWriteProperties(bulkWriteOperations);
//if (bulkWriteOperations.length) console.log(`Wrote ${bulkWriteOperations.length} props`);
}
function addChangedKeysToOp(op, keys, original, changed) {
// Loop through all keys that can be changed by computation
// 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 (!op) op = newOperation(original._id, changed.type);
let value = changed[key];
@@ -79,10 +81,10 @@ function addUnsetOp(op, key){
}
}
// We use this instead of bulkWriteProperties because it functions with latency
// compensation without needing to roll back changes, which causes multiple
// expensive redraws of the character sheet
function writePropertiesSequentially(bulkWriteOps){
// If we re-enable client-side sheet recalculation, this needs to be run on
// both client and server to preserve latency compensation. Bulkwrite breaks
// latency compensation and causes flickering
function writePropertiesSequentially(bulkWriteOps) {
bulkWriteOps.forEach(op => {
let updateOneOrMany = op.updateOne || op.updateMany;
CreatureProperties.update(updateOneOrMany.filter, updateOneOrMany.update, {
@@ -101,7 +103,7 @@ function writePropertiesSequentially(bulkWriteOps){
function bulkWriteProperties(bulkWriteOps){
if (!bulkWriteOps.length) return;
// bulkWrite is only available on the server
if (Meteor.isServer){
if (Meteor.isServer) {
CreatureProperties.rawCollection().bulkWrite(
bulkWriteOps,
{ordered : false},

View File

@@ -1,10 +1,61 @@
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import { EJSON } from 'meteor/ejson';
export default function writeScope(creatureId, computation) {
if (!creatureId) throw 'creatureId is required';
const scope = computation.scope;
let variables = computation.variables;
if (!variables) {
CreatureVariables.insert({ _creatureId: creatureId });
variables = {};
}
delete variables._id;
delete variables._creatureId;
let $set, $unset;
export default function writeScope(creatureId, scope){
// Remove large properties that aren't likely to be accessed
for (const key in scope){
// Remove large properties that aren't likely to be accessed
delete scope[key].parent;
delete scope[key].ancestors;
// Remove empty keys
for (const subKey in scope[key]) {
if (scope[key][subKey] === undefined) {
delete scope[key][subKey]
}
}
// Only update changed fields
if (!EJSON.equals(variables[key], scope[key])) {
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[key] = scope[key];
}
}
// 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 }});
}
Creatures.update(creatureId, {$set: {variables: scope}});
}

View File

@@ -6,24 +6,32 @@ import writeErrors from './computation/writeComputation/writeErrors.js';
export default function computeCreature(creatureId){
if (Meteor.isClient) return;
// console.log('compute ' + creatureId);
const computation = buildCreatureComputation(creatureId);
computeComputation(computation, creatureId);
}
function computeComputation(computation, creatureId) {
try {
computeCreatureComputation(computation);
writeAlteredProperties(computation);
writeScope(creatureId, computation.scope);
writeScope(creatureId, computation);
} catch (e){
const errorText = e.reason || e.message || e.toString();
computation.errors.push({
type: 'crash',
details: e.reason,
details: { error: errorText },
});
const logError = {
creatureId,
computeError: errorText,
};
if (e.stack) {
logError.location = e.stack.split('\n')[1];
}
console.error(logError);
throw e;
} finally {
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);
}

View 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];
}
}
}
}

View 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;

View File

@@ -47,6 +47,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug = Meteor.isProduction,
allowClientCode = false,
}){
@@ -54,7 +55,10 @@ if (Meteor.isServer && Meteor.settings.useS3) {
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload(fileRef){
onAfterUpload(fileRef) {
// Call the provided afterUpload hook first
onAfterUpload?.(fileRef);
// Start moving files to AWS:S3
// after fully received by the Meteor server
@@ -213,13 +217,11 @@ if (Meteor.isServer && Meteor.settings.useS3) {
return collection;
}
} else {
if (Meteor.isServer){
console.log('No S3 details specified, files will be stored in the local filesystem');
}
createS3FilesCollection = function({
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug = Meteor.isProduction,
allowClientCode = false,
}){
@@ -227,11 +229,12 @@ if (Meteor.isServer && Meteor.settings.useS3) {
collectionName,
storagePath,
onBeforeUpload,
onAfterUpload,
debug,
allowClientCode,
});
if (Meteor.isServer){
if (Meteor.isServer) {
// Use the normal file system to read files
collection.readJSONFile = async function(file){
const fileString = await fsp.readFile(file.path, 'utf8');

View File

@@ -24,6 +24,11 @@ let LibrarySchema = new SimpleSchema({
type: String,
max: STORAGE_LIMITS.name,
},
description: {
type: String,
optional: true,
max: STORAGE_LIMITS.summary,
},
});
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({
name: 'libraries.remove',
validate: new SimpleSchema({
@@ -102,4 +130,4 @@ export function removeLibaryWork(libraryId){
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, updateLibraryName, removeLibrary };
export { LibrarySchema, insertLibrary, updateLibraryName, updateLibraryDescription, removeLibrary };

View 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,
};

View 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);
}

View 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;
}

View File

@@ -8,7 +8,7 @@ import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.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({
name: 'organize.organizeDoc',
@@ -57,10 +57,11 @@ const organizeDoc = new ValidatedMethod({
let parentCreatures = getCreatureAncestors(parent);
if (!skipRecompute){
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
// Some Dependencies depend on ancestry, so a full recompute is needed
computeCreature(id);
// Mark the creatures for recompute
Creatures.update({
_id: { $in: creaturesToRecompute }
}, {
$set: { dirty: true },
});
}
},
@@ -85,9 +86,14 @@ const reorderDoc = new ValidatedMethod({
assertDocEditPermission(doc, this.userId);
safeUpdateDocOrder({docRef, order});
// Recompute the affected creatures
getCreatureAncestors(doc).forEach(id => {
computeCreature(id);
});
const ancestors = getCreatureAncestors(doc);
if (ancestors.length) {
Creatures.update({
_id: { $in: ancestors }
}, {
$set: { dirty: true },
});
}
},
});

View File

@@ -40,17 +40,22 @@ const restoreError = function(){
);
};
export function restore({_id, collection}){
export function restore({ _id, collection, extraUpdates}){
if (typeof collection === 'string') {
collection = getCollectionByName(collection);
}
const update = {
$unset: {
removed: 1,
removedAt: 1,
},
...extraUpdates
}
let numUpdated = collection.update({
_id,
removedWith: {$exists: false}
}, { $unset: {
removed: 1,
removedAt: 1,
}}, {
}, update , {
selector: {type: 'any'},
},);
if (numUpdated === 0) restoreError();

View File

@@ -5,9 +5,6 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
/*
* 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({
name: {

View File

@@ -47,6 +47,18 @@ let AttributeSchema = createPropertySchema({
spellSlotLevel: {
type: 'fieldToCompute',
optional: true,
},
// For type healthBar midColor, and lowColor can be set separately from the
// property's color, which is used as the undamaged color
'healthBarColorMid': {
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
'healthBarColorLow': {
type: String,
regEx: /^#([a-f0-9]{3}){1,2}\b$/i,
optional: true,
},
// The starting value, before effects
baseValue: {
@@ -133,6 +145,15 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
optional: 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()

View File

@@ -18,6 +18,8 @@ let BranchSchema = createPropertySchema({
'eachTarget',
// Pick one child at random
'random',
// Pick one child based on a given index
'index',
// if it has option children, asks to select one
// Otherwise presents its own text with yes/no
//'choice',

View File

@@ -29,7 +29,7 @@ let BuffSchema = createPropertySchema({
let ComputedOnlyBuffSchema = createPropertySchema({
description: {
type: 'inlineCalculationFieldToCompute',
type: 'computedOnlyInlineCalculationField',
optional: true,
max: STORAGE_LIMITS.description,
},

View File

@@ -1,34 +1,95 @@
import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.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
// lower levels are taken before higher levels
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
variableName: {
type: String,
optional: true,
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({
level: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
missingLevels: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'missingLevels.$': {
type: SimpleSchema.Integer,
},
}).extend(ComputedOnlySlotSchema);
// Computed fields
description: {
type: 'computedOnlyInlineCalculationField',
optional: true,
},
slotCondition: {
type: 'computedOnlyField',
optional: true,
},
// Denormalised fields
level: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
missingLevels: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'missingLevels.$': {
type: SimpleSchema.Integer,
},
});
const ComputedClassSchema = new SimpleSchema()
.extend(ClassSchema)

View File

@@ -1,5 +1,6 @@
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';
/*
* DamageMultipliers are multipliers that affect how much damage is taken from
@@ -20,6 +21,7 @@ let DamageMultiplierSchema = new SimpleSchema({
'damageTypes.$': {
type: String,
max: STORAGE_LIMITS.calculation,
regEx: VARIABLE_NAME_REGEX,
},
// The value of the damage multiplier
value: {

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
const DamageSchema = createPropertySchema({
// The roll that determines how much to damage the attribute
@@ -24,6 +25,7 @@ const DamageSchema = createPropertySchema({
type: String,
max: STORAGE_LIMITS.calculation,
defaultValue: 'slashing',
regEx: VARIABLE_NAME_REGEX,
},
});

View 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 };

View File

@@ -41,6 +41,11 @@ let RollSchema = createPropertySchema({
parseLevel: 'compile',
optional: true,
},
// Prevent the roll from showing up in the log
silent: {
type: Boolean,
optional: true,
},
});
let ComputedOnlyRollSchema = createPropertySchema({

View File

@@ -131,6 +131,15 @@ let ComputedOnlySkillSchema = createPropertySchema({
optional: 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()

View 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
};

View File

@@ -25,6 +25,7 @@ import { ComputedOnlySlotFillerSchema } from '/imports/api/properties/SlotFiller
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
import { ComputedOnlyTriggerSchema } from '/imports/api/properties/Triggers.js';
const propertySchemasIndex = {
action: ComputedOnlyActionSchema,
@@ -53,6 +54,7 @@ const propertySchemasIndex = {
spellList: ComputedOnlySpellListSchema,
spell: ComputedOnlySpellSchema,
toggle: ComputedOnlyToggleSchema,
trigger: ComputedOnlyTriggerSchema,
any: new SimpleSchema({}),
};

View File

@@ -25,6 +25,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedToggleSchema } from '/imports/api/properties/Toggles.js';
import { ComputedTriggerSchema } from '/imports/api/properties/Triggers.js';
const propertySchemasIndex = {
action: ComputedActionSchema,
@@ -51,6 +52,7 @@ const propertySchemasIndex = {
spellList: ComputedSpellListSchema,
spell: ComputedSpellSchema,
toggle: ComputedToggleSchema,
trigger: ComputedTriggerSchema,
container: ComputedContainerSchema,
item: ComputedItemSchema,
any: new SimpleSchema({}),

View File

@@ -23,6 +23,7 @@ import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
import { SpellSchema } from '/imports/api/properties/Spells.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 { ItemSchema } from '/imports/api/properties/Items.js';
@@ -51,6 +52,7 @@ const propertySchemasIndex = {
spellList: SpellListSchema,
spell: SpellSchema,
toggle: ToggleSchema,
trigger: TriggerSchema,
container: ContainerSchema,
item: ItemSchema,
any: new SimpleSchema({}),

View File

@@ -35,7 +35,7 @@ export function assertOwnership(doc, userId){
export function assertEditPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
let user = Meteor.users.findOne(userId, {
const user = Meteor.users.findOne(userId, {
fields: {
'services.patreon': 1,
'roles': 1,
@@ -83,6 +83,7 @@ export function assertViewPermission(doc, userId) {
assertdocExists(doc);
if (doc.public) return true;
assertIdValid(userId);
if (
doc.owner === userId ||
_.contains(doc.readers, userId) ||
@@ -90,6 +91,17 @@ export function assertViewPermission(doc, userId) {
){
return true;
} 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',
'You do not have permission to view this document');
}

View File

@@ -1,4 +1,9 @@
import SimpleSchema from 'simpl-schema';
import { set } from 'lodash';
set(Meteor.settings,
'packages.collection2.disableCollectionNamesInValidation',
true);
SimpleSchema.extendOptions([
'parseLevel',

View File

@@ -4,9 +4,11 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/methods/deleteMyAccount.js';
import '/imports/api/users/methods/addEmail.js';
import '/imports/api/users/methods/removeEmail.js';
import '/imports/api/users/methods/updateFileStorageUsed.js';
import { some } from 'lodash';
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({
username: {
@@ -69,10 +71,19 @@ const userSchema = new SimpleSchema({
subscribedLibraries: {
type: Array,
defaultValue: defaultLibraries,
max: 100,
maxCount: 100,
},
'subscribedLibraries.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedLibraryCollections: {
type: Array,
defaultValue: defaultLibraryCollections,
maxCount: 100,
},
'subscribedLibraryCollections.$': {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
subscribedCharacters: {
@@ -84,6 +95,10 @@ const userSchema = new SimpleSchema({
type: String,
regEx: SimpleSchema.RegEx.Id,
},
fileStorageUsed: {
type: Number,
optional: true,
},
profile: {
type: Object,
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({
name: 'users.findUserByUsernameOrEmail',
validate: new SimpleSchema({

View 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,
}
});
}
}

View File

@@ -9,18 +9,21 @@ const TIERS = Object.freeze([
minimumEntitledCents: 0,
invites: 0,
characterSlots: 5,
fileStorage: 50,
paidBenefits: false,
}, {
name: 'Dreamer',
minimumEntitledCents: 100,
invites: 0,
characterSlots: 5,
fileStorage: 50,
paidBenefits: false,
}, {
name: 'Wanderer',
minimumEntitledCents: 300,
invites: 0,
characterSlots: 5,
fileStorage: 50,
paidBenefits: false,
}, {
//cost per user $5
@@ -28,6 +31,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 500,
invites: 0,
characterSlots: 20,
fileStorage: 200,
paidBenefits: true,
}, {
//cost per user $3.33
@@ -35,6 +39,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 1000,
invites: 2,
characterSlots: 50,
fileStorage: 500,
paidBenefits: true,
}, {
//cost per user $3.333
@@ -42,6 +47,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 2000,
invites: 5,
characterSlots: 120,
fileStorage: 1000,
paidBenefits: true,
}, {
//cost per user $3.125
@@ -49,6 +55,7 @@ const TIERS = Object.freeze([
minimumEntitledCents: 5000,
invites: 15,
characterSlots: -1, // Unlimited characters
fileStorage: 2000,
paidBenefits: true,
},
]);
@@ -59,6 +66,7 @@ const GUEST_TIER = Object.freeze({
guest: true,
invites: 0,
characterSlots: 20,
fileStorage: 200,
paidBenefits: true,
});
@@ -68,6 +76,7 @@ const PATREON_DISABLED_TIER = Object.freeze({
name: 'Outlander',
invites: 0,
characterSlots: -1, // Can have infinitely many characters
fileStorage: 1000000, // 1TB file storage
paidBenefits: true,
});

View File

@@ -13,6 +13,7 @@ const DAMAGE_TYPES = Object.freeze([
'psychic',
'radiant',
'thunder',
'extra',
]);
export default DAMAGE_TYPES;

View File

@@ -6,6 +6,7 @@ if (Meteor.isServer){
const dbVersion = Migrations.getVersion();
if (
!Meteor.settings.public.maintenanceMode &&
dbVersion !== undefined &&
SCHEMA_VERSION !== dbVersion
){
Meteor.settings.public.maintenanceMode = {

View File

@@ -138,7 +138,7 @@ const PROPERTIES = Object.freeze({
slotFiller: {
icon: 'mdi-power-plug-outline',
name: 'Slot filler',
helpText: 'A slot filler allows for more advanced logic when it attemptst to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
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'],
},
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.',
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;

View File

@@ -31,6 +31,7 @@ const STORAGE_LIMITS = Object.freeze({
statsToTarget: 64,
tagCount: 64,
writersCount: 20,
libraryCollectionCount: 32,
});
export default STORAGE_LIMITS;

View File

@@ -1,2 +1,3 @@
import './migrateTo.js';
import './validateDatabase.js';
import './getVersion.js';

View File

@@ -16,8 +16,8 @@ const migrateTo = new ValidatedMethod({
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
numRequests: 1,
timeInterval: 10000,
},
run({version}) {
if (Meteor.isClient) return;

View File

@@ -0,0 +1,35 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
const validateDatabase = new ValidatedMethod({
name: 'validateDatabase',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 10000,
},
run() {
assertAdmin(this.userId);
// Very computationally expensive data diagnostics
// Only run in an offline instance you control
return;
if (Meteor.isClient) return;
Meteor.Collection.getAll().forEach(collection => {
if (!collection.instance._c2?._simpleSchemas) return;
collection.instance.find({}).forEach(doc => {
const schema = collection.instance.simpleSchema(doc);
let cleanDoc = schema.clean(doc);
try {
schema.validate(cleanDoc, {modifier: false});
} catch (e){
console.log(collection.name, doc._id, e.message || e.reason || e.toString());
}
});
});
},
});
export default validateDatabase;

View File

@@ -0,0 +1,39 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { get, set } from 'lodash';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { calculationUp } from '/imports/migrations/server/dbv1/dbv1.js';
export default function cleanAt1(archive){
archive.properties = archive.properties.map(prop => {
let cleanProp = prop;
try {
if (prop.type === 'attack') prop.type = 'action';
// Get the schema
const schema = CreatureProperties.simpleSchema(prop);
// Clean all the text fields with inline calcs
schema.inlineCalculationFields().forEach(key => {
applyFnToKey(prop, key, (prop, key) => {
let field = get(prop, key);
if (typeof field === 'string' || typeof field === 'number'){
field = calculationUp(field);
set(prop, key, {text: `${field}`});
}
});
});
schema.computedFields().forEach(key => {
applyFnToKey(prop, key, (prop, key) => {
let field = get(prop, key) || get(prop, key + 'Calculation');
if (typeof field === 'string' || typeof field === 'number'){
field = calculationUp(field);
set(prop, key, {calculation: `${field}`});
}
});
});
cleanProp = schema.clean(prop);
schema.validate(cleanProp);
} catch (e){
console.warn({propId: prop._id, error: e.message || e.reason || e.toString()});
}
return cleanProp;
});
}

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