diff --git a/app/imports/api/creature/creatureProperties/CreatureProperties.js b/app/imports/api/creature/creatureProperties/CreatureProperties.js
index 87b5f072..e8522fae 100644
--- a/app/imports/api/creature/creatureProperties/CreatureProperties.js
+++ b/app/imports/api/creature/creatureProperties/CreatureProperties.js
@@ -28,6 +28,12 @@ let CreaturePropertySchema = new SimpleSchema({
type: storedIconsSchema,
optional: true,
},
+ // Reference to the library node that this property was copied from
+ libraryNodeId: {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ 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: {
diff --git a/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js
new file mode 100644
index 00000000..1bba9421
--- /dev/null
+++ b/app/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js
@@ -0,0 +1,44 @@
+export default function getSlotFillFilter({slot, libraryIds}){
+ let filter = {
+ removed: {$ne: true},
+ $and: []
+ };
+ if (libraryIds){
+ filter['ancestors.id'] = {$in: libraryIds};
+ }
+ if (slot.slotType){
+ filter.$and.push({
+ $or: [{
+ type: slot.slotType
+ },{
+ type: 'slotFiller',
+ slotFillerType: slot.slotType,
+ }]
+ });
+ }
+ let tagsOr = [];
+ let tagsNor = [];
+ if (slot.slotTags && slot.slotTags.length){
+ tagsOr.push({tags: {$all: slot.slotTags}});
+ }
+ if (slot.extraTags && slot.extraTags.length){
+ slot.extraTags.forEach(extra => {
+ if (!extra.tags || !extra.tags.length) return;
+ if (extra.operation === 'OR'){
+ tagsOr.push({tags: {$all: extra.tags}});
+ } else if (extra.operation === 'NOT'){
+ tagsNor.push({tags: {$all: extra.tags}});
+ }
+ });
+ }
+ if (tagsOr.length){
+ filter.$and.push({$or: tagsOr});
+ }
+ if (tagsNor.length){
+ filter.$and.push({$nor: tagsNor});
+ }
+ if (!filter.$and.length){
+ delete filter.$and;
+ }
+ return filter;
+}
diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js
index a507d01f..0d05f29d 100644
--- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js
+++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js
@@ -107,6 +107,9 @@ function insertPropertyFromNode(nodeId, ancestors, order){
// It must get the first generated ID to prevent flickering
nodes = [node, ...nodes];
+ // set libraryNodeIds
+ storeLibraryNodeReferences(nodes, nodeId);
+
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
@@ -135,6 +138,13 @@ function insertPropertyFromNode(nodeId, ancestors, order){
return node;
}
+
+function storeLibraryNodeReferences(nodes){
+ nodes.forEach(node => {
+ node.libraryNodeId = node._id;
+ });
+}
+
// Covert node references into actual nodes
// TODO: check permissions for each library a reference node references
function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){
diff --git a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js
index 4228eca0..c1054490 100644
--- a/app/imports/api/creature/creatureProperties/methods/pushToProperty.js
+++ b/app/imports/api/creature/creatureProperties/methods/pushToProperty.js
@@ -4,6 +4,7 @@ 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 { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
+import { get } from 'lodash';
const pushToProperty = new ValidatedMethod({
name: 'creatureProperties.push',
@@ -19,9 +20,26 @@ const pushToProperty = new ValidatedMethod({
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
+ let joinedPath = path.join('.');
+
+ // Respect maxCount
+ let schema = CreatureProperties.simpleSchema(property);
+ let maxCount = schema.get(joinedPath, 'maxCount');
+
+ if (Number.isFinite(maxCount)){
+ let array = get(property, path);
+ let currentCount = array ? array.length : 0;
+ if (currentCount >= maxCount){
+ throw new Meteor.Error(
+ 'Array is full',
+ `Cannot have more than ${maxCount} values`
+ );
+ }
+ }
+
// Do work
CreatureProperties.update(_id, {
- $push: {[path.join('.')]: value},
+ $push: {[joinedPath]: value},
}, {
selector: {type: property.type},
});
diff --git a/app/imports/api/creature/creatures/getCreatureUrlName.js b/app/imports/api/creature/creatures/getCreatureUrlName.js
new file mode 100644
index 00000000..de8c1b7c
--- /dev/null
+++ b/app/imports/api/creature/creatures/getCreatureUrlName.js
@@ -0,0 +1,5 @@
+import getSlug from 'speakingurl';
+
+export default function getCreatureUrlName({name}){
+ return getSlug(name, {maintainCase: true}) || '-';
+}
diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js
index fe16563f..8b6bb22e 100644
--- a/app/imports/api/creature/experience/Experiences.js
+++ b/app/imports/api/creature/experience/Experiences.js
@@ -90,11 +90,6 @@ const insertExperience = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.insert.denied',
'You need to be logged in to insert an experience');
}
- let tier = getUserTier(this.userId);
- if (!tier.paidBenefits){
- throw new Meteor.Error('Experiences.methods.insert.denied',
- `The ${tier.name} tier does not allow you to grant experience`);
- }
let insertedIds = [];
creatureIds.forEach(creatureId => {
let id = insertExperienceForCreature({experience, creatureId, userId});
@@ -123,11 +118,6 @@ const removeExperience = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.remove.denied',
'You need to be logged in to remove an experience');
}
- let tier = getUserTier(this.userId);
- if (!tier.paidBenefits){
- throw new Meteor.Error('Experiences.methods.remove.denied',
- `The ${tier.name} tier does not allow you to remove an experience`);
- }
let experience = Experiences.findOne(experienceId);
if (!experience) return;
let creatureId = experience.creatureId
@@ -168,11 +158,6 @@ const recomputeExperiences = new ValidatedMethod({
throw new Meteor.Error('Experiences.methods.recompute.denied',
'You need to be logged in to recompute a creature\'s experiences');
}
- let tier = getUserTier(this.userId);
- if (!tier.paidBenefits){
- throw new Meteor.Error('Experiences.methods.recompute.denied',
- `The ${tier.name} tier does not allow you to recompute a creature's experiences`);
- }
assertEditPermission(creatureId, userId);
let xp = 0;
diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js
index 0cd20fc6..2f9f3997 100644
--- a/app/imports/api/properties/Actions.js
+++ b/app/imports/api/properties/Actions.js
@@ -1,7 +1,9 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import InlineComputationSchema from '/imports/api/properties/subSchemas/InlineComputationSchema.js';
-import { storedIconsSchema } from '/imports/api/icons/Icons.js'
+import { storedIconsSchema } from '/imports/api/icons/Icons.js';
+import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
+
/*
* Actions are things a character can do
* Any rolls that are children of actions will be rolled when taking the action
@@ -12,14 +14,17 @@ let ActionSchema = new SimpleSchema({
name: {
type: String,
optional: true,
+ max: STORAGE_LIMITS.name,
},
summary: {
type: String,
optional: true,
+ max: STORAGE_LIMITS.summary,
},
description: {
type: String,
optional: true,
+ max: STORAGE_LIMITS.description,
},
// What time-resource is used to take the action in combat
// long actions take longer than 1 round to cast
@@ -41,9 +46,11 @@ let ActionSchema = new SimpleSchema({
tags: {
type: Array,
defaultValue: [],
+ maxCount: STORAGE_LIMITS.tagCount,
},
'tags.$': {
type: String,
+ max: STORAGE_LIMITS.tagLength,
},
// Duplicate the ResourceSchema here so we can extend it elegantly.
resources: {
@@ -53,6 +60,7 @@ let ActionSchema = new SimpleSchema({
'resources.itemsConsumed': {
type: Array,
defaultValue: [],
+ maxCount: STORAGE_LIMITS.resourcesCount,
},
'resources.itemsConsumed.$': {
type: Object,
@@ -79,6 +87,7 @@ let ActionSchema = new SimpleSchema({
'resources.attributesConsumed': {
type: Array,
defaultValue: [],
+ maxCount: STORAGE_LIMITS.resourcesCount,
},
'resources.attributesConsumed.$': {
type: Object,
@@ -102,6 +111,7 @@ let ActionSchema = new SimpleSchema({
uses: {
type: String,
optional: true,
+ max: STORAGE_LIMITS.calculation,
},
// Integer of how many times it has already been used
usesUsed: {
@@ -120,14 +130,14 @@ const ComputedOnlyActionSchema = new SimpleSchema({
summaryCalculations: {
type: Array,
defaultValue: [],
- maxCount: 32,
+ maxCount: STORAGE_LIMITS.inlineCalculationCount,
},
'summaryCalculations.$': InlineComputationSchema,
descriptionCalculations: {
type: Array,
defaultValue: [],
- maxCount: 32,
+ maxCount: STORAGE_LIMITS.inlineCalculationCount,
},
'descriptionCalculations.$': InlineComputationSchema,
@@ -138,6 +148,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
usesErrors: {
type: Array,
optional: true,
+ maxCount: STORAGE_LIMITS.errorCount,
},
'usesErrors.$':{
type: ErrorSchema,
@@ -158,6 +169,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
},
'resources.itemsConsumed.$.itemName': {
type: String,
+ max: STORAGE_LIMITS.name,
optional: true,
},
'resources.itemsConsumed.$.itemIcon': {
@@ -167,6 +179,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
'resources.itemsConsumed.$.itemColor': {
type: String,
optional: true,
+ max: STORAGE_LIMITS.color,
},
'resources.attributesConsumed': Array,
'resources.attributesConsumed.$': Object,
@@ -182,6 +195,7 @@ const ComputedOnlyActionSchema = new SimpleSchema({
'resources.attributesConsumed.$.statName': {
type: String,
optional: true,
+ max: STORAGE_LIMITS.name,
},
// True if the uses left is zero, or any item or attribute consumed is
// insufficient
diff --git a/app/imports/api/properties/Slots.js b/app/imports/api/properties/Slots.js
index 872d6171..dd25fd41 100644
--- a/app/imports/api/properties/Slots.js
+++ b/app/imports/api/properties/Slots.js
@@ -21,6 +21,31 @@ let SlotSchema = new SimpleSchema({
'slotTags.$': {
type: String,
},
+ extraTags: {
+ type: Array,
+ defaultValue: [],
+ maxCount: 5,
+ },
+ 'extraTags.$': {
+ type: Object,
+ },
+ 'extraTags.$._id': {
+ type: String,
+ regEx: SimpleSchema.RegEx.Id,
+ autoValue(){
+ if (!this.isSet) return Random.id();
+ }
+ },
+ 'extraTags.$.operation': {
+ type: String,
+ allowedValues: ['OR', 'NOT'],
+ },
+ 'extraTags.$.tags': {
+ type: Array,
+ },
+ 'extraTags.$.tags.$': {
+ type: String,
+ },
quantityExpected: {
type: String,
optional: true,
@@ -37,7 +62,19 @@ let SlotSchema = new SimpleSchema({
hideWhenFull: {
type: Boolean,
optional: true,
- }
+ defaultValue: true,
+ },
+ unique: {
+ type: String,
+ allowedValues: [
+ // Can't choose the same slot filler twice in this slot
+ 'uniqueInSlot',
+ // Can't choose the same slot filler twice accross the whole creature
+ 'uniqueInCreature'
+ ],
+ optional: true,
+ defaultValue: 'uniqueInSlot',
+ },
});
const ComputedOnlySlotSchema = new SimpleSchema({
diff --git a/app/imports/constants/STORAGE_LIMITS.js b/app/imports/constants/STORAGE_LIMITS.js
new file mode 100644
index 00000000..86c38731
--- /dev/null
+++ b/app/imports/constants/STORAGE_LIMITS.js
@@ -0,0 +1,14 @@
+const STORAGE_LIMITS = Object.freeze({
+ name: 140,
+ color: 10000,
+ summary: 10000,
+ description: 49473, //the length of the Bee Movie script
+ inlineCalculationCount: 32,
+ errorCount: 32,
+ tagCount: 64,
+ tagLength: 140,
+ resourcesCount: 32,
+ calculation: 280,
+});
+
+export default STORAGE_LIMITS;
diff --git a/app/imports/server/publications/slotFillers.js b/app/imports/server/publications/slotFillers.js
index ca1d23b5..94dbf7fc 100644
--- a/app/imports/server/publications/slotFillers.js
+++ b/app/imports/server/publications/slotFillers.js
@@ -2,6 +2,7 @@ import { check } from 'meteor/check';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
+import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
Meteor.publish('slotFillers', function(slotId){
let self = this;
@@ -21,7 +22,7 @@ Meteor.publish('slotFillers', function(slotId){
fields: {subscribedLibraries: 1}
});
const subs = user && user.subscribedLibraries || [];
- let libraryIds = Libraries.find({
+ let libraries = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
@@ -29,28 +30,16 @@ Meteor.publish('slotFillers', function(slotId){
{_id: {$in: subs}},
]
}, {
- fields: {_id: 1},
- }).map(lib => lib._id);
+ fields: {_id: 1, name: 1},
+ });
+ let libraryIds = libraries.map(lib => lib._id);
// Build a filter for nodes in those libraries that match the slot
- let filter = {
- 'ancestors.id': {$in: libraryIds},
- removed: {$ne: true},
- };
- if (slot.slotTags && slot.slotTags.length){
- filter.tags = {$all: slot.slotTags};
- }
- if (slot.slotType){
- filter.$or = [{
- type: slot.slotType
- },{
- type: 'slotFiller',
- slotFillerType: slot.slotType,
- }];
- }
+ let filter = getSlotFillFilter({slot, libraryIds});
+
this.autorun(function(){
// Get the limit of the documents the user can fetch
- var limit = self.data('limit') || 20;
+ var limit = self.data('limit') || 50;
check(limit, Number);
// Get the search term
@@ -85,7 +74,7 @@ Meteor.publish('slotFillers', function(slotId){
self.setData('countAll', LibraryNodes.find(filter).count());
});
self.autorun(function () {
- return LibraryNodes.find(filter, options);
+ return [LibraryNodes.find(filter, options), libraries];
});
});
});
diff --git a/app/imports/ui/creature/slots/OldSlotFillDialog.vue b/app/imports/ui/creature/slots/OldSlotFillDialog.vue
new file mode 100644
index 00000000..949460ab
--- /dev/null
+++ b/app/imports/ui/creature/slots/OldSlotFillDialog.vue
@@ -0,0 +1,310 @@
+
+
+ {{ model.description }}
+
+ This slot requires a {{ slotPropertyTypeName }}
+
+ with the tag
+ Requirements of {{ numFiltered }} library properties were not met.
+
+
+ Nothing suitable was found in your libraries.
+
+
+ Nothing suitable was found in your libraries
+
+ matching "{{ searchValue }}"
+
+
+ {{ model.slotTags[0] }},
+
+
+ with the following tags:
+
+ {{ tag }},
+
+
+
+ that fills less than {{ model.spaceLeft }} {{ model.spaceLeft == 1 && 'slot' || 'slots' }}
+
+
+ {{ slotPropertyTypeName }} with tags:
+
+
- This slot requires a {{ slotPropertyTypeName }}
-
- with the tag {{ model.slotTags[0] }},
-
-
- with the following tags:
-
+
+ {{ tag }},
-
+ {{ libraryNode.slotQuantityFilled }} slots
+