diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js index 32007e068d..1f0304eb92 100644 --- a/test/api/v3/unit/models/group.test.js +++ b/test/api/v3/unit/models/group.test.js @@ -5,49 +5,53 @@ import { quests as questScrolls } from '../../../../../common/script/content'; import * as email from '../../../../../website/server/libs/api-v3/email'; describe('Group Model', () => { + 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(), + ]); + }); + context('Instance Methods', () => { describe('#startQuest', () => { - 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 }, - 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(), - ]); - }); - context('Failure Conditions', () => { it('throws an error if group is not a party', async () => { let guild = new Group({ @@ -74,11 +78,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', () => { @@ -296,5 +301,344 @@ 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 boss 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: { + soapBars: 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 () => { + delete progress.collect.soapBars; + + 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('sends message about victory', async () => { + progress.collect.soapBars = 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.soapBars = 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/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..58aa38fc97 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -491,10 +491,13 @@ 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]; +async function processCollectionQuest (options) { + let { + user, + progress, + quest, + group, + } = options; _.each(progress.collect, (v, k) => { group.quest.progress.collect[k] += v; @@ -505,7 +508,7 @@ schema.statics.collectQuest = async function collectQuest (user, progress) { return m; }, []); - foundText = foundText ? foundText.join(', ') : 'nothing'; + foundText = foundText.length > 0 ? foundText.join(', ') : 'nothing'; group.sendChat(`\`${user.profile.name} found ${foundText}.\``); group.markModified('quest.progress.collect'); @@ -518,14 +521,15 @@ schema.statics.collectQuest = async function collectQuest (user, progress) { 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? +async function processBossQuest (options) { + let { + user, + progress, + quest, + group, + } = options; let down = progress.down * quest.boss.str; // multiply by boss strength @@ -570,6 +574,28 @@ schema.statics.bossQuest = async function bossQuest (user, progress) { } return await group.save(); +} + +schema.statics.processBossQuest = processBossQuest; +schema.statics.processCollectionQuest = processCollectionQuest; + +schema.statics.processQuestProgress = async function processQuestProgress (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 (!quest) return; // TODO should this throw an error instead? + + let questType = quest.boss ? 'Boss' : 'Collection'; + + await this[`process${questType}Quest`]({ + user, + progress, + quest, + group, + }); }; // to set a boss: `db.groups.update({_id:TAVERN_ID},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`