diff --git a/app/imports/api/creature/CreatureProperties.js b/app/imports/api/creature/CreatureProperties.js
deleted file mode 100644
index 89ed39bc..00000000
--- a/app/imports/api/creature/CreatureProperties.js
+++ /dev/null
@@ -1,609 +0,0 @@
-import { Meteor } from 'meteor/meteor';
-import { Mongo } from 'meteor/mongo';
-import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
-import { ValidatedMethod } from 'meteor/mdg:validated-method';
-import SimpleSchema from 'simpl-schema';
-import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
-import ChildSchema, { RefSchema } from '/imports/api/parenting/ChildSchema.js';
-import { recomputeCreature } from '/imports/api/creature/computation/recomputeCreature.js';
-import LibraryNodes from '/imports/api/library/LibraryNodes.js';
-import Creatures from '/imports/api/creature/Creatures.js';
-import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
-import { softRemove, restore } from '/imports/api/parenting/softRemove.js';
-import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
-import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
-import {
- setLineageOfDocs,
- getAncestry,
- renewDocIds
-} from '/imports/api/parenting/parenting.js';
-import {setDocToLastOrder} from '/imports/api/parenting/order.js';
-import { storedIconsSchema } from '/imports/api/icons/Icons.js';
-import { reorderDocs } from '/imports/api/parenting/order.js';
-
-import '/imports/api/creature/actions/doAction.js';
-import '/imports/api/creature/actions/castSpellWithSlot.js';
-import '/imports/api/creature/creatureProperties/manageEquipment.js';
-
-let CreatureProperties = new Mongo.Collection('creatureProperties');
-
-let CreaturePropertySchema = new SimpleSchema({
- type: {
- type: String,
- allowedValues: Object.keys(propertySchemasIndex),
- },
- tags: {
- type: Array,
- defaultValue: [],
- },
- 'tags.$': {
- type: String,
- },
- disabled: {
- type: Boolean,
- optional: true,
- },
- icon: {
- type: storedIconsSchema,
- optional: true,
- },
- // Denormalised flag if this property is inactive on the sheet for any reason
- // Including being disabled, or a decendent of a disabled property
- inactive: {
- type: Boolean,
- optional: true,
- index: 1,
- },
- // Denormalised flag if this property was made inactive by an inactive
- // ancestor. True if this property has an inactive ancestor even if this
- // property is itself inactive
- deactivatedByAncestor: {
- type: Boolean,
- optional: true,
- index: 1,
- },
- // Denormalised list of all properties or creatures this property depends on
- dependencies: {
- type: Array,
- defaultValue: [],
- index: 1,
- },
- 'dependencies.$': {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- },
-});
-
-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}
- });
-}
-
-export function getCreature(property){
- if (!property) throw new Meteor.Error('No property provided');
- let creature = Creatures.findOne(property.ancestors[0].id);
- if (!creature) throw new Meteor.Error('Creature does not exist');
- return creature;
-}
-
-function assertPropertyEditPermission(property, userId){
- let creature = getCreature(property);
- return assertEditPermission(creature, userId);
-}
-
-function recomputeCreatures(property){
- for (let ref of property.ancestors){
- if (ref.collection === 'creatures') {
- reorderDocs({collection: CreatureProperties, ancestorId: ref.id});
- recomputeCreature.call({charId: ref.id});
- }
- }
-}
-
-const insertProperty = new ValidatedMethod({
- name: 'creatureProperties.insert',
- validate: null,
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({creatureProperty}) {
- delete creatureProperty._id;
- assertPropertyEditPermission(creatureProperty, this.userId);
- let _id = CreatureProperties.insert(creatureProperty);
- let property = CreatureProperties.findOne(_id);
- recomputeCreatures(property);
- },
-});
-
-const duplicateProperty = new ValidatedMethod({
- name: 'creatureProperties.duplicate',
- validate: new SimpleSchema({
- _id: {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- }
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({_id}) {
- let creatureProperty = CreatureProperties.findOne(_id);
- assertPropertyEditPermission(creatureProperty, this.userId);
- delete creatureProperty._id;
- CreatureProperties.insert(creatureProperty);
- recomputeCreatures(creatureProperty);
- },
-});
-
-const insertPropertyFromLibraryNode = new ValidatedMethod({
- name: 'creatureProperties.insertPropertyFromLibraryNode',
- validate: new SimpleSchema({
- nodeId: {
- type: String,
- regEx: SimpleSchema.RegEx.Id,
- },
- parentRef: {
- type: RefSchema,
- },
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({nodeId, parentRef}) {
- // get the new ancestry for the properties
- let {parentDoc, ancestors} = getAncestry({parentRef});
-
- // Check permission to edit
- if (parentRef.collection === 'creatures'){
- assertEditPermission(parentDoc, this.userId);
- } else if (parentRef.collection === 'creatureProperties'){
- assertPropertyEditPermission(parentDoc, this.userId);
- } else {
- throw `${parentRef.collection} is not a valid parent collection`
- }
-
- // Fetch the library node and its decendents, provided they have not been
- // removed
- let node = LibraryNodes.findOne({
- _id: nodeId,
- removed: {$ne: true},
- });
- if (!node) throw `Node not found for nodeId: ${nodeId}`;
- let oldParent = node.parent;
- let nodes = LibraryNodes.find({
- 'ancestors.id': nodeId,
- removed: {$ne: true},
- }).fetch();
- // The root node is last in the array of nodes
- nodes.push(node);
-
- // re-map all the ancestors
- setLineageOfDocs({
- docArray: nodes,
- newAncestry: ancestors,
- oldParent,
- });
-
- // Give the docs new IDs without breaking internal references
- renewDocIds({
- docArray: nodes,
- collectionMap: {'libraryNodes': 'creatureProperties'}
- });
-
- // Order the root node
- setDocToLastOrder({
- collection: CreatureProperties,
- doc: node,
- });
-
- // Insert the creature properties
- let insertedDocIds = CreatureProperties.batchInsert(nodes);
-
- // get the root inserted doc
- let rootId = insertedDocIds[insertedDocIds.length - 1];
-
- // Recompute the creatures doc was attached to
- recomputeCreatures(node);
-
- // Return the docId of the last property, the inserted root property
- return rootId;
- },
-})
-
-const updateProperty = 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':
- case 'order':
- case 'parent':
- case 'ancestors':
- 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}) {
- let property = CreatureProperties.findOne(_id);
- assertPropertyEditPermission(property, this.userId);
- let pathString = path.join('.');
- let modifier;
- // unset empty values
- if (value === null || value === undefined){
- modifier = {$unset: {[pathString]: 1}};
- } else {
- modifier = {$set: {[pathString]: value}};
- }
- CreatureProperties.update(_id, modifier, {
- selector: {type: property.type},
- });
- recomputeCreatures(property);
- },
-});
-
-export function damagePropertyWork({property, operation, value}){
- if (operation === 'set'){
- let currentValue = property.value;
- // Set represents what we want the value to be after damage
- // So we need the actual damage to get to that value
- let damage = currentValue - value;
- // Damage can't exceed total value
- if (damage > currentValue) damage = currentValue;
- // Damage must be positive
- if (damage < 0) damage = 0;
- CreatureProperties.update(property._id, {
- $set: {damage}
- }, {
- selector: property
- });
- return currentValue - damage;
- } else if (operation === 'increment'){
- let currentValue = property.value - (property.damage || 0);
- let currentDamage = property.damage;
- let increment = value;
- // Can't increase damage above the remaining value
- if (increment > currentValue) increment = currentValue;
- // Can't decrease damage below zero
- if (-increment > currentDamage) increment = -currentDamage;
- CreatureProperties.update(property._id, {
- $inc: {damage: increment}
- }, {
- selector: property
- });
- return increment;
- }
-}
-
-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: {
- damageMultipliers: 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: property, operation, value})
- });
- recomputeCreature.call({charId: creatureId});
- }
-})
-
-const damageProperty = new ValidatedMethod({
- name: 'creatureProperties.damage',
- validate: new SimpleSchema({
- _id: SimpleSchema.RegEx.Id,
- operation: {
- type: String,
- allowedValues: ['set', 'increment']
- },
- value: Number,
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 20,
- timeInterval: 5000,
- },
- run({_id, operation, value}) {
- let currentProperty = CreatureProperties.findOne(_id);
- // Check permissions
- assertPropertyEditPermission(currentProperty, this.userId);
- // Check if property can take damage
- let schema = CreatureProperties.simpleSchema(currentProperty);
- if (!schema.allowsKey('damage')){
- throw new Meteor.Error(
- 'Damage property failed',
- `Property of type "${currentProperty.type}" can't be damaged`
- );
- }
- damagePropertyWork({property: currentProperty, operation, value})
- recomputeCreatures(currentProperty);
- },
-});
-
-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}) {
- let creature = Creatures.findOne(creatureId, {
- fields: {
- damageMultipliers: 1,
- owner: 1,
- readers: 1,
- writers: 1,
- },
- });
- // Check permissions
- assertEditPermission(creature, this.userId);
- let healthBars = CreatureProperties.find({
- 'ancestors.id': creatureId,
- 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);
- let damageLeft = totalDamage;
- if (damageType === 'healing') damageLeft = -totalDamage;
- healthBars.forEach(healthBar => {
- if (damageLeft === 0) return;
- let damageAdded = damagePropertyWork({
- property: healthBar,
- operation: 'increment',
- value: damageLeft,
- });
- damageLeft -= damageAdded;
- });
- recomputeCreature.call({charId: creatureId});
- return totalDamage;
- },
-});
-
-export function adjustQuantityWork({property, operation, value}){
- // Check if property has quantity
- let schema = CreatureProperties.simpleSchema(property);
- if (!schema.allowsKey('quantity')){
- throw new Meteor.Error(
- 'Adjust quantity failed',
- `Property of type "${property.type}" doesn't have a quantity`
- );
- }
- if (operation === 'set'){
- CreatureProperties.update(property._id, {
- $set: {quantity: value}
- }, {
- selector: property
- });
- } 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}
- }, {
- selector: property
- });
- }
-}
-
-const adjustQuantity = new ValidatedMethod({
- name: 'creatureProperties.adjustQuantity',
- validate: new SimpleSchema({
- _id: SimpleSchema.RegEx.Id,
- operation: {
- type: String,
- allowedValues: ['set', 'increment']
- },
- value: Number,
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({_id, operation, value}) {
- let currentProperty = CreatureProperties.findOne(_id);
- // Check permissions
- assertPropertyEditPermission(currentProperty, this.userId);
- adjustQuantityWork({property: currentProperty, operation, value});
- recomputeCreatures(currentProperty);
- },
-});
-
-const selectAmmoItem = new ValidatedMethod({
- name: 'creatureProperties.selectAmmoItem',
- validate: new SimpleSchema({
- actionId: SimpleSchema.RegEx.Id,
- itemId: SimpleSchema.RegEx.Id,
- itemConsumedIndex: Number,
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({actionId, itemId, itemConsumedIndex}) {
- let action = CreatureProperties.findOne(actionId);
- // Check permissions
- assertPropertyEditPermission(action, this.userId);
- // Check that this index has a document to edit
- let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
- 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){
- 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}
- }, {
- selector: action,
- });
- recomputeCreatures(action);
- },
-});
-
-const pushToProperty = new ValidatedMethod({
- name: 'creatureProperties.push',
- validate: null,
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({_id, path, value}){
- let property = CreatureProperties.findOne(_id);
- assertPropertyEditPermission(property, this.userId);
- CreatureProperties.update(_id, {
- $push: {[path.join('.')]: value},
- }, {
- selector: {type: property.type},
- });
- recomputeCreatures(property);
- }
-});
-
-const pullFromProperty = new ValidatedMethod({
- name: 'creatureProperties.pull',
- validate: null,
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({_id, path, itemId}){
- let property = CreatureProperties.findOne(_id);
- assertPropertyEditPermission(property, this.userId);
- CreatureProperties.update(_id, {
- $pull: {[path.join('.')]: {_id: itemId}},
- }, {
- selector: {type: property.type},
- getAutoValues: false,
- });
- recomputeCreatures(property);
- }
-});
-
-const softRemoveProperty = new ValidatedMethod({
- name: 'creatureProperties.softRemove',
- validate: new SimpleSchema({
- _id: SimpleSchema.RegEx.Id
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({_id}){
- let property = CreatureProperties.findOne(_id);
- assertPropertyEditPermission(property, this.userId);
- softRemove({_id, collection: CreatureProperties});
- recomputeCreatures(property);
- }
-});
-
-const restoreProperty = new ValidatedMethod({
- name: 'creatureProperties.restore',
- validate: new SimpleSchema({
- _id: SimpleSchema.RegEx.Id
- }).validator(),
- mixins: [RateLimiterMixin],
- rateLimit: {
- numRequests: 5,
- timeInterval: 5000,
- },
- run({_id}){
- let property = CreatureProperties.findOne(_id);
- assertPropertyEditPermission(property, this.userId);
- restore({_id, collection: CreatureProperties});
- recomputeCreatures(property);
- }
-});
-
-export default CreatureProperties;
-export {
- CreaturePropertySchema,
- insertProperty,
- duplicateProperty,
- insertPropertyFromLibraryNode,
- updateProperty,
- dealDamage,
- damagePropertiesByName,
- damageProperty,
- adjustQuantity,
- selectAmmoItem,
- pushToProperty,
- pullFromProperty,
- softRemoveProperty,
- restoreProperty,
-};
diff --git a/app/imports/api/creature/Creatures.js b/app/imports/api/creature/Creatures.js
index c8154b84..633e9be9 100644
--- a/app/imports/api/creature/Creatures.js
+++ b/app/imports/api/creature/Creatures.js
@@ -4,7 +4,7 @@ import SimpleSchema from 'simpl-schema';
import deathSaveSchema from '/imports/api/properties/subSchemas/DeathSavesSchema.js'
import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
import SharingSchema from '/imports/api/sharing/SharingSchema.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import {assertEditPermission} from '/imports/api/sharing/sharingPermissions.js';
import { assertUserHasPaidBenefits } from '/imports/api/users/patreon/tiers.js';
diff --git a/app/imports/api/creature/actions/applyAdjustment.js b/app/imports/api/creature/actions/applyAdjustment.js
index d63020bb..bdd8d21c 100644
--- a/app/imports/api/creature/actions/applyAdjustment.js
+++ b/app/imports/api/creature/actions/applyAdjustment.js
@@ -1,6 +1,6 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
-import { damagePropertiesByName } from '/imports/api/creature/CreatureProperties.js';
+import damagePropertiesByName from '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
export default function applyAdjustment({
prop,
diff --git a/app/imports/api/creature/actions/applyBuff.js b/app/imports/api/creature/actions/applyBuff.js
index 6b55f617..2bde6c85 100644
--- a/app/imports/api/creature/actions/applyBuff.js
+++ b/app/imports/api/creature/actions/applyBuff.js
@@ -3,7 +3,7 @@ import {
renewDocIds
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function applyBuff({
prop,
diff --git a/app/imports/api/creature/actions/applyDamage.js b/app/imports/api/creature/actions/applyDamage.js
index 47a0648c..5c3fc3a1 100644
--- a/app/imports/api/creature/actions/applyDamage.js
+++ b/app/imports/api/creature/actions/applyDamage.js
@@ -1,6 +1,6 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
-import { dealDamage } from '/imports/api/creature/CreatureProperties.js';
+import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
export default function applyDamage({
prop,
diff --git a/app/imports/api/creature/actions/castSpellWithSlot.js b/app/imports/api/creature/actions/castSpellWithSlot.js
index c8b53864..9b97b8e2 100644
--- a/app/imports/api/creature/actions/castSpellWithSlot.js
+++ b/app/imports/api/creature/actions/castSpellWithSlot.js
@@ -1,7 +1,8 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
-import CreatureProperties, { getCreature, damagePropertyWork } from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties, { getCreature } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
import { doActionWork } from '/imports/api/creature/actions/doAction.js';
diff --git a/app/imports/api/creature/actions/doAction.js b/app/imports/api/creature/actions/doAction.js
index 1353631b..d118d9f6 100644
--- a/app/imports/api/creature/actions/doAction.js
+++ b/app/imports/api/creature/actions/doAction.js
@@ -1,7 +1,9 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
-import CreatureProperties, { getCreature } from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import Creatures from '/imports/api/creature/Creatures.js';
+import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
@@ -25,11 +27,11 @@ const doAction = new ValidatedMethod({
run({actionId, targetId}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
- let creature = getCreature(action);
+ let creature = getRootCreatureAncestor(action);
assertEditPermission(creature, this.userId);
let target = undefined;
if (targetId) {
- target = getCreature(targetId);
+ target = Creatures.findOne(targetId);
assertEditPermission(target, this.userId);
}
doActionWork({action, creature, target});
diff --git a/app/imports/api/creature/actions/spendResources.js b/app/imports/api/creature/actions/spendResources.js
index b4409866..1de6bc61 100644
--- a/app/imports/api/creature/actions/spendResources.js
+++ b/app/imports/api/creature/actions/spendResources.js
@@ -1,4 +1,5 @@
-import CreatureProperties, { damagePropertyWork, adjustQuantityWork } from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties, { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
+import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function spendResources(action){
// Check Uses
diff --git a/app/imports/api/creature/computation/getComputationProperties.js b/app/imports/api/creature/computation/getComputationProperties.js
index bbfc5606..96a75b76 100644
--- a/app/imports/api/creature/computation/getComputationProperties.js
+++ b/app/imports/api/creature/computation/getComputationProperties.js
@@ -1,4 +1,4 @@
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getComputationProperties(creatureId){
// find ids of all toggles that have conditions, even if they are inactive
diff --git a/app/imports/api/creature/computation/recomputeCreature.js b/app/imports/api/creature/computation/recomputeCreature.js
index 9618fe38..af4487b5 100644
--- a/app/imports/api/creature/computation/recomputeCreature.js
+++ b/app/imports/api/creature/computation/recomputeCreature.js
@@ -94,3 +94,17 @@ export function recomputeCreatureByDoc(creature){
recomputeSlotFullness(creatureId);
return computationMemo;
}
+
+// TODO
+export function recomputePropertyDependencies(property){
+ // Placeholder functionality, just recompute the whole creature
+ let creature = Creatures.findOne(property.ancestors[0].id);
+ recomputeCreatureByDoc(creature);
+}
+
+// TODO
+export function recomputeCreatureByIdAndDependencies({creatureId, dependencies}){
+ // Placeholder functionality, just recompute the whole creature
+ let creature = Creatures.findOne(creatureId);
+ recomputeCreatureByDoc(creature);
+}
diff --git a/app/imports/api/creature/computation/writeAlteredProperties.js b/app/imports/api/creature/computation/writeAlteredProperties.js
index f2dcdaf0..b8e15cbc 100644
--- a/app/imports/api/creature/computation/writeAlteredProperties.js
+++ b/app/imports/api/creature/computation/writeAlteredProperties.js
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor'
import { isEqual, forOwn } from 'lodash';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import propertySchemasIndex from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
export default function writeAlteredProperties(memo){
diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js
new file mode 100644
index 00000000..aa20f7df
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js
@@ -0,0 +1,77 @@
+import { Mongo } from 'meteor/mongo';
+import SimpleSchema from 'simpl-schema';
+import ColorSchema from '/imports/api/properties/subSchemas/ColorSchema.js';
+import ChildSchema from '/imports/api/parenting/ChildSchema.js';
+import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
+import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
+import { storedIconsSchema } from '/imports/api/icons/Icons.js';
+
+let CreatureProperties = new Mongo.Collection('creatureProperties');
+
+let CreaturePropertySchema = new SimpleSchema({
+ type: {
+ type: String,
+ allowedValues: Object.keys(propertySchemasIndex),
+ },
+ tags: {
+ type: Array,
+ defaultValue: [],
+ },
+ 'tags.$': {
+ type: String,
+ },
+ disabled: {
+ type: Boolean,
+ optional: true,
+ },
+ icon: {
+ type: storedIconsSchema,
+ optional: true,
+ },
+ // Denormalised flag if this property is inactive on the sheet for any reason
+ // Including being disabled, or a decendent of a disabled property
+ inactive: {
+ type: Boolean,
+ optional: true,
+ index: 1,
+ },
+ // Denormalised flag if this property was made inactive by an inactive
+ // ancestor. True if this property has an inactive ancestor even if this
+ // property is itself inactive
+ deactivatedByAncestor: {
+ type: Boolean,
+ optional: true,
+ index: 1,
+ },
+ // Denormalised list of all properties or creatures this property depends on
+ dependencies: {
+ type: Array,
+ defaultValue: [],
+ index: 1,
+ },
+ 'dependencies.$': {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ },
+});
+
+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}
+ });
+}
+
+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 {
+ CreaturePropertySchema,
+};
diff --git a/app/imports/api/creature/creatureProperties/assertPropertyEditPermission.js b/app/imports/api/creature/creatureProperties/assertPropertyEditPermission.js
deleted file mode 100644
index 5f91cf21..00000000
--- a/app/imports/api/creature/creatureProperties/assertPropertyEditPermission.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
-import getClosestPropertyAncestorCreature from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js';
-
-export default function assertPropertyEditPermission(prop, userId){
- let creature = getClosestPropertyAncestorCreature(prop);
- assertEditPermission(creature, userId);
-}
diff --git a/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js b/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js
deleted file mode 100644
index 9e8e3145..00000000
--- a/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Creatures from '/imports/api/creature/Creatures.js';
-import getClosestPropertyAncestorCreatureId from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js';
-
-export default function getClosestPropertyAncestorCreature(prop){
- let creatureId = getClosestPropertyAncestorCreatureId(prop);
- return Creatures.findOne(creatureId);
-}
diff --git a/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js b/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js
deleted file mode 100644
index 1a6b8836..00000000
--- a/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function getClosestPropertyAncestorCreatureId(prop){
- if (!prop.ancestors) throw 'Property has no ancestors';
- let creatureId;
- // Find the last ancestor in the creature collection
- for (let i = prop.ancestors.length - 1; i >= 0; i--){
- if (prop.ancestors[i].collection === 'creatures'){
- creatureId = prop.ancestors[i].id;
- break;
- }
- }
- if (!creatureId) throw 'This property has no creature ancestors';
- return creatureId;
-}
diff --git a/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js
new file mode 100644
index 00000000..d1768254
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/getRootCreatureAncestor.js
@@ -0,0 +1,5 @@
+import Creatures from '/imports/api/creature/Creatures.js';
+
+export default function getRootCreatureAncestor(property){
+ return Creatures.findOne(property.ancestors[0].id);
+}
diff --git a/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js
new file mode 100644
index 00000000..e2e3f1a4
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/adjustQuantity.js
@@ -0,0 +1,66 @@
+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 { recomputePropertyDependencies } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const adjustQuantity = new ValidatedMethod({
+ name: 'creatureProperties.adjustQuantity',
+ validate: new SimpleSchema({
+ _id: SimpleSchema.RegEx.Id,
+ operation: {
+ type: String,
+ allowedValues: ['set', 'increment']
+ },
+ value: Number,
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id, operation, value}) {
+ // Permissions
+ let property = CreatureProperties.findOne(_id);
+ let rootCreature = getRootCreatureAncestor(property);
+ assertEditPermission(rootCreature, this.userId);
+
+ // Do work
+ adjustQuantityWork({property, operation, value});
+
+ // Changing quantity does not change dependencies, recompute deps
+ recomputePropertyDependencies(property);
+ },
+});
+
+export function adjustQuantityWork({property, operation, value}){
+ // Check if property has quantity
+ let schema = CreatureProperties.simpleSchema(property);
+ if (!schema.allowsKey('quantity')){
+ throw new Meteor.Error(
+ 'Adjust quantity failed',
+ `Property of type "${property.type}" doesn't have a quantity`
+ );
+ }
+ if (operation === 'set'){
+ CreatureProperties.update(property._id, {
+ $set: {quantity: value}
+ }, {
+ selector: property
+ });
+ } 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}
+ }, {
+ selector: property
+ });
+ }
+}
+
+export default adjustQuantity;
diff --git a/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js
new file mode 100644
index 00000000..f20a39cb
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js
@@ -0,0 +1,57 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import SimpleSchema from 'simpl-schema';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import Creatures from '/imports/api/creature/Creatures.js';
+import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
+import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
+import { recomputePropertyDependencies } from '/imports/api/creature/computation/recomputeCreature.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: {
+ damageMultipliers: 1,
+ owner: 1,
+ readers: 1,
+ writers: 1,
+ },
+ });
+ assertEditPermission(creature, this.userId);
+ let lastProperty;
+ 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});
+ lastProperty = property;
+ });
+ recomputePropertyDependencies(lastProperty);
+ }
+});
+
+export default damagePropertiesByName;
diff --git a/app/imports/api/creature/creatureProperties/methods/damageProperty.js b/app/imports/api/creature/creatureProperties/methods/damageProperty.js
new file mode 100644
index 00000000..9c10ab99
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/damageProperty.js
@@ -0,0 +1,77 @@
+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 { recomputePropertyDependencies } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const damageProperty = new ValidatedMethod({
+ name: 'creatureProperties.damage',
+ validate: new SimpleSchema({
+ _id: SimpleSchema.RegEx.Id,
+ operation: {
+ type: String,
+ allowedValues: ['set', 'increment']
+ },
+ value: Number,
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 20,
+ timeInterval: 5000,
+ },
+ run({_id, operation, value}) {
+ // Check permissions
+ let property = CreatureProperties.findOne(_id);
+ 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`
+ );
+ }
+ damagePropertyWork({property, operation, value});
+ // Dependencies can't be changed through damage, only recompute deps
+ recomputePropertyDependencies(property);
+
+ },
+});
+
+export function damagePropertyWork({property, operation, value}){
+ if (operation === 'set'){
+ let currentValue = property.value;
+ // Set represents what we want the value to be after damage
+ // So we need the actual damage to get to that value
+ let damage = currentValue - value;
+ // Damage can't exceed total value
+ if (damage > currentValue) damage = currentValue;
+ // Damage must be positive
+ if (damage < 0) damage = 0;
+ CreatureProperties.update(property._id, {
+ $set: {damage}
+ }, {
+ selector: property
+ });
+ return currentValue - damage;
+ } else if (operation === 'increment'){
+ let currentValue = property.value - (property.damage || 0);
+ let currentDamage = property.damage;
+ let increment = value;
+ // Can't increase damage above the remaining value
+ if (increment > currentValue) increment = currentValue;
+ // Can't decrease damage below zero
+ if (-increment > currentDamage) increment = -currentDamage;
+ CreatureProperties.update(property._id, {
+ $inc: {damage: increment}
+ }, {
+ selector: property
+ });
+ return increment;
+ }
+}
+
+export default damageProperty;
diff --git a/app/imports/api/creature/creatureProperties/methods/dealDamage.js b/app/imports/api/creature/creatureProperties/methods/dealDamage.js
new file mode 100644
index 00000000..12f049ea
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/dealDamage.js
@@ -0,0 +1,68 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import SimpleSchema from 'simpl-schema';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import Creatures from '/imports/api/creature/Creatures.js';
+import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
+import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
+import { recomputeCreatureByIdAndDependencies } from '/imports/api/creature/computation/recomputeCreature.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: {
+ damageMultipliers: 1,
+ owner: 1,
+ readers: 1,
+ writers: 1,
+ },
+ });
+ assertEditPermission(creature, this.userId);
+
+ // Get all the health bars and do damage to them
+ let healthBars = CreatureProperties.find({
+ 'ancestors.id': creatureId,
+ 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);
+ let damageLeft = totalDamage;
+ if (damageType === 'healing') damageLeft = -totalDamage;
+ let dependencies = [];
+ healthBars.forEach(healthBar => {
+ if (damageLeft === 0) return;
+ let damageAdded = damagePropertyWork({
+ property: healthBar,
+ operation: 'increment',
+ value: damageLeft,
+ });
+ damageLeft -= damageAdded;
+ dependencies.push(healthBar.variableName);
+ dependencies.push(...healthBar.dependencies);
+ });
+ recomputeCreatureByIdAndDependencies({creatureId, dependencies});
+ return totalDamage;
+ },
+});
+
+export default dealDamage;
diff --git a/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js
new file mode 100644
index 00000000..650b9681
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/duplicateProperty.js
@@ -0,0 +1,33 @@
+import SimpleSchema from 'simpl-schema';
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
+import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
+import { insertPropertyWork } from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
+
+const duplicateProperty = new ValidatedMethod({
+ name: 'creatureProperties.duplicate',
+ validate: new SimpleSchema({
+ _id: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ }
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id}) {
+ let creatureProperty = CreatureProperties.findOne(_id);
+ let rootCreature = getRootCreatureAncestor(creatureProperty);
+ assertEditPermission(rootCreature, this.userId);
+ insertPropertyWork({
+ property: creatureProperty,
+ creature: rootCreature,
+ });
+ },
+});
+
+export default duplicateProperty;
diff --git a/app/imports/api/creature/creatureProperties/manageEquipment.js b/app/imports/api/creature/creatureProperties/methods/equipItem.js
similarity index 83%
rename from app/imports/api/creature/creatureProperties/manageEquipment.js
rename to app/imports/api/creature/creatureProperties/methods/equipItem.js
index a5f88759..b14ca3d8 100644
--- a/app/imports/api/creature/creatureProperties/manageEquipment.js
+++ b/app/imports/api/creature/creatureProperties/methods/equipItem.js
@@ -1,12 +1,12 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
-import getClosestPropertyAncestorCreature from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js';
+import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
-function getParentRefByTag(creatureId, tag){
+export function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
@@ -40,7 +40,7 @@ const equipItem = new ValidatedMethod({
let item = CreatureProperties.findOne(_id);
if (item.type !== 'item') throw new Meteor.Error('wrong type',
'Equip and unequip can only be performed on items');
- let creature = getClosestPropertyAncestorCreature(item);
+ let creature = getRootCreatureAncestor(item);
assertEditPermission(creature, this.userId);
CreatureProperties.update(_id, {
$set: {equipped},
@@ -61,4 +61,4 @@ const equipItem = new ValidatedMethod({
},
});
-export { equipItem, getParentRefByTag }
+export default equipItem;
diff --git a/app/imports/api/creature/creatureProperties/methods/index.js b/app/imports/api/creature/creatureProperties/methods/index.js
new file mode 100644
index 00000000..63e26999
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/index.js
@@ -0,0 +1,14 @@
+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';
+import '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
+import '/imports/api/creature/creatureProperties/methods/pullFromProperty.js';
+import '/imports/api/creature/creatureProperties/methods/pushToProperty.js';
+import '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
+import '/imports/api/creature/creatureProperties/methods/selectAmmoItem.js';
+import '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
+import '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
diff --git a/app/imports/api/creature/creatureProperties/methods/insertProperty.js b/app/imports/api/creature/creatureProperties/methods/insertProperty.js
new file mode 100644
index 00000000..188e86c7
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/insertProperty.js
@@ -0,0 +1,40 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+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 { reorderDocs } from '/imports/api/parenting/order.js';
+import { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const insertProperty = new ValidatedMethod({
+ name: 'creatureProperties.insert',
+ validate: null,
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({creatureProperty}) {
+ let rootCreature = getRootCreatureAncestor(creatureProperty);
+ assertEditPermission(rootCreature, this.userId);
+ insertPropertyWork({
+ property: creatureProperty,
+ creature: rootCreature,
+ });
+ },
+});
+
+export function insertPropertyWork({property, creature}){
+ delete property._id;
+ 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
+ recomputeCreatureByDoc(creature);
+ return _id;
+}
+
+export default insertProperty;
diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js
new file mode 100644
index 00000000..4589eadd
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js
@@ -0,0 +1,103 @@
+import SimpleSchema from 'simpl-schema';
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import 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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
+import {
+ 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';
+
+const insertPropertyFromLibraryNode = new ValidatedMethod({
+ name: 'creatureProperties.insertPropertyFromLibraryNode',
+ validate: new SimpleSchema({
+ nodeId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ },
+ parentRef: {
+ type: RefSchema,
+ },
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({nodeId, parentRef}) {
+ // get the new ancestry for the properties
+ let {parentDoc, ancestors} = getAncestry({parentRef});
+
+ // Check permission to edit
+ let rootCreature;
+ if (parentRef.collection === 'creatures'){
+ rootCreature = parentDoc;
+ } else if (parentRef.collection === 'creatureProperties'){
+ rootCreature = getRootCreatureAncestor(parentDoc);
+ } else {
+ throw `${parentRef.collection} is not a valid parent collection`
+ }
+ assertEditPermission(rootCreature, this.userId);
+
+ // Fetch the library node and its decendents, provided they have not been
+ // removed
+ let node = LibraryNodes.findOne({
+ _id: nodeId,
+ removed: {$ne: true},
+ });
+ if (!node) throw `Node not found for nodeId: ${nodeId}`;
+ let oldParent = node.parent;
+ let nodes = LibraryNodes.find({
+ 'ancestors.id': nodeId,
+ removed: {$ne: true},
+ }).fetch();
+ // The root node is last in the array of nodes
+ nodes.push(node);
+
+ // re-map all the ancestors
+ setLineageOfDocs({
+ docArray: nodes,
+ newAncestry: ancestors,
+ oldParent,
+ });
+
+ // Give the docs new IDs without breaking internal references
+ renewDocIds({
+ docArray: nodes,
+ collectionMap: {'libraryNodes': 'creatureProperties'}
+ });
+
+ // Order the root node
+ setDocToLastOrder({
+ collection: CreatureProperties,
+ doc: node,
+ });
+
+ // Insert the creature properties
+ let insertedDocIds = CreatureProperties.batchInsert(nodes);
+
+ // get the root inserted doc
+ let rootId = insertedDocIds[insertedDocIds.length - 1];
+
+ // Tree structure changed by inserts, reorder the tree
+ reorderDocs({
+ collection: CreatureProperties,
+ ancestorId: rootCreature._id,
+ });
+
+ // Inserting a creature property invalidates dependencies: full recompute
+ recomputeCreatureByDoc(rootCreature);
+
+ // Return the docId of the last property, the inserted root property
+ return rootId;
+ },
+});
+
+export default insertPropertyFromLibraryNode;
diff --git a/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js
new file mode 100644
index 00000000..dfcec70a
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/pullFromProperty.js
@@ -0,0 +1,36 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const pullFromProperty = new ValidatedMethod({
+ name: 'creatureProperties.pull',
+ validate: null,
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id, path, itemId}){
+ // Permissions
+ 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
+ recomputeCreatureByDoc(rootCreature);
+ // recomputePropertyDependencies(property);
+ }
+});
+
+export default pullFromProperty;
diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js
new file mode 100644
index 00000000..d8a0dac6
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js
@@ -0,0 +1,35 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const pushToProperty = new ValidatedMethod({
+ name: 'creatureProperties.push',
+ validate: null,
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id, path, value}){
+ // Permissions
+ let property = CreatureProperties.findOne(_id);
+ let rootCreature = getRootCreatureAncestor(property);
+ assertEditPermission(rootCreature, this.userId);
+
+ // Do work
+ CreatureProperties.update(_id, {
+ $push: {[path.join('.')]: value},
+ }, {
+ selector: {type: property.type},
+ });
+
+ // TODO figure out if this method can change deps or not
+ recomputeCreatureByDoc(rootCreature);
+ // recomputePropertyDependencies(property);
+ }
+});
+
+export default pushToProperty;
diff --git a/app/imports/api/creature/creatureProperties/methods/restoreProperty.js b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js
new file mode 100644
index 00000000..62c29eea
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/restoreProperty.js
@@ -0,0 +1,34 @@
+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 { 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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const restoreProperty = new ValidatedMethod({
+ name: 'creatureProperties.restore',
+ validate: new SimpleSchema({
+ _id: SimpleSchema.RegEx.Id
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id}){
+ // Permissions
+ let property = CreatureProperties.findOne(_id);
+ let rootCreature = getRootCreatureAncestor(property);
+ assertEditPermission(rootCreature, this.userId);
+
+ // Do work
+ restore({_id, collection: CreatureProperties});
+
+ // Changes dependency tree by removing children
+ recomputeCreatureByDoc(rootCreature);
+ }
+});
+
+export default restoreProperty;
diff --git a/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js
new file mode 100644
index 00000000..73aff869
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/selectAmmoItem.js
@@ -0,0 +1,52 @@
+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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const selectAmmoItem = new ValidatedMethod({
+ name: 'creatureProperties.selectAmmoItem',
+ validate: new SimpleSchema({
+ actionId: SimpleSchema.RegEx.Id,
+ itemId: SimpleSchema.RegEx.Id,
+ itemConsumedIndex: Number,
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({actionId, itemId, itemConsumedIndex}) {
+ // Permissions
+ let action = CreatureProperties.findOne(actionId);
+ let rootCreature = getRootCreatureAncestor(action);
+ assertEditPermission(rootCreature, this.userId);
+
+ // Check that this index has a document to edit
+ let itemConsumed = action.resources.itemsConsumed[itemConsumedIndex];
+ 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){
+ 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}
+ }, {
+ 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
+ recomputeCreatureByDoc(rootCreature);
+ },
+});
+
+export default selectAmmoItem;
diff --git a/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js
new file mode 100644
index 00000000..f44f1166
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/softRemoveProperty.js
@@ -0,0 +1,34 @@
+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 { 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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.js';
+
+const softRemoveProperty = new ValidatedMethod({
+ name: 'creatureProperties.softRemove',
+ validate: new SimpleSchema({
+ _id: SimpleSchema.RegEx.Id
+ }).validator(),
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ run({_id}){
+ // Permissions
+ 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
+ recomputeCreatureByDoc(rootCreature);
+ }
+});
+
+export default softRemoveProperty;
diff --git a/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js
new file mode 100644
index 00000000..0cfab37b
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js
@@ -0,0 +1,54 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/recomputeCreature.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':
+ case 'order':
+ case 'parent':
+ case 'ancestors':
+ 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}) {
+ // Permission
+ let property = CreatureProperties.findOne(_id, {
+ fields: {type: 1, ancestors: 1}
+ });
+ let rootCreature = getRootCreatureAncestor(property);
+ assertEditPermission(rootCreature, this.userId);
+
+ let pathString = path.join('.');
+ let modifier;
+ // unset empty values
+ if (value === null || value === undefined){
+ modifier = {$unset: {[pathString]: 1}};
+ } else {
+ modifier = {$set: {[pathString]: value}};
+ }
+ CreatureProperties.update(_id, modifier, {
+ selector: {type: property.type},
+ });
+
+ // Updating a property might change dependencies, unless we are certain
+ // it did not, a full recompute is required
+ recomputeCreatureByDoc(rootCreature);
+ },
+});
+
+export default updateCreatureProperty;
diff --git a/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js
new file mode 100644
index 00000000..6dd5b2d3
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/recomputeCreaturesByProperty.js
@@ -0,0 +1,12 @@
+import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
+
+/**
+ * Recomputes all ancestor creatures of this property
+ */
+export default function recomputeCreaturesByProperty(property){
+ for (let ref of property.ancestors){
+ if (ref.collection === 'creatures') {
+ recomputeCreatureById.call(ref.id);
+ }
+ }
+}
diff --git a/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js b/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js
index ca722736..ccaed171 100644
--- a/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js
+++ b/app/imports/api/creature/denormalise/recomputeDamageMultipliers.js
@@ -3,7 +3,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export const recomputeDamageMultipliers = new ValidatedMethod({
diff --git a/app/imports/api/creature/denormalise/recomputeInactiveProperties.js b/app/imports/api/creature/denormalise/recomputeInactiveProperties.js
index dbdb976c..e53f05ee 100644
--- a/app/imports/api/creature/denormalise/recomputeInactiveProperties.js
+++ b/app/imports/api/creature/denormalise/recomputeInactiveProperties.js
@@ -1,4 +1,4 @@
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function recomputeInactiveProperties(ancestorId){
let disabledFilter = {
diff --git a/app/imports/api/creature/denormalise/recomputeInventory.js b/app/imports/api/creature/denormalise/recomputeInventory.js
index 6cac3d69..ceb4824e 100644
--- a/app/imports/api/creature/denormalise/recomputeInventory.js
+++ b/app/imports/api/creature/denormalise/recomputeInventory.js
@@ -1,4 +1,4 @@
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import nodesToTree from '/imports/api/parenting/parenting.js';
export default function recomputeInventory(creatureId){
diff --git a/app/imports/api/creature/denormalise/recomputeSlotFullness.js b/app/imports/api/creature/denormalise/recomputeSlotFullness.js
index 06a058a3..2b4e60ba 100644
--- a/app/imports/api/creature/denormalise/recomputeSlotFullness.js
+++ b/app/imports/api/creature/denormalise/recomputeSlotFullness.js
@@ -1,4 +1,4 @@
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
// n + 1 database queries + n potential updates for n slots. Could be sped up.
export default function recomputeSlotFullness(ancestorId){
CreatureProperties.find({
diff --git a/app/imports/api/creature/removeCreature.js b/app/imports/api/creature/removeCreature.js
index 41d3dd43..899db6b3 100644
--- a/app/imports/api/creature/removeCreature.js
+++ b/app/imports/api/creature/removeCreature.js
@@ -3,7 +3,7 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.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';
diff --git a/app/imports/api/creature/restCreature.js b/app/imports/api/creature/restCreature.js
index f47e182c..753298ae 100644
--- a/app/imports/api/creature/restCreature.js
+++ b/app/imports/api/creature/restCreature.js
@@ -2,7 +2,7 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
diff --git a/app/imports/server/cron/deleteSoftRemovedDocuments.js b/app/imports/server/cron/deleteSoftRemovedDocuments.js
index 26baae62..779fbed4 100644
--- a/app/imports/server/cron/deleteSoftRemovedDocuments.js
+++ b/app/imports/server/cron/deleteSoftRemovedDocuments.js
@@ -1,4 +1,4 @@
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertAdmin } from '/imports/api/sharing/sharingPermissions.js';
import { SyncedCron } from 'meteor/percolate:synced-cron';
diff --git a/app/imports/server/publications/singleCharacter.js b/app/imports/server/publications/singleCharacter.js
index 8720767b..59becda1 100644
--- a/app/imports/server/publications/singleCharacter.js
+++ b/app/imports/server/publications/singleCharacter.js
@@ -1,6 +1,6 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
diff --git a/app/imports/server/publications/slotFillers.js b/app/imports/server/publications/slotFillers.js
index 9f3293c2..bb91d2f3 100644
--- a/app/imports/server/publications/slotFillers.js
+++ b/app/imports/server/publications/slotFillers.js
@@ -1,6 +1,6 @@
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
-import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
+import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
Meteor.publish('slotFillers', function(slotId){
this.autorun(function (){
diff --git a/app/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue b/app/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue
index 97be825b..0bb474aa 100644
--- a/app/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue
+++ b/app/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue
@@ -111,7 +111,7 @@
diff --git a/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue b/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue
index d788ddc7..4cff71b5 100644
--- a/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue
+++ b/app/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue
@@ -24,7 +24,7 @@