diff --git a/common/script/fns/randomDrop.js b/common/script/fns/randomDrop.js index ec6f350d4c..2d91f2e2f3 100644 --- a/common/script/fns/randomDrop.js +++ b/common/script/fns/randomDrop.js @@ -17,9 +17,7 @@ module.exports = function randomDrop (user, options, req = {}) { let acceptableDrops; let chance; let drop; - let dropK; let dropMultiplier; - let quest; let rarity; let task; @@ -38,15 +36,8 @@ module.exports = function randomDrop (user, options, req = {}) { }, 0) || 0)); chance = diminishingReturns(chance, 0.75); - if (user.party.quest.key) - quest = content.quests[user.party.quest.key]; - if (quest && quest.collect && predictableRandom(user, user.stats.gp) < chance) { - dropK = randomVal(user, quest.collect, { - key: true, - }); - if (!user.party.quest.progress.collect[dropK]) - user.party.quest.progress.collect[dropK] = 0; - user.party.quest.progress.collect[dropK]++; + if (predictableRandom(user, user.stats.gp) < chance) { + user.party.quest.progress.collect++; user.markModified('party.quest.progress'); } diff --git a/migrations/20160602_convert_quest_collection.js b/migrations/20160602_convert_quest_collection.js new file mode 100644 index 0000000000..172575cf1e --- /dev/null +++ b/migrations/20160602_convert_quest_collection.js @@ -0,0 +1,140 @@ +'use strict'; + +/**************************************** + * Author: Blade Barringer @crookedneighbor + * + * Reason: Collection quest data on the client is unreliable + * because the quest key on the user.party.quest.key property + * is unreliable. We were calculating the quest items found + * at the time a drop was created, when instead we could + * just calculate it from the party on the server. This + * necessitates changing the property type of party.quest.progress.collect + * from an object to a number, hence this migration. + ***************************************/ + +global.Promise = require('bluebird'); +const TaskQueue = require('cwait').TaskQueue; +const logger = require('./utils/logger'); +const Timer = require('./utils/timer'); +const connectToDb = require('./utils/connect').connectToDb; +const closeDb = require('./utils/connect').closeDb; + +const timer = new Timer(); + +// PROD: Enable prod db +// const DB_URI = 'mongodb://username:password@dsXXXXXX-a0.mlab.com:XXXXX,dsXXXXXX-a1.mlab.com:XXXXX/habitica?replicaSet=rs-dsXXXXXX'; +const DB_URI = 'mongodb://localhost/new-prod-copy'; + +const COLLECTION_QUESTS = [ + 'evilsanta2', + 'vice2', + 'egg', + 'atom1', + 'moonstone1', + 'goldenknight1', + 'dilatoryDistress1', +] + +let Users, Groups; + +connectToDb(DB_URI).then((db) => { + Users = db.collection('users_backup'); + Groups = db.collection('groups_backup'); + + return Promise.resolve(); +}) +.then(findUsersWithCollectionData) +.then(getUsersCollectionData) +.then(transformCollectionData) +.then(cleanUpEmptyCollectionData) +.then(() => { + timer.stop(); + closeDb(); +}).catch(reportError); + +function reportError (err) { + logger.error('Uh oh, an error occurred'); + closeDb(); + timer.stop(); + throw err; +} + +function findUsersWithCollectionData () { + logger.info('Looking up groups on collection quests...'); + + return Groups.find({'quest.key': {$in: COLLECTION_QUESTS }}, ['quest.members']).toArray().then((groups) => { + logger.success('Found', groups.length, 'parties on collection quests'); + logger.info('Parsing member data...'); + + let members = groups.reduce((array, party) => { + let questers = Object.keys(party.quest.members); + array.push.apply(array, questers); + return array; + }, []); + + logger.success('Found', members.length, 'users on collection quests'); + + return Promise.resolve(members); + }) +} + +function getUsersCollectionData (users) { + logger.info('Fetching collection data from users...'); + + return Users.find({_id: {$in: users}}, ['party.quest.progress']).toArray().then((docs) => { + let items = docs.reduce((array, user) => { + let total = 0; + let collect = user.party && user.party.quest && user.party.quest.progress && user.party.quest.progress.collect; + + if (!collect) return array; + if (typeof collect === 'number') return array; + + for (var i in collect) { + if (collect.hasOwnProperty(i)) { + total += collect[i]; + } + } + + array.push({_id: user._id, collect: total}); + return array; + }, []); + + return Promise.resolve(items); + }); +} + +function updateUserById (user) { + return Users.findOneAndUpdate({_id: user._id}, {$set: {'party.quest.progress.collect': user.collect}}, {returnOriginal: false}) +} + + +function transformCollectionData (users) { + let queue = new TaskQueue(Promise, 300); + + logger.info('About to update', users.length, 'user collection items...'); + + return Promise.map(users, queue.wrap(updateUserById)).then((result) => { + let updates = result.filter(res => res.lastErrorObject && res.lastErrorObject.updatedExisting) + let failures = result.filter(res => !(res.lastErrorObject && res.lastErrorObject.updatedExisting)); + + logger.success(updates.length, 'users have been fixed'); + + if (failures.length > 0) { + logger.error(failures.length, 'users could not be found'); + } + + return Promise.resolve(); + }); +} + +function cleanUpEmptyCollectionData () { + logger.info('Fetching users without collection data...'); + + return Users.updateMany({$or: [{'party.quest.progress.collect': { $type: 3}}, {'party.quest.progress.collect': { $exists: false}}]}, {$set: {'party.quest.progress.collect': 0}}).then((r) => { + let updates = r.result.n; + + logger.success(updates, 'users have been fixed'); + + return Promise.resolve(); + }); +} diff --git a/test/api/v3/integration/debug/POST-debug_quest-progress.test.js b/test/api/v3/integration/debug/POST-debug_quest-progress.test.js index 3ae3d48882..5ab0ee0af8 100644 --- a/test/api/v3/integration/debug/POST-debug_quest-progress.test.js +++ b/test/api/v3/integration/debug/POST-debug_quest-progress.test.js @@ -44,10 +44,7 @@ describe('POST /debug/quest-progress', () => { await user.sync(); - expect(user.party.quest.progress.collect).to.eql({ - tracks: 300, - branches: 300, - }); + expect(user.party.quest.progress.collect).to.eql(300); }); it('returns error when not in production mode', async () => { diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js index 850cf53646..f65a1f5557 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_abort.test.js @@ -102,7 +102,7 @@ describe('POST /groups/:groupId/quests/abort', () => { progress: { up: 0, down: 0, - collect: {}, + collect: 0, }, completed: null, RSVPNeeded: false, diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js index f3bd03a180..bd4ff20916 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_cancel.test.js @@ -114,7 +114,7 @@ describe('POST /groups/:groupId/quests/cancel', () => { progress: { up: 0, down: 0, - collect: {}, + collect: 0, }, completed: null, RSVPNeeded: false, diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js index 65d781c163..584a37a2d9 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_leave.test.js @@ -113,7 +113,7 @@ describe('POST /groups/:groupId/quests/leave', () => { progress: { up: 0, down: 0, - collect: {}, + collect: 0, }, completed: null, RSVPNeeded: false, diff --git a/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js b/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js index 596ad2f360..80641b1e15 100644 --- a/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js +++ b/test/api/v3/integration/quests/POST-groups_groupid_quests_reject.test.js @@ -116,7 +116,7 @@ describe('POST /groups/:groupId/quests/reject', () => { progress: { up: 0, down: 0, - collect: {}, + collect: 0, }, completed: null, RSVPNeeded: false, diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js index 32007e068d..e164bae769 100644 --- a/test/api/v3/unit/models/group.test.js +++ b/test/api/v3/unit/models/group.test.js @@ -5,49 +5,365 @@ import { quests as questScrolls } from '../../../../../common/script/content'; import * as email from '../../../../../website/server/libs/api-v3/email'; describe('Group Model', () => { - context('Instance Methods', () => { - describe('#startQuest', () => { - let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember; + let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember; + + beforeEach(async () => { + sandbox.stub(email, 'sendTxn'); + + party = new Group({ + name: 'test party', + type: 'party', + privacy: 'private', + }); + + questLeader = new User({ + party: { _id: party._id }, + profile: { name: 'Quest Leader' }, + items: { + quests: { + whale: 1, + }, + }, + }); + + party.leader = questLeader._id; + + participatingMember = new User({ + party: { _id: party._id }, + profile: { name: 'Participating Member' }, + }); + nonParticipatingMember = new User({ + party: { _id: party._id }, + profile: { name: 'Non-Participating Member' }, + }); + undecidedMember = new User({ + party: { _id: party._id }, + profile: { name: 'Undecided Member' }, + }); + + await Promise.all([ + party.save(), + questLeader.save(), + participatingMember.save(), + nonParticipatingMember.save(), + undecidedMember.save(), + ]); + }); + + describe('Static Methods', () => { + describe('processQuestProgress', () => { + let progress; beforeEach(async () => { - sandbox.stub(email, 'sendTxn'); + progress = { + up: 5, + down: -5, + collect: 5, + }; - party = new Group({ - name: 'test party', - type: 'party', - privacy: 'private', - }); + party.quest.members = { + [questLeader._id]: true, + [participatingMember._id]: true, + [nonParticipatingMember._id]: false, + [undecidedMember._id]: null, + }; - questLeader = new User({ - party: { _id: party._id }, - items: { - quests: { - whale: 1, - }, - }, - }); - - party.leader = questLeader._id; - - participatingMember = new User({ - party: { _id: party._id }, - }); - nonParticipatingMember = new User({ - party: { _id: party._id }, - }); - undecidedMember = new User({ - party: { _id: party._id }, - }); - - await Promise.all([ - party.save(), - questLeader.save(), - participatingMember.save(), - nonParticipatingMember.save(), - undecidedMember.save(), - ]); + await party.save(); }); + context('early returns', () => { + beforeEach(() => { + sandbox.stub(Group.prototype, '_processBossQuest').returns(Promise.resolve()); + sandbox.stub(Group.prototype, '_processCollectionQuest').returns(Promise.resolve()); + }); + + it('returns early if user is not in a party', async () => { + let userWithoutParty = new User(); + + await userWithoutParty.save(); + + await Group.processQuestProgress(userWithoutParty, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party._processBossQuest).to.not.be.called; + expect(party._processCollectionQuest).to.not.be.called; + }); + + it('returns early if party is not on quest', async () => { + party.quest.active = false; + await party.save(); + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party._processBossQuest).to.not.be.called; + expect(party._processCollectionQuest).to.not.be.called; + }); + + it('returns early if user is not on quest', async () => { + await Group.processQuestProgress(nonParticipatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party._processBossQuest).to.not.be.called; + expect(party._processCollectionQuest).to.not.be.called; + }); + + it('returns early if user has made no progress', async () => { + await Group.processQuestProgress(participatingMember, null); + + party = await Group.findOne({_id: party._id}); + + expect(party._processBossQuest).to.not.be.called; + expect(party._processCollectionQuest).to.not.be.called; + }); + + it('returns early if quest does not exist', async () => { + party.quest.key = 'foobar'; + await party.save(); + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party._processBossQuest).to.not.be.called; + expect(party._processCollectionQuest).to.not.be.called; + }); + + it('calls _processBossQuest if quest is a boss quest', async () => { + party.quest.key = 'whale'; + + await party.startQuest(questLeader); + await party.save(); + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype._processBossQuest).to.be.calledOnce; + expect(party._processCollectionQuest).to.not.be.called; + }); + + it('calls _processCollectionQuest if quest is a collection quest', async () => { + party.quest.key = 'evilsanta2'; + + await party.startQuest(questLeader); + await party.save(); + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party._processBossQuest).to.not.be.called; + expect(Group.prototype._processCollectionQuest).to.be.calledOnce; + }); + }); + + context('Boss Quests', () => { + beforeEach(async () => { + party.quest.key = 'whale'; + + await party.startQuest(questLeader); + await party.save(); + + sandbox.stub(Group.prototype, 'sendChat'); + }); + + it('applies user\'s progress to quest boss hp', async () => { + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party.quest.progress.hp).to.eql(495); + }); + + it('sends a chat message about progress', async () => { + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledOnce; + expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member attacks Wailing Whale for 5.0 damage.` `Wailing Whale attacks party for 7.5 damage.`'); + }); + + it('applies damage only to participating members of party', async () => { + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + let [ + updatedLeader, + updatedParticipatingMember, + updatedNonParticipatingMember, + updatedUndecidedMember, + ] = await Promise.all([ + User.findById(questLeader._id), + User.findById(participatingMember._id), + User.findById(nonParticipatingMember._id), + User.findById(undecidedMember._id), + ]); + + expect(updatedLeader.stats.hp).to.eql(42.5); + expect(updatedParticipatingMember.stats.hp).to.eql(42.5); + expect(updatedNonParticipatingMember.stats.hp).to.eql(50); + expect(updatedUndecidedMember.stats.hp).to.eql(50); + }); + + it('sends message about victory', async () => { + progress.up = 999; + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledTwice; + expect(Group.prototype.sendChat).to.be.calledWith('`You defeated Wailing Whale! Questing party members receive the rewards of victory.`'); + }); + + it('calls finishQuest when boss has <= 0 hp', async () => { + let quest = questScrolls[party.quest.key]; + let finishQuest = sandbox.spy(Group.prototype, 'finishQuest'); + + progress.up = 999; + + await Group.processQuestProgress(participatingMember, progress); + + expect(finishQuest).to.be.calledOnce; + expect(finishQuest).to.be.calledWith(quest); + }); + + context('with Rage', () => { + beforeEach(async () => { + party.quest.active = false; + party.quest.key = 'trex_undead'; + + await party.startQuest(questLeader); + await party.save(); + }); + + it('applies down progress to boss rage', async () => { + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party.quest.progress.rage).to.eql(10); + }); + + it('activates rage when progress.down triggers rage bar', async () => { + let quest = questScrolls[party.quest.key]; + + progress.down = -999; + party.quest.progress.hp = 300; + + await party.save(); + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledWith(quest.boss.rage.effect('en')); + expect(party.quest.progress.hp).to.eql(383.5); + expect(party.quest.progress.rage).to.eql(0); + }); + + it('rage sets boss hp to max hp if raging would have caused hp to be higher than the max', async () => { + progress.down = -999; + + party.quest.progress.hp = 490; + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party.quest.progress.hp).to.eql(500); + }); + }); + }); + + context('Collection Quests', () => { + beforeEach(async () => { + party.quest.key = 'atom1'; + + await party.startQuest(questLeader); + await party.save(); + + sandbox.stub(Group.prototype, 'sendChat'); + }); + + it('applies user\'s progress to found quest items', async () => { + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(party.quest.progress.collect.soapBars).to.eq(5); + }); + + it('sends a chat message about progress', async () => { + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledOnce; + expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found 5 Bars of Soap.`'); + }); + + it('sends a chat message if no progress is made', async () => { + progress.collect = 0; + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledOnce; + expect(Group.prototype.sendChat).to.be.calledWith('`Participating Member found nothing.`'); + }); + + it('handles collection quests with multiple items', async () => { + progress.collect = 10; + party.quest.key = 'evilsanta2'; + party.quest.active = false; + + await party.startQuest(questLeader); + await party.save(); + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledOnce; + expect(Group.prototype.sendChat).to.be.calledWithMatch(/`Participating Member found/); + expect(Group.prototype.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/); + }); + + it('sends message about victory', async () => { + progress.collect = 500; + + await Group.processQuestProgress(participatingMember, progress); + + party = await Group.findOne({_id: party._id}); + + expect(Group.prototype.sendChat).to.be.calledTwice; + expect(Group.prototype.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`'); + }); + + it('calls finishQuest when all items are found', async () => { + let quest = questScrolls[party.quest.key]; + let finishQuest = sandbox.spy(Group.prototype, 'finishQuest'); + + progress.collect = 999; + + await Group.processQuestProgress(participatingMember, progress); + + expect(finishQuest).to.be.calledOnce; + expect(finishQuest).to.be.calledWith(quest); + }); + }); + }); + }); + + context('Instance Methods', () => { + describe('#startQuest', () => { context('Failure Conditions', () => { it('throws an error if group is not a party', async () => { let guild = new Group({ @@ -74,11 +390,12 @@ describe('Group Model', () => { party.quest.key = 'whale'; party.quest.active = false; party.quest.leader = questLeader._id; - party.quest.members = { }; - party.quest.members[questLeader._id] = true; - party.quest.members[participatingMember._id] = true; - party.quest.members[nonParticipatingMember._id] = false; - party.quest.members[undecidedMember._id] = null; + party.quest.members = { + [questLeader._id]: true, + [participatingMember._id]: true, + [nonParticipatingMember._id]: false, + [undecidedMember._id]: null, + }; }); it('activates quest', () => { @@ -141,7 +458,7 @@ describe('Group Model', () => { expect(participatingMember.party.quest.key).to.eql('whale'); expect(participatingMember.party.quest.progress.down).to.eql(0); - expect(participatingMember.party.quest.progress.collect).to.eql({}); + expect(participatingMember.party.quest.progress.collect).to.eql(0); expect(participatingMember.party.quest.completed).to.eql(null); }); @@ -153,12 +470,12 @@ describe('Group Model', () => { expect(participatingMember.party.quest.key).to.eql('whale'); expect(participatingMember.party.quest.progress.down).to.eql(0); - expect(participatingMember.party.quest.progress.collect).to.eql({}); + expect(participatingMember.party.quest.progress.collect).to.eql(0); expect(participatingMember.party.quest.completed).to.eql(null); expect(questLeader.party.quest.key).to.eql('whale'); expect(questLeader.party.quest.progress.down).to.eql(0); - expect(questLeader.party.quest.progress.collect).to.eql({}); + expect(questLeader.party.quest.progress.collect).to.eql(0); expect(questLeader.party.quest.completed).to.eql(null); }); @@ -250,7 +567,6 @@ describe('Group Model', () => { $set: { 'party.quest.key': 'whale', 'party.quest.progress.down': 0, - 'party.quest.progress.collect': {}, 'party.quest.completed': null, }, } @@ -279,7 +595,7 @@ describe('Group Model', () => { expect(userQuest.key).to.eql('whale'); expect(userQuest.progress.down).to.eql(0); - expect(userQuest.progress.collect).to.eql({}); + expect(userQuest.progress.collect).to.eql(0); expect(userQuest.completed).to.eql(null); }); diff --git a/test/common/fns/randomDrop.test.js b/test/common/fns/randomDrop.test.js index 39920e02bf..b91021ab6e 100644 --- a/test/common/fns/randomDrop.test.js +++ b/test/common/fns/randomDrop.test.js @@ -6,7 +6,6 @@ import { generateDaily, generateReward, } from '../../helpers/common.helper'; -import content from '../../../common/script/content/index'; describe('common.fns.randomDrop', () => { let user; @@ -22,23 +21,16 @@ describe('common.fns.randomDrop', () => { }; }); - /** - * function signature as follows: - * randomDrop(user, modifiers) {} - * modifiers = { task, delta = null } - **/ - it('drops an item for the user.party.quest.progress', () => { - expect(user.party.quest.progress.collect).to.eql({}); + expect(user.party.quest.progress.collect).to.eql(0); user.party.quest.key = 'vice2'; - let collectWhat = Object.keys(content.quests[user.party.quest.key].collect)[0]; // lightCrystal predictableRandom = () => { return 0.0001; }; randomDrop(user, { task, predictableRandom }); - expect(user.party.quest.progress.collect[collectWhat]).to.eql(1); + expect(user.party.quest.progress.collect).to.eql(1); randomDrop(user, { task, predictableRandom }); - expect(user.party.quest.progress.collect[collectWhat]).to.eql(2); + expect(user.party.quest.progress.collect).to.eql(2); }); context('drops enabled', () => { diff --git a/test/server_side/controllers/groups.test.js b/test/server_side/controllers/groups.test.js index bf12df321b..88fe31752e 100644 --- a/test/server_side/controllers/groups.test.js +++ b/test/server_side/controllers/groups.test.js @@ -380,7 +380,7 @@ describe('Groups Controller', function() { expect(user.party.quest.progress).to.eql({ up: 0, down: 0, - collect: {} + collect: 0, }); }); diff --git a/website/server/controllers/api-v3/debug.js b/website/server/controllers/api-v3/debug.js index 9315f9bf4e..923a79814a 100644 --- a/website/server/controllers/api-v3/debug.js +++ b/website/server/controllers/api-v3/debug.js @@ -171,11 +171,7 @@ api.questProgress = { } if (quest.collect) { - let collect = user.party.quest.progress.collect; - _.each(quest.collect, (details, item) => { - collect[item] = collect[item] || 0; - collect[item] += 300; - }); + user.party.quest.progress.collect += 300; } user.markModified('party.quest.progress'); diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index c331d87bde..f3a6c48902 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -392,6 +392,17 @@ api.scoreTask = { let wasCompleted = task.completed; + // TEMPORARY, remove once collection migration completes + if (typeof user.party.quest.progress.collect === 'object') { + let totalItemsFound = _.reduce(user.party.quest.progress.collect, (total, amount) => { + return total + amount; + }, 0); + + user.party.quest.progress.collect = totalItemsFound; + } else if (!user.party.quest.progress.collect) { + user.party.quest.progress.collect = 0; + } + let [delta] = common.ops.scoreTask({task, user, direction}, req); // Drop system (don't run on the client, as it would only be discarded since ops are sent to the API, not the results) if (direction === 'up') user.fns.randomDrop({task, delta}, req); diff --git a/website/server/libs/api-v3/cron.js b/website/server/libs/api-v3/cron.js index ab3b941eb2..f6a4d337c7 100644 --- a/website/server/libs/api-v3/cron.js +++ b/website/server/libs/api-v3/cron.js @@ -277,8 +277,7 @@ export function cron (options = {}) { // After all is said and done, progress up user's effect on quest, return those values & reset the user's let progress = user.party.quest.progress; let _progress = _.cloneDeep(progress); - _.merge(progress, {down: 0, up: 0}); - progress.collect = _.transform(progress.collect, (m, v, k) => m[k] = 0); + _.merge(progress, {down: 0, up: 0, collect: 0}); // TODO: Clean PMs - keep 200 for subscribers and 50 for free users. Should also be done while resting in the inn // let numberOfPMs = Object.keys(user.inbox.messages).length; diff --git a/website/server/middlewares/api-v3/cron.js b/website/server/middlewares/api-v3/cron.js index 18d9907d0d..e4745f734e 100644 --- a/website/server/middlewares/api-v3/cron.js +++ b/website/server/middlewares/api-v3/cron.js @@ -158,13 +158,7 @@ async function cronAsync (req, res) { }); await Bluebird.all(toSave); - let quest = common.content.quests[user.party.quest.key]; - - if (quest) { - // If user is on a quest, roll for boss & player, or handle collections - let questType = quest.boss ? 'boss' : 'collect'; - await Group[`${questType}Quest`](user, progress); - } + await Group.processQuestProgress(user, progress); // Set _cronSignature, lastCron and auth.timestamps.loggedin to signal end of cron await User.update({ diff --git a/website/server/models/group.js b/website/server/models/group.js index fed02f4619..feddc8ba43 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -117,7 +117,7 @@ function _cleanQuestProgress (merge) { progress: { up: 0, down: 0, - collect: {}, + collect: 0, }, completed: null, RSVPNeeded: false, @@ -365,7 +365,6 @@ schema.methods.startQuest = async function startQuest (user) { if (userIsParticipating) { user.party.quest.key = this.quest.key; user.party.quest.progress.down = 0; - user.party.quest.progress.collect = collected; user.party.quest.completed = null; user.markModified('party.quest'); } @@ -389,7 +388,6 @@ schema.methods.startQuest = async function startQuest (user) { $set: { 'party.quest.key': this.quest.key, 'party.quest.progress.down': 0, - 'party.quest.progress.collect': collected, 'party.quest.completed': null, }, }, { multi: true }).exec(); @@ -491,42 +489,14 @@ function _isOnQuest (user, progress, group) { return group && progress && group.quest && group.quest.active && group.quest.members[user._id] === true; } -schema.statics.collectQuest = async function collectQuest (user, progress) { - let group = await this.getGroup({user, groupId: 'party'}); - if (!_isOnQuest(user, progress, group)) return; - let quest = shared.content.quests[group.quest.key]; - - _.each(progress.collect, (v, k) => { - group.quest.progress.collect[k] += v; - }); - - let foundText = _.reduce(progress.collect, (m, v, k) => { - m.push(`${v} ${quest.collect[k].text('en')}`); - return m; - }, []); - - foundText = foundText ? foundText.join(', ') : 'nothing'; - group.sendChat(`\`${user.profile.name} found ${foundText}.\``); - group.markModified('quest.progress.collect'); - - // Still needs completing - if (_.find(shared.content.quests[group.quest.key].collect, (v, k) => { - return group.quest.progress.collect[k] < v.count; - })) return await group.save(); - - await group.finishQuest(quest); - group.sendChat('`All items found! Party has received their rewards.`'); - - return await group.save(); -}; - -schema.statics.bossQuest = async function bossQuest (user, progress) { - let group = await this.getGroup({user, groupId: 'party'}); - if (!_isOnQuest(user, progress, group)) return; - - let quest = shared.content.quests[group.quest.key]; - if (!progress || !quest) return; // TODO why is this ever happening, progress should be defined at this point, log? +schema.methods._processBossQuest = async function processBossQuest (options) { + let { + user, + progress, + } = options; + let group = this; + let quest = questScrolls[group.quest.key]; let down = progress.down * quest.boss.str; // multiply by boss strength group.quest.progress.hp -= progress.up; @@ -572,6 +542,75 @@ schema.statics.bossQuest = async function bossQuest (user, progress) { return await group.save(); }; +schema.methods._processCollectionQuest = async function processCollectionQuest (options) { + let { + user, + progress, + } = options; + + let group = this; + let quest = questScrolls[group.quest.key]; + let itemsFound = {}; + + _.times(progress.collect, () => { + let item = shared.fns.randomVal(user, quest.collect, {key: true, seed: Math.random()}); + + if (!itemsFound[item]) { + itemsFound[item] = 0; + } + itemsFound[item]++; + group.quest.progress.collect[item]++; + }); + + let foundText = _.reduce(itemsFound, (m, v, k) => { + m.push(`${v} ${quest.collect[k].text('en')}`); + return m; + }, []); + + foundText = foundText.length > 0 ? foundText.join(', ') : 'nothing'; + group.sendChat(`\`${user.profile.name} found ${foundText}.\``); + group.markModified('quest.progress.collect'); + + // Still needs completing + if (_.find(quest.collect, (v, k) => { + return group.quest.progress.collect[k] < v.count; + })) return await group.save(); + + await group.finishQuest(quest); + group.sendChat('`All items found! Party has received their rewards.`'); + + return await group.save(); +}; + +schema.statics.processQuestProgress = async function processQuestProgress (user, progress) { + let group = await this.getGroup({user, groupId: 'party'}); + + if (!_isOnQuest(user, progress, group)) return; + + // TEMPORARY, remove once collection migration completes + if (typeof progress.collect === 'object') { + let totalItemsFound = _.reduce(progress.collect, (total, amount) => { + return total + amount; + }, 0); + + progress.collect = totalItemsFound; + } else if (!progress.collect) { + progress.collect = 0; + } + + let quest = shared.content.quests[group.quest.key]; + + if (!quest) return; // TODO should this throw an error instead? + + let questType = quest.boss ? 'Boss' : 'Collection'; + + await group[`_process${questType}Quest`]({ + user, + progress, + group, + }); +}; + // to set a boss: `db.groups.update({_id:TAVERN_ID},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})` // we export an empty object that is then populated with the query-returned data export let tavernQuest = {}; diff --git a/website/server/models/user.js b/website/server/models/user.js index db305e3330..ca544274da 100644 --- a/website/server/models/user.js +++ b/website/server/models/user.js @@ -376,9 +376,8 @@ export let schema = new Schema({ progress: { up: {type: Number, default: 0}, down: {type: Number, default: 0}, - collect: {type: Schema.Types.Mixed, default: () => { - return {}; - }}, // {feather:1, ingot:2} + // TEMPORARY - Switch type to Number after migration + collect: {type: Schema.Types.Mixed, default: 0}, }, completed: String, // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser RSVPNeeded: {type: Boolean, default: false}, // Set to true when invite is pending, set to false when quest invite is accepted or rejected, quest starts, or quest is cancelled