diff --git a/test/api/unit/models/user.test.js b/test/api/unit/models/user.test.js index 24652fa7b6..84495d94ba 100644 --- a/test/api/unit/models/user.test.js +++ b/test/api/unit/models/user.test.js @@ -384,6 +384,7 @@ describe('User Model', () => { user = await user.save(); // verify that it's been awarded expect(user.achievements.beastMaster).to.equal(true); + expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist; // reset the user user.achievements.beastMasterCount = 0; @@ -414,6 +415,25 @@ describe('User Model', () => { expect(user.achievements.beastMaster).to.not.equal(true); }); + it('adds achievements to notification list', async () => { + let user = new User(); + user = await user.save(); // necessary for user.isSelected to work correctly + + // Create conditions for achievements to be awarded + user.achievements.beastMasterCount = 3; + user.achievements.mountMasterCount = 3; + user.achievements.triadBingoCount = 3; + expect(user.achievements.beastMaster).to.not.equal(true); // verify that it was not awarded initially + expect(user.achievements.mountMaster).to.not.equal(true); // verify that it was not awarded initially + expect(user.achievements.triadBingo).to.not.equal(true); // verify that it was not awarded initially + + user = await user.save(); + // verify that it's been awarded + expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_BEAST_MASTER')).to.exist; + expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_MOUNT_MASTER')).to.exist; + expect(user.notifications.find(notification => notification.type === 'ACHIEVEMENT_TRIAD_BINGO')).to.exist; + }); + context('manage unallocated stats points notifications', () => { it('doesn\'t add a notification if there are no points to allocate', async () => { let user = new User(); diff --git a/test/api/v3/integration/groups/POST-groups_groupId_join.test.js b/test/api/v3/integration/groups/POST-groups_groupId_join.test.js index a3d88d82a4..e83ba0b7ad 100644 --- a/test/api/v3/integration/groups/POST-groups_groupId_join.test.js +++ b/test/api/v3/integration/groups/POST-groups_groupId_join.test.js @@ -314,6 +314,7 @@ describe('POST /group/:groupId/join', () => { name: 'Testing Party', type: 'party', }); + await leader.post(`/groups/${party._id}/invite`, { uuids: [member._id], }); @@ -325,7 +326,9 @@ describe('POST /group/:groupId/join', () => { await leader.sync(); expect(member).to.have.nested.property('achievements.partyUp', true); + expect(member.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_UP')).to.exist; expect(leader).to.have.nested.property('achievements.partyUp', true); + expect(leader.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_UP')).to.exist; }); it('does not award Party On achievement to party of size 2', async () => { @@ -349,7 +352,9 @@ describe('POST /group/:groupId/join', () => { await leader.sync(); expect(member).to.have.nested.property('achievements.partyOn', true); + expect(member.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_ON')).to.exist; expect(leader).to.have.nested.property('achievements.partyOn', true); + expect(leader.notifications.find(notification => notification.type === 'ACHIEVEMENT_PARTY_ON')).to.exist; }); }); }); diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue index 9416b5f3cd..592ab5f0f6 100644 --- a/website/client/components/groups/group.vue +++ b/website/client/components/groups/group.vue @@ -440,7 +440,6 @@ export default { if (this.isParty) { await this.$store.dispatch('party:getParty', true); this.group = this.$store.state.party.data; - this.checkForAchievements(); } else { const group = await this.$store.dispatch('guilds:getGroup', {groupId: this.searchId}); this.$set(this, 'group', group); @@ -462,21 +461,6 @@ export default { return n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupId; }); }, - checkForAchievements () { - // Checks if user's party has reached 2 players for the first time. - if (!this.user.achievements.partyUp && this.group.memberCount >= 2) { - // @TODO - // User.set({'achievements.partyUp':true}); - // Achievement.displayAchievement('partyUp'); - } - - // Checks if user's party has reached 4 players for the first time. - if (!this.user.achievements.partyOn && this.group.memberCount >= 4) { - // @TODO - // User.set({'achievements.partyOn':true}); - // Achievement.displayAchievement('partyOn'); - } - }, async join () { if (this.group.cancelledPlan && !confirm(this.$t('aboutToJoinCancelledGroupPlan'))) { return; diff --git a/website/client/components/notifications.vue b/website/client/components/notifications.vue index b5e688c1ad..de64c49455 100644 --- a/website/client/components/notifications.vue +++ b/website/client/components/notifications.vue @@ -175,6 +175,43 @@ const NOTIFICATIONS = { label: ($t) => `${$t('achievement')}: ${$t('achievementAridAuthority')}`, modalId: 'generic-achievement', }, + ACHIEVEMENT_PARTY_UP: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('achievementPartyUp')}`, + modalId: 'generic-achievement', + }, + ACHIEVEMENT_PARTY_ON: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('achievementPartyOn')}`, + modalId: 'generic-achievement', + }, + ACHIEVEMENT_BEAST_MASTER: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('beastAchievement')}`, + modalId: 'generic-achievement', + data: { + message: ($t) => $t('achievement'), + modalText: ($t) => $t('mountAchievement'), + }, + }, + ACHIEVEMENT_MOUNT_MASTER: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('mountAchievement')}`, + modalId: 'generic-achievement', + data: { + message: ($t) => $t('achievement'), + modalText: ($t) => $t('mountAchievement'), + }, + }, + ACHIEVEMENT_TRIAD_BINGO: { + achievement: true, + label: ($t) => `${$t('achievement')}: ${$t('triadBingoAchievement')}`, + modalId: 'generic-achievement', + data: { + message: ($t) => $t('achievement'), + modalText: ($t) => $t('triadBingoAchievement'), + }, + }, }; export default { @@ -230,7 +267,8 @@ export default { 'ULTIMATE_GEAR_ACHIEVEMENT', 'REBIRTH_ACHIEVEMENT', 'GUILD_JOINED_ACHIEVEMENT', 'CHALLENGE_JOINED_ACHIEVEMENT', 'INVITED_FRIEND_ACHIEVEMENT', 'NEW_CONTRIBUTOR_LEVEL', 'CRON', 'SCORED_TASK', 'LOGIN_INCENTIVE', 'ACHIEVEMENT_ALL_YOUR_BASE', 'ACHIEVEMENT_BACK_TO_BASICS', - 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY', 'GENERIC_ACHIEVEMENT', + 'GENERIC_ACHIEVEMENT', 'ACHIEVEMENT_PARTY_UP', 'ACHIEVEMENT_PARTY_ON', 'ACHIEVEMENT_BEAST_MASTER', + 'ACHIEVEMENT_MOUNT_MASTER', 'ACHIEVEMENT_TRIAD_BINGO', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY', ].forEach(type => { handledNotifications[type] = true; }); @@ -381,21 +419,30 @@ export default { if (!config) { return; } - if (config.achievement) { this.playSound('Achievement_Unlocked'); } else if (config.sound) { this.playSound(config.sound); } + let data = {}; if (notification.data) { - this.notificationData = notification.data; + data = notification.data; } + if (!data.modalText && config.data.modalText) { + data.modalText = config.data.modalText(this.$t); + } + if (!data.message && config.data.message) { + data.message = config.data.message(this.$t); + } + + this.notificationData = data; if (forceToModal) { this.$root.$emit('bv::show::modal', config.modalId); } else { this.text(config.label(this.$t), () => { + this.notificationData = data; this.$root.$emit('bv::show::modal', config.modalId); }, false); } @@ -607,6 +654,11 @@ export default { case 'ACHIEVEMENT_BACK_TO_BASICS': case 'ACHIEVEMENT_DUST_DEVIL': case 'ACHIEVEMENT_ARID_AUTHORITY': + case 'ACHIEVEMENT_PARTY_UP': + case 'ACHIEVEMENT_PARTY_ON': + case 'ACHIEVEMENT_BEAST_MASTER': + case 'ACHIEVEMENT_MOUNT_MASTER': + case 'ACHIEVEMENT_TRIAD_BINGO': case 'GENERIC_ACHIEVEMENT': this.showNotificationWithModal(notification); break; diff --git a/website/common/locales/en/achievements.json b/website/common/locales/en/achievements.json index 7de4a1baf9..d4af29ae35 100644 --- a/website/common/locales/en/achievements.json +++ b/website/common/locales/en/achievements.json @@ -22,9 +22,11 @@ "achievementDustDevil": "Dust Devil", "achievementDustDevilText": "Has collected all Desert Pets.", "achievementDustDevilModalText": "You collected all the Desert Pets!", + "achievementPartyUp": "You teamed up with a party member!", "achievementAridAuthority": "Arid Authority", "achievementAridAuthorityText": "Has tamed all Desert Mounts.", "achievementAridAuthorityModalText": "You tamed all the Desert Mounts!", "achievementKickstarter2019": "Pin Kickstarter Backer", - "achievementKickstarter2019Text": "Backed the 2019 Pin Kickstarter Project" + "achievementKickstarter2019Text": "Backed the 2019 Pin Kickstarter Project", + "achievementPartyOn": "Your party grew to 4 members!" } diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 41e1884ae5..9b97876844 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -606,13 +606,25 @@ api.joinGroup = { promises.push(User.update({ $or: [{'party._id': group._id}, {_id: user._id}], 'achievements.partyUp': {$ne: true}, - }, {$set: {'achievements.partyUp': true}}, {multi: true}).exec()); + }, {$set: {'achievements.partyUp': true}, $push: {notifications: {type: 'ACHIEVEMENT_PARTY_UP'}}}, {multi: true}).exec()); + if (inviter) { + if (inviter.achievements.partyUp !== true) { + // Since the notification list of the inviter is already updated in this save we need to add the notification here + inviter.addNotification('ACHIEVEMENT_PARTY_UP'); + } + } } if (group.memberCount > 3) { promises.push(User.update({ $or: [{'party._id': group._id}, {_id: user._id}], 'achievements.partyOn': {$ne: true}, - }, {$set: {'achievements.partyOn': true}}, {multi: true}).exec()); + }, {$set: {'achievements.partyOn': true}, $push: {notifications: {type: 'ACHIEVEMENT_PARTY_ON'}}}, {multi: true}).exec()); + if (inviter) { + if (inviter.achievements.partyOn !== true) { + // Since the notification list of the inviter is already updated in this save we need to add the notification here + inviter.addNotification('ACHIEVEMENT_PARTY_ON'); + } + } } } diff --git a/website/server/models/user/hooks.js b/website/server/models/user/hooks.js index 676231ef59..8db979406d 100644 --- a/website/server/models/user/hooks.js +++ b/website/server/models/user/hooks.js @@ -188,23 +188,26 @@ schema.pre('save', true, function preSaveUser (next, done) { // Determines if Beast Master should be awarded let beastMasterProgress = common.count.beastMasterProgress(this.items.pets); - if (beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) { + if ((beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) && this.achievements.beastMaster !== true) { this.achievements.beastMaster = true; + this.addNotification('ACHIEVEMENT_BEAST_MASTER'); } // Determines if Mount Master should be awarded let mountMasterProgress = common.count.mountMasterProgress(this.items.mounts); - if (mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) { + if ((mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) && this.achievements.mountMaster !== true) { this.achievements.mountMaster = true; + this.addNotification('ACHIEVEMENT_MOUNT_MASTER'); } // Determines if Triad Bingo should be awarded let dropPetCount = common.count.dropPetsCurrentlyOwned(this.items.pets); let qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90; - if (qualifiesForTriad || this.achievements.triadBingoCount > 0) { + if ((qualifiesForTriad || this.achievements.triadBingoCount > 0) && this.achievements.triadBingo !== true) { this.achievements.triadBingo = true; + this.addNotification('ACHIEVEMENT_TRIAD_BINGO'); } // EXAMPLE CODE for allowing all existing and new players to be diff --git a/website/server/models/userNotification.js b/website/server/models/userNotification.js index aa3ed7abfc..944c9504f0 100644 --- a/website/server/models/userNotification.js +++ b/website/server/models/userNotification.js @@ -40,6 +40,11 @@ const NOTIFICATION_TYPES = [ 'ACHIEVEMENT_MIND_OVER_MATTER', 'ACHIEVEMENT_DUST_DEVIL', 'ACHIEVEMENT_ARID_AUTHORITY', + 'ACHIEVEMENT_PARTY_UP', + 'ACHIEVEMENT_PARTY_ON', + 'ACHIEVEMENT_BEAST_MASTER', + 'ACHIEVEMENT_MOUNT_MASTER', + 'ACHIEVEMENT_TRIAD_BINGO', ]; const Schema = mongoose.Schema;