From f8e9131bdda032862f603ef60ea530023f1b99bd Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Mon, 18 Jul 2022 13:45:14 +0200 Subject: [PATCH] Vastly improved new character UX Characters now can limit which libraries they allow --- .../methods/insertPropertyFromLibraryNode.js | 11 +- .../api/creature/creatures/Creatures.js | 25 +- .../creatures/defaultCharacterProperties.js | 2 +- .../creatures/methods/insertCreature.js | 89 +++- .../api/creature/experience/Experiences.js | 4 +- .../writeComputation/writeScope.js | 11 +- .../api/library/getCreatureLibraryIds.js | 39 ++ app/imports/api/library/getUserLibraryIds.js | 31 ++ app/imports/api/users/Users.js | 4 +- .../server/publications/slotFillers.js | 24 +- .../character/CharacterCreationDialog.vue | 440 ++++++------------ .../ui/creature/creatureList/CreatureList.vue | 1 + .../ui/creature/slots/SlotCardsToFill.vue | 7 +- .../dialogStack/DeleteConfirmationDialog.vue | 2 + .../ui/library/LibraryCollectionHeader.vue | 15 +- app/imports/ui/library/LibraryList.vue | 202 ++++++++ app/imports/ui/library/LibraryListTile.vue | 23 +- app/imports/ui/pages/CharacterList.vue | 215 ++++----- app/imports/ui/pages/Library.vue | 39 +- app/imports/ui/router.js | 3 +- app/server/main.js | 2 +- 21 files changed, 686 insertions(+), 503 deletions(-) create mode 100644 app/imports/api/library/getCreatureLibraryIds.js create mode 100644 app/imports/api/library/getUserLibraryIds.js create mode 100644 app/imports/ui/library/LibraryList.vue diff --git a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js index 1360ffc3..10de2b4a 100644 --- a/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js +++ b/app/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js @@ -131,15 +131,14 @@ function insertPropertyFromNode(nodeId, ancestors, order){ node.order = order; } - // Mark root as dirty - node.dirty = true; + // Mark all nodes as dirty + dirtyNodes(nodes); // Insert the creature properties CreatureProperties.batchInsert(nodes); return node; } - function storeLibraryNodeReferences(nodes){ nodes.forEach(node => { if (node.libraryNodeId) return; @@ -147,6 +146,12 @@ function storeLibraryNodeReferences(nodes){ }); } +function dirtyNodes(nodes) { + nodes.forEach(node => { + node.dirty = true; + }); +} + // Covert node references into actual nodes // TODO: check permissions for each library a reference node references function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0){ diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.js index ef05ff40..311a013e 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.js @@ -80,6 +80,27 @@ let CreatureSchema = new SimpleSchema({ optional: true, max: STORAGE_LIMITS.url, }, + + // Libraries + allowedLibraries: { + type: Array, + optional: true, + maxCount: 100, + }, + 'allowedLibraries.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + allowedLibraryCollections: { + type: Array, + optional: true, + maxCount: 100, + }, + 'allowedLibraryCollections.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + // Mechanics deathSave: { type: deathSaveSchema, @@ -165,8 +186,8 @@ CreatureSchema.extend(SharingSchema); Creatures.attachSchema(CreatureSchema); -import '/imports/api/creature/creatures/methods/index.js'; -import '/imports/api/engine/actions/doAction.js'; export default Creatures; export { CreatureSchema }; + +import '/imports/api/engine/actions/doAction.js'; diff --git a/app/imports/api/creature/creatures/defaultCharacterProperties.js b/app/imports/api/creature/creatures/defaultCharacterProperties.js index dd3d5499..e7f05a11 100644 --- a/app/imports/api/creature/creatures/defaultCharacterProperties.js +++ b/app/imports/api/creature/creatures/defaultCharacterProperties.js @@ -10,7 +10,7 @@ export default function defaultCharacterProperties(creatureId){ { type: 'propertySlot', name: 'Ruleset', - description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'}, + description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base ruleset, your sheet will be empty.'}, slotTags: ['base'], tags: [], quantityExpected: {calculation: '1'}, diff --git a/app/imports/api/creature/creatures/methods/insertCreature.js b/app/imports/api/creature/creatures/methods/insertCreature.js index 7dde2bbb..210f7b49 100644 --- a/app/imports/api/creature/creatures/methods/insertCreature.js +++ b/app/imports/api/creature/creatures/methods/insertCreature.js @@ -1,57 +1,104 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method'; import { RateLimiterMixin } from 'ddp-rate-limiter-mixin'; -import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import simpleSchemaMixin from '/imports/api/creature/mixins/simpleSchemaMixin.js'; +import Creatures, { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import defaultCharacterProperties from '/imports/api/creature/creatures/defaultCharacterProperties.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import assertHasCharactersSlots from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js'; +import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'; +import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; +import { insertExperienceForCreature } from '/imports/api/creature/experience/Experiences.js'; +import SimpleSchema from 'simpl-schema'; const insertCreature = new ValidatedMethod({ - name: 'creatures.insertCreature', - - validate: null, - - mixins: [RateLimiterMixin], + mixins: [RateLimiterMixin, simpleSchemaMixin], + schema: CreatureSchema.pick( + 'name', + 'gender', + 'alignment', + 'allowedLibraries', + 'allowedLibraryCollections', + ).extend({ + 'startingLevel': { + type: SimpleSchema.Integer, + min: 0, + }, + }), rateLimit: { numRequests: 5, timeInterval: 5000, }, - run() { - if (!this.userId) { + run({ name, gender, alignment, startingLevel, + allowedLibraries, allowedLibraryCollections }) { + const userId = this.userId + if (!userId) { throw new Meteor.Error('Creatures.methods.insert.denied', - 'You need to be logged in to insert a creature'); + 'You need to be logged in to insert a creature'); } - assertHasCharactersSlots(this.userId); + assertHasCharactersSlots(userId); - // Create the creature document + // Create the creature document let creatureId = Creatures.insert({ - owner: this.userId, - }); + owner: userId, + name, + gender, + alignment, + allowedLibraries, + allowedLibraryCollections, + }); + + // Insert experience to get character to starting level + if (startingLevel) { + insertExperienceForCreature({ + experience: { + name: 'Starting level', + levels: startingLevel, + creatureId + }, + creatureId, + userId, + }); + } // Insert the default properties // Not batchInsert because we want the properties cleaned by the schema - let baseId; + let baseId, rulesetSlot; defaultCharacterProperties(creatureId).forEach(prop => { let id = CreatureProperties.insert(prop); if (prop.name === 'Ruleset'){ baseId = id; + rulesetSlot = prop; } }); - if (Meteor.isServer){ - // Insert the 5e ruleset as the default base - insertPropertyFromLibraryNode.call({ - nodeIds: ['iHbhfcg3AL5isSWbw'], - parentRef: {id: baseId, collection: 'creatureProperties'}, - order: 0.5, - }); + // If the user only has a single ruleset subscribed, use it by default + if (Meteor.isServer) { + insertDefaultRuleset(baseId, userId, rulesetSlot); } return creatureId; }, }); +// If the user only has a single ruleset subscribed, insert it by default +function insertDefaultRuleset(baseId, userId, slot) { + const libraryIds = getUserLibraryIds(userId); + const filter = getSlotFillFilter({ slot, libraryIds }); + const fillCursor = LibraryNodes.find(filter, { fields: { _id: 1 } }); + const numRulesets = fillCursor.count(); + if (numRulesets === 1) { + const ruleset = fillCursor.fetch()[0] + insertPropertyFromLibraryNode.call({ + nodeIds: [ruleset._id], + parentRef: {id: baseId, collection: 'creatureProperties'}, + order: 0.5, + }); + } +} + export default insertCreature; diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index 60501d02..58b1aeed 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -47,7 +47,6 @@ let ExperienceSchema = new SimpleSchema({ Experiences.attachSchema(ExperienceSchema); const insertExperienceForCreature = function({experience, creatureId, userId}){ - assertEditPermission(creatureId, userId); if (experience.xp){ Creatures.update(creatureId, { $inc: { 'denormalizedStats.xp': experience.xp }, @@ -93,6 +92,7 @@ const insertExperience = new ValidatedMethod({ } let insertedIds = []; creatureIds.forEach(creatureId => { + assertEditPermission(creatureId, userId); let id = insertExperienceForCreature({experience, creatureId, userId}); insertedIds.push(id); }); @@ -181,4 +181,4 @@ const recomputeExperiences = new ValidatedMethod({ }); export default Experiences; -export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences }; +export { ExperienceSchema, insertExperience, insertExperienceForCreature, removeExperience, recomputeExperiences }; diff --git a/app/imports/api/engine/computation/writeComputation/writeScope.js b/app/imports/api/engine/computation/writeComputation/writeScope.js index 1c5bfde3..d96c2bfe 100644 --- a/app/imports/api/engine/computation/writeComputation/writeScope.js +++ b/app/imports/api/engine/computation/writeComputation/writeScope.js @@ -5,8 +5,13 @@ import { EJSON } from 'meteor/ejson'; export default function writeScope(creatureId, computation) { if (!creatureId) throw 'creatureId is required'; const scope = computation.scope; - const variables = computation.variables || {}; + let variables = computation.variables; + if (!variables) { + CreatureVariables.insert({ _creatureId: creatureId }); + variables = {}; + } delete variables._id; + delete variables._creatureId; let $set, $unset; @@ -48,9 +53,9 @@ export default function writeScope(creatureId, computation) { const update = {}; if ($set) update.$set = $set; if ($unset) update.$unset = $unset; - CreatureVariables.upsert({_creatureId: creatureId}, update); + CreatureVariables.update({_creatureId: creatureId}, update); } if (computation.creature?.dirty) { - Creatures.update({_creatureId: creatureId}, {$unset: { dirty: 1 }}); + Creatures.update({_id: creatureId}, {$unset: { dirty: 1 }}); } } diff --git a/app/imports/api/library/getCreatureLibraryIds.js b/app/imports/api/library/getCreatureLibraryIds.js new file mode 100644 index 00000000..beb9c7d3 --- /dev/null +++ b/app/imports/api/library/getCreatureLibraryIds.js @@ -0,0 +1,39 @@ +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; +import Creatures from '/imports/api/creature/creatures/Creatures.js'; +import getUserLibraryIds from './getUserLibraryIds'; +import { intersection, union } from 'lodash'; + +export default function getCreatureLibraryIds(creature, userId) { + if (!userId) return []; + + // Get the ids of libraries the user is permitted to view + const userLibIds = getUserLibraryIds(userId); + + // If given a creature Id, get the creature document + if (typeof creature === 'string') { + creature = Creatures.findOne(creature, { + fields: { + allowedLibraries: 1, + allowedLibraryCollections: 1, + } + }); + if (!creature) return []; + } + + // If the creature does not restrict the libraries, let it use them all + if (!creature.allowedLibraryCollections && !creature.allowedLibraries) { + return userLibIds; + } + + // Get the ids of the libraries that the creature allows + const allowedCollections = creature.allowedLibraryCollections || []; + let creatureLibIds = creature.allowedLibraries || []; + LibraryCollections.find({ + _id: { $in: allowedCollections } + }, { fields: { libraries: 1 } }).forEach(collection => { + creatureLibIds = union(creatureLibIds, collection.libraries); + }); + + // return all the ids that the creature allows and the user can view + return intersection(userLibIds, creatureLibIds); +} \ No newline at end of file diff --git a/app/imports/api/library/getUserLibraryIds.js b/app/imports/api/library/getUserLibraryIds.js new file mode 100644 index 00000000..55f7498d --- /dev/null +++ b/app/imports/api/library/getUserLibraryIds.js @@ -0,0 +1,31 @@ +import LibraryCollections from '/imports/api/library/LibraryCollections.js'; +import Libraries from '/imports/api/library/Libraries.js'; +import { union } from 'lodash'; + +export default function getUserLibraryIds(userId) { + if (!userId) return []; + const user = Meteor.users.findOne(userId); + let subbedIds = user?.subscribedLibraries || []; + const subCollections = user?.subscribedLibraryCollections || []; + LibraryCollections.find({ + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: subCollections }, public: true }, + ] + }, { fields: { libraries: 1 } }).forEach(collection => { + subbedIds = union(subbedIds, collection.libraries); + }); + const libraryIds = Libraries.find({ + $or: [ + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: subbedIds }, public: true }, + ] + }, { + fields: { _id: 1 } + }).map(lib => lib._id); + return libraryIds; +} \ No newline at end of file diff --git a/app/imports/api/users/Users.js b/app/imports/api/users/Users.js index 6f228322..b3152af8 100644 --- a/app/imports/api/users/Users.js +++ b/app/imports/api/users/Users.js @@ -71,7 +71,7 @@ const userSchema = new SimpleSchema({ subscribedLibraries: { type: Array, defaultValue: defaultLibraries, - max: 100, + maxCount: 100, }, 'subscribedLibraries.$': { type: String, @@ -80,7 +80,7 @@ const userSchema = new SimpleSchema({ subscribedLibraryCollections: { type: Array, defaultValue: defaultLibraryCollections, - max: 100, + maxCount: 100, }, 'subscribedLibraryCollections.$': { type: String, diff --git a/app/imports/server/publications/slotFillers.js b/app/imports/server/publications/slotFillers.js index e97fa20f..95a77c6c 100644 --- a/app/imports/server/publications/slotFillers.js +++ b/app/imports/server/publications/slotFillers.js @@ -3,6 +3,7 @@ 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' +import getUserLibraryIds from '/imports/api/library/getUserLibraryIds.js'; import { LIBRARY_NODE_TREE_FIELDS } from '/imports/server/publications/library.js'; const FIELDS = { @@ -27,21 +28,17 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ } // Get all the ids of libraries the user can access - const user = Meteor.users.findOne(userId, { - fields: {subscribedLibraries: 1} - }); - const subs = user && user.subscribedLibraries || []; - let libraries = Libraries.find({ + const libraryIds = getUserLibraryIds(userId); + const libraries = Libraries.find({ $or: [ - {owner: this.userId}, - {writers: this.userId}, - {readers: this.userId}, - {_id: {$in: subs}}, + { owner: userId }, + { writers: userId }, + { readers: userId }, + { _id: { $in: libraryIds }, public: true }, ] }, { - fields: {_id: 1, name: 1}, + sort: { name: 1 } }); - let libraryIds = libraries.map(lib => lib._id); // Build a filter for nodes in those libraries that match the slot let filter = getSlotFillFilter({slot, libraryIds}); @@ -83,7 +80,10 @@ Meteor.publish('slotFillers', function(slotId, searchTerm){ self.setData('countAll', LibraryNodes.find(filter).count()); }); self.autorun(function () { - return [LibraryNodes.find(filter, options), libraries]; + return [ + LibraryNodes.find(filter, options), + libraries + ]; }); }); }); diff --git a/app/imports/ui/creature/character/CharacterCreationDialog.vue b/app/imports/ui/creature/character/CharacterCreationDialog.vue index c9be196f..69e33330 100644 --- a/app/imports/ui/creature/character/CharacterCreationDialog.vue +++ b/app/imports/ui/creature/character/CharacterCreationDialog.vue @@ -4,29 +4,28 @@ New Character - + - Name + Biography + {{ biographyAlert }} - Ability Scores - - - - Class + Libraries @@ -34,195 +33,44 @@ - + + - - -

Point Cost:

-

- {{ cost }} -

- /27 -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Base ValuesRace BonusScoreModifier
Strength - - - - {{ baseStrength + strengthBonus }}{{ mod(baseStrength + strengthBonus) }}
Dexterity - - - - {{ baseDexterity + dexterityBonus }}{{ mod(baseDexterity + dexterityBonus) }}
Constitution - - - - {{ baseConstitution + constitutionBonus }}{{ mod(baseConstitution + constitutionBonus) }}
Intelligence - - - - {{ baseIntelligence + intelligenceBonus }}{{ mod(baseIntelligence + intelligenceBonus) }}
Wisdom - - - - {{ baseWisdom + wisdomBonus }}{{ mod(baseWisdom + wisdomBonus) }}
Charisma - - - - {{ baseCharisma + charismaBonus }}{{ mod(baseCharisma + charismaBonus) }}
-
- - - @@ -243,15 +91,16 @@ Next Create @@ -261,109 +110,112 @@ diff --git a/app/imports/ui/creature/creatureList/CreatureList.vue b/app/imports/ui/creature/creatureList/CreatureList.vue index 1cdcdf96..dac86161 100644 --- a/app/imports/ui/creature/creatureList/CreatureList.vue +++ b/app/imports/ui/creature/creatureList/CreatureList.vue @@ -19,6 +19,7 @@ :is-selected="selectedCreature === creature._id" v-bind="selection ? {} : {to: creature.url}" :dense="dense" + :data-id="dense ? undefined : creature._id" @click="$emit('creature-selected', creature._id)" /> diff --git a/app/imports/ui/creature/slots/SlotCardsToFill.vue b/app/imports/ui/creature/slots/SlotCardsToFill.vue index 506ebc20..0decfebf 100644 --- a/app/imports/ui/creature/slots/SlotCardsToFill.vue +++ b/app/imports/ui/creature/slots/SlotCardsToFill.vue @@ -2,7 +2,7 @@
@@ -54,7 +55,7 @@ export default { }, callback(nodeIds){ if (!nodeIds || !nodeIds.length) return; - let newPropertyId = insertPropertyFromLibraryNode.call({ + insertPropertyFromLibraryNode.call({ nodeIds, parentRef: { 'id': slotId, @@ -66,7 +67,6 @@ export default { snackbar({text: error.reason || error.message || error.toString()}); } }); - return `slot-child-${newPropertyId}`; } }); }, @@ -100,5 +100,4 @@ export default { diff --git a/app/imports/ui/dialogStack/DeleteConfirmationDialog.vue b/app/imports/ui/dialogStack/DeleteConfirmationDialog.vue index ffe5d186..d7f90460 100644 --- a/app/imports/ui/dialogStack/DeleteConfirmationDialog.vue +++ b/app/imports/ui/dialogStack/DeleteConfirmationDialog.vue @@ -12,6 +12,8 @@

diff --git a/app/imports/ui/library/LibraryCollectionHeader.vue b/app/imports/ui/library/LibraryCollectionHeader.vue index c31bc9fe..39713450 100644 --- a/app/imports/ui/library/LibraryCollectionHeader.vue +++ b/app/imports/ui/library/LibraryCollectionHeader.vue @@ -2,8 +2,19 @@ - + + + + @@ -56,6 +67,8 @@ export default { open: Boolean, selection: Boolean, dense: Boolean, + isSelected: Boolean, + disabled: Boolean, }, data(){return { renaming: false, diff --git a/app/imports/ui/library/LibraryList.vue b/app/imports/ui/library/LibraryList.vue new file mode 100644 index 00000000..c850ddd7 --- /dev/null +++ b/app/imports/ui/library/LibraryList.vue @@ -0,0 +1,202 @@ + + + diff --git a/app/imports/ui/library/LibraryListTile.vue b/app/imports/ui/library/LibraryListTile.vue index 7ae679cb..d2a9fa7f 100644 --- a/app/imports/ui/library/LibraryListTile.vue +++ b/app/imports/ui/library/LibraryListTile.vue @@ -4,9 +4,21 @@ > - + + + + @@ -31,7 +43,12 @@ export default { }, selection: Boolean, isSelected: Boolean, - dense: Boolean, + selectedByCollection: Boolean, + disabled: Boolean, + to: { + type: Object, + required: true, + } } } diff --git a/app/imports/ui/pages/CharacterList.vue b/app/imports/ui/pages/CharacterList.vue index 05a8c707..3a675911 100644 --- a/app/imports/ui/pages/CharacterList.vue +++ b/app/imports/ui/pages/CharacterList.vue @@ -74,127 +74,108 @@ diff --git a/app/imports/ui/pages/Library.vue b/app/imports/ui/pages/Library.vue index a1f1866a..65a2d18f 100644 --- a/app/imports/ui/pages/Library.vue +++ b/app/imports/ui/pages/Library.vue @@ -10,38 +10,7 @@ xl="8" > - - - - - - - + { meta: { title: 'Library Collection', }, - },{ + }, { + name: 'characterSheet', path: '/character/:id', alias: '/character/:id/:urlName', components: { diff --git a/app/server/main.js b/app/server/main.js index 31140ade..642113e0 100644 --- a/app/server/main.js +++ b/app/server/main.js @@ -15,4 +15,4 @@ import '/imports/migrations/methods/index.js' import '/imports/constants/MAINTENANCE_MODE.js'; import '/imports/api/creature/creatureProperties/methods/index.js'; import '/imports/api/creature/archive/methods/index.js'; - +import '/imports/api/creature/creatures/methods/index.js';