Merge branch 'version-2' into version-2-tabletop
This commit is contained in:
@@ -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;
|
||||
@@ -9,7 +9,6 @@ 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 { incrementFileStorageUsed } from '/imports/api/users/methods/updateFileStorageUsed.js';
|
||||
|
||||
export function getArchiveObj(creatureId){
|
||||
// Build the archive document
|
||||
@@ -32,7 +31,7 @@ export function getArchiveObj(creatureId){
|
||||
return archiveCreature;
|
||||
}
|
||||
|
||||
export function archiveCreature(creatureId, userId){
|
||||
export function archiveCreature(creatureId){
|
||||
const archive = getArchiveObj(creatureId);
|
||||
const buffer = Buffer.from(JSON.stringify(archive, null, 2));
|
||||
ArchiveCreatureFiles.write(buffer, {
|
||||
@@ -44,12 +43,11 @@ export function archiveCreature(creatureId, userId){
|
||||
creatureId: archive.creature._id,
|
||||
creatureName: archive.creature.name,
|
||||
},
|
||||
}, (error, file) => {
|
||||
}, (error) => {
|
||||
if (error){
|
||||
throw error;
|
||||
} else {
|
||||
removeCreatureWork(creatureId);
|
||||
incrementFileStorageUsed(userId, file.size);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
@@ -1,5 +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';
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
||||
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 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';
|
||||
|
||||
const removeArchiveCreature = new ValidatedMethod({
|
||||
|
||||
@@ -10,13 +10,14 @@ import { removeCreatureWork } from '/imports/api/creature/creatures/methods/remo
|
||||
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.')
|
||||
@@ -25,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);
|
||||
@@ -78,7 +92,7 @@ const restoreCreaturefromFile = new ValidatedMethod({
|
||||
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 });
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -4,25 +4,25 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let CreatureFolders = new Mongo.Collection('creatureFolders');
|
||||
|
||||
let creatureFolderSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
trim: false,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
creatures: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'creatures.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
creatures: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
},
|
||||
'creatures.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
owner: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
archived: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
|
||||
@@ -18,23 +18,23 @@ let CreaturePropertySchema = new SimpleSchema({
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: {
|
||||
type: String,
|
||||
allowedValues: Object.keys(propertySchemasIndex),
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
tags: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.tagCount,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.tagLength,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
icon: {
|
||||
type: storedIconsSchema,
|
||||
optional: true,
|
||||
@@ -82,28 +82,31 @@ 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);
|
||||
|
||||
for (let key in propertySchemasIndex){
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(CreaturePropertySchema);
|
||||
for (let key in propertySchemasIndex) {
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(propertySchemasIndex[key]);
|
||||
schema.extend(CreaturePropertySchema);
|
||||
schema.extend(ColorSchema);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
CreatureProperties.attachSchema(schema, {
|
||||
selector: {type: key}
|
||||
});
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
CreatureProperties.attachSchema(schema, {
|
||||
selector: { type: key }
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
CreaturePropertySchema,
|
||||
CreaturePropertySchema,
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
@@ -21,43 +20,40 @@ const adjustQuantity = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, operation, value}) {
|
||||
run({ _id, operation, value }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// 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);
|
||||
adjustQuantityWork({ property, operation, value });
|
||||
},
|
||||
});
|
||||
|
||||
export function adjustQuantityWork({property, operation, value}){
|
||||
export function adjustQuantityWork({ property, operation, value }) {
|
||||
// Check if property has quantity
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('quantity')){
|
||||
if (!schema.allowsKey('quantity')) {
|
||||
throw new Meteor.Error(
|
||||
'Adjust quantity failed',
|
||||
`Property of type "${property.type}" doesn't have a quantity`
|
||||
);
|
||||
}
|
||||
if (operation === 'set'){
|
||||
if (operation === 'set') {
|
||||
CreatureProperties.update(property._id, {
|
||||
$set: {quantity: value}
|
||||
$set: { quantity: value, dirty: true }
|
||||
}, {
|
||||
selector: property
|
||||
});
|
||||
} else if (operation === 'increment'){
|
||||
} else if (operation === 'increment') {
|
||||
// value here is 'damage'
|
||||
value = -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);
|
||||
// Check if property can take damage
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
if (!schema.allowsKey('damage')){
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${property.type}" can't be damaged`
|
||||
);
|
||||
}
|
||||
let result = damagePropertyWork({property, operation, value});
|
||||
// Dependencies can't be changed through damage, only recompute deps
|
||||
computeCreatureDependencyGroup(property);
|
||||
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(prop);
|
||||
if (!schema.allowsKey('damage')) {
|
||||
throw new Meteor.Error(
|
||||
'Damage property failed',
|
||||
`Property of type "${prop.type}" can't be damaged`
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
if (operation === 'set'){
|
||||
const total = property.total || 0;
|
||||
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 = 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;
|
||||
} else if (operation === 'increment'){
|
||||
let currentValue = property.value || 0;
|
||||
let currentDamage = property.damage || 0;
|
||||
let increment = value;
|
||||
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 = 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;
|
||||
@@ -5,13 +5,12 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
||||
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||
import {
|
||||
setLineageOfDocs,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
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){
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
@@ -32,7 +31,7 @@ const duplicateProperty = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let creature = getRootCreatureAncestor(property);
|
||||
|
||||
@@ -45,17 +44,17 @@ const duplicateProperty = new ValidatedMethod({
|
||||
|
||||
// Get all the descendants
|
||||
let nodes = CreatureProperties.find({
|
||||
'ancestors.id': _id,
|
||||
removed: {$ne: true},
|
||||
}, {
|
||||
'ancestors.id': _id,
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
limit: DUPLICATE_CHILDREN_LIMIT + 1,
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
// Alert the user if the limit was hit
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
|
||||
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {
|
||||
nodes.pop();
|
||||
if (Meteor.isClient){
|
||||
if (Meteor.isClient) {
|
||||
snackbar({
|
||||
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
|
||||
});
|
||||
@@ -64,22 +63,25 @@ const duplicateProperty = new ValidatedMethod({
|
||||
|
||||
// re-map all the ancestors
|
||||
setLineageOfDocs({
|
||||
docArray: nodes,
|
||||
newAncestry : [
|
||||
docArray: nodes,
|
||||
newAncestry: [
|
||||
...property.ancestors,
|
||||
{id: propertyId, collection: 'creatureProperties'}
|
||||
{ id: propertyId, collection: 'creatureProperties' }
|
||||
],
|
||||
oldParent : {id: _id, collection: 'creatureProperties'},
|
||||
});
|
||||
oldParent: { id: _id, collection: 'creatureProperties' },
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({docArray: nodes});
|
||||
renewDocIds({ docArray: nodes });
|
||||
|
||||
// Order the root node
|
||||
property.order += 0.5;
|
||||
|
||||
// Mark the sheet as needing recompute
|
||||
property.dirty = true;
|
||||
|
||||
// Insert the properties
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
@@ -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,15 +4,14 @@ 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';
|
||||
|
||||
// Equipping or unequipping an item will also change its parent
|
||||
const equipItem = new ValidatedMethod({
|
||||
name: 'creatureProperties.equip',
|
||||
validate({_id, equipped}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
validate({ _id, equipped }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
if (equipped !== true && equipped !== false) {
|
||||
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
|
||||
}
|
||||
@@ -22,20 +21,20 @@ const equipItem = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, equipped}) {
|
||||
run({ _id, equipped }) {
|
||||
let item = CreatureProperties.findOne(_id);
|
||||
if (item.type !== 'item') throw new Meteor.Error('wrong type',
|
||||
'Equip and unequip can only be performed on items');
|
||||
'Equip and unequip can only be performed on items');
|
||||
let creature = getRootCreatureAncestor(item);
|
||||
assertEditPermission(creature, this.userId);
|
||||
CreatureProperties.update(_id, {
|
||||
$set: {equipped},
|
||||
$set: { equipped, dirty: true },
|
||||
}, {
|
||||
selector: {type: 'item'},
|
||||
});
|
||||
selector: { type: 'item' },
|
||||
});
|
||||
let tag = equipped ? BUILT_IN_TAGS.equipment : BUILT_IN_TAGS.carried;
|
||||
let parentRef = getParentRefByTag(creature._id, tag);
|
||||
if (!parentRef) parentRef = {id: creature._id, collection: 'creatures'};
|
||||
if (!parentRef) parentRef = { id: creature._id, collection: 'creatures' };
|
||||
|
||||
organizeDoc.call({
|
||||
docRef: {
|
||||
@@ -46,8 +45,6 @@ const equipItem = new ValidatedMethod({
|
||||
order: Number.MAX_SAFE_INTEGER,
|
||||
skipRecompute: true,
|
||||
});
|
||||
|
||||
computeCreature(creature._id);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3,28 +3,27 @@ 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',
|
||||
validate({_id}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
validate({ _id }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}) {
|
||||
run({ _id }) {
|
||||
// Permission
|
||||
let property = CreatureProperties.findOne(_id, {
|
||||
fields: {type: 1, ancestors: 1, enabled: 1, disabled: 1}
|
||||
fields: { type: 1, ancestors: 1, enabled: 1, disabled: 1 }
|
||||
});
|
||||
if (property.type !== 'toggle'){
|
||||
if (property.type !== 'toggle') {
|
||||
throw new Meteor.Error('wrong property',
|
||||
'This method can only be applied to toggles');
|
||||
}
|
||||
if (!property.enabled && !property.disabled){
|
||||
if (!property.enabled && !property.disabled) {
|
||||
throw new Meteor.Error('Computed toggle',
|
||||
'Can\'t flip a toggle that is computed')
|
||||
}
|
||||
@@ -33,15 +32,15 @@ const flipToggle = new ValidatedMethod({
|
||||
|
||||
// Invert the current value, disabled is the canonical store of value
|
||||
const currentValue = !property.disabled;
|
||||
CreatureProperties.update(_id, {$set: {
|
||||
enabled: !currentValue,
|
||||
disabled: currentValue,
|
||||
}}, {
|
||||
selector: {type: 'toggle'},
|
||||
});
|
||||
|
||||
// Updating a toggle is likely to change the whole tree, do a full recompute
|
||||
computeCreature(rootCreature._id);
|
||||
CreatureProperties.update(_id, {
|
||||
$set: {
|
||||
enabled: !currentValue,
|
||||
disabled: currentValue,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: { type: 'toggle' },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -13,7 +12,7 @@ import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
|
||||
const insertProperty = new ValidatedMethod({
|
||||
name: 'creatureProperties.insert',
|
||||
validate: new SimpleSchema({
|
||||
validate: new SimpleSchema({
|
||||
creatureProperty: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
@@ -25,25 +24,25 @@ const insertProperty = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureProperty, parentRef}) {
|
||||
run({ creatureProperty, parentRef }) {
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
creatureProperty.parent = parentRef;
|
||||
creatureProperty.ancestors = ancestors;
|
||||
|
||||
return insertPropertyWork({
|
||||
return insertPropertyWork({
|
||||
property: creatureProperty,
|
||||
creature: rootCreature,
|
||||
});
|
||||
@@ -76,31 +75,31 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureProperty, creatureId, tag, tagDefaultName}) {
|
||||
run({ creatureProperty, creatureId, tag, tagDefaultName }) {
|
||||
let parentRef = getParentRefByTag(creatureId, tag);
|
||||
|
||||
if (!parentRef){
|
||||
if (!parentRef) {
|
||||
// Use the creature as the parent and mark that we need to insert the folder first later
|
||||
var insertFolderFirst = true;
|
||||
parentRef = {id: creatureId, collection: 'creatures'};
|
||||
parentRef = { id: creatureId, collection: 'creatures' };
|
||||
}
|
||||
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Add the folder first if we need to
|
||||
if (insertFolderFirst){
|
||||
if (insertFolderFirst) {
|
||||
let order = getHighestOrder({
|
||||
collection: CreatureProperties,
|
||||
ancestorId: parentRef.id,
|
||||
@@ -114,7 +113,7 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
order,
|
||||
});
|
||||
// Make the folder our new parent
|
||||
let newParentRef = {id, collection: 'creatureProperties'};
|
||||
let newParentRef = { id, collection: 'creatureProperties' };
|
||||
ancestors = [parentRef, newParentRef];
|
||||
parentRef = newParentRef;
|
||||
creatureProperty.order = order + 1;
|
||||
@@ -123,23 +122,22 @@ const insertPropertyAsChildOfTag = new ValidatedMethod({
|
||||
creatureProperty.parent = parentRef;
|
||||
creatureProperty.ancestors = ancestors;
|
||||
|
||||
return insertPropertyWork({
|
||||
return insertPropertyWork({
|
||||
property: creatureProperty,
|
||||
creature: rootCreature,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function insertPropertyWork({property, creature}){
|
||||
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,60 +5,59 @@ 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,
|
||||
getAncestry,
|
||||
renewDocIds
|
||||
setLineageOfDocs,
|
||||
getAncestry,
|
||||
renewDocIds
|
||||
} from '/imports/api/parenting/parenting.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
const insertPropertyFromLibraryNode = new ValidatedMethod({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
name: 'creatureProperties.insertPropertyFromLibraryNode',
|
||||
validate: new SimpleSchema({
|
||||
nodeIds: {
|
||||
type: Array,
|
||||
max: 20,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parentRef: {
|
||||
type: RefSchema,
|
||||
},
|
||||
'nodeIds.$': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
parentRef: {
|
||||
type: RefSchema,
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
}).validator(),
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({nodeIds, parentRef, order}) {
|
||||
// get the new ancestry for the properties
|
||||
let {parentDoc, ancestors} = getAncestry({parentRef});
|
||||
run({ nodeIds, parentRef, order }) {
|
||||
// get the new ancestry for the properties
|
||||
let { parentDoc, ancestors } = getAncestry({ parentRef });
|
||||
|
||||
// Check permission to edit
|
||||
// Check permission to edit
|
||||
let rootCreature;
|
||||
if (parentRef.collection === 'creatures'){
|
||||
if (parentRef.collection === 'creatures') {
|
||||
rootCreature = parentDoc;
|
||||
} else if (parentRef.collection === 'creatureProperties'){
|
||||
} else if (parentRef.collection === 'creatureProperties') {
|
||||
rootCreature = getRootCreatureAncestor(parentDoc);
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
} else {
|
||||
throw `${parentRef.collection} is not a valid parent collection`
|
||||
}
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// {libraryId: hasViewPermission}
|
||||
//let libraryPermissionMemoir = {};
|
||||
let node;
|
||||
nodeIds.forEach(nodeId => {
|
||||
nodeIds.forEach(nodeId => {
|
||||
// TODO: Check library view permission for each node before starting
|
||||
node = insertPropertyFromNode(nodeId, ancestors, order);
|
||||
});
|
||||
@@ -71,21 +70,18 @@ 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;
|
||||
},
|
||||
// Return the docId of the last property, the inserted root property
|
||||
return rootId;
|
||||
},
|
||||
});
|
||||
|
||||
function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
function insertPropertyFromNode(nodeId, ancestors, order) {
|
||||
// Fetch the library node and its decendents, provided they have not been
|
||||
// removed
|
||||
// TODO: Check permission to read the library this node is in
|
||||
let node = LibraryNodes.findOne({
|
||||
_id: nodeId,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
});
|
||||
if (!node) {
|
||||
if (Meteor.isClient) return;
|
||||
@@ -99,7 +95,7 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
let oldParent = node.parent;
|
||||
let nodes = LibraryNodes.find({
|
||||
'ancestors.id': nodeId,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
}).fetch();
|
||||
|
||||
// Convert all references into actual nodes
|
||||
@@ -122,11 +118,11 @@ function insertPropertyFromNode(nodeId, ancestors, order){
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({
|
||||
docArray: nodes,
|
||||
collectionMap: {'libraryNodes': 'creatureProperties'}
|
||||
collectionMap: { 'libraryNodes': 'creatureProperties' }
|
||||
});
|
||||
|
||||
// Order the root node
|
||||
if (order === undefined){
|
||||
if (order === undefined) {
|
||||
setDocToLastOrder({
|
||||
collection: CreatureProperties,
|
||||
doc: node,
|
||||
@@ -135,22 +131,30 @@ 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){
|
||||
function storeLibraryNodeReferences(nodes) {
|
||||
nodes.forEach(node => {
|
||||
if (node.libraryNodeId) return;
|
||||
node.libraryNodeId = node._id;
|
||||
});
|
||||
}
|
||||
|
||||
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){
|
||||
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
|
||||
depth += 1;
|
||||
// New nodes added this function
|
||||
let newNodes = [];
|
||||
@@ -161,9 +165,9 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
if (node.type !== 'reference') return true;
|
||||
|
||||
// We have gone too deep, keep the reference node as an error
|
||||
if (depth >= 10){
|
||||
if (depth >= 10) {
|
||||
if (Meteor.isClient) console.warn('Reference depth limit exceeded');
|
||||
node.cache = {error: 'Reference depth limit exceeded'};
|
||||
node.cache = { error: 'Reference depth limit exceeded' };
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -173,17 +177,17 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
referencedNode.order = node.order;
|
||||
// We are definitely replacing this node, so add it to the list
|
||||
visitedRefs.add(node._id);
|
||||
} catch (e){
|
||||
node.cache = {error: e.reason || e.message || e.toString()};
|
||||
} catch (e) {
|
||||
node.cache = { error: e.reason || e.message || e.toString() };
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get all the descendants of the referenced node
|
||||
let descendents = LibraryNodes.find({
|
||||
'ancestors.id': referencedNode._id,
|
||||
removed: {$ne: true},
|
||||
removed: { $ne: true },
|
||||
}, {
|
||||
sort: {order: 1},
|
||||
sort: { order: 1 },
|
||||
}).fetch();
|
||||
|
||||
// We are adding the referenced node and its descendants
|
||||
@@ -191,20 +195,20 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
|
||||
|
||||
// re-map all the ancestors to parent the new sub-tree into our existing
|
||||
// node tree
|
||||
setLineageOfDocs({
|
||||
docArray: addedNodes,
|
||||
newAncestry: node.ancestors,
|
||||
oldParent: referencedNode.parent,
|
||||
});
|
||||
setLineageOfDocs({
|
||||
docArray: addedNodes,
|
||||
newAncestry: node.ancestors,
|
||||
oldParent: referencedNode.parent,
|
||||
});
|
||||
|
||||
// Filter all the looped references
|
||||
addedNodes = addedNodes.filter(addedNode => {
|
||||
// Add all non-reference nodes
|
||||
if (addedNode.type !== 'reference'){
|
||||
if (addedNode.type !== 'reference') {
|
||||
return true;
|
||||
}
|
||||
// If this exact reference has already been resolved before, filter it out
|
||||
if (visitedRefs.has(addedNode._id)){
|
||||
if (visitedRefs.has(addedNode._id)) {
|
||||
return false;
|
||||
} else {
|
||||
// Otherwise mark it as visited, and keep it
|
||||
|
||||
@@ -3,34 +3,30 @@ 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',
|
||||
validate: null,
|
||||
name: 'creatureProperties.pull',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, itemId}){
|
||||
run({ _id, path, itemId }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
CreatureProperties.update(_id, {
|
||||
$pull: {[path.join('.')]: {_id: itemId}},
|
||||
}, {
|
||||
selector: {type: property.type},
|
||||
getAutoValues: false,
|
||||
});
|
||||
|
||||
// TODO figure out if this method can change deps or not
|
||||
computeCreature(rootCreature._id);
|
||||
// recomputePropertyDependencies(property);
|
||||
}
|
||||
CreatureProperties.update(_id, {
|
||||
$pull: { [path.join('.')]: { _id: itemId } },
|
||||
$set: { dirty: true }
|
||||
}, {
|
||||
selector: { type: property.type },
|
||||
getAutoValues: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default pullFromProperty;
|
||||
|
||||
@@ -3,20 +3,19 @@ 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({
|
||||
name: 'creatureProperties.push',
|
||||
validate: null,
|
||||
name: 'creatureProperties.push',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}){
|
||||
run({ _id, path, value }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
@@ -26,10 +25,10 @@ const pushToProperty = new ValidatedMethod({
|
||||
let schema = CreatureProperties.simpleSchema(property);
|
||||
let maxCount = schema.get(joinedPath, 'maxCount');
|
||||
|
||||
if (Number.isFinite(maxCount)){
|
||||
if (Number.isFinite(maxCount)) {
|
||||
let array = get(property, path);
|
||||
let currentCount = array ? array.length : 0;
|
||||
if (currentCount >= maxCount){
|
||||
if (currentCount >= maxCount) {
|
||||
throw new Meteor.Error(
|
||||
'Array is full',
|
||||
`Cannot have more than ${maxCount} values`
|
||||
@@ -38,15 +37,13 @@ const pushToProperty = new ValidatedMethod({
|
||||
}
|
||||
|
||||
// Do work
|
||||
CreatureProperties.update(_id, {
|
||||
$push: {[joinedPath]: value},
|
||||
}, {
|
||||
selector: {type: property.type},
|
||||
});
|
||||
|
||||
// TODO figure out if this method can change deps or not
|
||||
computeCreature(rootCreature._id);
|
||||
}
|
||||
CreatureProperties.update(_id, {
|
||||
$push: { [joinedPath]: value },
|
||||
$set: { dirty: true },
|
||||
}, {
|
||||
selector: { type: property.type },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default pushToProperty;
|
||||
|
||||
@@ -5,30 +5,32 @@ 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',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'creatureProperties.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
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 }
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default restoreProperty;
|
||||
|
||||
@@ -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',
|
||||
@@ -18,34 +17,29 @@ const selectAmmoItem = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({actionId, itemId, itemConsumedIndex}) {
|
||||
run({ actionId, itemId, itemConsumedIndex }) {
|
||||
// Permissions
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
let action = CreatureProperties.findOne(actionId);
|
||||
let rootCreature = getRootCreatureAncestor(action);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Check that this index has a document to edit
|
||||
let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
|
||||
if (!itemConsumed){
|
||||
if (!itemConsumed) {
|
||||
throw new Meteor.Error('Resouce not found',
|
||||
'Could not set ammo, because the ammo document was not found');
|
||||
}
|
||||
let itemToLink = CreatureProperties.findOne(itemId);
|
||||
if (!itemToLink){
|
||||
if (!itemToLink) {
|
||||
throw new Meteor.Error('Item not found',
|
||||
'Could not set ammo: the item was not found');
|
||||
}
|
||||
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,30 +5,26 @@ 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',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
name: 'creatureProperties.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id}){
|
||||
run({ _id }) {
|
||||
// Permissions
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let property = CreatureProperties.findOne(_id);
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
|
||||
// Do work
|
||||
softRemove({_id, collection: CreatureProperties});
|
||||
|
||||
// Changes dependency tree by removing children
|
||||
computeCreature(rootCreature._id);
|
||||
}
|
||||
softRemove({ _id, collection: CreatureProperties });
|
||||
}
|
||||
});
|
||||
|
||||
export default softRemoveProperty;
|
||||
|
||||
@@ -3,32 +3,31 @@ 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',
|
||||
validate({_id, path}){
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]){
|
||||
case 'type':
|
||||
validate({ _id, path }) {
|
||||
if (!_id) throw new Meteor.Error('No _id', '_id is required');
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]) {
|
||||
case 'type':
|
||||
case 'order':
|
||||
case 'parent':
|
||||
case 'ancestors':
|
||||
case 'damage':
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'This property can\'t be updated directly');
|
||||
}
|
||||
case 'damage':
|
||||
throw new Meteor.Error('Permission denied',
|
||||
'This property can\'t be updated directly');
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
run({ _id, path, value }) {
|
||||
// Permission
|
||||
let property = CreatureProperties.findOne(_id, {
|
||||
fields: {type: 1, ancestors: 1}
|
||||
fields: { type: 1, ancestors: 1 }
|
||||
});
|
||||
let rootCreature = getRootCreatureAncestor(property);
|
||||
assertEditPermission(rootCreature, this.userId);
|
||||
@@ -36,18 +35,14 @@ const updateCreatureProperty = new ValidatedMethod({
|
||||
let pathString = path.join('.');
|
||||
let modifier;
|
||||
// unset empty values
|
||||
if (value === null || value === undefined){
|
||||
modifier = {$unset: {[pathString]: 1}};
|
||||
if (value === null || value === undefined) {
|
||||
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);
|
||||
CreatureProperties.update(_id, modifier, {
|
||||
selector: { type: property.type },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||
* Recomputes all ancestor creatures of this property
|
||||
*/
|
||||
export default function recomputeCreaturesByProperty(property){
|
||||
for (let ref of property.ancestors){
|
||||
if (ref.collection === 'creatures') {
|
||||
computeCreature.call(ref.id);
|
||||
}
|
||||
}
|
||||
for (let ref of property.ancestors){
|
||||
if (ref.collection === 'creatures') {
|
||||
computeCreature.call(ref.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;
|
||||
@@ -8,21 +8,21 @@ import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
let Creatures = new Mongo.Collection('creatures');
|
||||
|
||||
let CreatureSettingsSchema = new SimpleSchema({
|
||||
//slowed down by carrying too much?
|
||||
useVariantEncumbrance: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide spellcasting tab
|
||||
hideSpellcasting: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//slowed down by carrying too much?
|
||||
useVariantEncumbrance: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide spellcasting tab
|
||||
hideSpellcasting: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Hide all the unused stats
|
||||
hideUnusedStats: {
|
||||
type: Boolean,
|
||||
@@ -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,
|
||||
@@ -53,73 +58,99 @@ let CreatureSettingsSchema = new SimpleSchema({
|
||||
});
|
||||
|
||||
let CreatureSchema = new SimpleSchema({
|
||||
// Strings
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: '',
|
||||
optional: true,
|
||||
// Strings
|
||||
name: {
|
||||
type: String,
|
||||
defaultValue: '',
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
alignment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
alignment: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
picture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.url,
|
||||
},
|
||||
},
|
||||
avatarPicture: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.url,
|
||||
},
|
||||
// Mechanics
|
||||
deathSave: {
|
||||
type: deathSaveSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
|
||||
// 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,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Stats that are computed and denormalised outside of recomputation
|
||||
denormalizedStats: {
|
||||
type: Object,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Sum of all XP gained by this character
|
||||
'denormalizedStats.xp': {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
'denormalizedStats.xp': {
|
||||
type: SimpleSchema.Integer,
|
||||
defaultValue: 0,
|
||||
},
|
||||
// Sum of all levels granted by milestone XP
|
||||
'denormalizedStats.milestoneLevels': {
|
||||
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,
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
defaultValue: 'pc',
|
||||
allowedValues: ['pc', 'npc', 'monster'],
|
||||
},
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
defaultValue: 'pc',
|
||||
allowedValues: ['pc', 'npc', 'monster'],
|
||||
},
|
||||
damageMultipliers: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
variables: {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
defaultValue: {}
|
||||
},
|
||||
computeErrors: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
@@ -130,7 +161,7 @@ let CreatureSchema = new SimpleSchema({
|
||||
'computeErrors.$.type': {
|
||||
type: String,
|
||||
},
|
||||
'computeErrors.$.details' : {
|
||||
'computeErrors.$.details': {
|
||||
type: Object,
|
||||
blackbox: true,
|
||||
optional: true,
|
||||
@@ -147,11 +178,11 @@ let CreatureSchema = new SimpleSchema({
|
||||
optional: true,
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
type: CreatureSettingsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
// Settings
|
||||
settings: {
|
||||
type: CreatureSettingsSchema,
|
||||
defaultValue: {},
|
||||
},
|
||||
});
|
||||
|
||||
CreatureSchema.extend(ColorSchema);
|
||||
@@ -160,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,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,57 +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 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');
|
||||
'You need to be logged in to insert a creature');
|
||||
}
|
||||
|
||||
assertHasCharactersSlots(this.userId);
|
||||
assertHasCharactersSlots(userId);
|
||||
|
||||
// Create the creature document
|
||||
// Create the creature document
|
||||
let creatureId = Creatures.insert({
|
||||
owner: this.userId,
|
||||
});
|
||||
|
||||
// Insert the default properties
|
||||
// Not batchInsert because we want the properties cleaned by the schema
|
||||
let baseId;
|
||||
defaultCharacterProperties(creatureId).forEach(prop => {
|
||||
let id = CreatureProperties.insert(prop);
|
||||
if (prop.name === 'Ruleset'){
|
||||
baseId = id;
|
||||
}
|
||||
owner: userId,
|
||||
name,
|
||||
gender,
|
||||
alignment,
|
||||
allowedLibraries,
|
||||
allowedLibraryCollections,
|
||||
});
|
||||
|
||||
if (Meteor.isServer){
|
||||
// Insert the 5e ruleset as the default base
|
||||
insertPropertyFromLibraryNode.call({
|
||||
nodeIds: ['iHbhfcg3AL5isSWbw'],
|
||||
parentRef: {id: baseId, collection: 'creatureProperties'},
|
||||
order: 0.5,
|
||||
// Insert experience to get character to starting level
|
||||
if (startingLevel) {
|
||||
insertExperienceForCreature({
|
||||
experience: {
|
||||
name: 'Starting level',
|
||||
levels: startingLevel,
|
||||
creatureId
|
||||
},
|
||||
creatureId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
return creatureId;
|
||||
// Insert the default properties
|
||||
// Not batchInsert because we want the properties cleaned by the schema
|
||||
let baseId, rulesetSlot;
|
||||
defaultCharacterProperties(creatureId).forEach(prop => {
|
||||
let id = CreatureProperties.insert(prop);
|
||||
if (prop.name === 'Ruleset') {
|
||||
baseId = id;
|
||||
rulesetSlot = prop;
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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 { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
|
||||
|
||||
const updateCreature = new ValidatedMethod({
|
||||
name: 'creatures.update',
|
||||
validate({_id, path}){
|
||||
if (!_id) return false;
|
||||
// Allowed fields
|
||||
let allowedFields = [
|
||||
validate({ _id, path }) {
|
||||
if (!_id) return false;
|
||||
// Allowed fields
|
||||
let allowedFields = [
|
||||
'name',
|
||||
'alignment',
|
||||
'gender',
|
||||
@@ -17,26 +17,26 @@ const updateCreature = new ValidatedMethod({
|
||||
'color',
|
||||
'settings',
|
||||
];
|
||||
if (!allowedFields.includes(path[0])){
|
||||
throw new Meteor.Error('Creatures.methods.update.denied',
|
||||
'This field can\'t be updated using this method');
|
||||
}
|
||||
if (!allowedFields.includes(path[0])) {
|
||||
throw new Meteor.Error('Creatures.methods.update.denied',
|
||||
'This field can\'t be updated using this method');
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({_id, path, value}) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
run({ _id, path, value }) {
|
||||
let creature = Creatures.findOne(_id);
|
||||
assertEditPermission(creature, this.userId);
|
||||
if (value === undefined || value === null){
|
||||
if (value === undefined || value === null) {
|
||||
Creatures.update(_id, {
|
||||
$unset: {[path.join('.')]: 1},
|
||||
$unset: { [path.join('.')]: 1 },
|
||||
});
|
||||
} else {
|
||||
Creatures.update(_id, {
|
||||
$set: {[path.join('.')]: value},
|
||||
$set: { [path.join('.')]: value },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,23 +3,22 @@ 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');
|
||||
|
||||
let ExperienceSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// The amount of XP this experience gives
|
||||
xp: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
},
|
||||
// The amount of XP this experience gives
|
||||
xp: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
// Setting levels instead of value grants whole levels
|
||||
levels: {
|
||||
type: SimpleSchema.Integer,
|
||||
@@ -27,17 +26,17 @@ let ExperienceSchema = new SimpleSchema({
|
||||
min: 0,
|
||||
index: 1,
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -47,21 +46,21 @@ 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
|
||||
}});
|
||||
const insertExperienceForCreature = function ({ experience, creatureId }) {
|
||||
if (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;
|
||||
};
|
||||
|
||||
@@ -85,15 +84,16 @@ const insertExperience = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({experience, creatureIds}) {
|
||||
run({ experience, creatureIds }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.insert.denied',
|
||||
'You need to be logged in to insert an experience');
|
||||
'You need to be logged in to insert an experience');
|
||||
}
|
||||
let insertedIds = [];
|
||||
creatureIds.forEach(creatureId => {
|
||||
let id = insertExperienceForCreature({experience, creatureId, userId});
|
||||
assertEditPermission(creatureId, userId);
|
||||
let id = insertExperienceForCreature({ experience, creatureId, userId });
|
||||
insertedIds.push(id);
|
||||
});
|
||||
return insertedIds;
|
||||
@@ -113,29 +113,30 @@ const removeExperience = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({experienceId}) {
|
||||
run({ experienceId }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.remove.denied',
|
||||
'You need to be logged in to remove an experience');
|
||||
'You need to be logged in to remove an experience');
|
||||
}
|
||||
let experience = Experiences.findOne(experienceId);
|
||||
if (!experience) return;
|
||||
let creatureId = experience.creatureId
|
||||
assertEditPermission(creatureId, userId);
|
||||
if (experience.xp){
|
||||
Creatures.update(creatureId, {$inc: {
|
||||
'denormalizedStats.xp': -experience.xp
|
||||
}});
|
||||
if (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;
|
||||
},
|
||||
});
|
||||
@@ -153,11 +154,11 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({creatureId}) {
|
||||
run({ creatureId }) {
|
||||
let userId = this.userId;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Experiences.methods.recompute.denied',
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
'You need to be logged in to recompute a creature\'s experiences');
|
||||
}
|
||||
assertEditPermission(creatureId, userId);
|
||||
|
||||
@@ -166,18 +167,20 @@ const recomputeExperiences = new ValidatedMethod({
|
||||
Experiences.find({
|
||||
creatureId
|
||||
}, {
|
||||
fields: {xp: 1, levels: 1}
|
||||
fields: { xp: 1, levels: 1 }
|
||||
}).forEach(experience => {
|
||||
xp += experience.xp || 0;
|
||||
milestoneLevels += experience.levels || 0;
|
||||
});
|
||||
Creatures.update(creatureId, {$set: {
|
||||
'denormalizedStats.xp': xp,
|
||||
'denormalizedStats.milestoneLevels': milestoneLevels
|
||||
}});
|
||||
computeCreature(creatureId);
|
||||
Creatures.update(creatureId, {
|
||||
$set: {
|
||||
'denormalizedStats.xp': xp,
|
||||
'denormalizedStats.milestoneLevels': milestoneLevels,
|
||||
dirty: true,
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default Experiences;
|
||||
export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences };
|
||||
export { ExperienceSchema, insertExperience, insertExperienceForCreature, removeExperience, recomputeExperiences };
|
||||
|
||||
@@ -2,33 +2,33 @@ import SimpleSchema from 'simpl-schema';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
let ExperienceSchema = new SimpleSchema({
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
title: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
// Potentially long description of the event
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// Potentially long description of the event
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
// The real-world date that it occured
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
// The date in-world of this event
|
||||
worldDate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// The real-world date that it occured
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
},
|
||||
// The date in-world of this event
|
||||
worldDate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.name,
|
||||
},
|
||||
},
|
||||
// Tags to better find this entry later
|
||||
tags: {
|
||||
type: Array,
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
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';
|
||||
import {assertEditPermission} from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import {assertUserInTabletop} from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { assertUserInTabletop } from '/imports/api/tabletop/methods/shared/tabletopPermissions.js';
|
||||
|
||||
import {parse, prettifyParseError} from '/imports/parser/parser.js';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
||||
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
|
||||
}
|
||||
|
||||
@@ -26,17 +27,17 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
'content.$': {
|
||||
type: LogContentSchema,
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function() {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
// The real-world date that it occured, usually sorted by date
|
||||
date: {
|
||||
type: Date,
|
||||
autoValue: function () {
|
||||
// If the date isn't set, set it to now
|
||||
if (!this.isSet) {
|
||||
return new Date();
|
||||
}
|
||||
},
|
||||
index: 1,
|
||||
},
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
@@ -57,23 +58,23 @@ let CreatureLogSchema = new SimpleSchema({
|
||||
|
||||
CreatureLogs.attachSchema(CreatureLogSchema);
|
||||
|
||||
function removeOldLogs(creatureId){
|
||||
function removeOldLogs(creatureId) {
|
||||
// Find the first log that is over the limit
|
||||
let firstExpiredLog = CreatureLogs.find({
|
||||
creatureId
|
||||
}, {
|
||||
sort: {date: -1},
|
||||
sort: { date: -1 },
|
||||
skip: PER_CREATURE_LOG_LIMIT,
|
||||
});
|
||||
if (!firstExpiredLog) return;
|
||||
// Remove all logs older than the one over the limit
|
||||
CreatureLogs.remove({
|
||||
creatureId,
|
||||
date: {$lte: firstExpiredLog.date},
|
||||
date: { $lte: firstExpiredLog.date },
|
||||
});
|
||||
}
|
||||
|
||||
function logToMessageData(log){
|
||||
function logToMessageData(log) {
|
||||
let embed = {
|
||||
fields: [],
|
||||
};
|
||||
@@ -85,8 +86,8 @@ function logToMessageData(log){
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
|
||||
function logWebhook({log, creature}){
|
||||
if (Meteor.isServer){
|
||||
function logWebhook({ log, creature }) {
|
||||
if (Meteor.isServer) {
|
||||
sendWebhookAsCreature({
|
||||
creature,
|
||||
data: logToMessageData(log),
|
||||
@@ -101,23 +102,25 @@ const insertCreatureLog = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({log}){
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({ log }) {
|
||||
const creatureId = log.creatureId;
|
||||
const creature = Creatures.findOne(creatureId, {fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
tabletop: 1,
|
||||
}});
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
tabletop: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
// Build the new log
|
||||
let id = insertCreatureLogWork({log, creature, method: this})
|
||||
let id = insertCreatureLogWork({ log, creature, method: this })
|
||||
return id;
|
||||
},
|
||||
});
|
||||
@@ -129,35 +132,35 @@ const insertTabletopLog = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({log}){
|
||||
validate: new SimpleSchema({
|
||||
log: CreatureLogSchema.omit('date'),
|
||||
}).validator(),
|
||||
run({ log }) {
|
||||
const tabletopId = log.tabletopId;
|
||||
assertUserInTabletop(tabletopId, this.userId);
|
||||
// Build the new log
|
||||
let id = insertCreatureLogWork({log, method: this})
|
||||
let id = insertCreatureLogWork({ log, method: this })
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export function insertCreatureLogWork({log, creature, method}){
|
||||
export function insertCreatureLogWork({ log, creature, method }) {
|
||||
// Build the new log
|
||||
if (typeof log === 'string'){
|
||||
log = {content: [{value: log}]};
|
||||
if (typeof log === 'string') {
|
||||
log = { content: [{ value: log }] };
|
||||
}
|
||||
if (!log.content?.length) return;
|
||||
log.date = new Date();
|
||||
if (creature) log.tabletopId = creature.tabletop;
|
||||
// Insert it
|
||||
let id = CreatureLogs.insert(log);
|
||||
if (Meteor.isServer){
|
||||
if (Meteor.isServer) {
|
||||
method?.unblock();
|
||||
if (creature){
|
||||
if (creature) {
|
||||
removeOldLogs(creature._id);
|
||||
logWebhook({log, creature});
|
||||
logWebhook({ log, creature });
|
||||
}
|
||||
if (log.tabletopId){
|
||||
if (log.tabletopId) {
|
||||
// Todo remove old tabletop logs
|
||||
// Log webhook if it's different to creature webhook
|
||||
}
|
||||
@@ -166,9 +169,9 @@ export function insertCreatureLogWork({log, creature, method}){
|
||||
}
|
||||
|
||||
|
||||
function equalIgnoringWhitespace(a, b){
|
||||
function equalIgnoringWhitespace(a, b) {
|
||||
if (typeof a !== 'string' || typeof b !== 'string') return a === b;
|
||||
return a.replace(/\s/g,'') === b.replace(/\s/g, '');
|
||||
return a.replace(/\s/g, '') === b.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
const logRoll = new ValidatedMethod({
|
||||
@@ -178,39 +181,41 @@ const logRoll = new ValidatedMethod({
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
validate: new SimpleSchema({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
run({roll, creatureId}){
|
||||
const creature = Creatures.findOne(creatureId, {fields: {
|
||||
variables: 1,
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}});
|
||||
validate: new SimpleSchema({
|
||||
roll: {
|
||||
type: String,
|
||||
},
|
||||
creatureId: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
}).validator(),
|
||||
run({ roll, creatureId }) {
|
||||
const creature = Creatures.findOne(creatureId, {
|
||||
fields: {
|
||||
readers: 1,
|
||||
writers: 1,
|
||||
owner: 1,
|
||||
'settings.discordWebhook': 1,
|
||||
name: 1,
|
||||
avatarPicture: 1,
|
||||
}
|
||||
});
|
||||
assertEditPermission(creature, this.userId);
|
||||
const variables = CreatureVariables.findOne({ _creatureId: creatureId });
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
try {
|
||||
parsedResult = parse(roll);
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
let error = prettifyParseError(e);
|
||||
logContent.push({name: 'Parse Error', value: error});
|
||||
logContent.push({ name: 'Parse Error', value: error });
|
||||
}
|
||||
if (parsedResult) try {
|
||||
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
|
||||
@@ -218,19 +223,19 @@ 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
|
||||
});
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
logContent = [{name: 'Calculation error'}];
|
||||
logContent = [{ name: 'Calculation error' }];
|
||||
}
|
||||
const log = {
|
||||
content: logContent,
|
||||
@@ -238,11 +243,11 @@ const logRoll = new ValidatedMethod({
|
||||
date: new Date(),
|
||||
};
|
||||
|
||||
let id = insertCreatureLogWork({log, creature, method: this});
|
||||
let id = insertCreatureLogWork({ log, creature, method: this });
|
||||
|
||||
return id;
|
||||
},
|
||||
});
|
||||
|
||||
export default CreatureLogs;
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, insertTabletopLog};
|
||||
export { CreatureLogSchema, insertCreatureLog, logRoll, insertTabletopLog, PER_CREATURE_LOG_LIMIT };
|
||||
|
||||
Reference in New Issue
Block a user