From d1e7eb2fa011d95f58ab8728ce2b47c09e8cd9c2 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Fri, 5 Jun 2020 16:14:26 +0200 Subject: [PATCH] Added basic XP system --- app/imports/api/creature/Creatures.js | 7 +- .../api/creature/experience/Experiences.js | 164 ++++++++++++++++++ .../journal/JournalEntry.js} | 21 ++- app/imports/api/creature/removeCreature.js | 11 +- .../computedPropertySchemasIndex.js | 2 - .../api/properties/propertySchemasIndex.js | 2 - app/imports/constants/PROPERTIES.js | 4 - .../server/publications/experiences.js | 32 ++++ app/imports/server/publications/index.js | 1 + .../characterSheetTabs/PersonaTab.vue | 108 +++++++++++- .../creature/experiences/ExperienceForm.vue | 66 +++++++ .../experiences/ExperienceInsertDialog.vue | 96 ++++++++++ .../experiences/ExperienceListDialog.vue | 92 ++++++++++ .../ui/dialogStack/DialogComponentIndex.js | 5 +- 14 files changed, 589 insertions(+), 22 deletions(-) create mode 100644 app/imports/api/creature/experience/Experiences.js rename app/imports/api/{properties/Experiences.js => creature/journal/JournalEntry.js} (66%) create mode 100644 app/imports/server/publications/experiences.js create mode 100644 app/imports/ui/creature/experiences/ExperienceForm.vue create mode 100644 app/imports/ui/creature/experiences/ExperienceInsertDialog.vue create mode 100644 app/imports/ui/creature/experiences/ExperienceListDialog.vue diff --git a/app/imports/api/creature/Creatures.js b/app/imports/api/creature/Creatures.js index 2c70a916..45caaa1a 100644 --- a/app/imports/api/creature/Creatures.js +++ b/app/imports/api/creature/Creatures.js @@ -65,16 +65,21 @@ let CreatureSchema = new SimpleSchema({ type: String, optional: true, }, - // Mechanics deathSave: { type: deathSaveSchema, defaultValue: {}, }, + // Sum of all XP gained by this character xp: { type: SimpleSchema.Integer, defaultValue: 0, }, + // Sum of all levels granted by milestone XP + xpLevels: { + type: SimpleSchema.Integer, + defaultValue: 0, + }, weightCarried: { type: Number, defaultValue: 0, diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js new file mode 100644 index 00000000..bceb43b8 --- /dev/null +++ b/app/imports/api/creature/experience/Experiences.js @@ -0,0 +1,164 @@ +import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; +import { getUserTier } from '/imports/api/users/patreon/tiers.js'; +import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js'; +import Creatures from '/imports/api/creature/Creatures.js'; + +let Experiences = new Mongo.Collection('experiences'); + +let ExperienceSchema = new SimpleSchema({ + name: { + type: String, + optional: true, + }, + // The amount of XP this experience gives + xp: { + type: SimpleSchema.Integer, + optional: true, + min: 0, + }, + // Setting levels instead of value grants whole levels + levels: { + type: SimpleSchema.Integer, + optional: true, + min: 0, + index: 1, + }, + // The real-world date that it occured, usually sorted by date + date: { + type: Date, + autoValue: function() { + // If the date isn't set, set it to now + if (!this.isSet) { + return new Date(); + } + }, + index: 1, + }, + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + }, +}); + +Experiences.attachSchema(ExperienceSchema); + +const insertExperienceForCreature = function({experience, creatureId, userId}){ + assertEditPermission(creatureId, userId); + if (experience.xp){ + Creatures.update(creatureId, {$inc: {xp: experience.xp}}); + } + if (experience.levels) { + Creatures.update(creatureId, {$inc: {xpLevels: experience.levels}}); + } + experience.creatureId = creatureId; + return Experiences.insert(experience); +}; + +const insertExperience = new ValidatedMethod({ + name: 'Experiences.methods.insert', + validate: new SimpleSchema({ + experience: { + type: ExperienceSchema.omit('creatureId'), + }, + creatureIds: { + type: Array, + max: 12, + }, + 'creatureIds.$': { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + run({experience, creatureIds}) { + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('Experiences.methods.insert.denied', + 'You need to be logged in to insert an experience'); + } + 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}); + insertedIds.push(id); + }); + return insertedIds; + }, +}); + +const removeExperience = new ValidatedMethod({ + name: 'Experiences.methods.remove', + validate: new SimpleSchema({ + experienceId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + run({experienceId}) { + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('Experiences.methods.remove.denied', + 'You need to be logged in to remove an experience'); + } + 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 + assertEditPermission(creatureId, userId); + if (experience.xp){ + Creatures.update(creatureId, {$inc: {xp: -experience.xp}}); + } + if (experience.levels) { + Creatures.update(creatureId, {$inc: {xpLevels: -experience.levels}}); + } + experience.creatureId = creatureId; + return Experiences.remove(experienceId); + }, +}); + +const recomputeExperiences = new ValidatedMethod({ + name: 'Experiences.methods.recompute', + validate: new SimpleSchema({ + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, + }).validator(), + run({creatureId}) { + let userId = this.userId; + if (!userId) { + throw new Meteor.Error('Experiences.methods.recompute.denied', + 'You need to be logged in to recompute a creature\'s experiences'); + } + 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; + let xpLevels = 0; + Experiences.find({ + creatureId + }, { + fields: {xp: 1, levels: 1} + }).forEach(experience => { + xp += experience.xp || 0; + xpLevels += experience.levels || 0; + }); + Creatures.update(creatureId, {$set: {xp, xpLevels}}); + }, +}); + +export default Experiences; +export { ExperienceSchema, insertExperience, removeExperience, recomputeExperiences }; diff --git a/app/imports/api/properties/Experiences.js b/app/imports/api/creature/journal/JournalEntry.js similarity index 66% rename from app/imports/api/properties/Experiences.js rename to app/imports/api/creature/journal/JournalEntry.js index 2b4a0336..39067fd1 100644 --- a/app/imports/api/properties/Experiences.js +++ b/app/imports/api/creature/journal/JournalEntry.js @@ -1,7 +1,7 @@ import SimpleSchema from 'simpl-schema'; let ExperienceSchema = new SimpleSchema({ - name: { + title: { type: String, optional: true, }, @@ -10,11 +10,6 @@ let ExperienceSchema = new SimpleSchema({ type: String, optional: true, }, - // The amount of XP this experience gives - value: { - type: SimpleSchema.Integer, - optional: true, - }, // The real-world date that it occured date: { type: Date, @@ -30,6 +25,20 @@ let ExperienceSchema = new SimpleSchema({ type: String, optional: true, }, + // Tags to better find this entry later + tags: { + type: Array, + defaultValue: [], + }, + 'tags.$': { + type: String, + }, + // ID of the journal this entry belongs to + journalId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + index: 1, + } }); export { ExperienceSchema }; diff --git a/app/imports/api/creature/removeCreature.js b/app/imports/api/creature/removeCreature.js index aee7fe09..4f263e98 100644 --- a/app/imports/api/creature/removeCreature.js +++ b/app/imports/api/creature/removeCreature.js @@ -1,14 +1,17 @@ import SimpleSchema from 'simpl-schema'; +import { ValidatedMethod } from 'meteor/mdg:validated-method'; import Creatures from '/imports/api/creature/Creatures.js'; import CreatureProperties from '/imports/api/creature/CreatureProperties.js' import { assertOwnership } from '/imports/api/creature/creaturePermissions.js'; +import Experiences from '/imports/api/creature/experience/Experiences.js'; -function removeRelatedDocuments(charId){ - CreatureProperties.remove({'ancestors.id': charId}); -}; +function removeRelatedDocuments(creatureId){ + CreatureProperties.remove({'ancestors.id': creatureId}); + Experiences.remove({creatureId}); +} const removeCreature = new ValidatedMethod({ - name: "Creatures.methods.removeCreature", // DDP method name + name: 'Creatures.methods.removeCreature', // DDP method name validate: new SimpleSchema({ charId: { type: String, diff --git a/app/imports/api/properties/computedPropertySchemasIndex.js b/app/imports/api/properties/computedPropertySchemasIndex.js index 1b4ce80f..56f4ea98 100644 --- a/app/imports/api/properties/computedPropertySchemasIndex.js +++ b/app/imports/api/properties/computedPropertySchemasIndex.js @@ -9,7 +9,6 @@ import { ContainerSchema } from '/imports/api/properties/Containers.js'; import { DamageSchema } from '/imports/api/properties/Damages.js'; import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; import { ComputedEffectSchema } from '/imports/api/properties/Effects.js'; -import { ExperienceSchema } from '/imports/api/properties/Experiences.js'; import { FeatureSchema } from '/imports/api/properties/Features.js'; import { FolderSchema } from '/imports/api/properties/Folders.js'; import { ItemSchema } from '/imports/api/properties/Items.js'; @@ -33,7 +32,6 @@ const propertySchemasIndex = { damage: DamageSchema, damageMultiplier: DamageMultiplierSchema, effect: ComputedEffectSchema, - experience: ExperienceSchema, feature: FeatureSchema, folder: FolderSchema, note: NoteSchema, diff --git a/app/imports/api/properties/propertySchemasIndex.js b/app/imports/api/properties/propertySchemasIndex.js index 585c5f67..088430d3 100644 --- a/app/imports/api/properties/propertySchemasIndex.js +++ b/app/imports/api/properties/propertySchemasIndex.js @@ -8,7 +8,6 @@ import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js'; import { DamageSchema } from '/imports/api/properties/Damages.js'; import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js'; import { EffectSchema } from '/imports/api/properties/Effects.js'; -import { ExperienceSchema } from '/imports/api/properties/Experiences.js'; import { FeatureSchema } from '/imports/api/properties/Features.js'; import { FolderSchema } from '/imports/api/properties/Folders.js'; import { NoteSchema } from '/imports/api/properties/Notes.js'; @@ -33,7 +32,6 @@ const propertySchemasIndex = { damage: DamageSchema, damageMultiplier: DamageMultiplierSchema, effect: EffectSchema, - experience: ExperienceSchema, feature: FeatureSchema, folder: FolderSchema, note: NoteSchema, diff --git a/app/imports/constants/PROPERTIES.js b/app/imports/constants/PROPERTIES.js index a5b13416..43bfdab9 100644 --- a/app/imports/constants/PROPERTIES.js +++ b/app/imports/constants/PROPERTIES.js @@ -39,10 +39,6 @@ const PROPERTIES = Object.freeze({ icon: '$vuetify.icons.effect', name: 'Effect' }, - experience: { - icon: '$vuetify.icons.experience', - name: 'Experience' - }, feature: { icon: 'subject', name: 'Feature' diff --git a/app/imports/server/publications/experiences.js b/app/imports/server/publications/experiences.js new file mode 100644 index 00000000..b268c2ba --- /dev/null +++ b/app/imports/server/publications/experiences.js @@ -0,0 +1,32 @@ +import SimpleSchema from 'simpl-schema'; +import Creatures from '/imports/api/creature/Creatures.js'; +import Experiences from '/imports/api/creature/experience/Experiences.js'; + +let schema = new SimpleSchema({ + creatureId: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, +}); + +Meteor.publish('experiences', function(creatureId){ + schema.validate({ creatureId }); + this.autorun(function (){ + let userId = this.userId; + let creatureCursor = Creatures.find({ + _id: creatureId, + $or: [ + {readers: userId}, + {writers: userId}, + {owner: userId}, + {public: true}, + ], + }); + if (!creatureCursor.count()) return this.ready(); + return [ + Experiences.find({ + creatureId, + }), + ]; + }); +}); diff --git a/app/imports/server/publications/index.js b/app/imports/server/publications/index.js index 054aa2ff..ec61661f 100644 --- a/app/imports/server/publications/index.js +++ b/app/imports/server/publications/index.js @@ -1,5 +1,6 @@ import '/imports/server/publications/characterList.js'; import '/imports/server/publications/library.js'; import '/imports/server/publications/singleCharacter.js'; +import '/imports/server/publications/experiences.js'; import '/imports/server/publications/users.js'; import '/imports/server/publications/icons.js'; diff --git a/app/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue b/app/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue index f2da564a..aa152899 100644 --- a/app/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue +++ b/app/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue @@ -20,6 +20,61 @@ +
+ + + Level {{ creature.variables.level.value }} + + + + + + {{ creature.xpLevels }} Levels gained + + + {{ creature.xp }} XP + + + + + info + + + + + add + + + + + + + {{ classLevel.name }} + + + + {{ classLevel.level }} + + + + +
{ + let name = classLevel.vairableName; + if ( + !highestLevels[name] || + highestLevels[name].level < classLevel.level + ){ + highestLevels[name] = classLevel; + } + }); + for (let name in highestLevels){ + highestLevelsList.push(highestLevels[name]); + } + highestLevelsList.sort((a, b) => a.level - b.level); + return highestLevelsList; + }, + }, methods: { showCharacterForm(){ this.$store.commit('pushDialogStack', { @@ -70,6 +155,25 @@ export default { }, }); }, + addExperience(){ + this.$store.commit('pushDialogStack', { + component: 'experience-insert-dialog', + elementId: 'experience-add-button', + data: { + creatureIds: [this.creatureId], + startAsMilestone: !!this.creature.xpLevels + }, + }); + }, + showExperienceList(){ + this.$store.commit('pushDialogStack', { + component: 'experience-list-dialog', + elementId: 'experience-info-button', + data: { + creatureId: this.creatureId, + }, + }); + }, }, }; diff --git a/app/imports/ui/creature/experiences/ExperienceForm.vue b/app/imports/ui/creature/experiences/ExperienceForm.vue new file mode 100644 index 00000000..949498d5 --- /dev/null +++ b/app/imports/ui/creature/experiences/ExperienceForm.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/app/imports/ui/creature/experiences/ExperienceInsertDialog.vue b/app/imports/ui/creature/experiences/ExperienceInsertDialog.vue new file mode 100644 index 00000000..17f6f7d4 --- /dev/null +++ b/app/imports/ui/creature/experiences/ExperienceInsertDialog.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/app/imports/ui/creature/experiences/ExperienceListDialog.vue b/app/imports/ui/creature/experiences/ExperienceListDialog.vue new file mode 100644 index 00000000..b0c08a81 --- /dev/null +++ b/app/imports/ui/creature/experiences/ExperienceListDialog.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/app/imports/ui/dialogStack/DialogComponentIndex.js b/app/imports/ui/dialogStack/DialogComponentIndex.js index ec247f93..2008373c 100644 --- a/app/imports/ui/dialogStack/DialogComponentIndex.js +++ b/app/imports/ui/dialogStack/DialogComponentIndex.js @@ -3,6 +3,8 @@ import CreaturePropertyCreationDialog from '/imports/ui/creature/creaturePropert import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue' import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue' import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue'; +import ExperienceInsertDialog from '/imports/ui/creature/experiences/ExperienceInsertDialog.vue'; +import ExperienceListDialog from '/imports/ui/creature/experiences/ExperienceListDialog.vue'; import InviteDialog from '/imports/ui/user/InviteDialog.vue'; import LibraryCreationDialog from '/imports/ui/library/LibraryCreationDialog.vue'; import LibraryEditDialog from '/imports/ui/library/LibraryEditDialog.vue'; @@ -13,13 +15,14 @@ import ShareDialog from '/imports/ui/sharing/ShareDialog.vue'; import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue'; import UsernameDialog from '/imports/ui/user/UsernameDialog.vue'; - export default { CreatureFormDialog, CreaturePropertyCreationDialog, CreaturePropertyDialog, CreaturePropertyFromLibraryDialog, DeleteConfirmationDialog, + ExperienceInsertDialog, + ExperienceListDialog, InviteDialog, LibraryCreationDialog, LibraryEditDialog,