From c699874e36e00aedf4123340cc05d053d8a4dc8a Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Mon, 1 Feb 2016 17:55:55 -0600 Subject: [PATCH] feat: Add startQuest method on group model --- test/api/v3/unit/models/group.test.js | 134 ++++++++++++++++++++++++++ website/src/models/group.js | 60 ++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 test/api/v3/unit/models/group.test.js diff --git a/test/api/v3/unit/models/group.test.js b/test/api/v3/unit/models/group.test.js new file mode 100644 index 0000000000..bdc49355f7 --- /dev/null +++ b/test/api/v3/unit/models/group.test.js @@ -0,0 +1,134 @@ +import { model as Group } from '../../../../../website/src/models/group'; +import { model as User } from '../../../../../website/src/models/user'; +import { quests as questScrolls } from '../../../../../common/script/content'; + +describe('Group Model', () => { + context('Instance Methods', () => { + let party; + + beforeEach(() => { + party = new Group({ + type: 'party', + }); + }); + + describe('#startQuest', () => { + context('Failure Conditions', () => { + it('throws an error if group is not a party', () => { + let guild = new Group({ + type: 'guild', + }); + + expect(() => { + guild.startQuest(); + }).to.throw('Must be a party to use this method'); + }); + + it('throws an error if party is not on a quest', () => { + expect(() => { + party.startQuest(); + }).to.throw('Party does not have a pending quest'); + }); + + it('throws an error if quest is already active', () => { + party.quest.key = 'whale'; + party.quest.active = true; + + expect(() => { + party.startQuest(); + }).to.throw('Quest is already active'); + }); + }); + + context('Successes', () => { + beforeEach(() => { + party.quest.key = 'whale'; + party.quest.active = false; + party.quest.leader = 'quest-leader'; + party.quest.members = { + 'quest-leader': true, + 'participating-member': true, + 'non-participating-member': false, + 'undecided-member': null, + }; + + sandbox.stub(User, 'update').returns({ exec: sandbox.spy() }); + }); + + it('activates quest', () => { + party.startQuest(); + + expect(party.quest.active).to.eql(true); + }); + + it('sets up boss quest', () => { + let bossQuest = questScrolls.whale; + party.quest.key = bossQuest.key; + + party.startQuest(); + + expect(party.quest.progress.hp).to.eql(bossQuest.boss.hp); + }); + + it('sets up rage meter for rage boss quest', () => { + let rageBossQuest = questScrolls.trex_undead; + party.quest.key = rageBossQuest.key; + + party.startQuest(); + + expect(party.quest.progress.rage).to.eql(0); + }); + + it('sets up collection quest', () => { + let collectionQuest = questScrolls.vice2; + party.quest.key = collectionQuest.key; + party.startQuest(); + + expect(party.quest.progress.collect).to.eql({ + lightCrystal: 0, + }); + }); + + it('sets up collection quest with multiple items', () => { + let collectionQuest = questScrolls.evilsanta2; + party.quest.key = collectionQuest.key; + party.startQuest(); + + expect(party.quest.progress.collect).to.eql({ + tracks: 0, + branches: 0, + }); + }); + + it('updates quest object for participating members', () => { + party.startQuest(); + + expect(User.update).to.be.calledTwice; + expect(User.update).to.not.be.calledWith({ _id: 'non-participating-member' }); + expect(User.update).to.not.be.calledWith({ _id: 'undecided-member' }); + expect(User.update).to.be.calledWith( + { _id: 'participating-member' }, + sinon.match({ $set: { 'party.quest.key': 'whale' }}), + ); + expect(User.update).to.be.calledWith( + { _id: 'quest-leader' }, + sinon.match({ $set: { 'party.quest.key': 'whale' }}), + ); + }); + + it('removes quest scroll from quest leader', () => { + party.startQuest(); + + expect(User.update).to.be.calledWith( + { _id: 'quest-leader' }, + sinon.match({ $inc: { 'items.quests.whale': -1 }}), + ); + }); + + it('sends email to participating members that quest has started'); + + it('sends email only to members who have not opted out'); + }); + }); + }); +}); diff --git a/website/src/models/group.js b/website/src/models/group.js index 1bbb0145a5..e416317a47 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -8,8 +8,10 @@ import _ from 'lodash'; import { model as Challenge} from './challenge'; import validator from 'validator'; import { removeFromArray } from '../libs/api-v3/collectionManipulators'; +import { BadRequest } from '../libs/api-v3/errors'; import * as firebase from '../libs/api-v2/firebase'; import baseModel from '../libs/api-v3/baseModel'; +import { quests as questScrolls } from '../../../common/script/content'; import Q from 'q'; import nconf from 'nconf'; @@ -168,6 +170,64 @@ schema.methods.isMember = function isGroupMember (user) { } }; +schema.methods.startQuest = function startQuest () { + if (this.type !== 'party') throw new BadRequest('Must be a party to use this method'); + if (!this.quest.key) throw new BadRequest('Party does not have a pending quest'); + if (this.quest.active) throw new BadRequest('Quest is already active'); + + let quest = questScrolls[this.quest.key]; + let collected = {}; + if (quest.collect) { + collected = _.transform(quest.collect, (result, n, itemToCollect) => { + result[itemToCollect] = 0; + }); + } + + let backgroundOperations = []; + + this.markModified('quest'); + this.quest.active = true; + if (quest.boss) { + this.quest.progress.hp = quest.boss.hp; + if (quest.boss.rage) this.quest.progress.rage = 0; + } else if (quest.collect) { + this.quest.progress.collect = collected; + } + + _.each(this.quest.members, (participating, memberId) => { + if (!participating) return; + + let update = { + $set: { + // Do *not* reset party.quest.progress.up + // See https://github.com/HabitRPG/habitrpg/issues/2168#issuecomment-31556322 + 'party.quest.key': this.quest.key, + 'party.quest.progress.down': 0, + 'party.quest.collect': collected, + 'party.quest.completed': null, + }, + $inc: { _v: 1 }, + }; + + if (this.quest.leader === memberId) { + update.$inc[`items.quests.${this.quest.key}`] = -1; + } + + backgroundOperations.push(User.update({ _id: memberId }, update).exec()); + }); + + // TODO Add emails to users that quest has started to background ops + + // These operations should run in the background + // and not hold up the quest routes from resolving + // TODO: What here? + // Q.all(backgroundOperations).then(() => { + // }).catch(err => { + // TODO: How to handle errors? + // IE, user deleted their account? + // }); +}; + export function chatDefaults (msg, user) { let message = { id: shared.uuid(),