From bf5ad2db1ff3a6f63e5dc386d3f9840831f7bacb Mon Sep 17 00:00:00 2001 From: Travis Date: Wed, 28 Dec 2016 01:38:52 -0600 Subject: [PATCH] Fixing Exponential Quest Reward Scrolls (#7800) * adding quest owner specific rewards. closes #2715 * Updating model to prevent this from being a breaking change. * Removing duplicate translatable string and readding accidentally deleted portion * capitalizing according to pr. * fixing according to comments on pr * removing final mistakes * fixing whitespace * re-adding the onlyOwner field that got deleted when the index.js file was moved and fixed console errors. * moving cleaning of empty obejct for quest owner updates into quest owner updates method * Fixing so tests pass by updating variable name and removing unnecessary parameter definition. * adding a new test and refactoring client side code to use controller method. --- test/api/v3/unit/models/group.test.js | 24 +++++- .../js/controllers/inventoryCtrl.js | 4 + .../client-old/js/directives/directives.js | 3 +- .../client-old/js/services/questServices.js | 21 ++++-- website/common/locales/en/quests.json | 7 +- website/common/script/content/index.js | 24 ++++-- website/server/models/group.js | 74 ++++++++++++------- .../views/shared/modals/quest-rewards.jade | 9 ++- website/views/shared/modals/quests.jade | 6 +- 9 files changed, 123 insertions(+), 49 deletions(-) diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js index 124ac9300a..04b545d45e 100644 --- a/test/api/v3/unit/models/group.test.js +++ b/test/api/v3/unit/models/group.test.js @@ -1259,14 +1259,34 @@ describe('Group Model', () => { expect(updatedParticipatingMember.items.hatchingPotions.Shade).to.eql(2); }); - it('awards quests', async () => { + it('awards quest scrolls to owner', async () => { + let questAwardQuest = questScrolls.vice2; + + await party.finishQuest(questAwardQuest); + + let updatedLeader = await User.findById(questLeader._id); + + expect(updatedLeader.items.quests.vice3).to.eql(1); + }); + + it('awards non quest leader rewards to quest leader', async () => { + let gearQuest = questScrolls.vice3; + + await party.finishQuest(gearQuest); + + let updatedLeader = await User.findById(questLeader._id); + + expect(updatedLeader.items.gear.owned.weapon_special_2).to.eql(true); + }); + + it('doesn\'t award quest owner rewards to all participants', async () => { let questAwardQuest = questScrolls.vice2; await party.finishQuest(questAwardQuest); let updatedParticipatingMember = await User.findById(participatingMember._id); - expect(updatedParticipatingMember.items.quests.vice3).to.eql(1); + expect(updatedParticipatingMember.items.quests.vice3).to.not.exist; }); it('awards pets', async () => { diff --git a/website/client-old/js/controllers/inventoryCtrl.js b/website/client-old/js/controllers/inventoryCtrl.js index 73f6d18db8..959cf53522 100644 --- a/website/client-old/js/controllers/inventoryCtrl.js +++ b/website/client-old/js/controllers/inventoryCtrl.js @@ -444,6 +444,10 @@ habitrpg.controller("InventoryCtrl", }); }; + $scope.getQuestOwnerRewards = function(quest) { + return _.filter(quest.drop.items, 'onlyOwner'); + }; + function findPet (fn) { var pets = Object.keys(user.items.pets); return pets.find(function (petKey) { diff --git a/website/client-old/js/directives/directives.js b/website/client-old/js/directives/directives.js index cd43ccb67c..789783cf22 100644 --- a/website/client-old/js/directives/directives.js +++ b/website/client-old/js/directives/directives.js @@ -71,7 +71,8 @@ habitrpg.directive('questRewards', ['$rootScope', function($rootScope){ restrict: 'AE', templateUrl: 'partials/options.social.party.quest-rewards.html', link: function(scope, element, attrs){ - scope.header = attrs.header || 'Rewards'; + scope.headerParticipant = attrs.headerParticipant || env.t('rewardsAllParticipants'); + scope.headerQuestOwner = attrs.headerQuestOwner || env.t('rewardsQuestOwner'); scope.quest = $rootScope.Content.quests[attrs.key]; } } diff --git a/website/client-old/js/services/questServices.js b/website/client-old/js/services/questServices.js index 815318636c..017b779e89 100644 --- a/website/client-old/js/services/questServices.js +++ b/website/client-old/js/services/questServices.js @@ -83,17 +83,26 @@ angular.module('habitrpg') } } text += '---\n\n'; - text += '**' + window.env.t('rewards') + ':**\n\n'; - if(quest.drop.items) { - for (var item in quest.drop.items) { - text += quest.drop.items[item].text() + '\n\n'; - } + text += '**' + window.env.t('rewardsAllParticipants') + ':**\n\n'; + var participantRewards = _.reject(quest.drop.items, 'onlyOwner'); + if(participantRewards.length > 0) { + _.each(participantRewards, function(item) { + text += item.text() + '\n\n'; + }); } if(quest.drop.exp) text += quest.drop.exp + ' ' + window.env.t('experience') + '\n\n'; if(quest.drop.gp) text += quest.drop.gp + ' ' + window.env.t('gold') + '\n\n'; + var ownerRewards = _.filter(quest.drop.items, 'onlyOwner'); + if(ownerRewards.length > 0) { + text += '**' + window.env.t('rewardsQuestOwner') + ':**\n\n'; + _.each(ownerRewards, function(item){ + text += item.text() + '\n\n'; + }); + } + return text; } @@ -140,7 +149,7 @@ angular.module('habitrpg') var quest = response.data.quest; if (!quest) quest = response.data.data; resolve(quest); - });; + }); }); } diff --git a/website/common/locales/en/quests.json b/website/common/locales/en/quests.json index 9c25f6d412..366451d5d9 100644 --- a/website/common/locales/en/quests.json +++ b/website/common/locales/en/quests.json @@ -9,7 +9,12 @@ "goldQuests": "Gold-Purchasable Quests", "questDetails": "Quest Details", "invitations": "Invitations", - "completed": " Completed!", + "completed": "Completed!", + "rewardsAllParticipants": "Rewards for all Quest Participants", + "rewardsQuestOwner": "Additional Rewards for Quest Owner", + "questOwnerReceived": "The Quest Owner Has Also Received", + "youWillReceive": "You Will Receive", + "questOwnerWillReceive": "The Quest Owner Will Also Receive", "youReceived": "You've Received", "dropQuestCongrats": "Congratulations on earning this quest scroll! You can invite your party to begin the quest now, or come back to it any time in your Inventory > Quests.", "questSend": "Clicking \"Invite\" will send an invitation to your party members. When all members have accepted or denied, the quest begins. See status under Social > Party.", diff --git a/website/common/script/content/index.js b/website/common/script/content/index.js index 54e030fff6..3ea23833a3 100644 --- a/website/common/script/content/index.js +++ b/website/common/script/content/index.js @@ -867,7 +867,8 @@ api.quests = { { type: 'quests', key: "vice2", - text: t('questVice1DropVice2Quest') + text: t('questVice1DropVice2Quest'), + onlyOwner: true } ], gp: 20, @@ -892,7 +893,8 @@ api.quests = { { type: 'quests', key: 'vice3', - text: t('questVice2DropVice3Quest') + text: t('questVice2DropVice3Quest'), + onlyOwner: true } ], gp: 20, @@ -1116,7 +1118,8 @@ api.quests = { { type: 'quests', key: "atom2", - text: t('questAtom1Drop') + text: t('questAtom1Drop'), + onlyOwner: true } ], gp: 7, @@ -1140,7 +1143,8 @@ api.quests = { { type: 'quests', key: "atom3", - text: t('questAtom2Drop') + text: t('questAtom2Drop'), + onlyOwner: true } ], gp: 20, @@ -1293,7 +1297,8 @@ api.quests = { { type: 'quests', key: "moonstone2", - text: t('questMoonstone1DropMoonstone2Quest') + text: t('questMoonstone1DropMoonstone2Quest'), + onlyOwner: true } ], gp: 50, @@ -1317,7 +1322,8 @@ api.quests = { { type: 'quests', key: 'moonstone3', - text: t('questMoonstone2DropMoonstone3Quest') + text: t('questMoonstone2DropMoonstone3Quest'), + onlyOwner: true } ], gp: 500, @@ -1398,7 +1404,8 @@ api.quests = { { type: 'quests', key: "goldenknight2", - text: t('questGoldenknight1DropGoldenknight2Quest') + text: t('questGoldenknight1DropGoldenknight2Quest'), + onlyOwner: true } ], gp: 15, @@ -1422,7 +1429,8 @@ api.quests = { { type: 'quests', key: 'goldenknight3', - text: t('questGoldenknight2DropGoldenknight3Quest') + text: t('questGoldenknight2DropGoldenknight3Quest'), + onlyOwner: true } ], gp: 75, diff --git a/website/server/models/group.js b/website/server/models/group.js index 1a6b652606..16b643c6b6 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -579,6 +579,39 @@ schema.statics.cleanGroupQuest = function cleanGroupQuest () { }; }; +function _getUserUpdateForQuestReward (itemToAward, allAwardedItems) { + let updates = { + $set: {}, + $inc: {}, + }; + let dropK = itemToAward.key; + + switch (itemToAward.type) { + case 'gear': { + // TODO This means they can lose their new gear on death, is that what we want? + updates.$set[`items.gear.owned.${dropK}`] = true; + break; + } + case 'eggs': + case 'food': + case 'hatchingPotions': + case 'quests': { + updates.$inc[`items.${itemToAward.type}.${dropK}`] = _.where(allAwardedItems, {type: itemToAward.type, key: itemToAward.key}).length; + break; + } + case 'pets': { + updates.$set[`items.pets.${dropK}`] = 5; + break; + } + case 'mounts': { + updates.$set[`items.mounts.${dropK}`] = true; + break; + } + } + updates = _.omit(updates, _.isEmpty); + return updates; +} + async function _updateUserWithRetries (userId, updates, numTry = 1) { return await User.update({_id: userId}, updates).exec() .then((raw) => { @@ -611,33 +644,18 @@ schema.methods.finishQuest = async function finishQuest (quest) { updates.$set['party.quest'] = _cleanQuestProgress({completed: questK}); // clear quest progress } - _.each(quest.drop.items, (item) => { - let dropK = item.key; - - switch (item.type) { - case 'gear': { - // TODO This means they can lose their new gear on death, is that what we want? - updates.$set[`items.gear.owned.${dropK}`] = true; - break; - } - case 'eggs': - case 'food': - case 'hatchingPotions': - case 'quests': { - updates.$inc[`items.${item.type}.${dropK}`] = _.where(quest.drop.items, {type: item.type, key: item.key}).length; - break; - } - case 'pets': { - updates.$set[`items.pets.${dropK}`] = 5; - break; - } - case 'mounts': { - updates.$set[`items.mounts.${dropK}`] = true; - break; - } - } + _.each(_.reject(quest.drop.items, 'onlyOwner'), (item) => { + _.merge(updates, _getUserUpdateForQuestReward(item, quest.drop.items)); }); + let questOwnerUpdates = {}; + let questLeader = this.quest.leader; + + _.each(_.filter(quest.drop.items, 'onlyOwner'), (item) => { + _.merge(questOwnerUpdates, _getUserUpdateForQuestReward(item, quest.drop.items)); + }); + _.merge(questOwnerUpdates, updates); + let participants = this._id === TAVERN_ID ? {} : this.getParticipatingQuestMembers(); this.quest = {}; this.markModified('quest'); @@ -647,7 +665,11 @@ schema.methods.finishQuest = async function finishQuest (quest) { } let promises = participants.map(userId => { - return _updateUserWithRetries(userId, updates); + if (userId === questLeader) { + return _updateUserWithRetries(userId, questOwnerUpdates); + } else { + return _updateUserWithRetries(userId, updates); + } }); return Bluebird.all(promises); diff --git a/website/views/shared/modals/quest-rewards.jade b/website/views/shared/modals/quest-rewards.jade index 497542c593..af6a43ef48 100644 --- a/website/views/shared/modals/quest-rewards.jade +++ b/website/views/shared/modals/quest-rewards.jade @@ -1,8 +1,8 @@ script(id='partials/options.social.party.quest-rewards.html', type='text/ng-template') hr - h5 {{::header}} + h5 {{::headerParticipant}} table.table.table-striped - tr(ng-repeat='drop in quest.drop.items') + tr(ng-repeat='drop in _.reject(quest.drop.items, \'onlyOwner\')') td {{::drop.text()}} tr(ng-if='::quest.drop.exp > 0') td {{::quest.drop.exp}}  @@ -12,3 +12,8 @@ script(id='partials/options.social.party.quest-rewards.html', type='text/ng-temp =env.t('gold') tr(ng-if='::quest.drop.unlock()') td {{::quest.drop.unlock()}} + div(ng-if='getQuestOwnerRewards(quest).length > 0') + h5 {{::headerQuestOwner}} + table.table.table-striped + tr(ng-repeat='drop in getQuestOwnerRewards(quest)') + td {{::drop.text()}} diff --git a/website/views/shared/modals/quests.jade b/website/views/shared/modals/quests.jade index 1c9d28555c..5ac90e3477 100644 --- a/website/views/shared/modals/quests.jade +++ b/website/views/shared/modals/quests.jade @@ -20,7 +20,7 @@ mixin questInfo p(ng-repeat='(k,v) in ::selectedQuest.collect') | {{:: env.t('collectionItems', { number: selectedQuest.collect[k].count, items: selectedQuest.collect[k].text() })}} div(ng-bind-html='::selectedQuest.notes()') - quest-rewards(key='{{::selectedQuest.key}}', header=env.t('rewards')) + quest-rewards(key='{{::selectedQuest.key}}', header-participant=env.t('rewardsAllParticipants'), header-quest-owner=env.t('rewardsQuestOwner')) script(type='text/ng-template', id='modals/questCompleted.html') .modal-header @@ -29,7 +29,7 @@ script(type='text/ng-template', id='modals/questCompleted.html') .modal-body .col-centered(ng-class='::Content.quests[user.party.quest.completed].completion() ? "pull-right-sm" : ""', class='quest_{{user.party.quest.completed}}') p(ng-bind-html='::Content.quests[user.party.quest.completed].completion()') - quest-rewards(key='{{user.party.quest.completed}}', header=env.t('youReceived')) + quest-rewards(key='{{user.party.quest.completed}}', header-participant=env.t('youReceived'), header-quest-owner=env.t('questOwnerReceived')) .modal-footer button.btn.btn-primary(ng-click='set({"party.quest.completed":""}); $close()')=env.t('ok') @@ -84,7 +84,7 @@ script(type='text/ng-template', id='modals/questInvitation.html') strong=env.t('collect') + ': ' | {{::Content.quests[user.party.quest.key].collect[k].count}} {{::Content.quests[user.party.quest.key].collect[k].text()}} div(ng-bind-html='::Content.quests[user.party.quest.key].notes()') - quest-rewards(key='{{::user.party.quest.key}}', header=env.t('rewards')) + quest-rewards(key='{{::user.party.quest.key}}', header-participant=env.t('rewardsAllParticipants'), header-quest-owner=env.t('rewardsQuestOwner')) .modal-footer button.btn.btn-default(ng-click='questHold = true; $close()')=env.t('askLater') button.btn.btn-default(ng-click='questReject(); $close()')=env.t('reject')