diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js index 93d80878b5..e164bae769 100644 --- a/test/api/v3/unit/models/group.test.js +++ b/test/api/v3/unit/models/group.test.js @@ -50,6 +50,318 @@ describe('Group Model', () => { ]); }); + describe('Static Methods', () => { + describe('processQuestProgress', () => { + let progress; + + beforeEach(async () => { + progress = { + up: 5, + down: -5, + collect: 5, + }; + + party.quest.members = { + [questLeader._id]: true, + [participatingMember._id]: true, + [nonParticipatingMember._id]: false, + [undecidedMember._id]: null, + }; + + 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', () => { @@ -300,363 +612,5 @@ describe('Group Model', () => { }); }); }); - - describe('processQuestProgress', () => { - let progress; - - beforeEach(async () => { - progress = { - up: 5, - down: -5, - }; - - party.quest.active = true; - party.quest.members = { - [questLeader._id]: true, - [participatingMember._id]: true, - [nonParticipatingMember._id]: false, - [undecidedMember._id]: null, - }; - - await party.save(); - - sandbox.stub(Group, 'processBossQuest').returns(Promise.resolve()); - sandbox.stub(Group, '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); - - expect(Group.processBossQuest).to.not.be.called; - expect(Group.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); - - expect(Group.processBossQuest).to.not.be.called; - expect(Group.processCollectionQuest).to.not.be.called; - }); - - it('returns early if user is not on quest', async () => { - await Group.processQuestProgress(nonParticipatingMember, progress); - - expect(Group.processBossQuest).to.not.be.called; - expect(Group.processCollectionQuest).to.not.be.called; - }); - - it('returns early if user has made no progress', async () => { - await Group.processQuestProgress(participatingMember, null); - - expect(Group.processBossQuest).to.not.be.called; - expect(Group.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); - - expect(Group.processBossQuest).to.not.be.called; - expect(Group.processCollectionQuest).to.not.be.called; - }); - - it('calls processBossQuest if quest is a boss quest', async () => { - party.quest.key = 'whale'; - await party.save(); - - await Group.processQuestProgress(participatingMember, progress); - - expect(Group.processBossQuest).to.be.calledOnce; - expect(Group.processCollectionQuest).to.not.be.called; - }); - - it('calls processCollectionQuest if quest is a collection quest', async () => { - party.quest.key = 'evilsanta2'; - await party.save(); - - await Group.processQuestProgress(participatingMember, progress); - - expect(Group.processBossQuest).to.not.be.called; - expect(Group.processCollectionQuest).to.be.calledOnce; - }); - }); - - describe('processBossQuest', () => { - let progress, quest; - - beforeEach(async () => { - progress = { - up: 5, - down: -5, - }; - quest = questScrolls.whale; - - party.quest.members = { - [questLeader._id]: true, - [participatingMember._id]: true, - [nonParticipatingMember._id]: false, - [undecidedMember._id]: null, - }; - party.quest.key = 'whale'; - - await party.save(); - await party.startQuest(questLeader); - - sandbox.stub(party, 'sendChat'); - }); - - it('applies user\'s progress to quest boss hp', async () => { - await Group.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.quest.progress.hp).to.eql(495); - }); - - it('sends a chat message about progress', async () => { - await Group.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.sendChat).to.be.calledOnce; - expect(party.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.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - 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.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.sendChat).to.be.calledTwice; - expect(party.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 () => { - progress.up = 999; - sandbox.spy(party, 'finishQuest'); - - await Group.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.finishQuest).to.be.calledOnce; - expect(party.finishQuest).to.be.calledWith(quest); - }); - - context('rage quests', () => { - beforeEach(async () => { - party.quest.active = false; - party.quest.key = 'trex_undead'; - quest = questScrolls[party.quest.key]; - - await party.save(); - - await party.startQuest(questLeader); - }); - - it('applies down progress to boss rage', async () => { - await Group.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.quest.progress.rage).to.eql(10); - }); - - it('activates rage when progress.down triggers rage bar', async () => { - progress.down = -999; - - party.quest.progress.hp = 300; - - await Group.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.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.processBossQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.quest.progress.hp).to.eql(500); - }); - }); - }); - - describe('processCollectionQuest', () => { - let progress, quest; - - beforeEach(async () => { - progress = { - collect: 5, - }; - quest = questScrolls.atom1; - - party.quest.members = { - [questLeader._id]: true, - [participatingMember._id]: true, - [nonParticipatingMember._id]: false, - [undecidedMember._id]: null, - }; - party.quest.key = 'atom1'; - - await party.save(); - await party.startQuest(questLeader); - - sandbox.stub(party, 'sendChat'); - }); - - it('applies user\'s progress to found quest items', async () => { - await Group.processCollectionQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.quest.progress.collect.soapBars).to.eq(5); - }); - - it('sends a chat message about progress', async () => { - await Group.processCollectionQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.sendChat).to.be.calledOnce; - expect(party.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.processCollectionQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.sendChat).to.be.calledOnce; - expect(party.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; - quest = questScrolls.evilsanta2; - - await party.save(); - await party.startQuest(questLeader); - - await Group.processCollectionQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.sendChat).to.be.calledOnce; - expect(party.sendChat).to.be.calledWithMatch(/`Participating Member found/);; - expect(party.sendChat).to.be.calledWithMatch(/\d* (Tracks|Broken Twigs)/); - }); - - it('sends message about victory', async () => { - progress.collect = 500; - - await Group.processCollectionQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.sendChat).to.be.calledTwice; - expect(party.sendChat).to.be.calledWith('`All items found! Party has received their rewards.`'); - }); - - it('calls finishQuest when all items are found', async () => { - progress.collect = 999; - sandbox.spy(party, 'finishQuest'); - - await Group.processCollectionQuest({ - user: participatingMember, - progress, - quest, - group: party, - }); - - expect(party.finishQuest).to.be.calledOnce; - expect(party.finishQuest).to.be.calledWith(quest); - }); - }); }); }); diff --git a/website/server/models/group.js b/website/server/models/group.js index f11227bf43..feddc8ba43 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -489,54 +489,14 @@ function _isOnQuest (user, progress, group) { return group && progress && group.quest && group.quest.active && group.quest.members[user._id] === true; } -async function processCollectionQuest (options) { +schema.methods._processBossQuest = async function processBossQuest (options) { let { user, progress, - quest, - group, - } = options; - - 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(); -} - -async function processBossQuest (options) { - let { - user, - progress, - quest, - group, } = 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; @@ -580,10 +540,47 @@ async function processBossQuest (options) { } return await group.save(); -} +}; -schema.statics.processBossQuest = processBossQuest; -schema.statics.processCollectionQuest = processCollectionQuest; +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'}); @@ -607,10 +604,9 @@ schema.statics.processQuestProgress = async function processQuestProgress (user, let questType = quest.boss ? 'Boss' : 'Collection'; - await this[`process${questType}Quest`]({ + await group[`_process${questType}Quest`]({ user, progress, - quest, group, }); };