Compare commits
164 Commits
2.0-beta.3
...
2.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6ed9ffb74 | ||
|
|
a84da7d8a5 | ||
|
|
249aebea0f | ||
|
|
11a527481e | ||
|
|
8d729216b5 | ||
|
|
1677e8c424 | ||
|
|
987aacbb67 | ||
|
|
2714d0b9d5 | ||
|
|
1d98c41168 | ||
|
|
e42ec4b862 | ||
|
|
59fc5ab851 | ||
|
|
5d14c392e8 | ||
|
|
c6ca8c1fa4 | ||
|
|
28307e26c3 | ||
|
|
6d42eb62f0 | ||
|
|
877c9ca099 | ||
|
|
9b652fc133 | ||
|
|
7d66c06107 | ||
|
|
21629138f0 | ||
|
|
59a488256b | ||
|
|
766519b4a3 | ||
|
|
e7f73d0e54 | ||
|
|
193d5eec50 | ||
|
|
9284c9ad76 | ||
|
|
f86152675f | ||
|
|
cbac5264cd | ||
|
|
34e3325464 | ||
|
|
79c9e67ce2 | ||
|
|
4c2aabf90d | ||
|
|
48331d3806 | ||
|
|
45f05d0d34 | ||
|
|
58629c92f4 | ||
|
|
719af548f0 | ||
|
|
f2a1861279 | ||
|
|
38c3b6ff1f | ||
|
|
23e848fe40 | ||
|
|
4d6cdf50bd | ||
|
|
1cf9f3b5fd | ||
|
|
8164b79667 | ||
|
|
360df79004 | ||
|
|
a8f163ff33 | ||
|
|
36b3b80850 | ||
|
|
1d22f4c054 | ||
|
|
99e4e8d6bb | ||
|
|
2bb3265356 | ||
|
|
263f2d8424 | ||
|
|
ee0e764294 | ||
|
|
13fc0c0b12 | ||
|
|
ecfeeaccd9 | ||
|
|
b324fb1f03 | ||
|
|
8d34cc1369 | ||
|
|
839c2488b2 | ||
|
|
fd79bc2bb3 | ||
|
|
1050442606 | ||
|
|
53ed271ea2 | ||
|
|
6ccbf204eb | ||
|
|
d44d4e0315 | ||
|
|
2b8f7e4927 | ||
|
|
65e7ce6dce | ||
|
|
24cc87d6f7 | ||
|
|
03578b2066 | ||
|
|
6ea882a053 | ||
|
|
bec65be170 | ||
|
|
0483a7effc | ||
|
|
4c5c537f29 | ||
|
|
a0c2822dac | ||
|
|
27a21aed59 | ||
|
|
1da2d319fb | ||
|
|
82879aaa60 | ||
|
|
be654d5d45 | ||
|
|
f88ffcf0c3 | ||
|
|
8b62bac83f | ||
|
|
e698b4b838 | ||
|
|
566d6a4fef | ||
|
|
6f7e742eb9 | ||
|
|
0c06f60b7e | ||
|
|
f8e9131bdd | ||
|
|
bf9639ae59 | ||
|
|
ee89a052bc | ||
|
|
59ef7527b7 | ||
|
|
b8a03245ea | ||
|
|
1a71c2cfa7 | ||
|
|
292388dead | ||
|
|
00272e7b55 | ||
|
|
f07f05ca2c | ||
|
|
9dd84a83d2 | ||
|
|
b2f89eceee | ||
|
|
b484a27637 | ||
|
|
da5143693f | ||
|
|
9cc4186171 | ||
|
|
9f59a6cf86 | ||
|
|
fdaa035bfb | ||
|
|
b31760af0c | ||
|
|
a8ffa2f786 | ||
|
|
9b1ec46064 | ||
|
|
11f373ddd8 | ||
|
|
e7e8f938ed | ||
|
|
28934baac9 | ||
|
|
385ac17812 | ||
|
|
a04935c5b4 | ||
|
|
ccc861b6fa | ||
|
|
6702f431d0 | ||
|
|
1b3efae81a | ||
|
|
7a35c66904 | ||
|
|
78cd8ffc8d | ||
|
|
23fa6fe634 | ||
|
|
caf50d1578 | ||
|
|
df7889edd9 | ||
|
|
ddc7f87a4a | ||
|
|
33fa22c187 | ||
|
|
2e3f0320f3 | ||
|
|
0b7c20e616 | ||
|
|
abb8890070 | ||
|
|
8dbcae1060 | ||
|
|
3a18bce7e6 | ||
|
|
3e97baaaaa | ||
|
|
ea32c54f57 | ||
|
|
6b724cf365 | ||
|
|
8b44c83741 | ||
|
|
2ca9ac5342 | ||
|
|
7609e916c6 | ||
|
|
f440e030cf | ||
|
|
13b6689ba4 | ||
|
|
b28bcbe079 | ||
|
|
ffa6353a3f | ||
|
|
33f60c2c6d | ||
|
|
dcc95486b3 | ||
|
|
88bc223daa | ||
|
|
cbc42f8500 | ||
|
|
cc24690a67 | ||
|
|
4d5cb3ed50 | ||
|
|
c3d9ee7589 | ||
|
|
d9f572504d | ||
|
|
cb80f3a6da | ||
|
|
e89d415e7e | ||
|
|
ac8f19bcfb | ||
|
|
788cbb182d | ||
|
|
c68667be9c | ||
|
|
fada07e048 | ||
|
|
12fc9b1be3 | ||
|
|
e7f718c785 | ||
|
|
9732db8d67 | ||
|
|
73ca6dc364 | ||
|
|
10242b596f | ||
|
|
782f2cdc73 | ||
|
|
a8ebf6a1de | ||
|
|
7dcd0aeff2 | ||
|
|
a19e7d0514 | ||
|
|
2442ae4fa0 | ||
|
|
545050cfa3 | ||
|
|
b876c2801d | ||
|
|
698c9c7bbf | ||
|
|
7544243640 | ||
|
|
4b4e3a8928 | ||
|
|
92a588bfcc | ||
|
|
43e956eb6a | ||
|
|
c4429f5dd7 | ||
|
|
4edfe1bcb9 | ||
|
|
473a9f0253 | ||
|
|
94cdca4f31 | ||
|
|
10d0a3f763 | ||
|
|
afe6c044cd | ||
|
|
e6c7d79d7d | ||
|
|
49fa9cc470 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@2.6.1
|
||||
METEOR@2.7.3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
accounts-base@2.2.1
|
||||
accounts-base@2.2.3
|
||||
accounts-google@1.4.0
|
||||
accounts-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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
// Archived creatures is an immutable collection of creatures that are no longer
|
||||
// in use and can be safely archived by the mongoDB hosting service.
|
||||
// It keeps the working datasets like creatureProperties much smaller
|
||||
// than they would otherwise be.
|
||||
let ArchivedCreatures = new Mongo.Collection('archivedCreatures');
|
||||
|
||||
// We use blackbox objects for everything:
|
||||
// - saves time checking every object against a schema
|
||||
// - doesn't accidentaly create indices defined in subschemas
|
||||
// - The objects we are archiving have already been checked against their
|
||||
// own schemas
|
||||
let ArchivedCreatureSchema = new SimpleSchema({
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
// The primary index on this collection
|
||||
index: 1,
|
||||
},
|
||||
archiveDate: {
|
||||
type: Date,
|
||||
// Indexed so the archiving system can archive documents when they
|
||||
// get to a certain age
|
||||
index: 1,
|
||||
},
|
||||
creature: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
properties: {
|
||||
type: Array,
|
||||
},
|
||||
'properties.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
experiences: {
|
||||
type: Array,
|
||||
},
|
||||
'experiences.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
logs: {
|
||||
type: Array,
|
||||
},
|
||||
'logs.$': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
ArchivedCreatures.attachSchema(ArchivedCreatureSchema);
|
||||
|
||||
import '/imports/api/creature/archive/methods/index.js';
|
||||
export default ArchivedCreatures;
|
||||
@@ -68,7 +68,7 @@ const archiveCreatureToFile = new ValidatedMethod({
|
||||
async run({creatureId}) {
|
||||
assertOwnership(creatureId, this.userId);
|
||||
if (Meteor.isServer){
|
||||
archiveCreature(creatureId);
|
||||
archiveCreature(creatureId, this.userId);
|
||||
} else {
|
||||
removeCreatureWork(creatureId);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// import '/imports/api/creature/archive/methods/archiveCreatures.js';
|
||||
import '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
||||
import '/imports/api/creature/archive/methods/restoreCreatures.js';
|
||||
import '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
||||
import '/imports/api/creature/archive/methods/removeArchiveCreature.js';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
import { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
|
||||
const removeArchiveCreature = new ValidatedMethod({
|
||||
name: 'ArchiveCreatureFiles.methods.removeArchiveCreature',
|
||||
validate: new SimpleSchema({
|
||||
'fileId': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
async run({ fileId }) {
|
||||
// fetch the file
|
||||
const file = ArchiveCreatureFiles.findOne({ _id: fileId }).get();
|
||||
if (!file) {
|
||||
throw new Meteor.Error('File not found',
|
||||
'The requested creature archive does not exist');
|
||||
}
|
||||
// Assert ownership
|
||||
const userId = file?.userId;
|
||||
if (!userId || userId !== this.userId) {
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'You can only restore creatures you own');
|
||||
}
|
||||
//Remove the archive once the restore succeeded
|
||||
ArchiveCreatureFiles.remove({ _id: fileId });
|
||||
// Update the user's file storage limits
|
||||
incrementFileStorageUsed(userId, -file.size);
|
||||
},
|
||||
});
|
||||
|
||||
export default removeArchiveCreature;
|
||||
@@ -8,12 +8,16 @@ import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
||||
import { 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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import { assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import Experiences from '/imports/api/creature/experience/Experiences.js';
|
||||
import ArchivedCreatures from '/imports/api/creature/archive/ArchivedCreatures.js';
|
||||
import { removeCreatureWork } from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
|
||||
export function restoreCreature(archiveId){
|
||||
// Get the archive
|
||||
const archivedCreature = ArchivedCreatures.findOne(archiveId);
|
||||
|
||||
// Insert the creature sub documents
|
||||
// They still have their original _id's
|
||||
Creatures.insert(archivedCreature.creature);
|
||||
try {
|
||||
// Add all the properties
|
||||
if (archivedCreature.properties && archivedCreature.properties.length){
|
||||
CreatureProperties.batchInsert(archivedCreature.properties);
|
||||
}
|
||||
if (archivedCreature.experiences && archivedCreature.experiences.length){
|
||||
Experiences.batchInsert(archivedCreature.experiences);
|
||||
}
|
||||
if (archivedCreature.logs && archivedCreature.logs.length){
|
||||
CreatureLogs.batchInsert(archivedCreature.logs);
|
||||
}
|
||||
// Remove the archived creature
|
||||
ArchivedCreatures.remove(archiveId);
|
||||
} catch (e) {
|
||||
// If the above fails, delete the inserted creature
|
||||
removeCreatureWork(archivedCreature.creature._id);
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Do not recompute. The creature was in a computed and ordered state when
|
||||
// we archived it, just restore everything as-is
|
||||
|
||||
return archivedCreature.creature._id;
|
||||
}
|
||||
|
||||
const restoreCreatures = new ValidatedMethod({
|
||||
name: 'Creatures.methods.restoreCreatures',
|
||||
validate: new SimpleSchema({
|
||||
archiveIds: {
|
||||
type: Array,
|
||||
max: 10,
|
||||
},
|
||||
'archiveIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 1,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({archiveIds}) {
|
||||
for (let id of archiveIds){
|
||||
let archivedCreature = ArchivedCreatures.findOne(id, {
|
||||
fields: {owner: 1}
|
||||
});
|
||||
assertOwnership(archivedCreature, this.userId)
|
||||
}
|
||||
let creatureIds = [];
|
||||
for (let id of archiveIds){
|
||||
let creatureId = restoreCreature(id);
|
||||
creatureIds.push(creatureId);
|
||||
}
|
||||
return creatureIds;
|
||||
},
|
||||
});
|
||||
|
||||
export default restoreCreatures;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { slice } from 'lodash';
|
||||
import PER_CREATURE_LOG_LIMIT from '/imports/api/creature/log/CreatureLogs.js';
|
||||
|
||||
export default function verifyArchiveSafety({ meta, creature, properties, experiences, logs }){
|
||||
const creatureId = creature._id;
|
||||
|
||||
// Check lengths of arrays
|
||||
if (logs.length > PER_CREATURE_LOG_LIMIT) {
|
||||
logs = slice(logs, 0, PER_CREATURE_LOG_LIMIT);
|
||||
}
|
||||
|
||||
// Check that everything belongs to the right creature
|
||||
logs.forEach(log => {
|
||||
if (log.creatureId !== creatureId) {
|
||||
throw new Meteor.Error('Malicious log', 'Log contains an entry for the wrong creature');
|
||||
}
|
||||
});
|
||||
experiences.forEach(experience => {
|
||||
if (experience.creatureId !== creatureId) {
|
||||
throw new Meteor.Error('Malicious experience', 'Experiences contains an entry for the wrong creature');
|
||||
}
|
||||
});
|
||||
properties.forEach(prop => {
|
||||
if (prop.ancestors[0].id !== creatureId) {
|
||||
throw new Meteor.Error('Malicious prop', 'Properties contains an entry for the wrong creature');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -82,6 +82,13 @@ const DenormalisedOnlyCreaturePropertySchema = new SimpleSchema({
|
||||
index: 1,
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
|
||||
const damagePropertiesByName = new ValidatedMethod({
|
||||
name: 'CreatureProperties.damagePropertiesByName',
|
||||
validate: new SimpleSchema({
|
||||
creatureId: SimpleSchema.RegEx.Id,
|
||||
variableName: {
|
||||
type: String,
|
||||
},
|
||||
operation: {
|
||||
type: String,
|
||||
allowedValues: ['set', 'increment']
|
||||
},
|
||||
value: Number,
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, variableName, operation, value}) {
|
||||
// Check permissions
|
||||
let creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
variables: 1,
|
||||
owner: 1,
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
},
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
variableName,
|
||||
removed: {$ne: false},
|
||||
inactive: {$ne: true},
|
||||
}).forEach(property => {
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('damage')) return;
|
||||
// Damage the property
|
||||
damagePropertyWork({property, operation, value});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default damagePropertiesByName;
|
||||
@@ -2,9 +2,9 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { computeCreatureDependencyGroup } from '/imports/api/engine/computeCreature.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const damageProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.damage',
|
||||
@@ -21,60 +21,119 @@ const damageProperty = new ValidatedMethod({
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, operation, value}) {
|
||||
// Check permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
if (!property) throw new Meteor.Error(
|
||||
run({ _id, operation, value }) {
|
||||
|
||||
// Get action context
|
||||
let prop = CreatureProperties.findOne(_id);
|
||||
if (!prop) throw new Meteor.Error(
|
||||
'Damage property failed', 'Property doesn\'t exist'
|
||||
);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
if (!schema.allowsKey('damage')){
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${property.type}" can't be damaged`
|
||||
`Property of type "${prop.type}" can't be damaged`
|
||||
);
|
||||
}
|
||||
let result = damagePropertyWork({property, operation, value});
|
||||
// Dependencies can't be changed through damage, only recompute deps
|
||||
computeCreatureDependencyGroup(property);
|
||||
}
|
||||
|
||||
// Replace the prop by its actionContext counterpart if possible
|
||||
if (prop.variableName) {
|
||||
const actionContextProp = actionContext.scope[prop.variableName];
|
||||
if (actionContextProp?._id === prop._id) {
|
||||
prop = actionContextProp;
|
||||
}
|
||||
}
|
||||
|
||||
const result = damagePropertyWork({ prop, operation, value, actionContext });
|
||||
|
||||
// Insert the log
|
||||
actionContext.writeLog();
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
export function damagePropertyWork({property, operation, value}){
|
||||
let damage, newValue;
|
||||
export function damagePropertyWork({ prop, operation, value, actionContext }) {
|
||||
|
||||
// Save the value to the scope before applying the before triggers
|
||||
if (operation === 'increment') {
|
||||
if (value >= 0) {
|
||||
actionContext.scope['$damage'] = value;
|
||||
} else {
|
||||
actionContext.scope['$healing'] = -value;
|
||||
}
|
||||
} else {
|
||||
actionContext.scope['$set'] = value;
|
||||
}
|
||||
|
||||
applyTriggers(actionContext.triggers?.damageProperty?.before, prop, actionContext);
|
||||
|
||||
// fetch the value from the scope after the before triggers, in case they changed them
|
||||
if (operation === 'increment') {
|
||||
if (value >= 0) {
|
||||
value = actionContext.scope['$damage'];
|
||||
} else {
|
||||
value = -actionContext.scope['$healing'];
|
||||
}
|
||||
} else {
|
||||
value = actionContext.scope['$set'];
|
||||
}
|
||||
|
||||
let damage, newValue, increment;
|
||||
if (operation === 'set'){
|
||||
const total = property.total || 0;
|
||||
const total = prop.total || 0;
|
||||
// Set represents what we want the value to be after damage
|
||||
// So we need the actual damage to get to that value
|
||||
damage = total - value;
|
||||
// Damage can't exceed total value
|
||||
if (damage > total) damage = total;
|
||||
if (damage > total && !prop.ignoreLowerLimit) damage = total;
|
||||
// Damage must be positive
|
||||
if (damage < 0) damage = 0;
|
||||
newValue = property.total - damage;
|
||||
if (damage < 0 && !prop.ignoreUpperLimit) damage = 0;
|
||||
newValue = prop.total - damage;
|
||||
// Write the results
|
||||
CreatureProperties.update(prop._id, {
|
||||
$set: { damage, value: newValue, dirty: true }
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
// Also write it straight to the prop so that it is updated in the actionContext
|
||||
prop.damage = damage;
|
||||
prop.value = newValue;
|
||||
} else if (operation === 'increment'){
|
||||
let currentValue = property.value || 0;
|
||||
let currentDamage = property.damage || 0;
|
||||
let increment = value;
|
||||
let currentValue = prop.value || 0;
|
||||
let currentDamage = prop.damage || 0;
|
||||
increment = value;
|
||||
// Can't increase damage above the remaining value
|
||||
if (increment > currentValue) increment = currentValue;
|
||||
if (increment > currentValue && !prop.ignoreLowerLimit) increment = currentValue;
|
||||
// Can't decrease damage below zero
|
||||
if (-increment > currentDamage) increment = -currentDamage;
|
||||
if (-increment > currentDamage && !prop.ignoreUpperLimit) increment = -currentDamage;
|
||||
damage = currentDamage + increment;
|
||||
newValue = property.total - damage;
|
||||
newValue = prop.total - damage;
|
||||
// Write the results
|
||||
CreatureProperties.update(prop._id, {
|
||||
$inc: { damage: increment, value: -increment },
|
||||
$set: { dirty: true },
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
// Also write it straight to the prop so that it is updated in the actionContext
|
||||
prop.damage += increment;
|
||||
prop.value -= increment;
|
||||
}
|
||||
|
||||
// Write the results
|
||||
CreatureProperties.update(property._id, {
|
||||
$set: {damage, value: newValue}
|
||||
}, {
|
||||
selector: property
|
||||
});
|
||||
return damage;
|
||||
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
|
||||
|
||||
if (operation === 'set') {
|
||||
return damage;
|
||||
} else if (operation === 'increment') {
|
||||
return increment;
|
||||
}
|
||||
}
|
||||
|
||||
export default damageProperty;
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/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',
|
||||
validate: new SimpleSchema({
|
||||
creatureId: SimpleSchema.RegEx.Id,
|
||||
damageType: {
|
||||
type: String,
|
||||
},
|
||||
amount: Number,
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 20,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, damageType, amount}) {
|
||||
// permissions
|
||||
let creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
owner: 1,
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
},
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
|
||||
const totalDamage = dealDamageWork({creature, damageType, amount})
|
||||
computeCreature(creatureId);
|
||||
return totalDamage;
|
||||
},
|
||||
});
|
||||
|
||||
export function dealDamageWork({creature, damageType, amount}){
|
||||
// Get all the health bars and do damage to them
|
||||
let healthBars = CreatureProperties.find({
|
||||
'ancestors.id': creature._id,
|
||||
type: 'attribute',
|
||||
attributeType:'healthBar',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
sort: {order: -1},
|
||||
});
|
||||
//let multiplier = creature.damageMultipliers[damageType];
|
||||
//if (multiplier === undefined) multiplier = 1;
|
||||
//let totalDamage = Math.floor(amount * multiplier);
|
||||
const totalDamage = amount;
|
||||
let damageLeft = totalDamage;
|
||||
if (damageType === 'healing') damageLeft = -totalDamage;
|
||||
let propertyIds = [];
|
||||
healthBars.forEach(healthBar => {
|
||||
if (damageLeft === 0) return;
|
||||
let damageAdded = damagePropertyWork({
|
||||
property: healthBar,
|
||||
operation: 'increment',
|
||||
value: damageLeft,
|
||||
});
|
||||
damageLeft -= damageAdded;
|
||||
propertyIds.push(healthBar._id);
|
||||
});
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
export default dealDamage;
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -15,9 +15,28 @@ export default function getSlotFillFilter({slot, libraryIds}){
|
||||
slotFillerType: slot.slotType,
|
||||
}]
|
||||
});
|
||||
} else if (slot.type === 'class') {
|
||||
filter.$and.push({
|
||||
$or: [{
|
||||
type: 'classLevel',
|
||||
},{
|
||||
type: 'slotFiller',
|
||||
slotFillerType: 'classLevel',
|
||||
}]
|
||||
});
|
||||
if (slot.variableName) {
|
||||
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 +46,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;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/duplicateProperty.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/equipItem.js';
|
||||
import '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
21
app/imports/api/creature/creatures/CreatureVariables.js
Normal file
21
app/imports/api/creature/creatures/CreatureVariables.js
Normal file
@@ -0,0 +1,21 @@
|
||||
//set up the collection for creature variables
|
||||
let CreatureVariables = new Mongo.Collection('creatureVariables');
|
||||
|
||||
// Unique index on _creatureId
|
||||
if (Meteor.isServer) {
|
||||
CreatureVariables._ensureIndex({ _creatureId: 1 }, { unique: true })
|
||||
}
|
||||
|
||||
/** No schema because the structure isn't known until compute time
|
||||
* Expect documents to looke like:
|
||||
* {
|
||||
* _id: "nE8Ngd6K4L4jSxLY2",
|
||||
* _creatureId: "nE8Ngd6K4L4jSxLY2", // indexed reference to the creature
|
||||
* explicitlyDefinedVariableName: {...some creatureProperty}
|
||||
* implicitVariableName: {value: 10},
|
||||
* undefinedVariableName: {},
|
||||
* }
|
||||
* Where top level fields that don't start with `_` are variables on the sheet
|
||||
**/
|
||||
|
||||
export default CreatureVariables;
|
||||
@@ -38,6 +38,11 @@ let CreatureSettingsSchema = new SimpleSchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Hide calculation errors
|
||||
hideCalculationErrors: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// How much each hitDice resets on a long rest
|
||||
hitDiceResetMultiplier: {
|
||||
type: Number,
|
||||
@@ -80,6 +85,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 +126,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 +164,7 @@ let CreatureSchema = new SimpleSchema({
|
||||
'computeErrors.$.details' : {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
|
||||
// Tabletop
|
||||
@@ -159,8 +191,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';
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { getUserTier } from '/imports/api/users/patreon/tiers.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
export default function assertHasCharactersSlots(userId) {
|
||||
if (characterSlotsRemaining(userId) <= 0) {
|
||||
throw new Meteor.Error('characterSlotLimit',
|
||||
'No character slots left')
|
||||
}
|
||||
}
|
||||
|
||||
export function characterSlotsRemaining(userId) {
|
||||
let tier = getUserTier(userId);
|
||||
const currentCharacterCount = Creatures.find({
|
||||
owner: userId,
|
||||
}, {
|
||||
fields: { _id: 1 },
|
||||
}).count();
|
||||
if (tier.characterSlots === -1) {
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return tier.characterSlots - currentCharacterCount;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||
|
||||
const changeAllowedLibraries = new ValidatedMethod({
|
||||
name: 'creatures.changeAllowedLibraries',
|
||||
mixins: [RateLimiterMixin, simpleSchemaMixin],
|
||||
schema: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
allowedLibraries: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraries.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
allowedLibraryCollections: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
maxCount: 100,
|
||||
},
|
||||
'allowedLibraryCollections.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}),
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, allowedLibraries, allowedLibraryCollections}) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
let $set;
|
||||
if (allowedLibraries) {
|
||||
$set = { allowedLibraries }
|
||||
}
|
||||
if (allowedLibraryCollections) {
|
||||
if (!$set) $set = {};
|
||||
$set.allowedLibraryCollections = allowedLibraryCollections;
|
||||
}
|
||||
if (!$set) return;
|
||||
Creatures.update(_id, {$set});
|
||||
},
|
||||
});
|
||||
|
||||
const toggleAllUserLibraries = new ValidatedMethod({
|
||||
name: 'creatures.removeLibraryLimits',
|
||||
mixins: [RateLimiterMixin, simpleSchemaMixin],
|
||||
schema: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
},
|
||||
}),
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, value}) {
|
||||
if (value) {
|
||||
Creatures.update(_id, {
|
||||
$unset: {
|
||||
allowedLibraryCollections: 1,
|
||||
allowedLibraries: 1,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
Creatures.update(_id, {
|
||||
$set: {
|
||||
allowedLibraryCollections: [],
|
||||
allowedLibraries: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export {changeAllowedLibraries, toggleAllUserLibraries};
|
||||
@@ -2,3 +2,4 @@ import '/imports/api/creature/creatures/methods/insertCreature.js';
|
||||
import '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
import '/imports/api/creature/creatures/methods/restCreature.js';
|
||||
import '/imports/api/creature/creatures/methods/updateCreature.js';
|
||||
import '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
|
||||
|
||||
@@ -1,70 +1,104 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { 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;
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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 { union } from 'lodash';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import { applyTriggers } 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,
|
||||
@@ -23,94 +24,121 @@ const restCreature = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId, restType}) {
|
||||
let creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
owner: 1,
|
||||
writers: 1,
|
||||
settings: 1,
|
||||
}
|
||||
}) ;
|
||||
// Need edit permissions
|
||||
assertEditPermission(creature, this.userId);
|
||||
run({ creatureId, restType }) {
|
||||
// Get action context
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// 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': 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,
|
||||
// Join, sort, and apply before triggers
|
||||
const beforeTriggers = union(
|
||||
actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
|
||||
).sort((a, b) => a.order - b.order);
|
||||
applyTriggers(beforeTriggers, null, actionContext);
|
||||
|
||||
// Rest
|
||||
actionContext.addLog({
|
||||
name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
|
||||
});
|
||||
// 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);
|
||||
doRestWork(restType, actionContext);
|
||||
|
||||
// Join, sort, and apply after triggers
|
||||
const afterTriggers = union(
|
||||
actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after
|
||||
).sort((a, b) => a.order - b.order);
|
||||
applyTriggers(afterTriggers, null, actionContext);
|
||||
|
||||
// Insert log
|
||||
actionContext.writeLog();
|
||||
},
|
||||
});
|
||||
|
||||
function doRestWork(restType, actionContext) {
|
||||
const creatureId = actionContext.creature._id;
|
||||
// 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': creatureId,
|
||||
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': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
fields: {
|
||||
hitDiceSize: 1,
|
||||
damage: 1,
|
||||
total: 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.total || 0), 0);
|
||||
let resetMultiplier = actionContext.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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -17,6 +17,11 @@ let LogContentSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.summary,
|
||||
},
|
||||
// Inline with other content fields
|
||||
inline: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
context: {
|
||||
type: Object,
|
||||
optional: true,
|
||||
|
||||
78
app/imports/api/engine/actions/ActionContext.js
Normal file
78
app/imports/api/engine/actions/ActionContext.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import {
|
||||
getCreature, getVariables, getPropertiesOfType
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { groupBy, remove } from 'lodash';
|
||||
|
||||
export default class ActionContext{
|
||||
constructor(creatureId, targetIds = [], method) {
|
||||
// Get the creature
|
||||
this.creature = getCreature(creatureId)
|
||||
|
||||
if (!this.creature) {
|
||||
throw new Meteor.Error('No Creature', `No creature could be found with id: ${creatureId}`)
|
||||
}
|
||||
// Create a log
|
||||
this.log = CreatureLogSchema.clean({
|
||||
creatureId: creatureId,
|
||||
creatureName: this.creature.name,
|
||||
});
|
||||
|
||||
// Get the variables of the acting creature
|
||||
this.creature.variables = getVariables(creatureId);
|
||||
delete this.creature.variables._id;
|
||||
delete this.creature.variables._creatureId;
|
||||
// Alias as scope
|
||||
this.scope = this.creature.variables;
|
||||
|
||||
// Get the targets and their variables
|
||||
this.targets = [];
|
||||
targetIds.forEach(targetId => {
|
||||
let target;
|
||||
if (targetId === creatureId) {
|
||||
target = this.creature;
|
||||
} else {
|
||||
target = getCreature(targetId);
|
||||
target.variables = getVariables(targetId);
|
||||
delete target.variables._id;
|
||||
delete target.variables._creatureId;
|
||||
}
|
||||
this.targets.push(target);
|
||||
});
|
||||
|
||||
// Store a reference to the method for inserting the log
|
||||
this.method = method;
|
||||
|
||||
// Get triggers
|
||||
this.triggers = getPropertiesOfType(creatureId, 'trigger');
|
||||
// Remove deleted or inactive triggers
|
||||
remove(this.triggers, trigger => trigger.removed || trigger.inactive);
|
||||
// Sort triggers by order
|
||||
this.triggers.sort((a, b) => a.order - b.order);
|
||||
// Group the triggers into triggers.<event>.<timing> or
|
||||
// triggers.doActionProperty.<propertyType>.<timing>
|
||||
this.triggers = groupBy(this.triggers, 'event');
|
||||
for (let event in this.triggers) {
|
||||
if (event === 'doActionProperty') {
|
||||
this.triggers[event] = groupBy(this.triggers[event], 'actionPropertyType');
|
||||
for (let propertyType in this.triggers[event]) {
|
||||
this.triggers[event][propertyType] = groupBy(this.triggers[event][propertyType], 'timing');
|
||||
}
|
||||
} else {
|
||||
this.triggers[event] = groupBy(this.triggers[event], 'timing');
|
||||
}
|
||||
}
|
||||
}
|
||||
addLog(content) {
|
||||
if (content.name || content.value){
|
||||
this.log.content.push(content);
|
||||
}
|
||||
}
|
||||
writeLog() {
|
||||
insertCreatureLogWork({
|
||||
log: this.log,
|
||||
creature: this.creature,
|
||||
method: this.method,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import action from './applyPropertyByType/applyAction.js';
|
||||
import adjustment from './applyPropertyByType/applyAdjustment.js';
|
||||
import branch from './applyPropertyByType/applyBranch.js';
|
||||
import buff from './applyPropertyByType/applyBuff.js';
|
||||
import buffRemover from './applyPropertyByType/applyBuffRemover.js';
|
||||
import damage from './applyPropertyByType/applyDamage.js';
|
||||
import note from './applyPropertyByType/applyNote.js';
|
||||
import roll from './applyPropertyByType/applyRoll.js';
|
||||
@@ -13,6 +14,7 @@ const applyPropertyByType = {
|
||||
adjustment,
|
||||
branch,
|
||||
buff,
|
||||
buffRemover,
|
||||
damage,
|
||||
note,
|
||||
roll,
|
||||
@@ -21,7 +23,7 @@ const applyPropertyByType = {
|
||||
toggle,
|
||||
};
|
||||
|
||||
export default function applyProperty(node, opts, ...rest){
|
||||
opts.scope[`#${node.node.type}`] = node.node;
|
||||
return applyPropertyByType[node.node.type]?.(node, opts, ...rest);
|
||||
export default function applyProperty(node, actionContext, ...rest) {
|
||||
actionContext.scope[`#${node.node.type}`] = node.node;
|
||||
applyPropertyByType[node.node.type]?.(node, actionContext, ...rest);
|
||||
}
|
||||
|
||||
@@ -6,23 +6,24 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyAction(node, {creature, targets, scope, log}){
|
||||
export default function applyAction(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
if (prop.target === 'self') targets = [creature];
|
||||
if (prop.target === 'self') actionContext.targets = [actionContext.creature];
|
||||
const targets = actionContext.targets;
|
||||
|
||||
// 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 (content.name || content.value){
|
||||
log.content.push(content);
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (!prop.silent) actionContext.addLog(content);
|
||||
|
||||
// Spend the resources
|
||||
const failed = spendResources({prop, log, scope});
|
||||
const failed = spendResources(prop, actionContext);
|
||||
if (failed) return;
|
||||
|
||||
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||
@@ -31,28 +32,29 @@ export default function applyAction(node, {creature, targets, scope, log}){
|
||||
if (attack && attack.calculation){
|
||||
if (targets.length){
|
||||
targets.forEach(target => {
|
||||
applyAttackToTarget({attack, target, scope, log});
|
||||
applyAttackToTarget({attack, target, actionContext});
|
||||
// Apply the children, but only to the current target
|
||||
applyChildren(node, {targets: [target], scope, log});
|
||||
actionContext.targets = [target];
|
||||
applyChildren(node, actionContext);
|
||||
});
|
||||
} else {
|
||||
applyAttackWithoutTarget({attack, scope, log});
|
||||
applyChildren(node, {creature, targets, scope, log});
|
||||
applyAttackWithoutTarget({attack, actionContext});
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
} else {
|
||||
applyChildren(node, {creature, targets, scope, log});
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttackWithoutTarget({attack, scope, log}){
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
delete scope['$criticalMiss'];
|
||||
delete scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, scope, log);
|
||||
function applyAttackWithoutTarget({attack, actionContext}){
|
||||
delete actionContext.scope['$attackHit'];
|
||||
delete actionContext.scope['$attackMiss'];
|
||||
delete actionContext.scope['$criticalHit'];
|
||||
delete actionContext.scope['$criticalMiss'];
|
||||
delete actionContext.scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, actionContext);
|
||||
const scope = actionContext.scope;
|
||||
let {
|
||||
resultPrefix,
|
||||
result,
|
||||
@@ -65,13 +67,22 @@ function applyAttackWithoutTarget({attack, scope, log}){
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
log.content.push({
|
||||
if (!criticalMiss){
|
||||
scope['$attackHit'] = {value: true}
|
||||
}
|
||||
if (!criticalHit){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
}
|
||||
|
||||
actionContext.addLog({
|
||||
name,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
function applyAttackToTarget({attack, target, scope, log}){
|
||||
function applyAttackToTarget({attack, target, actionContext}){
|
||||
const scope = actionContext.scope;
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
delete scope['$criticalHit'];
|
||||
@@ -79,7 +90,7 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
delete scope['$attackDiceRoll'];
|
||||
delete scope['$attackRoll'];
|
||||
|
||||
recalculateCalculation(attack, scope, log);
|
||||
recalculateCalculation(attack, actionContext);
|
||||
|
||||
let {
|
||||
resultPrefix,
|
||||
@@ -100,23 +111,25 @@ function applyAttackToTarget({attack, target, scope, log}){
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
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({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value:'Target has no `armor`',
|
||||
});
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,27 +137,27 @@ 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;
|
||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||
} else {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
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;
|
||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||
} else {
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else {
|
||||
value = rollDice(1, 20)[0];
|
||||
resultPrefix = `1d20 [${value}] ${rollModifierText} = `
|
||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||
}
|
||||
scope['$attackRoll'] = {value};
|
||||
const result = value + attack.value;
|
||||
@@ -158,28 +171,24 @@ 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};
|
||||
}
|
||||
|
||||
function applyChildren(node, args){
|
||||
node.children.forEach(child => applyProperty(child, args));
|
||||
function applyChildren(node, actionContext) {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function spendResources({prop, log, scope}){
|
||||
function spendResources(prop, actionContext){
|
||||
// Check Uses
|
||||
if (prop.usesLeft < 0){
|
||||
log.content.push({
|
||||
if (prop.usesLeft <= 0){
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||
});
|
||||
@@ -187,7 +196,7 @@ function spendResources({prop, log, scope}){
|
||||
}
|
||||
// Resources
|
||||
if (prop.insufficientResources){
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||
});
|
||||
@@ -199,7 +208,7 @@ function spendResources({prop, log, scope}){
|
||||
let gainLog = [];
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, scope, log);
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
if (!itemConsumed.itemId){
|
||||
throw 'No ammo was selected for this prop';
|
||||
}
|
||||
@@ -230,7 +239,7 @@ function spendResources({prop, log, scope}){
|
||||
}
|
||||
});
|
||||
} catch (e){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
});
|
||||
@@ -248,26 +257,28 @@ function spendResources({prop, log, scope}){
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Uses left',
|
||||
value: prop.usesLeft - (prop.usesUsed || 0) - 1,
|
||||
value: prop.usesLeft - 1,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Damage stats
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
recalculateCalculation(attConsumed.quantity, scope, log);
|
||||
recalculateCalculation(attConsumed.quantity, actionContext);
|
||||
|
||||
if (!attConsumed.quantity?.value) return;
|
||||
let stat = scope[attConsumed.variableName];
|
||||
let stat = actionContext.scope[attConsumed.variableName];
|
||||
if (!stat){
|
||||
spendLog.push(stat.name + ': ' + ' not found');
|
||||
return;
|
||||
}
|
||||
damagePropertyWork({
|
||||
property: stat,
|
||||
prop: stat,
|
||||
operation: 'increment',
|
||||
value: attConsumed.quantity.value,
|
||||
actionContext,
|
||||
});
|
||||
if (attConsumed.quantity.value > 0){
|
||||
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
|
||||
@@ -277,12 +288,14 @@ function spendResources({prop, log, scope}){
|
||||
});
|
||||
|
||||
// Log all the spending
|
||||
if (gainLog.length) log.content.push({
|
||||
if (gainLog.length && !prop.silent) actionContext.addLog({
|
||||
name: 'Gained',
|
||||
value: gainLog.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
if (spendLog.length) log.content.push({
|
||||
if (spendLog.length && !prop.silent) actionContext.addLog({
|
||||
name: 'Spent',
|
||||
value: spendLog.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,57 +1,61 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyAdjustment(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyAdjustment(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
const damageTargets = prop.target === 'self' ? [creature] : targets;
|
||||
const damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
if (!prop.amount) {
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
// Evaluate the amount
|
||||
recalculateCalculation(prop.amount, scope, log);
|
||||
|
||||
recalculateCalculation(prop.amount, actionContext);
|
||||
|
||||
const value = +prop.amount.value;
|
||||
if (!isFinite(value)) {
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
if (damageTargets?.length) {
|
||||
damageTargets.forEach(target => {
|
||||
let stat = target.variables[prop.stat];
|
||||
if (!stat) {
|
||||
log({
|
||||
if (!stat?.type) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
|
||||
});
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
damagePropertyWork({
|
||||
property: stat,
|
||||
prop: stat,
|
||||
operation: prop.operation,
|
||||
value: value,
|
||||
value,
|
||||
actionContext,
|
||||
});
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Attribute damage',
|
||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||
` ${value}`,
|
||||
inline: true,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Attribute damage',
|
||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||
` ${value}`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
|
||||
return applyChildren(node, {creature, targets, scope, log});
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
function applyChildren(node, args){
|
||||
node.children.forEach(child => applyProperty(child, args));
|
||||
function applyChildren(node, actionContext){
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
@@ -1,47 +1,76 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyBranch(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyBranch(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
const scope = actionContext.scope;
|
||||
const targets = actionContext.targets;
|
||||
const prop = node.node;
|
||||
switch(prop.branchType){
|
||||
case 'if':
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) applyChildren();
|
||||
break;
|
||||
case 'index':
|
||||
if (node.children.length){
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
actionContext.addLog({
|
||||
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;
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index - 1], actionContext);
|
||||
}
|
||||
break;
|
||||
case 'hit':
|
||||
if (scope['$attackHit']?.value) applyChildren();
|
||||
if (scope['$attackHit']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'miss':
|
||||
if (scope['$attackMiss']?.value) applyChildren();
|
||||
if (scope['$attackMiss']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'failedSave':
|
||||
if (scope['$saveFailed']?.value) applyChildren();
|
||||
if (scope['$saveFailed']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'successfulSave':
|
||||
if (scope['$saveSucceeded']?.value) applyChildren();
|
||||
if (scope['$saveSucceeded']?.value){
|
||||
if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
|
||||
applyChildren();
|
||||
}
|
||||
break;
|
||||
case 'random':
|
||||
if (node.children.length){
|
||||
let index = rollDice(1, node.children.length)[0] - 1;
|
||||
applyProperty(node.children[index], {
|
||||
creature, targets, scope, log
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
applyProperty(node.children[index], actionContext);
|
||||
}
|
||||
break;
|
||||
case 'eachTarget':
|
||||
if (targets.length){
|
||||
if (targets.length) {
|
||||
targets.forEach(target => {
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets: [target], scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
actionContext.targets = [target]
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
});
|
||||
} else {
|
||||
applyChildren();
|
||||
|
||||
@@ -7,31 +7,67 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||
import { get } from 'lodash';
|
||||
import resolve, { map } from '/imports/parser/resolve.js';
|
||||
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';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
|
||||
|
||||
export default function applyBuff(node, {creature, targets, scope, log}){
|
||||
export default function applyBuff(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
let buffTargets = prop.target === 'self' ? [creature] : targets;
|
||||
let buffTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
// Then copy the decendants of the buff to the targets
|
||||
let propList = [prop];
|
||||
function addChildrenToPropList(children){
|
||||
function addChildrenToPropList(children, { skipCrystalize } = {}){
|
||||
children.forEach(child => {
|
||||
if (skipCrystalize) child.node._skipCrystalize = true;
|
||||
propList.push(child.node);
|
||||
addChildrenToPropList(child.children);
|
||||
// recursively add the child's children, but don't crystalize nested buffs
|
||||
addChildrenToPropList(child.children, {
|
||||
skipCrystalize: skipCrystalize || child.node.type === 'buff'
|
||||
});
|
||||
});
|
||||
}
|
||||
addChildrenToPropList(node.children);
|
||||
crystalizeVariables({propList, scope, log});
|
||||
if (!prop.skipCrystalization) {
|
||||
crystalizeVariables({propList, actionContext});
|
||||
}
|
||||
|
||||
let oldParent = {
|
||||
id: prop.parent.id,
|
||||
collection: prop.parent.collection,
|
||||
};
|
||||
buffTargets.forEach(target => {
|
||||
// Apply the buff
|
||||
copyNodeListToTarget(propList, target, oldParent);
|
||||
|
||||
//Log the buff
|
||||
if ((prop.name || prop.description?.value) && !prop.silent){
|
||||
if (target._id === actionContext.creature._id){
|
||||
// Targeting self
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
});
|
||||
} else {
|
||||
// Targeting other
|
||||
insertCreatureLog.call({
|
||||
log: {
|
||||
creatureId: target._id,
|
||||
content: [{
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
}],
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
|
||||
// Don't apply the children of the buff, they get copied to the target instead
|
||||
}
|
||||
@@ -57,13 +93,18 @@ function copyNodeListToTarget(propList, target, oldParent){
|
||||
* Replaces all variables with their resolved values
|
||||
* except variables of the form `$target.thing.total` become `thing.total`
|
||||
*/
|
||||
function crystalizeVariables({propList, scope, log}){
|
||||
function crystalizeVariables({propList, actionContext}){
|
||||
propList.forEach(prop => {
|
||||
if (prop._skipCrystalize) {
|
||||
delete prop._skipCrystalize;
|
||||
return;
|
||||
}
|
||||
// Iterate through all the calculations and crystalize them
|
||||
computedSchemas[prop.type].computedFields().forEach( calcKey => {
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const calcObj = get(prop, key);
|
||||
if (!calcObj?.parseNode) return;
|
||||
map(calcObj.parseNode, node => {
|
||||
calcObj.parseNode = map(calcObj.parseNode, node => {
|
||||
// Skip nodes that aren't symbols or accessors
|
||||
if (
|
||||
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
||||
@@ -73,21 +114,57 @@ function crystalizeVariables({propList, scope, log}){
|
||||
// strip $target
|
||||
if (node.parseType === 'accessor'){
|
||||
node.name = node.path.shift();
|
||||
if (!node.path.length){
|
||||
return symbol.create({name: node.name})
|
||||
}
|
||||
} else {
|
||||
// Can't strip symbols
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Variable `$target` should not be used without a property: $target.property'
|
||||
value: 'Variable `$target` should not be used without a property: $target.property',
|
||||
});
|
||||
}
|
||||
return node;
|
||||
} else {
|
||||
// Resolve all other variables
|
||||
const {result, context} = resolve('reduce', node, scope);
|
||||
logErrors(context.errors, log);
|
||||
const {result, context} = resolve('reduce', node, actionContext.scope);
|
||||
logErrors(context.errors, actionContext);
|
||||
return result;
|
||||
}
|
||||
});
|
||||
calcObj.calculation = toString(calcObj.parseNode);
|
||||
calcObj.hash = cyrb53(calcObj.calculation);
|
||||
});
|
||||
});
|
||||
// For each key in the schema
|
||||
computedSchemas[prop.type].inlineCalculationFields().forEach( calcKey => {
|
||||
// That ends in .inlineCalculations
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const inlineCalcObj = get(prop, key);
|
||||
if (!inlineCalcObj) return;
|
||||
|
||||
// If there is no text, skip
|
||||
if (!inlineCalcObj.text){
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace all the existing calculations
|
||||
let index = -1;
|
||||
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
|
||||
index += 1;
|
||||
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
|
||||
});
|
||||
|
||||
// Set the value to the uncomputed string
|
||||
inlineCalcObj.value = inlineCalcObj.text;
|
||||
|
||||
// Write a new hash
|
||||
const inlineCalcHash = cyrb53(inlineCalcObj.text);
|
||||
if (inlineCalcHash === inlineCalcObj.hash) {
|
||||
// Skip if nothing changed
|
||||
return;
|
||||
}
|
||||
inlineCalcObj.hash = inlineCalcHash;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { findLast, difference, intersection, filter } from 'lodash';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { getProperyAncestors, getPropertiesOfType } from '/imports/api/engine/loadCreatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
export default function applyBuffRemover(node, actionContext) {
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
|
||||
const prop = node.node;
|
||||
|
||||
// Log Name
|
||||
if (prop.name && !prop.silent){
|
||||
actionContext.addLog({ name: prop.name });
|
||||
}
|
||||
|
||||
// Remove buffs
|
||||
if (prop.targetParentBuff) {
|
||||
// Remove nearest ancestor buff
|
||||
const ancestors = getProperyAncestors(actionContext.creature._id, prop._id);
|
||||
const nearestBuff = findLast(ancestors, ancestor => ancestor.type === 'buff');
|
||||
if (!nearestBuff) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Buff remover does not have a parent buff to remove',
|
||||
});
|
||||
return;
|
||||
}
|
||||
removeBuff(nearestBuff, actionContext, prop);
|
||||
} else {
|
||||
// Get all the buffs targeted by tags
|
||||
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
|
||||
const targetedBuffs = filter(allBuffs, buff => {
|
||||
if (buff.inactive) return false;
|
||||
if (buffRemoverMatchTags(prop, buff)) return true;
|
||||
});
|
||||
// Remove the buffs
|
||||
if (prop.removeAll) {
|
||||
// Remove all matching buffs
|
||||
targetedBuffs.forEach(buff => {
|
||||
removeBuff(buff, actionContext, prop);
|
||||
});
|
||||
} else {
|
||||
// Sort in reverse order
|
||||
targetedBuffs.sort((a, b) => b.order - a.order);
|
||||
// Remove the one with the highest order
|
||||
const buff = targetedBuffs[0];
|
||||
if (buff) {
|
||||
removeBuff(buff, actionContext, prop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function removeBuff(buff, actionContext, prop) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Removed',
|
||||
value: `${buff.name || 'Buff'}`
|
||||
});
|
||||
softRemove({ _id: buff._id, collection: CreatureProperties });
|
||||
}
|
||||
|
||||
function buffRemoverMatchTags(buffRemover, prop) {
|
||||
let matched = false;
|
||||
const propTags = getEffectivePropTags(prop);
|
||||
// Check the target tags
|
||||
if (
|
||||
!buffRemover.targetTags?.length ||
|
||||
difference(buffRemover.targetTags, propTags).length === 0
|
||||
) {
|
||||
matched = true;
|
||||
}
|
||||
// Check the extra tags
|
||||
buffRemover.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;
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
import { some, intersection, difference, remove } 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';
|
||||
import resolve, { Context, toString } from '/imports/parser/resolve.js';
|
||||
import logErrors from './shared/logErrors.js';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import {
|
||||
getPropertiesOfType
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyDamage(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyDamage(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
const prop = node.node;
|
||||
const scope = actionContext.scope;
|
||||
|
||||
// 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;
|
||||
let damageTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
// Determine if the hit is critical
|
||||
let criticalHit = scope['$criticalHit']?.value &&
|
||||
prop.damageType !== 'healing' // Can't critically heal
|
||||
@@ -35,23 +38,20 @@ 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);
|
||||
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
|
||||
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
logValue.push(toString(rolled));
|
||||
logErrors(context.errors, log);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// 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);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// Store the result
|
||||
if (reduced.parseType === 'constant'){
|
||||
@@ -61,14 +61,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,15 +89,25 @@ export default function applyDamage(node, {
|
||||
// Iterate through all the targets
|
||||
damageTargets.forEach(target => {
|
||||
|
||||
// Apply weaknesses/resistances/immunities
|
||||
damage = applyDamageMultipliers({
|
||||
target,
|
||||
damage,
|
||||
damageProp: prop,
|
||||
logValue
|
||||
});
|
||||
|
||||
actionContext.target = [target];
|
||||
// Deal the damage to the target
|
||||
let damageDealt = dealDamageWork({
|
||||
creature: target,
|
||||
let damageDealt = dealDamage({
|
||||
target,
|
||||
damageType: prop.damageType,
|
||||
amount: damage,
|
||||
actionContext
|
||||
});
|
||||
|
||||
// Log the damage done
|
||||
if (target._id === creature._id){
|
||||
if (target._id === actionContext.creature._id){
|
||||
// Target is same as self, log damage as such
|
||||
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
||||
} else {
|
||||
@@ -107,9 +128,114 @@ export default function applyDamage(node, {
|
||||
// There are no targets, just log the result
|
||||
logValue.push(`**${damage}** ${suffix}`);
|
||||
}
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: logName,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function dealDamage({target, damageType, amount, actionContext}){
|
||||
// Get all the health bars and do damage to them
|
||||
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
||||
|
||||
// Keep only the healthbars that can take damage/healing
|
||||
remove(healthBars, (bar) =>
|
||||
bar.attributeType !== 'healthBar' ||
|
||||
bar.inactive ||
|
||||
bar.removed ||
|
||||
bar.overridden ||
|
||||
(amount >= 0 && bar.healthBarNoDamage) ||
|
||||
(amount < 0 && bar.healthBarNoHealing)
|
||||
);
|
||||
|
||||
// Sort healthbars by damage/healing order or tree order as a fallback
|
||||
healthBars.sort((a, b) => {
|
||||
let diff;
|
||||
if (amount >= 0) {
|
||||
diff = a.healthBarDamageOrder - b.healthBarDamageOrder;
|
||||
} else {
|
||||
diff = a.healthBarHealingOrder - b.healthBarHealingOrder;
|
||||
}
|
||||
if (Number.isFinite(diff)) {
|
||||
return diff;
|
||||
} else {
|
||||
return a.order - b.order;
|
||||
}
|
||||
});
|
||||
|
||||
// Deal the damage to each healthbar in order until all damage is done
|
||||
const totalDamage = amount;
|
||||
let damageLeft = totalDamage;
|
||||
if (damageType === 'healing') damageLeft = -totalDamage;
|
||||
healthBars.forEach(healthBar => {
|
||||
if (damageLeft === 0) return;
|
||||
// Replace the healthbar by the one in the action context if we can
|
||||
// The damagePropertyWork function bashes the prop with the damage
|
||||
// So we can use the new value in later action properties
|
||||
if (healthBar.variableName) {
|
||||
const targetHealthBar = target.variables[healthBar.variableName];
|
||||
if (targetHealthBar?._id === healthBar._id) {
|
||||
healthBar = targetHealthBar;
|
||||
}
|
||||
}
|
||||
// Do the damage
|
||||
let damageAdded = damagePropertyWork({
|
||||
prop: healthBar,
|
||||
operation: 'increment',
|
||||
value: damageLeft,
|
||||
actionContext
|
||||
});
|
||||
damageLeft -= damageAdded;
|
||||
});
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyNote(node, {creature, targets, scope, log}){
|
||||
export default function applyNote(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
// Log Name, summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
recalculateInlineCalculations(prop.summary, scope, log);
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
if (content.name || content.value){
|
||||
log.content.push(content);
|
||||
actionContext.addLog(content);
|
||||
}
|
||||
// Log description
|
||||
if (prop.description?.text){
|
||||
recalculateInlineCalculations(prop.description, scope, log);
|
||||
log.content.push({value: prop.description.value});
|
||||
recalculateInlineCalculations(prop.description, actionContext);
|
||||
actionContext.addLog({value: prop.description.value});
|
||||
}
|
||||
// Apply triggers
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
// Apply children
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
@@ -1,21 +1,61 @@
|
||||
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';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyRoll(node, {creature, targets, scope, log}){
|
||||
export default function applyRoll(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
if (prop.roll?.calculation){
|
||||
recalculateCalculation(prop.roll, scope, log);
|
||||
const applyChildren = function(){
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
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, actionContext);
|
||||
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, actionContext.scope);
|
||||
if (rolled.parseType !== 'constant'){
|
||||
logValue.push(toString(rolled));
|
||||
}
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// 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, actionContext.scope, context);
|
||||
logErrors(context.errors, actionContext);
|
||||
|
||||
// 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;
|
||||
|
||||
actionContext.scope[prop.variableName] = value;
|
||||
logValue.push(`**${value}**`);
|
||||
|
||||
if (!prop.silent){
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: logValue.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
log.content.push({
|
||||
name: prop.name,
|
||||
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
|
||||
});
|
||||
}
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
@@ -2,45 +2,57 @@ import rollDice from '/imports/parser/rollDice.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
export default function applySavingThrow(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
let saveTargets = prop.target === 'self' ? [creature] : targets;
|
||||
let saveTargets = prop.target === 'self' ? [actionContext.creature] : actionContext.targets;
|
||||
|
||||
recalculateCalculation(prop.dc, scope, log);
|
||||
recalculateCalculation(prop.dc, actionContext);
|
||||
|
||||
const dc = (prop.dc?.value);
|
||||
if (!isFinite(dc)){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Saving throw requires a DC',
|
||||
});
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
log.content.push({
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: ' DC ' + dc,
|
||||
value: `DC **${dc}**`,
|
||||
inline: true,
|
||||
});
|
||||
const scope = actionContext.scope;
|
||||
|
||||
// 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 };
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
// Each target makes the saving throw
|
||||
saveTargets.forEach(target => {
|
||||
delete scope['$saveFailed'];
|
||||
delete scope['$saveSucceeded'];
|
||||
delete scope['$saveDiceRoll'];
|
||||
delete scope['$saveRoll'];
|
||||
|
||||
const applyChildren = function(){
|
||||
node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets: [target], scope, log
|
||||
}));
|
||||
const applyChildren = function () {
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
actionContext.targets = [target]
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
};
|
||||
|
||||
const save = target.variables[prop.stat];
|
||||
|
||||
if (!save){
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: 'Saving throw error',
|
||||
value: 'No saving throw found: ' + prop.stat,
|
||||
});
|
||||
@@ -54,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;
|
||||
@@ -82,9 +94,10 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||
} else {
|
||||
scope['$saveFailed'] = {value: true};
|
||||
}
|
||||
log.content.push({
|
||||
name: 'Save',
|
||||
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
value: resultPrefix + '\n**' + result + '**',
|
||||
inline: true,
|
||||
});
|
||||
return applyChildren();
|
||||
});
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
|
||||
export default function applyToggle(node, {
|
||||
creature, targets, scope, log
|
||||
}){
|
||||
export default function applyToggle(node, actionContext){
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
recalculateCalculation(prop.condition, scope, log);
|
||||
recalculateCalculation(prop.condition, actionContext);
|
||||
if (prop.condition?.value) {
|
||||
return node.children.forEach(child => applyProperty(child, {
|
||||
creature, targets, scope, log
|
||||
}));
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import operator from '/imports/parser/parseTree/operator.js';
|
||||
import { parse } from '/imports/parser/parser.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, log){
|
||||
export default function applyEffectsToCalculationParseNode(calcObj, actionContext){
|
||||
if (!calcObj.effects) return;
|
||||
calcObj.effects.forEach(effect => {
|
||||
if (effect.operation !== 'add') return;
|
||||
@@ -18,7 +18,7 @@ export default function applyEffectsToCalculationParseNode(calcObj, log){
|
||||
fn: 'add'
|
||||
});
|
||||
} catch (e){
|
||||
logErrors([e], log)
|
||||
logErrors([e], actionContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default function logErrors(errors, log){
|
||||
export default function logErrors(errors, actionContext){
|
||||
errors?.forEach(error => {
|
||||
if (error.type !== 'info'){
|
||||
log.content.push({name: 'Error', value: error.message});
|
||||
actionContext.addLog({name: 'Error', value: error.message});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import logErrors from './logErrors.js';
|
||||
|
||||
export default function recalculateCalculation(calc, scope, log, context){
|
||||
export default function recalculateCalculation(calc, actionContext, context){
|
||||
if (!calc?.parseNode) return;
|
||||
calc._parseLevel = 'reduce';
|
||||
applyEffectsToCalculationParseNode(calc, log);
|
||||
evaluateCalculation(calc, scope, context);
|
||||
logErrors(calc.errors, log);
|
||||
applyEffectsToCalculationParseNode(calc, actionContext.log);
|
||||
evaluateCalculation(calc, actionContext.scope, context);
|
||||
logErrors(calc.errors, actionContext.log);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import embedInlineCalculations from '/imports/api/engine/computation/utility/embedInlineCalculations.js';
|
||||
import recalculateCalculation from './recalculateCalculation.js'
|
||||
|
||||
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
|
||||
export default function recalculateInlineCalculations(inlineCalcObj, actionContext){
|
||||
// Skip if there are no calculations
|
||||
if (!inlineCalcObj?.calculations?.length) return;
|
||||
if (!inlineCalcObj?.inlineCalculations?.length) return;
|
||||
// Recalculate each calculation with the current scope
|
||||
inlineCalcObj.inlineCalculations.forEach(calc => {
|
||||
recalculateCalculation(calc, scope, log);
|
||||
recalculateCalculation(calc, actionContext);
|
||||
});
|
||||
// Embed the new calculated values
|
||||
embedInlineCalculations(inlineCalcObj);
|
||||
|
||||
111
app/imports/api/engine/actions/applyTriggers.js
Normal file
111
app/imports/api/engine/actions/applyTriggers.js
Normal file
@@ -0,0 +1,111 @@
|
||||
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 function applyNodeTriggers(node, timing, actionContext) {
|
||||
const prop = node.node;
|
||||
const type = prop.type;
|
||||
const triggers = actionContext.triggers?.doActionProperty?.[type]?.[timing];
|
||||
if (triggers) {
|
||||
triggers.forEach(trigger => {
|
||||
applyTrigger(trigger, prop, actionContext);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function applyTriggers(triggers = [], prop, actionContext) {
|
||||
// Apply the triggers
|
||||
triggers.forEach(trigger => {
|
||||
applyTrigger(trigger, prop, actionContext)
|
||||
});
|
||||
}
|
||||
|
||||
export function applyTrigger(trigger, prop, actionContext) {
|
||||
// If there is a prop we are applying the trigger from,
|
||||
// don't fire if the tags don't match
|
||||
if (prop && !triggerMatchTags(trigger, prop)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent trigger from firing if it's inactive
|
||||
if (trigger.inactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent triggers from firing if their condition is false
|
||||
if (trigger.condition?.parseNode) {
|
||||
recalculateCalculation(trigger.condition, actionContext);
|
||||
if (!trigger.condition.value) return;
|
||||
}
|
||||
|
||||
// Prevent triggers from firing themselves in a loop
|
||||
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.description,
|
||||
inline: false,
|
||||
}
|
||||
if (trigger.description?.text){
|
||||
recalculateInlineCalculations(trigger.description, actionContext);
|
||||
content.value = trigger.description.value;
|
||||
}
|
||||
if(!trigger.silent) actionContext.addLog(content);
|
||||
|
||||
// Get all the trigger's properties and apply them
|
||||
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
const propertyForest = nodeArrayToTree(properties);
|
||||
propertyForest.forEach(node => {
|
||||
applyProperty(node, actionContext);
|
||||
});
|
||||
|
||||
trigger.firing = false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
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 {
|
||||
getProperyAncestors, getPropertyDecendants
|
||||
} 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 ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doAction',
|
||||
@@ -35,51 +36,33 @@ const doAction = new ValidatedMethod({
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, targetIds = [], scope}) {
|
||||
run({ actionId, targetIds = [], scope }) {
|
||||
// Get action context
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
const creatureId = action.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
let creature = getRootCreatureAncestor(action);
|
||||
|
||||
assertEditPermission(creature, this.userId);
|
||||
|
||||
// Get all the targets and make sure we can edit them
|
||||
let targets = [];
|
||||
targetIds.forEach(targetId => {
|
||||
let target = Creatures.findOne(targetId);
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
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});
|
||||
doActionWork({properties, ancestors, actionContext, methodScope: scope});
|
||||
|
||||
// Recompute all involved creatures
|
||||
computeCreature(creature._id);
|
||||
targets.forEach(target => {
|
||||
computeCreature(target._id);
|
||||
Creatures.update({
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: {dirty: true},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -87,7 +70,7 @@ const doAction = new ValidatedMethod({
|
||||
export default doAction;
|
||||
|
||||
export function doActionWork({
|
||||
creature, targets, properties, ancestors, method, methodScope = {}
|
||||
properties, ancestors, actionContext, methodScope = {},
|
||||
}){
|
||||
// get the docs
|
||||
const ancestorScope = getAncestorScope(ancestors);
|
||||
@@ -96,28 +79,15 @@ export function doActionWork({
|
||||
throw new Meteor.Error(`The action has ${propertyForest.length} top level properties, expected 1`);
|
||||
}
|
||||
|
||||
// Create the log
|
||||
let log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
// Include the ancestry and method scope in the context scope
|
||||
Object.assign(actionContext.scope, ancestorScope, methodScope);
|
||||
|
||||
// Apply the top level property, it is responsible for applying its children
|
||||
// recursively
|
||||
const scope = {
|
||||
...creature.variables,
|
||||
...ancestorScope,
|
||||
...methodScope
|
||||
}
|
||||
applyProperty(propertyForest[0], {
|
||||
creature,
|
||||
targets,
|
||||
scope,
|
||||
log,
|
||||
});
|
||||
applyProperty(propertyForest[0], actionContext);
|
||||
|
||||
// Insert the log
|
||||
insertCreatureLogWork({log, creature, method});
|
||||
actionContext.writeLog();
|
||||
}
|
||||
|
||||
// Assumes ancestors are in tree order already
|
||||
|
||||
@@ -1,11 +1,53 @@
|
||||
import '/imports/api/simpleSchemaConfig.js';
|
||||
//import testTypes from './testTypes/index.js';
|
||||
import { doActionWork } from './doAction.js';
|
||||
import createAction from './tests/createAction.testFn.js';
|
||||
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
function cleanProp(prop){
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
return schema.clean(prop);
|
||||
}
|
||||
|
||||
function cleanCreature(creature){
|
||||
let schema = Creatures.simpleSchema(creature);
|
||||
return schema.clean(creature);
|
||||
}
|
||||
|
||||
// Fake ActionContext to test actions with
|
||||
const creatureId = 'actionTestCreatureId';
|
||||
const creatureName = 'Action Test Creature';
|
||||
const testActionContext = {
|
||||
creature: cleanCreature({
|
||||
_id: creatureId,
|
||||
}),
|
||||
log: CreatureLogSchema.clean({
|
||||
creatureId: creatureId,
|
||||
creatureName: creatureName,
|
||||
}),
|
||||
scope: {},
|
||||
addLog(content) {
|
||||
if (content.name || content.value){
|
||||
this.log.content.push(content);
|
||||
}
|
||||
},
|
||||
writeLog: () => { },
|
||||
}
|
||||
|
||||
const action = cleanProp({
|
||||
type: 'action',
|
||||
});
|
||||
const actionAncestors = [];
|
||||
|
||||
describe('Do Action', function(){
|
||||
it('Does an empty action', function(){
|
||||
doActionWork(createAction({properties: [{type: 'action'}]}));
|
||||
doActionWork({
|
||||
properties: [action],
|
||||
ancestors: actionAncestors,
|
||||
actionContext: testActionContext,
|
||||
methodScope: {},
|
||||
});
|
||||
});
|
||||
//testTypes.forEach(test => it(test.text, test.fn));
|
||||
});
|
||||
|
||||
125
app/imports/api/engine/actions/doCastSpell.js
Normal file
125
app/imports/api/engine/actions/doCastSpell.js
Normal file
@@ -0,0 +1,125 @@
|
||||
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 {
|
||||
getProperyAncestors, getPropertyDecendants
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
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 ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCastSpell',
|
||||
validate: new SimpleSchema({
|
||||
spellId: SimpleSchema.RegEx.Id,
|
||||
slotId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
optional: true,
|
||||
},
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: 20,
|
||||
optional: true,
|
||||
},
|
||||
'targetIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
scope: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ spellId, slotId, targetIds = [], scope = {} }) {
|
||||
// Get action context
|
||||
let spell = CreatureProperties.findOne(spellId);
|
||||
const creatureId = spell.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, targetIds, this);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
actionContext.targets.forEach(target => {
|
||||
assertEditPermission(target, this.userId);
|
||||
});
|
||||
|
||||
const ancestors = getProperyAncestors(creatureId, spell._id);
|
||||
ancestors.sort((a, b) => a.order - b.order);
|
||||
|
||||
const properties = getPropertyDecendants(creatureId, spell._id);
|
||||
properties.push(spell);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
|
||||
// Spend the appropriate slot
|
||||
let slotLevel = spell.level || 0;
|
||||
let slot;
|
||||
|
||||
if (slotId && !spell.castWithoutSpellSlots){
|
||||
slot = CreatureProperties.findOne(slotId);
|
||||
if (!slot){
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot not found to cast spell');
|
||||
}
|
||||
if (!slot.value){
|
||||
throw new Meteor.Error('No slot',
|
||||
'Slot depleted');
|
||||
}
|
||||
if (slot.attributeType !== 'spellSlot'){
|
||||
throw new Meteor.Error('Not a slot',
|
||||
'The given property is not a valid spell slot');
|
||||
}
|
||||
if (!slot.spellSlotLevel?.value){
|
||||
throw new Meteor.Error('No slot level',
|
||||
'Slot does not have a spell slot level');
|
||||
}
|
||||
if (slot.spellSlotLevel.value < spell.level){
|
||||
throw new Meteor.Error('Slot too small',
|
||||
'Slot is not large enough to cast spell');
|
||||
}
|
||||
slotLevel = slot.spellSlotLevel.value;
|
||||
damagePropertyWork({
|
||||
prop: slot,
|
||||
operation: 'increment',
|
||||
value: 1,
|
||||
actionContext,
|
||||
});
|
||||
}
|
||||
|
||||
// Post the slot level spent to the log
|
||||
if (slot?.spellSlotLevel?.value){
|
||||
actionContext.addLog({
|
||||
name: `Casting using a level ${slotLevel} spell slot`
|
||||
});
|
||||
} else if (slotLevel) {
|
||||
actionContext.addLog({
|
||||
name: `Casting at level ${slotLevel}`
|
||||
});
|
||||
}
|
||||
|
||||
actionContext.scope['slotLevel'] = slotLevel;
|
||||
|
||||
// Do the action
|
||||
doActionWork({
|
||||
properties, ancestors, actionContext, methodScope: scope,
|
||||
});
|
||||
|
||||
// Force the characters involved to recalculate
|
||||
Creatures.update({
|
||||
_id: { $in: [creatureId, ...targetIds] }
|
||||
}, {
|
||||
$set: { dirty: true },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default doAction;
|
||||
@@ -1,13 +1,12 @@
|
||||
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 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';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
|
||||
const doCheck = new ValidatedMethod({
|
||||
name: 'creatureProperties.doCheck',
|
||||
@@ -25,37 +24,32 @@ const doCheck = new ValidatedMethod({
|
||||
},
|
||||
run({propId, scope}) {
|
||||
const prop = CreatureProperties.findOne(propId);
|
||||
const creature = getRootCreatureAncestor(prop);
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
Object.assign(actionContext.scope, scope);
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(creature, this.userId);
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
|
||||
// Do the check
|
||||
doCheckWork({creature, prop, method: this, methodScope: scope});
|
||||
|
||||
// Recompute all involved creatures
|
||||
computeCreature(creature._id);
|
||||
doCheckWork({prop, actionContext});
|
||||
},
|
||||
});
|
||||
|
||||
export default doCheck;
|
||||
|
||||
export function doCheckWork({
|
||||
creature, prop, method, methodScope = {}
|
||||
}){
|
||||
// Create the log
|
||||
let log = CreatureLogSchema.clean({
|
||||
creatureId: creature._id,
|
||||
creatureName: creature.name,
|
||||
});
|
||||
export function doCheckWork({prop, actionContext}){
|
||||
|
||||
rollCheck({prop, log, methodScope});
|
||||
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
||||
rollCheck(prop, actionContext);
|
||||
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
|
||||
|
||||
// Insert the log
|
||||
insertCreatureLogWork({log, creature, method});
|
||||
actionContext.writeLog();
|
||||
}
|
||||
|
||||
function rollCheck({prop, log, methodScope}){
|
||||
function rollCheck(prop, actionContext) {
|
||||
const scope = actionContext.scope;
|
||||
// get the modifier for the roll
|
||||
let rollModifier;
|
||||
let logName = `${prop.name} check`;
|
||||
@@ -81,7 +75,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
const rollModifierText = numberToSignedString(rollModifier, true);
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (methodScope['$checkAdvantage'] === 1){
|
||||
if (scope['$checkAdvantage'] === 1){
|
||||
logName += ' (Advantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
@@ -91,7 +85,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||
}
|
||||
} else if (methodScope['$checkAdvantage'] === -1){
|
||||
} else if (scope['$checkAdvantage'] === -1){
|
||||
logName += ' (Disadvantage)';
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
@@ -107,7 +101,7 @@ function rollCheck({prop, log, methodScope}){
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||
}
|
||||
const result = (value + rollModifier) || 0;
|
||||
log.content.push({
|
||||
actionContext.addLog({
|
||||
name: logName,
|
||||
value: `${resultPrefix} **${result}**`,
|
||||
});
|
||||
|
||||
2
app/imports/api/engine/actions/index.js
Normal file
2
app/imports/api/engine/actions/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import './doCastSpell.js';
|
||||
import './doCheck.js';
|
||||
@@ -1,54 +0,0 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||
import doAction from '../doAction.js';
|
||||
|
||||
const commitAction = new ValidatedMethod({
|
||||
name: 'creatureProperties.doAction',
|
||||
validate: new SimpleSchema({
|
||||
actionId: SimpleSchema.RegEx.Id,
|
||||
targetIds: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: 20,
|
||||
optional: true,
|
||||
},
|
||||
'targetIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 10,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, targetIds = []}) {
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
// Check permissions
|
||||
let creature = getRootCreatureAncestor(action);
|
||||
|
||||
assertEditPermission(creature, this.userId);
|
||||
let targets = [];
|
||||
targetIds.forEach(targetId => {
|
||||
let target = Creatures.findOne(targetId);
|
||||
assertEditPermission(target, this.userId);
|
||||
targets.push(target);
|
||||
});
|
||||
doAction({action, creature, targets, method: this});
|
||||
|
||||
// recompute creatures
|
||||
computeCreature(creature._id);
|
||||
|
||||
targets.forEach(target => {
|
||||
computeCreature(target._id);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default commitAction;
|
||||
@@ -1,26 +0,0 @@
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
export default function createAction({
|
||||
creature = {_id: 'creatureId'},
|
||||
targets = [],
|
||||
properties = [],
|
||||
ancestors = [],
|
||||
method
|
||||
} = {}){
|
||||
properties = properties.map(cleanProp);
|
||||
ancestors = ancestors.map(cleanProp);
|
||||
creature = cleanCreature(creature);
|
||||
ancestors = ancestors.map(cleanCreature);
|
||||
return {creature, targets, properties, ancestors, method};
|
||||
}
|
||||
|
||||
function cleanProp(prop){
|
||||
let schema = CreatureProperties.simpleSchema(prop);
|
||||
return schema.clean(prop);
|
||||
}
|
||||
|
||||
function cleanCreature(creature){
|
||||
let schema = Creatures.simpleSchema(creature);
|
||||
return schema.clean(creature);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import applyAction from './applyAction.testFn.js';
|
||||
|
||||
export default [{
|
||||
text: 'Applies actions',
|
||||
fn: applyAction,
|
||||
},];
|
||||
@@ -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);
|
||||
|
||||
@@ -31,9 +31,10 @@ function childrenActive(prop){
|
||||
switch (prop.type){
|
||||
// Only equipped items have active children
|
||||
case 'item': return !!prop.equipped;
|
||||
// The children of actions are always inactive
|
||||
// The children of actions, spells, and triggers are always inactive
|
||||
case 'action': return false;
|
||||
case 'spell': return false;
|
||||
case 'trigger': return false;
|
||||
// The children of notes are always inactive
|
||||
case 'note': return false;
|
||||
// Other children are active
|
||||
|
||||
@@ -10,8 +10,10 @@ export default function computeToggleDependencies(node, dependencyGraph){
|
||||
prop.enabled
|
||||
) return;
|
||||
walkDown(node.children, child => {
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
// Only for children that aren't inactive
|
||||
if (child.node.inactive) return;
|
||||
// The child nodes depend on the toggle condition compuation
|
||||
child.node._computationDetails.toggleAncestors.push(prop);
|
||||
dependencyGraph.addLink(child.node._id, prop._id, 'toggle');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { get, intersection, difference } from 'lodash';
|
||||
import { get, intersection, difference, union } from 'lodash';
|
||||
|
||||
const linkDependenciesByType = {
|
||||
action: linkAction,
|
||||
@@ -14,6 +14,7 @@ const linkDependenciesByType = {
|
||||
effect: linkEffects,
|
||||
proficiency: linkProficiencies,
|
||||
roll: linkRoll,
|
||||
pointBuy: linkPointBuy,
|
||||
propertySlot: linkSlot,
|
||||
skill: linkSkill,
|
||||
spell: linkAction,
|
||||
@@ -30,7 +31,6 @@ function dependOnCalc({dependencyGraph, prop, key}){
|
||||
let calc = get(prop, key);
|
||||
if (!calc) return;
|
||||
if (calc.type !== '_calculation'){
|
||||
console.log(calc);
|
||||
throw `Expected calculation got ${calc.type}`
|
||||
}
|
||||
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
|
||||
@@ -63,7 +63,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
||||
dependOnCalc({
|
||||
dependencyGraph,
|
||||
prop,
|
||||
key: `${prop._id}.resources.itemsConsumed.${index}.quantity`,
|
||||
key: `resources.itemsConsumed[${index}].quantity`,
|
||||
});
|
||||
});
|
||||
// Link attributes consumed
|
||||
@@ -74,7 +74,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
||||
dependOnCalc({
|
||||
dependencyGraph,
|
||||
prop,
|
||||
key: `${prop._id}.resources.attributesConsumed.${index}.quantity`,
|
||||
key: `resources.attributesConsumed[${index}].quantity`,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -106,7 +106,8 @@ function linkBuff(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'duration'});
|
||||
}
|
||||
|
||||
function linkClassLevel(dependencyGraph, prop){
|
||||
function linkClassLevel(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName && prop.level){
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
|
||||
@@ -122,17 +123,33 @@ function linkDamage(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
}
|
||||
|
||||
function linkEffects(dependencyGraph, prop, computation){
|
||||
function linkEffects(dependencyGraph, prop, computation) {
|
||||
// The effect depends on its amount calculation
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
// Inactive effects aren't going to impact their targeted stats
|
||||
if (prop.inactive) return;
|
||||
// The stats depend on the effect
|
||||
if (prop.targetByTags){
|
||||
if (prop.inactive) {
|
||||
// Inactive effects apply to no stats
|
||||
return;
|
||||
} else 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 {
|
||||
@@ -145,16 +162,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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -166,8 +185,8 @@ function getTargetListFromTags(tags, computation){
|
||||
const targetTagIdLists = [];
|
||||
if (!tags) return [];
|
||||
tags.forEach(tag => {
|
||||
const idList = computation.propsWithTag[tag];
|
||||
if (idList) targetTagIdLists.push(idList);
|
||||
const idList = computation.propsWithTag[tag] || [];
|
||||
targetTagIdLists.push(idList);
|
||||
});
|
||||
const targets = intersection(...targetTagIdLists);
|
||||
return targets;
|
||||
@@ -209,22 +228,46 @@ function linkRoll(dependencyGraph, prop){
|
||||
}
|
||||
|
||||
function linkVariableName(dependencyGraph, prop){
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName){
|
||||
// The variableName of the prop depends on the prop if the prop is active
|
||||
if (prop.variableName && !prop.inactive){
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
|
||||
}
|
||||
}
|
||||
|
||||
function linkDamageMultiplier(dependencyGraph, prop){
|
||||
function linkDamageMultiplier(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
function linkPointBuy(dependencyGraph, prop){
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'min' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'max' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'total' });
|
||||
prop.values?.forEach(row => {
|
||||
// Wrap the document in a new object so we don't bash it unintentionally
|
||||
const pointBuyRow = {
|
||||
...row,
|
||||
type: 'pointBuyRow',
|
||||
tableName: prop.name,
|
||||
tableId: prop._id,
|
||||
}
|
||||
dependencyGraph.addNode(row._id, pointBuyRow);
|
||||
linkVariableName(dependencyGraph, pointBuyRow);
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
|
||||
});
|
||||
if (prop.inactive) return;
|
||||
}
|
||||
|
||||
function linkProficiencies(dependencyGraph, prop){
|
||||
// The stats depend on the proficiency
|
||||
if (prop.inactive) return;
|
||||
prop.stats.forEach(statName => {
|
||||
if (!statName) return;
|
||||
dependencyGraph.addLink(statName, prop._id, prop.type);
|
||||
@@ -236,6 +279,10 @@ function linkSavingThrow(dependencyGraph, prop){
|
||||
}
|
||||
|
||||
function linkSkill(dependencyGraph, prop){
|
||||
// Depends on base value
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
// Link dependents
|
||||
if (prop.inactive) return;
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// The prop depends on the variable references as the ability
|
||||
if (prop.ability){
|
||||
|
||||
@@ -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
|
||||
@@ -102,6 +89,10 @@ export function buildComputationFromProps(properties, creature){
|
||||
// Walk the property trees computing things that need to be inherited
|
||||
walkDown(forest, node => {
|
||||
computeInactiveStatus(node);
|
||||
});
|
||||
// Inactive status must be complete for the whole tree before toggle deps
|
||||
// are calculated
|
||||
walkDown(forest, node => {
|
||||
computeToggleDependencies(node, dependencyGraph);
|
||||
computeSlotQuantityFilled(node, dependencyGraph);
|
||||
});
|
||||
@@ -114,5 +105,6 @@ export function buildComputationFromProps(properties, creature){
|
||||
linkTypeDependencies(dependencyGraph, prop, computation);
|
||||
linkCalculationDependencies(dependencyGraph, prop, computation);
|
||||
});
|
||||
|
||||
return computation;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import _variable from './computeByType/computeVariable.js';
|
||||
import action from './computeByType/computeAction.js';
|
||||
import attribute from './computeByType/computeAttribute.js';
|
||||
import skill from './computeByType/computeSkill.js';
|
||||
import pointBuy from './computeByType/computePointBuy.js';
|
||||
import propertySlot from './computeByType/computeSlot.js';
|
||||
import container from './computeByType/computeContainer.js';
|
||||
import _calculation from './computeByType/computeCalculation.js';
|
||||
@@ -13,6 +14,7 @@ export default Object.freeze({
|
||||
attribute,
|
||||
container,
|
||||
skill,
|
||||
pointBuy,
|
||||
propertySlot,
|
||||
spell: action,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { has } from 'lodash';
|
||||
import evaluateCalculation from '../../utility/evaluateCalculation.js';
|
||||
|
||||
export default function computePointBuy(computation, node) {
|
||||
const prop = node.data;
|
||||
const tableMin = prop.min?.value || null;
|
||||
const tableMax = prop.max?.value || null;
|
||||
prop.spent = 0;
|
||||
prop.values?.forEach(row => {
|
||||
// Clean up added properties
|
||||
// delete row.tableId;
|
||||
// delete row.tableName;
|
||||
// delete row.type;
|
||||
|
||||
row.spent = 0;
|
||||
if (row.value === undefined) return;
|
||||
const min = has(row, 'min.value') ? row.min.value : tableMin;
|
||||
const max = has(row, 'max.value') ? row.max.value : tableMax;
|
||||
const costFunction = EJSON.clone(row.cost || prop.cost);
|
||||
if (costFunction) costFunction.parseLevel = 'reduce';
|
||||
|
||||
// Check min and max
|
||||
if (min !== null && row.value < min) {
|
||||
row.value = min;
|
||||
}
|
||||
if (max !== null && row.value > max) {
|
||||
row.value = max;
|
||||
}
|
||||
// Evaluate the cost function
|
||||
if (!costFunction) return;
|
||||
evaluateCalculation(costFunction, { ...computation.scope, value: row.value });
|
||||
// Write calculation errors
|
||||
costFunction.errors?.forEach(error => {
|
||||
if (error?.message) {
|
||||
row.errors = row.errors || [];
|
||||
error.message = 'Cost calculation error.\n' + error.message;
|
||||
row.errors.push(error);
|
||||
}
|
||||
});
|
||||
if (Number.isFinite(costFunction.value)) {
|
||||
row.spent = costFunction.value;
|
||||
prop.spent += costFunction.value;
|
||||
}
|
||||
});
|
||||
prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0);
|
||||
if (prop.spent > prop.total?.value) {
|
||||
prop.errors = prop.errors || [];
|
||||
prop.errors.push({
|
||||
type: 'pointBuyError',
|
||||
message: 'Spent more than total points available',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// by computeVariableAsSkill
|
||||
export default function computeSkill(computation, node){
|
||||
const prop = node.data;
|
||||
prop.proficiency = prop.baseProficiency;
|
||||
prop.proficiency = prop.baseProficiency || 0;
|
||||
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
|
||||
// Multiply the proficiency bonus by the actual proficiency
|
||||
if(prop.proficiency === 0.49){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@ export default function aggregateDefinition({node, linkedNode, link}){
|
||||
// get current defining prop
|
||||
const definingProp = node.data.definingProp;
|
||||
// Find the last defining prop
|
||||
if (!definingProp || prop.order > definingProp.order){
|
||||
if (
|
||||
!definingProp ||
|
||||
prop.type !== 'pointBuyRow' && (
|
||||
definingProp.type === 'pointBuyRow' ||
|
||||
prop.order > definingProp.order
|
||||
)
|
||||
) {
|
||||
// override the current defining prop
|
||||
overrideProp(definingProp, node);
|
||||
// set this prop as the new defining prop
|
||||
@@ -18,9 +24,32 @@ export default function aggregateDefinition({node, linkedNode, link}){
|
||||
}
|
||||
|
||||
// Aggregate the base value due to the defining properties
|
||||
const propBaseValue = prop.baseValue?.value;
|
||||
let propBaseValue = prop.baseValue?.value;
|
||||
// Point buy rows use prop.value instead of prop.baseValue
|
||||
if (prop.type === 'pointBuyRow') {
|
||||
propBaseValue = prop.value;
|
||||
}
|
||||
|
||||
if (propBaseValue === undefined) return;
|
||||
// Store a summary of the definition as a base value effect
|
||||
node.data.effects = node.data.effects || [];
|
||||
if (prop.type === 'pointBuyRow') {
|
||||
node.data.effects.push({
|
||||
_id: prop.tableId,
|
||||
name: prop.tableName,
|
||||
operation: 'base',
|
||||
amount: { value: propBaseValue },
|
||||
type: 'pointBuy',
|
||||
});
|
||||
} else {
|
||||
node.data.effects.push({
|
||||
_id: prop._id,
|
||||
name: prop.name,
|
||||
operation: 'base',
|
||||
amount: { value: propBaseValue },
|
||||
type: prop.type,
|
||||
});
|
||||
}
|
||||
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
|
||||
node.data.baseValue = propBaseValue;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export default function aggregateEffect({node, linkedNode, link}){
|
||||
name: linkedNode.data.name,
|
||||
operation: linkedNode.data.operation,
|
||||
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value},
|
||||
type: linkedNode.data.type,
|
||||
// ancestors: linkedNode.data.ancestors,
|
||||
});
|
||||
|
||||
|
||||
@@ -43,18 +43,26 @@ export default function aggregateInventory({node, linkedNode, link}){
|
||||
}
|
||||
}
|
||||
|
||||
function quantity(prop){
|
||||
if (typeof prop.quantity === 'number'){
|
||||
return prop.quantity;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function weight(prop){
|
||||
return (prop.weight || 0) + (prop.contentsWeight || 0);
|
||||
return (prop.weight || 0) * quantity(prop) + (prop.contentsWeight || 0);
|
||||
}
|
||||
|
||||
function carriedWeight(prop){
|
||||
return (prop.weight || 0) + (prop.carriedWeight || 0);
|
||||
return (prop.weight || 0) * quantity(prop) + (prop.carriedWeight || 0);
|
||||
}
|
||||
|
||||
function value (prop){
|
||||
return (prop.value || 0) + (prop.contentsValue || 0);
|
||||
return (prop.value || 0) * quantity(prop) + (prop.contentsValue || 0);
|
||||
}
|
||||
|
||||
function carriedValue (prop){
|
||||
return (prop.value || 0) + (prop.carriedValue || 0);
|
||||
return (prop.value || 0) * quantity(prop) + (prop.carriedValue || 0);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -29,8 +29,12 @@ 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;
|
||||
|
||||
// Store effects
|
||||
prop.effects = node.data.effects;
|
||||
|
||||
// If there is no aggregator, determine if the prop can hide, then exit
|
||||
if (!aggregator){
|
||||
@@ -41,7 +45,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;
|
||||
@@ -83,7 +87,7 @@ function aggregateAbilityEffects({computation, skillNode, abilityNode}){
|
||||
// to a skill from its ability
|
||||
if (link.data === 'effect'){
|
||||
if (![
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail'
|
||||
'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional'
|
||||
].includes(linkedNode.data.operation)){
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,7 +29,7 @@ export default function getAggregatorResult(node){
|
||||
if (aggregator.set !== undefined) {
|
||||
result = aggregator.set;
|
||||
}
|
||||
if (!node.definingProp?.decimal && Number.isFinite(result)){
|
||||
if (!node.data.definingProp?.decimal && Number.isFinite(result)){
|
||||
result = Math.floor(result);
|
||||
} else if (Number.isFinite(result)){
|
||||
result = stripFloatingPointOddities(result);
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function(){
|
||||
assert.equal(prop.usesLeft, 2);
|
||||
|
||||
const rolled = computation.propsById['rolledDescriptionId'];
|
||||
assert.equal(rolled.summary.value, 'test roll gets compiled d4 + 4 properly');
|
||||
assert.equal(rolled.summary.value, 'test roll gets compiled 8 properly');
|
||||
|
||||
const itemConsumed = prop.resources.itemsConsumed[0];
|
||||
assert.equal(itemConsumed.quantity.value, 3);
|
||||
@@ -67,7 +67,7 @@ var testProperties = [
|
||||
type: 'action',
|
||||
ancestors: [{id: 'charId'}],
|
||||
summary: {
|
||||
text: 'test roll gets compiled {1d4 + (2 + 2)} properly',
|
||||
text: 'test roll gets compiled {4 + (2 + 2)} properly',
|
||||
},
|
||||
}),
|
||||
clean({
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -14,16 +14,14 @@ export default function(){
|
||||
|
||||
assert.equal(scope('itemsAttuned'), 1);
|
||||
|
||||
assert.equal(prop('childContainerId').carriedWeight, 23);
|
||||
assert.equal(prop('childContainerId').contentsWeight, 23);
|
||||
assert.equal(prop('childContainerId').carriedWeight, 69);
|
||||
assert.equal(prop('childContainerId').contentsWeight, 69);
|
||||
|
||||
assert.equal(scope('weightCarried'), 58);
|
||||
assert.equal(scope('weightCarried'), 104);
|
||||
assert.equal(scope('valueCarried'), 129);
|
||||
|
||||
assert.equal(scope('weightCarried'), 58);
|
||||
assert.equal(scope('valueCarried'), 71);
|
||||
|
||||
assert.equal(scope('weightTotal'), 58);
|
||||
assert.equal(scope('valueTotal'), 71);
|
||||
assert.equal(scope('weightTotal'), 104);
|
||||
assert.equal(scope('valueTotal'), 129);
|
||||
}
|
||||
|
||||
var testProperties = [
|
||||
@@ -62,8 +60,9 @@ var testProperties = [
|
||||
clean({
|
||||
_id: 'grandchildItemId',
|
||||
type: 'item',
|
||||
weight: 23,
|
||||
value: 29,
|
||||
weight: 23, // 69 total
|
||||
value: 29, // 87 total
|
||||
quantity: 3,
|
||||
ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -51,17 +51,30 @@ function compute(computation, node){
|
||||
|
||||
function pushDependenciesToStack(nodeId, graph, stack, computation){
|
||||
graph.forEachLinkedNode(nodeId, linkedNode => {
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited){
|
||||
const pather = path.nba(graph, {
|
||||
oriented: true
|
||||
});
|
||||
const loop = pather.find(nodeId, nodeId);
|
||||
computation.errors.push({
|
||||
type: 'dependencyLoop',
|
||||
details: {
|
||||
nodes: loop.map(node => node.id)
|
||||
},
|
||||
});
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited) {
|
||||
// This is a dependency loop, find a path from the node to itself
|
||||
// and store that path as a dependency loop error
|
||||
const pather = path.nba(graph, { oriented: true });
|
||||
let loop = [];
|
||||
// Pather doesn't like going from a node to iteself, so find all the
|
||||
// paths going from the next node back to the original node
|
||||
// and return the shortest one
|
||||
graph.forEachLinkedNode(nodeId, nextNode => {
|
||||
const newLoop = pather.find(nextNode.id, nodeId);
|
||||
if (!newLoop.length) return;
|
||||
if (!loop.length || newLoop.length < loop.length - 1) {
|
||||
loop = [linkedNode, ...newLoop];
|
||||
}
|
||||
}, true);
|
||||
|
||||
if (loop.length) {
|
||||
computation.errors.push({
|
||||
type: 'dependencyLoop',
|
||||
details: {
|
||||
nodes: loop.map(node => node.id)
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
stack.push(linkedNode);
|
||||
}, true);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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.variableName) tags.push(prop.variableName);
|
||||
if (prop.damageType) tags.push(prop.damageType);
|
||||
if (prop.skillType) tags.push(prop.skillType);
|
||||
if (prop.attributeType) tags.push(prop.attributeType);
|
||||
if (prop.reset) tags.push(prop.reset);
|
||||
return tags;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ export default function walkDown(tree, callback){
|
||||
let stack = [...tree];
|
||||
while(stack.length){
|
||||
let node = stack.pop();
|
||||
callback(node);
|
||||
callback(node, stack);
|
||||
stack.push(...node.children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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}});
|
||||
}
|
||||
|
||||
@@ -5,16 +5,33 @@ import writeScope from './computation/writeComputation/writeScope.js';
|
||||
import writeErrors from './computation/writeComputation/writeErrors.js';
|
||||
|
||||
export default function computeCreature(creatureId){
|
||||
if (Meteor.isClient) return;
|
||||
// console.log('compute ' + creatureId);
|
||||
const computation = buildCreatureComputation(creatureId);
|
||||
computeCreatureComputation(computation);
|
||||
writeAlteredProperties(computation);
|
||||
writeScope(creatureId, computation.scope);
|
||||
writeErrors(creatureId, computation.errors);
|
||||
computeComputation(computation, creatureId);
|
||||
}
|
||||
|
||||
// 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);
|
||||
function computeComputation(computation, creatureId) {
|
||||
try {
|
||||
computeCreatureComputation(computation);
|
||||
writeAlteredProperties(computation);
|
||||
writeScope(creatureId, computation);
|
||||
} catch (e){
|
||||
const errorText = e.reason || e.message || e.toString();
|
||||
computation.errors.push({
|
||||
type: 'crash',
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
303
app/imports/api/engine/loadCreatures.js
Normal file
303
app/imports/api/engine/loadCreatures.js
Normal file
@@ -0,0 +1,303 @@
|
||||
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 },
|
||||
});
|
||||
// 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 },
|
||||
}).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 },
|
||||
}).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) {
|
||||
const cloneCreature = EJSON.clone(creature);
|
||||
return cloneCreature;
|
||||
}
|
||||
}
|
||||
// console.time(`Cache miss on Creature: ${creatureId}`);
|
||||
const creature = Creatures.findOne(creatureId);
|
||||
// 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) {
|
||||
const cloneVarables = EJSON.clone(variables);
|
||||
return cloneVarables;
|
||||
}
|
||||
}
|
||||
// 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 },
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
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 },
|
||||
}, {
|
||||
sort: { order: 1 },
|
||||
}).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 },
|
||||
}).observeChanges({
|
||||
added(id, fields) {
|
||||
fields._id = id;
|
||||
self.addProperty(fields);
|
||||
if (fields.dirty) compute();
|
||||
},
|
||||
changed(id, fields) {
|
||||
self.changeProperty(id, fields);
|
||||
if (fields.dirty) compute();
|
||||
},
|
||||
removed(id) {
|
||||
self.removeProperty(id);
|
||||
compute();
|
||||
},
|
||||
});
|
||||
|
||||
// Observe the creature itself
|
||||
self.creatureObserver = Creatures.find({
|
||||
_id: creatureId,
|
||||
}).observeChanges({
|
||||
added(id, fields) {
|
||||
fields._id = id;
|
||||
self.addCreature(fields)
|
||||
if (fields.dirty) compute();
|
||||
},
|
||||
changed(id, fields) {
|
||||
self.changeCreature(id, fields);
|
||||
if (fields.dirty) compute();
|
||||
},
|
||||
removed(id) {
|
||||
self.removeCreature(id);
|
||||
},
|
||||
});
|
||||
|
||||
// Observe the creature's variables
|
||||
self.variablesObserver = CreatureVariables.find({
|
||||
_creatureId: creatureId,
|
||||
}, {
|
||||
fields: { _creatureId: 0},
|
||||
}).observeChanges({
|
||||
added(id, fields) {
|
||||
fields._id = id;
|
||||
self.addVariables(fields)
|
||||
},
|
||||
changed(id, fields) {
|
||||
self.changeVariables(id, fields);
|
||||
},
|
||||
removed(id) {
|
||||
self.removeVariables(id);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
stop() {
|
||||
this.propertyObserver.stop();
|
||||
this.creatureObserver.stop();
|
||||
this.variablesObserver.stop();
|
||||
}
|
||||
addProperty(prop) {
|
||||
this.properties.set(prop._id, prop);
|
||||
}
|
||||
changeProperty(id, fields) {
|
||||
LoadedCreature.changeMap(id, fields, this.properties);
|
||||
}
|
||||
removeProperty(id) {
|
||||
this.properties.delete(id)
|
||||
}
|
||||
addCreature(creature) {
|
||||
this.creature = creature;
|
||||
}
|
||||
changeCreature(id, fields) {
|
||||
LoadedCreature.changeDoc(this.creature, fields);
|
||||
}
|
||||
removeCreature() {
|
||||
delete this.creature;
|
||||
}
|
||||
addVariables(variables) {
|
||||
this.variables = variables;
|
||||
}
|
||||
changeVariables(id, fields) {
|
||||
LoadedCreature.changeDoc(this.variables, fields);
|
||||
}
|
||||
removeVariables() {
|
||||
delete this.variables;
|
||||
}
|
||||
static changeMap(id, fields, map) {
|
||||
const doc = map.get(id);
|
||||
LoadedCreature.changeDoc(doc, fields);
|
||||
}
|
||||
static changeDoc(doc, fields) {
|
||||
if (!doc) return;
|
||||
for (let key in fields) {
|
||||
if (key === undefined) {
|
||||
delete doc[key];
|
||||
} else {
|
||||
doc[key] = fields[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/imports/api/files/UserImages.js
Normal file
19
app/imports/api/files/UserImages.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
||||
|
||||
const UserImages = createS3FilesCollection({
|
||||
collectionName: 'userImages',
|
||||
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
|
||||
onBeforeUpload(file) {
|
||||
// Allow upload files under 10MB
|
||||
if (file.size > 10485760) {
|
||||
return 'Please upload with size equal or less than 10MB';
|
||||
}
|
||||
// Allow common image extensions
|
||||
if (!/gif|png|jpe?g|webp/i.test(file.extension || '')) {
|
||||
return 'Please upload an image file only';
|
||||
}
|
||||
return true
|
||||
}
|
||||
});
|
||||
|
||||
export default UserImages;
|
||||
@@ -1,5 +1,4 @@
|
||||
// https://github.com/VeliovGroup/Meteor-Files/blob/master/docs/aws-s3-integration.md
|
||||
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { each, clone } from 'lodash';
|
||||
import { Random } from 'meteor/random';
|
||||
@@ -37,8 +36,9 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
secretAccessKey: s3Conf.secret,
|
||||
endpoint: s3Conf.endpoint,
|
||||
sslEnabled: true, // optional
|
||||
maxRetries: 10,
|
||||
httpOptions: {
|
||||
timeout: 6000,
|
||||
timeout: 12000,
|
||||
agent: false
|
||||
}
|
||||
});
|
||||
@@ -47,14 +47,18 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
collectionName,
|
||||
storagePath,
|
||||
onBeforeUpload,
|
||||
debug = Meteor.isProduction,
|
||||
onAfterUpload,
|
||||
debug = !Meteor.isProduction,
|
||||
allowClientCode = false,
|
||||
}){
|
||||
const collection = new FilesCollection({
|
||||
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,25 +217,24 @@ 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,
|
||||
debug = Meteor.isProduction,
|
||||
onAfterUpload,
|
||||
debug = !Meteor.isProduction,
|
||||
allowClientCode = false,
|
||||
}){
|
||||
const collection = new FilesCollection({
|
||||
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');
|
||||
|
||||
@@ -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 };
|
||||
|
||||
135
app/imports/api/library/LibraryCollections.js
Normal file
135
app/imports/api/library/LibraryCollections.js
Normal file
@@ -0,0 +1,135 @@
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
|
||||
import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js';
|
||||
import { assertEditPermission, assertOwnership } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import { getUserTier } from '/imports/api/users/patreon/tiers.js'
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
/**
|
||||
* LibraryCollections are groups of libraries that are subscribed together at once
|
||||
*/
|
||||
const LibraryCollections = new Mongo.Collection('libraryCollections');
|
||||
|
||||
const LibraryCollectionSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.summary,
|
||||
},
|
||||
libraries: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.libraryCollectionCount,
|
||||
},
|
||||
'libraries.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
});
|
||||
|
||||
LibraryCollectionSchema.extend(SharingSchema);
|
||||
LibraryCollections.attachSchema(LibraryCollectionSchema);
|
||||
|
||||
export default LibraryCollections;
|
||||
|
||||
const insertLibraryCollection = new ValidatedMethod({
|
||||
name: 'libraryCollections.insert',
|
||||
mixins: [
|
||||
simpleSchemaMixin,
|
||||
],
|
||||
schema: LibraryCollectionSchema.omit('owner'),
|
||||
run(libraryCollection) {
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
||||
'You need to be logged in to insert a library');
|
||||
}
|
||||
let tier = getUserTier(this.userId);
|
||||
if (!tier.paidBenefits){
|
||||
throw new Meteor.Error('LibraryCollections.methods.insert.denied',
|
||||
`The ${tier.name} tier does not allow you to insert a library collection`);
|
||||
}
|
||||
libraryCollection.owner = this.userId;
|
||||
return LibraryCollections.insert(libraryCollection);
|
||||
},
|
||||
});
|
||||
|
||||
const updateLibraryCollection = new ValidatedMethod({
|
||||
name: 'libraryCollections.update',
|
||||
mixins: [
|
||||
simpleSchemaMixin,
|
||||
],
|
||||
schema: {
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
update: {
|
||||
type: LibraryCollectionSchema
|
||||
.pick('name', 'description', 'libraries')
|
||||
.extend({ //make libraries optional
|
||||
libraries: {
|
||||
optional: true,
|
||||
defaultValue: undefined,
|
||||
},
|
||||
}),
|
||||
}
|
||||
},
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, update}){
|
||||
const libraryCollection = LibraryCollections.findOne(_id, {
|
||||
fields: {
|
||||
owner: 1,
|
||||
writers: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(libraryCollection, this.userId);
|
||||
return LibraryCollections.update(_id, {$set: update});
|
||||
},
|
||||
});
|
||||
|
||||
const removeLibraryCollection = new ValidatedMethod({
|
||||
name: 'libraryCollections.remove',
|
||||
validate: new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.id
|
||||
},
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
const libraryCollection = LibraryCollections.findOne(_id, {
|
||||
fields: {
|
||||
owner: 1,
|
||||
}
|
||||
});
|
||||
assertOwnership(libraryCollection, this.userId);
|
||||
return LibraryCollections.remove(_id);
|
||||
}
|
||||
});
|
||||
|
||||
function getLibraryIdsByCollectionId(libraryCollectionId) {
|
||||
const libraryCollection = LibraryCollections.findOne(libraryCollectionId)
|
||||
return libraryCollection?.libraries || [];
|
||||
}
|
||||
|
||||
export {
|
||||
LibraryCollectionSchema,
|
||||
insertLibraryCollection,
|
||||
updateLibraryCollection,
|
||||
removeLibraryCollection,
|
||||
getLibraryIdsByCollectionId,
|
||||
};
|
||||
39
app/imports/api/library/getCreatureLibraryIds.js
Normal file
39
app/imports/api/library/getCreatureLibraryIds.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import getUserLibraryIds from './getUserLibraryIds';
|
||||
import { intersection, union } from 'lodash';
|
||||
|
||||
export default function getCreatureLibraryIds(creature, userId) {
|
||||
if (!userId) return [];
|
||||
|
||||
// Get the ids of libraries the user is permitted to view
|
||||
const userLibIds = getUserLibraryIds(userId);
|
||||
|
||||
// If given a creature Id, get the creature document
|
||||
if (typeof creature === 'string') {
|
||||
creature = Creatures.findOne(creature, {
|
||||
fields: {
|
||||
allowedLibraries: 1,
|
||||
allowedLibraryCollections: 1,
|
||||
}
|
||||
});
|
||||
if (!creature) return [];
|
||||
}
|
||||
|
||||
// If the creature does not restrict the libraries, let it use them all
|
||||
if (!creature.allowedLibraryCollections && !creature.allowedLibraries) {
|
||||
return userLibIds;
|
||||
}
|
||||
|
||||
// Get the ids of the libraries that the creature allows
|
||||
const allowedCollections = creature.allowedLibraryCollections || [];
|
||||
let creatureLibIds = creature.allowedLibraries || [];
|
||||
LibraryCollections.find({
|
||||
_id: { $in: allowedCollections }
|
||||
}, { fields: { libraries: 1 } }).forEach(collection => {
|
||||
creatureLibIds = union(creatureLibIds, collection.libraries);
|
||||
});
|
||||
|
||||
// return all the ids that the creature allows and the user can view
|
||||
return intersection(userLibIds, creatureLibIds);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user