diff --git a/app/imports/api/creature/CreatureProperties.js b/app/imports/api/creature/CreatureProperties.js
index 2581ab6c..85af5e32 100644
--- a/app/imports/api/creature/CreatureProperties.js
+++ b/app/imports/api/creature/CreatureProperties.js
@@ -22,6 +22,7 @@ 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/creatureProperties/manageEquipment.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
@@ -50,6 +51,15 @@ let CreaturePropertySchema = new SimpleSchema({
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,
},
});
@@ -205,7 +215,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
const updateProperty = new ValidatedMethod({
name: 'creatureProperties.update',
validate({_id, path}){
- if (!_id) return false;
+ 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':
diff --git a/app/imports/api/creature/creatureProperties/assertPropertyEditPermission.js b/app/imports/api/creature/creatureProperties/assertPropertyEditPermission.js
new file mode 100644
index 00000000..5f91cf21
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/assertPropertyEditPermission.js
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 00000000..9e8e3145
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 00000000..1a6b8836
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js
@@ -0,0 +1,13 @@
+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/manageEquipment.js b/app/imports/api/creature/creatureProperties/manageEquipment.js
new file mode 100644
index 00000000..a5f88759
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/manageEquipment.js
@@ -0,0 +1,64 @@
+import { ValidatedMethod } from 'meteor/mdg:validated-method';
+import CreatureProperties from '/imports/api/creature/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 INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
+
+function getParentRefByTag(creatureId, tag){
+ let prop = CreatureProperties.findOne({
+ 'ancestors.id': creatureId,
+ removed: {$ne: true},
+ inactive: {$ne: true},
+ tags: tag,
+ }, {
+ sort: {order: 1},
+ });
+ if (prop){
+ return {id: prop._id, collection: 'creatureProperties'};
+ } else {
+ return {id: creatureId, collection: 'creatures'};
+ }
+}
+
+// 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');
+ if (equipped !== true && equipped !== false) {
+ throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
+ }
+ },
+ mixins: [RateLimiterMixin],
+ rateLimit: {
+ numRequests: 5,
+ timeInterval: 5000,
+ },
+ 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');
+ let creature = getClosestPropertyAncestorCreature(item);
+ assertEditPermission(creature, this.userId);
+ CreatureProperties.update(_id, {
+ $set: {equipped},
+ }, {
+ selector: {type: 'item'},
+ });
+ let tag = equipped ? INVENTORY_TAGS.equipment : INVENTORY_TAGS.carried;
+ let parentRef = getParentRefByTag(creature._id, tag);
+ // organizeDoc handles recompuation
+ organizeDoc.call({
+ docRef: {
+ id: _id,
+ collection: 'creatureProperties',
+ },
+ parentRef,
+ order: Number.MAX_SAFE_INTEGER,
+ });
+ },
+});
+
+export { equipItem, getParentRefByTag }
diff --git a/app/imports/api/creature/denormalise/recomputeInactiveProperties.js b/app/imports/api/creature/denormalise/recomputeInactiveProperties.js
index eb988f8b..dbdb976c 100644
--- a/app/imports/api/creature/denormalise/recomputeInactiveProperties.js
+++ b/app/imports/api/creature/denormalise/recomputeInactiveProperties.js
@@ -15,28 +15,43 @@ export default function recomputeInactiveProperties(ancestorId){
fields: {_id: 1},
}).map(prop => prop._id);
- // Set all the properties inactive that aren't already inactive but should be
+ // Deactivate relevant properties
+ // Inactive properties
CreatureProperties.update({
'ancestors.id': ancestorId,
- $or: [{
- '_id': {$in: disabledIds}
- }, {
- 'ancestors.id': {$in: disabledIds}
- }],
- inactive: {$ne: true},
+ '_id': {$in: disabledIds},
+ $or: [{inactive: {$ne: true}}, {deactivatedByAncestor: true}],
}, {
$set: {inactive: true},
+ $unset: {deactivatedByAncestor: 1},
}, {
multi: true,
selector: {type: 'any'},
});
+ // Decendants of inactive properties
+ CreatureProperties.update({
+ 'ancestors.id': {$eq: ancestorId, $in: disabledIds},
+ $or: [{inactive: {$ne: true}}, {deactivatedByAncestor: {$ne: true}}],
+ }, {
+ $set: {
+ inactive: true,
+ deactivatedByAncestor: true,
+ },
+ }, {
+ multi: true,
+ selector: {type: 'any'},
+ });
+
// Remove inactive from all the properties that are inactive but shouldn't be
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
- inactive: true,
+ $or: [{inactive: true}, {deactivatedByAncestor: true}],
}, {
- $unset: {inactive: 1},
+ $unset: {
+ inactive: 1,
+ deactivatedByAncestor: 1,
+ },
}, {
multi: true,
selector: {type: 'any'},
diff --git a/app/imports/constants/INVENTORY_TAGS.js b/app/imports/constants/INVENTORY_TAGS.js
new file mode 100644
index 00000000..25a00cc9
--- /dev/null
+++ b/app/imports/constants/INVENTORY_TAGS.js
@@ -0,0 +1,7 @@
+const INVENTORY_TAGS = Object.freeze({
+ inventory: 'inventory',
+ equipment: 'equipment',
+ carried: 'carried',
+});
+
+export default INVENTORY_TAGS;
diff --git a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue b/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue
index 3f29124c..70a1cb70 100644
--- a/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue
+++ b/app/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue
@@ -12,7 +12,7 @@
@@ -27,7 +27,7 @@
@@ -51,7 +51,8 @@ import ContainerCard from '/imports/ui/properties/components/inventory/Container
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import { updateProperty } from '/imports/api/creature/CreatureProperties.js';
-import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
+import { getParentRefByTag } from '/imports/api/creature/creatureProperties/manageEquipment.js';
+import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export default {
components: {
@@ -78,6 +79,7 @@ export default {
'ancestors.id': this.creatureId,
type: 'container',
removed: {$ne: true},
+ inactive: {$ne: true},
}, {
sort: {order: 1},
});
@@ -90,29 +92,43 @@ export default {
},
type: 'container',
removed: {$ne: true},
+ inactive: {$ne: true},
}, {
sort: {order: 1},
});
},
carriedItems(){
- return getActiveProperties({
- ancestorId: this.creatureId,
- includeUnequipped: true,
- filter: {
- type: 'item',
- equipped: {$ne: true},
- 'parent.id': this.creatureId
- },
- });
+ return CreatureProperties.find({
+ 'ancestors.id': {
+ $eq: this.creatureId,
+ $nin: this.containerIds
+ },
+ type: 'item',
+ equipped: {$ne: true},
+ removed: {$ne: true},
+ deactivatedByAncestor: {$ne: true},
+ }, {
+ sort: {order: 1},
+ });
},
equippedItems(){
- return getActiveProperties({
- ancestorId: this.creatureId,
- filter: {
- type: 'item',
- equipped: true,
- },
- });
+ return CreatureProperties.find({
+ 'ancestors.id': {
+ $eq: this.creatureId,
+ },
+ type: 'item',
+ equipped: true,
+ removed: {$ne: true},
+ inactive: {$ne: true},
+ }, {
+ sort: {order: 1},
+ });
+ },
+ equipmentParentRef(){
+ return getParentRefByTag(this.creatureId, INVENTORY_TAGS.equipment);
+ },
+ carriedParentRef(){
+ return getParentRefByTag(this.creatureId, INVENTORY_TAGS.carried);
},
},
computed: {
diff --git a/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue b/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue
index df8f118b..c3868b73 100644
--- a/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue
+++ b/app/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue
@@ -60,12 +60,12 @@
diff --git a/app/imports/ui/properties/components/inventory/ContainerCard.vue b/app/imports/ui/properties/components/inventory/ContainerCard.vue
index 731abc09..3ee931a4 100644
--- a/app/imports/ui/properties/components/inventory/ContainerCard.vue
+++ b/app/imports/ui/properties/components/inventory/ContainerCard.vue
@@ -22,7 +22,7 @@