diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json
index 7af8dc9af0..183b358e2a 100644
--- a/common/locales/en/api-v3.json
+++ b/common/locales/en/api-v3.json
@@ -113,14 +113,13 @@
"missingKeyParam": "\"req.params.key\" is required.",
"mysterySetNotFound": "Mystery set not found, or set already owned.",
"itemNotFound": "Item \"<%= key %>\" not found.",
- "cannoyBuyItem": "You can't buy this item.",
+ "cannotBuyItem": "You can't buy this item.",
"missingTypeKeyEquip": "\"key\" and \"type\" are required parameters.",
"missingPetFoodFeed": "\"pet\" and \"food\" are required parameters.",
"invalidPetName": "Invalid pet name supplied.",
"missingEggHatchingPotionHatch": "\"egg\" and \"hatchingPotion\" are required parameters.",
"invalidTypeEquip": "\"type\" must be one of 'equipped', 'pet', 'mount', 'costume'.",
"cannotDeleteActiveAccount": "You have an active subscription, cancel your plan before deleting your account.",
- "cannoyBuyItem": "You can't buy this item",
"messageRequired": "A message is required.",
"toUserIDRequired": "A toUserId is required",
"notAuthorizedToSendMessageToThisUser": "Can't send message to this user.",
@@ -170,5 +169,6 @@
"pushDeviceAdded": "Push device added successfully",
"pushDeviceAlreadyAdded": "The user already has the push device",
"resetComplete": "Reset completed",
- "lvl10ChangeClass": "To change class you must be at least level 10."
+ "lvl10ChangeClass": "To change class you must be at least level 10.",
+ "equipmentAlreadyOwned": "You already own that piece of equipment"
}
diff --git a/common/script/index.js b/common/script/index.js
index 1db3a88bb7..5191ba5802 100644
--- a/common/script/index.js
+++ b/common/script/index.js
@@ -113,6 +113,9 @@ import scoreTask from './ops/scoreTask';
import sleep from './ops/sleep';
import allocate from './ops/allocate';
import buy from './ops/buy';
+import buyGear from './ops/buyGear';
+import buyPotion from './ops/buyPotion';
+import buyArmoire from './ops/buyArmoire';
import buyMysterySet from './ops/buyMysterySet';
import buyQuest from './ops/buyQuest';
import buySpecialSpell from './ops/buySpecialSpell';
@@ -150,6 +153,9 @@ api.ops = {
sleep,
allocate,
buy,
+ buyGear,
+ buyPotion,
+ buyArmoire,
buyMysterySet,
buySpecialSpell,
buyQuest,
@@ -266,6 +272,9 @@ api.wrap = function wrapUser (user, main = true) {
releaseMounts: _.partial(importedOps.releaseMounts, user),
releaseBoth: _.partial(importedOps.releaseBoth, user),
buy: _.partial(importedOps.buy, user),
+ buyPotion: _.partial(importedOps.buyPotion, user),
+ buyArmoire: _.partial(importedOps.buyArmoire, user),
+ buyGear: _.partial(importedOps.buyGear, user),
buyQuest: _.partial(importedOps.buyQuest, user),
buyMysterySet: _.partial(importedOps.buyMysterySet, user),
hourglassPurchase: _.partial(importedOps.hourglassPurchase, user),
diff --git a/common/script/ops/buy.js b/common/script/ops/buy.js
index 268be94b39..d62842acea 100644
--- a/common/script/ops/buy.js
+++ b/common/script/ops/buy.js
@@ -1,142 +1,24 @@
-import content from '../content/index';
import i18n from '../i18n';
import _ from 'lodash';
-import count from '../count';
-import splitWhitespace from '../libs/splitWhitespace';
import {
BadRequest,
- NotAuthorized,
- NotFound,
} from '../libs/errors';
-import predictableRandom from '../fns/predictableRandom';
-import randomVal from '../fns/randomVal';
-import handleTwoHanded from '../fns/handleTwoHanded';
-import ultimateGear from '../fns/ultimateGear';
+import buyPotion from './buyPotion';
+import buyArmoire from './buyArmoire';
+import buyGear from './buyGear';
module.exports = function buy (user, req = {}, analytics) {
let key = _.get(req, 'params.key');
if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
- let item;
+ let buyRes;
if (key === 'potion') {
- item = content.potion;
+ buyRes = buyPotion(user, req, analytics);
} else if (key === 'armoire') {
- item = content.armoire;
+ buyRes = buyArmoire(user, req, analytics);
} else {
- item = content.gear.flat[key];
- }
- if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
-
- if (user.stats.gp < item.value) {
- throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
+ buyRes = buyGear(user, req, analytics);
}
- if (item.canOwn && !item.canOwn(user)) {
- throw new NotAuthorized(i18n.t('cannoyBuyItem', req.language));
- }
-
- let armoireResp;
- let armoireResult;
- let eligibleEquipment;
- let drop;
- let message;
-
- if (item.key === 'potion') {
- user.stats.hp += 15;
- if (user.stats.hp > 50) {
- user.stats.hp = 50;
- }
- } else if (item.key === 'armoire') {
- armoireResult = predictableRandom(user, user.stats.gp);
- eligibleEquipment = _.filter(content.gear.flat, (eligible) => {
- return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key];
- });
-
- if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) {
- eligibleEquipment.sort();
- drop = randomVal(user, eligibleEquipment);
-
- user.items.gear.owned[drop.key] = true;
- user.flags.armoireOpened = true;
- message = i18n.t('armoireEquipment', {
- image: ``,
- dropText: drop.text(req.language),
- }, req.language);
-
- if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
- user.flags.armoireEmpty = true;
- }
-
- armoireResp = {
- type: 'gear',
- dropKey: drop.key,
- dropText: drop.text(req.language),
- };
- } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
- drop = randomVal(user, _.where(content.food, {
- canDrop: true,
- }));
- user.items.food[drop.key] = user.items.food[drop.key] || 0;
- user.items.food[drop.key] += 1;
-
- message = i18n.t('armoireFood', {
- image: ``,
- dropArticle: drop.article,
- dropText: drop.text(req.language),
- }, req.language);
- armoireResp = {
- type: 'food',
- dropKey: drop.key,
- dropArticle: drop.article,
- dropText: drop.text(req.language),
- };
- } else {
- let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10);
- user.stats.exp += armoireExp;
- message = i18n.t('armoireExp', req.language);
- armoireResp = {
- type: 'experience',
- value: armoireExp,
- };
- }
- } else {
- if (user.preferences.autoEquip) {
- user.items.gear.equipped[item.type] = item.key;
- message = handleTwoHanded(user, item, undefined, req);
- }
- user.items.gear.owned[item.key] = true;
-
- if (item.last) ultimateGear(user);
- }
-
- user.stats.gp -= item.value;
-
- if (!message) {
- message = i18n.t('messageBought', {
- itemText: item.text(req.language),
- }, req.language);
- }
-
- if (analytics) {
- analytics.track('acquire item', {
- uuid: user._id,
- itemKey: key,
- acquireMethod: 'Gold',
- goldCost: item.value,
- category: 'behavior',
- });
- }
-
- let res = {
- data: _.pick(user, splitWhitespace('items achievements stats flags')),
- message,
- };
-
- if (armoireResp) res.armoire = armoireResp;
-
- if (req.v2 === true) {
- return res.data;
- } else {
- return res;
- }
+ return buyRes;
};
diff --git a/common/script/ops/buyArmoire.js b/common/script/ops/buyArmoire.js
new file mode 100644
index 0000000000..9b2a8af8e2
--- /dev/null
+++ b/common/script/ops/buyArmoire.js
@@ -0,0 +1,116 @@
+import content from '../content/index';
+import i18n from '../i18n';
+import _ from 'lodash';
+import count from '../count';
+import splitWhitespace from '../libs/splitWhitespace';
+import {
+ NotAuthorized,
+} from '../libs/errors';
+import predictableRandom from '../fns/predictableRandom';
+import randomVal from '../fns/randomVal';
+
+module.exports = function buyArmoire (user, req = {}, analytics) {
+ let item = content.armoire;
+
+ if (user.stats.gp < item.value) {
+ throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
+ }
+
+ if (item.canOwn && !item.canOwn(user)) {
+ throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
+ }
+
+ let armoireResp;
+ let armoireResult;
+ let eligibleEquipment;
+ let drop;
+ let message;
+
+ armoireResult = predictableRandom(user, user.stats.gp);
+ eligibleEquipment = _.filter(content.gear.flat, (eligible) => {
+ return eligible.klass === 'armoire' && !user.items.gear.owned[eligible.key];
+ });
+
+ if (!_.isEmpty(eligibleEquipment) && (armoireResult < 0.6 || !user.flags.armoireOpened)) {
+ eligibleEquipment.sort();
+ drop = randomVal(user, eligibleEquipment);
+
+ if (user.items.gear.owned[drop.key]) {
+ throw new NotAuthorized(i18n.t('equipmentAlradyOwned', req.language));
+ }
+
+ user.items.gear.owned[drop.key] = true;
+ user.flags.armoireOpened = true;
+ message = i18n.t('armoireEquipment', {
+ image: ``,
+ dropText: drop.text(req.language),
+ }, req.language);
+
+ if (count.remainingGearInSet(user.items.gear.owned, 'armoire') === 0) {
+ user.flags.armoireEmpty = true;
+ }
+
+ armoireResp = {
+ type: 'gear',
+ dropKey: drop.key,
+ dropText: drop.text(req.language),
+ };
+ } else if ((!_.isEmpty(eligibleEquipment) && armoireResult < 0.8) || armoireResult < 0.5) { // eslint-disable-line no-extra-parens
+ drop = randomVal(user, _.where(content.food, {
+ canDrop: true,
+ }));
+ user.items.food[drop.key] = user.items.food[drop.key] || 0;
+ user.items.food[drop.key] += 1;
+
+ message = i18n.t('armoireFood', {
+ image: ``,
+ dropArticle: drop.article,
+ dropText: drop.text(req.language),
+ }, req.language);
+ armoireResp = {
+ type: 'food',
+ dropKey: drop.key,
+ dropArticle: drop.article,
+ dropText: drop.text(req.language),
+ };
+ } else {
+ let armoireExp = Math.floor(predictableRandom(user, user.stats.exp) * 40 + 10);
+ user.stats.exp += armoireExp;
+ message = i18n.t('armoireExp', req.language);
+ armoireResp = {
+ type: 'experience',
+ value: armoireExp,
+ };
+ }
+
+ user.stats.gp -= item.value;
+
+ if (!message) {
+ message = i18n.t('messageBought', {
+ itemText: item.text(req.language),
+ }, req.language);
+ }
+
+ if (analytics) {
+ analytics.track('acquire item', {
+ uuid: user._id,
+ itemKey: 'Armoire',
+ acquireMethod: 'Gold',
+ goldCost: item.value,
+ category: 'behavior',
+ });
+ }
+
+ let res = {
+ data: _.pick(user, splitWhitespace('items flags')),
+ message,
+ };
+
+ if (armoireResp) res.armoire = armoireResp;
+
+ if (req.v2 === true) {
+ return res.data;
+ } else {
+ return res;
+ }
+};
diff --git a/common/script/ops/buyGear.js b/common/script/ops/buyGear.js
new file mode 100644
index 0000000000..2bad8135cd
--- /dev/null
+++ b/common/script/ops/buyGear.js
@@ -0,0 +1,72 @@
+import content from '../content/index';
+import i18n from '../i18n';
+import _ from 'lodash';
+import splitWhitespace from '../libs/splitWhitespace';
+import {
+ BadRequest,
+ NotAuthorized,
+ NotFound,
+} from '../libs/errors';
+import handleTwoHanded from '../fns/handleTwoHanded';
+import ultimateGear from '../fns/ultimateGear';
+
+module.exports = function buyGear (user, req = {}, analytics) {
+ let key = _.get(req, 'params.key');
+ if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language));
+
+ let item = content.gear.flat[key];
+
+ if (!item) throw new NotFound(i18n.t('itemNotFound', {key}, req.language));
+
+ if (user.stats.gp < item.value) {
+ throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
+ }
+
+ if (item.canOwn && !item.canOwn(user)) {
+ throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
+ }
+
+ let message;
+
+ if (user.items.gear.owned[item.key]) {
+ throw new NotAuthorized(i18n.t('equipmentAlreadyOwned', req.language));
+ }
+
+ if (user.preferences.autoEquip) {
+ user.items.gear.equipped[item.type] = item.key;
+ message = handleTwoHanded(user, item, undefined, req);
+ }
+
+ user.items.gear.owned[item.key] = true;
+
+ if (item.last) ultimateGear(user);
+
+ user.stats.gp -= item.value;
+
+ if (!message) {
+ message = i18n.t('messageBought', {
+ itemText: item.text(req.language),
+ }, req.language);
+ }
+
+ if (analytics) {
+ analytics.track('acquire item', {
+ uuid: user._id,
+ itemKey: key,
+ acquireMethod: 'Gold',
+ goldCost: item.value,
+ category: 'behavior',
+ });
+ }
+
+ let res = {
+ data: _.pick(user, splitWhitespace('items achievements stats flags')),
+ message,
+ };
+
+ if (req.v2 === true) {
+ return res.data;
+ } else {
+ return res;
+ }
+};
diff --git a/common/script/ops/buyPotion.js b/common/script/ops/buyPotion.js
new file mode 100644
index 0000000000..f797ff903b
--- /dev/null
+++ b/common/script/ops/buyPotion.js
@@ -0,0 +1,52 @@
+import content from '../content/index';
+import i18n from '../i18n';
+import _ from 'lodash';
+import splitWhitespace from '../libs/splitWhitespace';
+import {
+ NotAuthorized,
+} from '../libs/errors';
+
+module.exports = function buyPotion (user, req = {}, analytics) {
+ let item = content.potion;
+
+ if (user.stats.gp < item.value) {
+ throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
+ }
+
+ if (item.canOwn && !item.canOwn(user)) {
+ throw new NotAuthorized(i18n.t('cannotBuyItem', req.language));
+ }
+
+ user.stats.hp += 15;
+ if (user.stats.hp > 50) {
+ user.stats.hp = 50;
+ }
+
+ user.stats.gp -= item.value;
+
+ let message = i18n.t('messageBought', {
+ itemText: item.text(req.language),
+ }, req.language);
+
+
+ if (analytics) {
+ analytics.track('acquire item', {
+ uuid: user._id,
+ itemKey: 'Potion',
+ acquireMethod: 'Gold',
+ goldCost: item.value,
+ category: 'behavior',
+ });
+ }
+
+ let res = {
+ data: _.pick(user, splitWhitespace('stats')),
+ message,
+ };
+
+ if (req.v2 === true) {
+ return res.data;
+ } else {
+ return res;
+ }
+};
diff --git a/common/script/ops/index.js b/common/script/ops/index.js
index a2775f7d2a..4b06ac93e6 100644
--- a/common/script/ops/index.js
+++ b/common/script/ops/index.js
@@ -30,6 +30,9 @@ import releasePets from './releasePets';
import releaseMounts from './releaseMounts';
import releaseBoth from './releaseBoth';
import buy from './buy';
+import buyGear from './buyGear';
+import buyPotion from './buyPotion';
+import buyArmoire from './buyArmoire';
import buyQuest from './buyQuest';
import buyMysterySet from './buyMysterySet';
import hourglassPurchase from './hourglassPurchase';
@@ -77,6 +80,9 @@ module.exports = {
releaseMounts,
releaseBoth,
buy,
+ buyGear,
+ buyPotion,
+ buyArmoire,
buyQuest,
buyMysterySet,
hourglassPurchase,
diff --git a/test/api/v3/integration/user/POST-user_buy.test.js b/test/api/v3/integration/user/POST-user_buy.test.js
index bca65a2ab7..08e20617f0 100644
--- a/test/api/v3/integration/user/POST-user_buy.test.js
+++ b/test/api/v3/integration/user/POST-user_buy.test.js
@@ -26,17 +26,28 @@ describe('POST /user/buy/:key', () => {
});
});
- it('buys an item', async () => {
+ it('buys a potion', async () => {
+ await user.update({
+ 'stats.gp': 400,
+ });
+
let potion = content.potion;
let res = await user.post('/user/buy/potion');
await user.sync();
+ expect(user.stats.hp).to.equal(50);
expect(res.data).to.eql({
- items: JSON.parse(JSON.stringify(user.items)), // otherwise dates can't be compared
- achievements: user.achievements,
stats: user.stats,
- flags: JSON.parse(JSON.stringify(user.flags)), // otherwise dates can't be compared
});
expect(res.message).to.equal(t('messageBought', {itemText: potion.text()}));
});
+
+ it('buys a piece of gear', async () => {
+ let key = 'armor_warrior_1';
+
+ await user.post(`/user/buy/${key}`);
+ await user.sync();
+
+ expect(user.items.gear.owned).to.eql({ armor_warrior_1: true }); // eslint-disable-line camelcase
+ });
});
diff --git a/test/api/v3/integration/user/POST-user_buy_armoire.test.js b/test/api/v3/integration/user/POST-user_buy_armoire.test.js
new file mode 100644
index 0000000000..7538be64b8
--- /dev/null
+++ b/test/api/v3/integration/user/POST-user_buy_armoire.test.js
@@ -0,0 +1,44 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+import shared from '../../../../../common/script';
+
+let content = shared.content;
+
+describe('POST /user/buy-armoire', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser({
+ 'stats.hp': 40,
+ });
+ });
+
+ // More tests in common code unit tests
+
+ it('returns an error if user does not have enough gold', async () => {
+ await expect(user.post('/user/buy-potion'))
+ .to.eventually.be.rejected.and.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('messageNotEnoughGold'),
+ });
+ });
+
+ xit('buys a piece of armoire', async () => {
+ await user.update({
+ 'stats.gp': 400,
+ });
+
+ let potion = content.potion;
+ let res = await user.post('/user/buy-potion');
+ await user.sync();
+
+ expect(user.stats.hp).to.equal(50);
+ expect(res.data).to.eql({
+ stats: user.stats,
+ });
+ expect(res.message).to.equal(t('messageBought', {itemText: potion.text()}));
+ });
+});
diff --git a/test/api/v3/integration/user/POST-user_buy_gear.test.js b/test/api/v3/integration/user/POST-user_buy_gear.test.js
new file mode 100644
index 0000000000..5347b94da9
--- /dev/null
+++ b/test/api/v3/integration/user/POST-user_buy_gear.test.js
@@ -0,0 +1,34 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+
+describe('POST /user/buy-gear/:key', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser({
+ 'stats.gp': 400,
+ });
+ });
+
+ // More tests in common code unit tests
+
+ it('returns an error if the item is not found', async () => {
+ await expect(user.post('/user/buy-gear/notExisting'))
+ .to.eventually.be.rejected.and.eql({
+ code: 404,
+ error: 'NotFound',
+ message: t('itemNotFound', {key: 'notExisting'}),
+ });
+ });
+
+ it('buys a piece of gear', async () => {
+ let key = 'armor_warrior_1';
+
+ await user.post(`/user/buy-gear/${key}`);
+ await user.sync();
+
+ expect(user.items.gear.owned).to.eql({ armor_warrior_1: true }); // eslint-disable-line camelcase
+ });
+});
diff --git a/test/api/v3/integration/user/POST-user_buy_potion.test.js b/test/api/v3/integration/user/POST-user_buy_potion.test.js
new file mode 100644
index 0000000000..4616ae8f34
--- /dev/null
+++ b/test/api/v3/integration/user/POST-user_buy_potion.test.js
@@ -0,0 +1,44 @@
+import {
+ generateUser,
+ translate as t,
+} from '../../../../helpers/api-integration/v3';
+import shared from '../../../../../common/script';
+
+let content = shared.content;
+
+describe('POST /user/buy-potion', () => {
+ let user;
+
+ beforeEach(async () => {
+ user = await generateUser({
+ 'stats.hp': 40,
+ });
+ });
+
+ // More tests in common code unit tests
+
+ it('returns an error if user does not have enough gold', async () => {
+ await expect(user.post('/user/buy-potion'))
+ .to.eventually.be.rejected.and.eql({
+ code: 401,
+ error: 'NotAuthorized',
+ message: t('messageNotEnoughGold'),
+ });
+ });
+
+ it('buys a potion', async () => {
+ await user.update({
+ 'stats.gp': 400,
+ });
+
+ let potion = content.potion;
+ let res = await user.post('/user/buy-potion');
+ await user.sync();
+
+ expect(user.stats.hp).to.equal(50);
+ expect(res.data).to.eql({
+ stats: user.stats,
+ });
+ expect(res.message).to.equal(t('messageBought', {itemText: potion.text()}));
+ });
+});
diff --git a/test/common/ops/buy.js b/test/common/ops/buy.js
index b50d160623..abaf3c849d 100644
--- a/test/common/ops/buy.js
+++ b/test/common/ops/buy.js
@@ -1,15 +1,10 @@
/* eslint-disable camelcase */
-
-import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../helpers/common.helper';
-import count from '../../../common/script/count';
import buy from '../../../common/script/ops/buy';
-import shared from '../../../common/script';
-import content from '../../../common/script/content/index';
import {
- NotAuthorized,
+ BadRequest,
} from '../../../common/script/libs/errors';
import i18n from '../../../common/script/i18n';
@@ -30,277 +25,27 @@ describe('shared.ops.buy', () => {
},
stats: { gp: 200 },
});
-
- sinon.stub(shared.fns, 'randomVal');
- sinon.stub(shared.fns, 'predictableRandom');
});
- afterEach(() => {
- shared.fns.randomVal.restore();
- shared.fns.predictableRandom.restore();
+ it('returns error when key is not provided', (done) => {
+ try {
+ buy(user);
+ } catch (err) {
+ expect(err).to.be.an.instanceof(BadRequest);
+ expect(err.message).to.equal(i18n.t('missingKeyParam'));
+ done();
+ }
});
- context('Potion', () => {
- it('recovers 15 hp', () => {
- user.stats.hp = 30;
- buy(user, {params: {key: 'potion'}});
- expect(user.stats.hp).to.eql(45);
- });
-
- it('does not increase hp above 50', () => {
- user.stats.hp = 45;
- buy(user, {params: {key: 'potion'}});
- expect(user.stats.hp).to.eql(50);
- });
-
- it('deducts 25 gp', () => {
- user.stats.hp = 45;
- buy(user, {params: {key: 'potion'}});
-
- expect(user.stats.gp).to.eql(175);
- });
-
- it('does not purchase if not enough gp', (done) => {
- user.stats.hp = 45;
- user.stats.gp = 5;
- try {
- buy(user, {params: {key: 'potion'}});
- } catch (err) {
- expect(err).to.be.an.instanceof(NotAuthorized);
- expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
- expect(user.stats.hp).to.eql(45);
- expect(user.stats.gp).to.eql(5);
-
- done();
- }
- });
+ it('recovers 15 hp', () => {
+ user.stats.hp = 30;
+ buy(user, {params: {key: 'potion'}});
+ expect(user.stats.hp).to.eql(45);
});
- context('Gear', () => {
- it('adds equipment to inventory', () => {
- user.stats.gp = 31;
-
- buy(user, {params: {key: 'armor_warrior_1'}});
-
- expect(user.items.gear.owned).to.eql({ weapon_warrior_0: true, armor_warrior_1: true });
- });
-
- it('deducts gold from user', () => {
- user.stats.gp = 31;
-
- buy(user, {params: {key: 'armor_warrior_1'}});
-
- expect(user.stats.gp).to.eql(1);
- });
-
- it('auto equips equipment if user has auto-equip preference turned on', () => {
- user.stats.gp = 31;
- user.preferences.autoEquip = true;
-
- buy(user, {params: {key: 'armor_warrior_1'}});
-
- expect(user.items.gear.equipped).to.have.property('armor', 'armor_warrior_1');
- });
-
- it('buys equipment but does not auto-equip', () => {
- user.stats.gp = 31;
- user.preferences.autoEquip = false;
-
- buy(user, {params: {key: 'armor_warrior_1'}});
-
- expect(user.items.gear.equipped.property).to.not.equal('armor_warrior_1');
- });
-
- // TODO after user.ops.equip is done
- xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
- user.stats.gp = 100;
- user.preferences.autoEquip = true;
- buy(user, {params: {key: 'shield_warrior_1'}});
- user.ops.equip({params: {key: 'shield_warrior_1'}});
- buy(user, {params: {key: 'weapon_warrior_1'}});
- user.ops.equip({params: {key: 'weapon_warrior_1'}});
-
- buy(user, {params: {key: 'weapon_wizard_1'}});
-
- expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
- expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
- });
-
- // TODO after user.ops.equip is done
- xit('buys two-handed equipment but does not automatically remove sword or shield', () => {
- user.stats.gp = 100;
- user.preferences.autoEquip = false;
- buy(user, {params: {key: 'shield_warrior_1'}});
- user.ops.equip({params: {key: 'shield_warrior_1'}});
- buy(user, {params: {key: 'weapon_warrior_1'}});
- user.ops.equip({params: {key: 'weapon_warrior_1'}});
-
- buy(user, {params: {key: 'weapon_wizard_1'}});
-
- expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
- expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
- });
-
- it('does not buy equipment without enough Gold', (done) => {
- user.stats.gp = 20;
-
- try {
- buy(user, {params: {key: 'armor_warrior_1'}});
- } catch (err) {
- expect(err).to.be.an.instanceof(NotAuthorized);
- expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
- expect(user.items.gear.owned).to.not.have.property('armor_warrior_1');
- done();
- }
- });
- });
-
- context('Enchanted Armoire', () => {
- let YIELD_EQUIPMENT = 0.5;
- let YIELD_FOOD = 0.7;
- let YIELD_EXP = 0.9;
-
- let fullArmoire = {};
-
- _(content.gearTypes).each((type) => {
- _(content.gear.tree[type].armoire).each((gearObject) => {
- let armoireKey = gearObject.key;
-
- fullArmoire[armoireKey] = true;
- }).value();
- }).value();
-
- beforeEach(() => {
- user.achievements.ultimateGearSets = { rogue: true };
- user.flags.armoireOpened = true;
- user.stats.exp = 0;
- user.items.food = {};
- });
-
- context('failure conditions', () => {
- it('does not open if user does not have enough gold', (done) => {
- shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
- user.stats.gp = 50;
-
- try {
- buy(user, {params: {key: 'armoire'}});
- } catch (err) {
- expect(err).to.be.an.instanceof(NotAuthorized);
- expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
- expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
- expect(user.items.food).to.be.empty;
- expect(user.stats.exp).to.eql(0);
- done();
- }
- });
-
- it('does not open without Ultimate Gear achievement', (done) => {
- shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
- user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false};
-
- try {
- buy(user, {params: {key: 'armoire'}});
- } catch (err) {
- expect(err).to.be.an.instanceof(NotAuthorized);
- expect(err.message).to.equal(i18n.t('cannoyBuyItem'));
- expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
- expect(user.items.food).to.be.empty;
- expect(user.stats.exp).to.eql(0);
- done();
- }
- });
- });
-
- context('non-gear awards', () => {
- // Skipped because can't stub predictableRandom correctly
- xit('gives Experience', () => {
- shared.fns.predictableRandom.returns(YIELD_EXP);
-
- buy(user, {params: {key: 'armoire'}});
-
- expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
- expect(user.items.food).to.be.empty;
- expect(user.stats.exp).to.eql(46);
- expect(user.stats.gp).to.eql(100);
- });
-
- // Skipped because can't stub predictableRandom correctly
- xit('gives food', () => {
- let honey = content.food.Honey;
-
- shared.fns.randomVal.returns(honey);
- shared.fns.predictableRandom.returns(YIELD_FOOD);
-
- buy(user, {params: {key: 'armoire'}});
-
- expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
- expect(user.items.food).to.eql({Honey: 1});
- expect(user.stats.exp).to.eql(0);
- expect(user.stats.gp).to.eql(100);
- });
-
- // Skipped because can't stub predictableRandom correctly
- xit('does not give equipment if all equipment has been found', () => {
- shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
- user.items.gear.owned = fullArmoire;
- user.stats.gp = 150;
-
- buy(user, {params: {key: 'armoire'}});
-
- expect(user.items.gear.owned).to.eql(fullArmoire);
- let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
-
- expect(armoireCount).to.eql(0);
-
- expect(user.stats.exp).to.eql(30);
- expect(user.stats.gp).to.eql(50);
- });
- });
-
- context('gear awards', () => {
- beforeEach(() => {
- let shield = content.gear.tree.shield.armoire.gladiatorShield;
-
- shared.fns.randomVal.returns(shield);
- });
-
- // Skipped because can't stub predictableRandom correctly
- xit('always drops equipment the first time', () => {
- delete user.flags.armoireOpened;
- shared.fns.predictableRandom.returns(YIELD_EXP);
-
- buy(user, {params: {key: 'armoire'}});
-
- expect(user.items.gear.owned).to.eql({
- weapon_warrior_0: true,
- shield_armoire_gladiatorShield: true,
- });
-
- let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
-
- expect(armoireCount).to.eql(_.size(fullArmoire) - 1);
- expect(user.items.food).to.be.empty;
- expect(user.stats.exp).to.eql(0);
- expect(user.stats.gp).to.eql(100);
- });
-
- // Skipped because can't stub predictableRandom correctly
- xit('gives more equipment', () => {
- shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
- user.items.gear.owned = {
- weapon_warrior_0: true,
- head_armoire_hornedIronHelm: true,
- };
- user.stats.gp = 200;
-
- buy(user, {params: {key: 'armoire'}});
-
- expect(user.items.gear.owned).to.eql({weapon_warrior_0: true, shield_armoire_gladiatorShield: true, head_armoire_hornedIronHelm: true});
- let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
-
- expect(armoireCount).to.eql(_.size(fullArmoire) - 2);
- expect(user.stats.gp).to.eql(100);
- });
- });
+ it('adds equipment to inventory', () => {
+ user.stats.gp = 31;
+ buy(user, {params: {key: 'armor_warrior_1'}});
+ expect(user.items.gear.owned).to.eql({ weapon_warrior_0: true, armor_warrior_1: true });
});
});
diff --git a/test/common/ops/buyArmoire.js b/test/common/ops/buyArmoire.js
new file mode 100644
index 0000000000..2c71c9b428
--- /dev/null
+++ b/test/common/ops/buyArmoire.js
@@ -0,0 +1,187 @@
+/* eslint-disable camelcase */
+
+import sinon from 'sinon'; // eslint-disable-line no-shadow
+import {
+ generateUser,
+} from '../../helpers/common.helper';
+import count from '../../../common/script/count';
+import buyArmoire from '../../../common/script/ops/buyArmoire';
+import shared from '../../../common/script';
+import content from '../../../common/script/content/index';
+import {
+ NotAuthorized,
+} from '../../../common/script/libs/errors';
+import i18n from '../../../common/script/i18n';
+
+describe('shared.ops.buyArmoire', () => {
+ let user;
+ let YIELD_EQUIPMENT = 0.5;
+ let YIELD_FOOD = 0.7;
+ let YIELD_EXP = 0.9;
+
+ let fullArmoire = {};
+
+ _(content.gearTypes).each((type) => {
+ _(content.gear.tree[type].armoire).each((gearObject) => {
+ let armoireKey = gearObject.key;
+
+ fullArmoire[armoireKey] = true;
+ }).value();
+ }).value();
+
+
+ beforeEach(() => {
+ user = generateUser({
+ items: {
+ gear: {
+ owned: {
+ weapon_warrior_0: true,
+ },
+ equipped: {
+ weapon_warrior_0: true,
+ },
+ },
+ },
+ stats: { gp: 200 },
+ });
+
+ user.achievements.ultimateGearSets = { rogue: true };
+ user.flags.armoireOpened = true;
+ user.stats.exp = 0;
+ user.items.food = {};
+
+ sinon.stub(shared.fns, 'randomVal');
+ sinon.stub(shared.fns, 'predictableRandom');
+ });
+
+ afterEach(() => {
+ shared.fns.randomVal.restore();
+ shared.fns.predictableRandom.restore();
+ });
+
+ context('failure conditions', () => {
+ it('does not open if user does not have enough gold', (done) => {
+ shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
+ user.stats.gp = 50;
+
+ try {
+ buyArmoire(user);
+ } catch (err) {
+ expect(err).to.be.an.instanceof(NotAuthorized);
+ expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
+ expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
+ expect(user.items.food).to.be.empty;
+ expect(user.stats.exp).to.eql(0);
+ done();
+ }
+ });
+
+ it('does not open without Ultimate Gear achievement', (done) => {
+ shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
+ user.achievements.ultimateGearSets = {healer: false, wizard: false, rogue: false, warrior: false};
+
+ try {
+ buyArmoire(user);
+ } catch (err) {
+ expect(err).to.be.an.instanceof(NotAuthorized);
+ expect(err.message).to.equal(i18n.t('cannotBuyItem'));
+ expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
+ expect(user.items.food).to.be.empty;
+ expect(user.stats.exp).to.eql(0);
+ done();
+ }
+ });
+ });
+
+ context('non-gear awards', () => {
+ // Skipped because can't stub predictableRandom correctly
+ xit('gives Experience', () => {
+ shared.fns.predictableRandom.returns(YIELD_EXP);
+
+ buyArmoire(user);
+
+ expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
+ expect(user.items.food).to.be.empty;
+ expect(user.stats.exp).to.eql(46);
+ expect(user.stats.gp).to.eql(100);
+ });
+
+ // Skipped because can't stub predictableRandom correctly
+ xit('gives food', () => {
+ let honey = content.food.Honey;
+
+ shared.fns.randomVal.returns(honey);
+ shared.fns.predictableRandom.returns(YIELD_FOOD);
+
+ buyArmoire(user);
+
+ expect(user.items.gear.owned).to.eql({weapon_warrior_0: true});
+ expect(user.items.food).to.eql({Honey: 1});
+ expect(user.stats.exp).to.eql(0);
+ expect(user.stats.gp).to.eql(100);
+ });
+
+ // Skipped because can't stub predictableRandom correctly
+ xit('does not give equipment if all equipment has been found', () => {
+ shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
+ user.items.gear.owned = fullArmoire;
+ user.stats.gp = 150;
+
+ buyArmoire(user);
+
+ expect(user.items.gear.owned).to.eql(fullArmoire);
+ let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
+
+ expect(armoireCount).to.eql(0);
+
+ expect(user.stats.exp).to.eql(30);
+ expect(user.stats.gp).to.eql(50);
+ });
+ });
+
+ context('gear awards', () => {
+ beforeEach(() => {
+ let shield = content.gear.tree.shield.armoire.gladiatorShield;
+
+ shared.fns.randomVal.returns(shield);
+ });
+
+ // Skipped because can't stub predictableRandom correctly
+ xit('always drops equipment the first time', () => {
+ delete user.flags.armoireOpened;
+ shared.fns.predictableRandom.returns(YIELD_EXP);
+
+ buyArmoire(user);
+
+ expect(user.items.gear.owned).to.eql({
+ weapon_warrior_0: true,
+ shield_armoire_gladiatorShield: true,
+ });
+
+ let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
+
+ expect(armoireCount).to.eql(_.size(fullArmoire) - 1);
+ expect(user.items.food).to.be.empty;
+ expect(user.stats.exp).to.eql(0);
+ expect(user.stats.gp).to.eql(100);
+ });
+
+ // Skipped because can't stub predictableRandom correctly
+ xit('gives more equipment', () => {
+ shared.fns.predictableRandom.returns(YIELD_EQUIPMENT);
+ user.items.gear.owned = {
+ weapon_warrior_0: true,
+ head_armoire_hornedIronHelm: true,
+ };
+ user.stats.gp = 200;
+
+ buyArmoire(user);
+
+ expect(user.items.gear.owned).to.eql({weapon_warrior_0: true, shield_armoire_gladiatorShield: true, head_armoire_hornedIronHelm: true});
+ let armoireCount = count.remainingGearInSet(user.items.gear.owned, 'armoire');
+
+ expect(armoireCount).to.eql(_.size(fullArmoire) - 2);
+ expect(user.stats.gp).to.eql(100);
+ });
+ });
+});
diff --git a/test/common/ops/buyGear.js b/test/common/ops/buyGear.js
new file mode 100644
index 0000000000..fc26057ef8
--- /dev/null
+++ b/test/common/ops/buyGear.js
@@ -0,0 +1,132 @@
+/* eslint-disable camelcase */
+
+import sinon from 'sinon'; // eslint-disable-line no-shadow
+import {
+ generateUser,
+} from '../../helpers/common.helper';
+import buyGear from '../../../common/script/ops/buyGear';
+import shared from '../../../common/script';
+import {
+ NotAuthorized,
+} from '../../../common/script/libs/errors';
+import i18n from '../../../common/script/i18n';
+
+describe('shared.ops.buyGear', () => {
+ let user;
+
+ beforeEach(() => {
+ user = generateUser({
+ items: {
+ gear: {
+ owned: {
+ weapon_warrior_0: true,
+ },
+ equipped: {
+ weapon_warrior_0: true,
+ },
+ },
+ },
+ stats: { gp: 200 },
+ });
+
+ sinon.stub(shared.fns, 'randomVal');
+ sinon.stub(shared.fns, 'predictableRandom');
+ });
+
+ afterEach(() => {
+ shared.fns.randomVal.restore();
+ shared.fns.predictableRandom.restore();
+ });
+
+ context('Gear', () => {
+ it('adds equipment to inventory', () => {
+ user.stats.gp = 31;
+
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+
+ expect(user.items.gear.owned).to.eql({ weapon_warrior_0: true, armor_warrior_1: true });
+ });
+
+ it('deducts gold from user', () => {
+ user.stats.gp = 31;
+
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+
+ expect(user.stats.gp).to.eql(1);
+ });
+
+ it('auto equips equipment if user has auto-equip preference turned on', () => {
+ user.stats.gp = 31;
+ user.preferences.autoEquip = true;
+
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+
+ expect(user.items.gear.equipped).to.have.property('armor', 'armor_warrior_1');
+ });
+
+ it('buyGears equipment but does not auto-equip', () => {
+ user.stats.gp = 31;
+ user.preferences.autoEquip = false;
+
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+
+ expect(user.items.gear.equipped.property).to.not.equal('armor_warrior_1');
+ });
+
+ it('does not buyGear equipment twice', (done) => {
+ user.stats.gp = 62;
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+
+ try {
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+ } catch (err) {
+ expect(err).to.be.an.instanceof(NotAuthorized);
+ expect(err.message).to.equal(i18n.t('equipmentAlreadyOwned'));
+ done();
+ }
+ });
+
+ // TODO after user.ops.equip is done
+ xit('removes one-handed weapon and shield if auto-equip is on and a two-hander is bought', () => {
+ user.stats.gp = 100;
+ user.preferences.autoEquip = true;
+ buyGear(user, {params: {key: 'shield_warrior_1'}});
+ user.ops.equip({params: {key: 'shield_warrior_1'}});
+ buyGear(user, {params: {key: 'weapon_warrior_1'}});
+ user.ops.equip({params: {key: 'weapon_warrior_1'}});
+
+ buyGear(user, {params: {key: 'weapon_wizard_1'}});
+
+ expect(user.items.gear.equipped).to.have.property('shield', 'shield_base_0');
+ expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_wizard_1');
+ });
+
+ // TODO after user.ops.equip is done
+ xit('buyGears two-handed equipment but does not automatically remove sword or shield', () => {
+ user.stats.gp = 100;
+ user.preferences.autoEquip = false;
+ buyGear(user, {params: {key: 'shield_warrior_1'}});
+ user.ops.equip({params: {key: 'shield_warrior_1'}});
+ buyGear(user, {params: {key: 'weapon_warrior_1'}});
+ user.ops.equip({params: {key: 'weapon_warrior_1'}});
+
+ buyGear(user, {params: {key: 'weapon_wizard_1'}});
+
+ expect(user.items.gear.equipped).to.have.property('shield', 'shield_warrior_1');
+ expect(user.items.gear.equipped).to.have.property('weapon', 'weapon_warrior_1');
+ });
+
+ it('does not buyGear equipment without enough Gold', (done) => {
+ user.stats.gp = 20;
+
+ try {
+ buyGear(user, {params: {key: 'armor_warrior_1'}});
+ } catch (err) {
+ expect(err).to.be.an.instanceof(NotAuthorized);
+ expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
+ expect(user.items.gear.owned).to.not.have.property('armor_warrior_1');
+ done();
+ }
+ });
+ });
+});
diff --git a/test/common/ops/buyPotion.js b/test/common/ops/buyPotion.js
new file mode 100644
index 0000000000..5230040a35
--- /dev/null
+++ b/test/common/ops/buyPotion.js
@@ -0,0 +1,65 @@
+/* eslint-disable camelcase */
+import {
+ generateUser,
+} from '../../helpers/common.helper';
+import buyPotion from '../../../common/script/ops/buyPotion';
+import {
+ NotAuthorized,
+} from '../../../common/script/libs/errors';
+import i18n from '../../../common/script/i18n';
+
+describe('shared.ops.buyPotion', () => {
+ let user;
+
+ beforeEach(() => {
+ user = generateUser({
+ items: {
+ gear: {
+ owned: {
+ weapon_warrior_0: true,
+ },
+ equipped: {
+ weapon_warrior_0: true,
+ },
+ },
+ },
+ stats: { gp: 200 },
+ });
+ });
+
+ context('Potion', () => {
+ it('recovers 15 hp', () => {
+ user.stats.hp = 30;
+ buyPotion(user);
+ expect(user.stats.hp).to.eql(45);
+ });
+
+ it('does not increase hp above 50', () => {
+ user.stats.hp = 45;
+ buyPotion(user);
+ expect(user.stats.hp).to.eql(50);
+ });
+
+ it('deducts 25 gp', () => {
+ user.stats.hp = 45;
+ buyPotion(user);
+
+ expect(user.stats.gp).to.eql(175);
+ });
+
+ it('does not purchase if not enough gp', (done) => {
+ user.stats.hp = 45;
+ user.stats.gp = 5;
+ try {
+ buyPotion(user);
+ } catch (err) {
+ expect(err).to.be.an.instanceof(NotAuthorized);
+ expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
+ expect(user.stats.hp).to.eql(45);
+ expect(user.stats.gp).to.eql(5);
+
+ done();
+ }
+ });
+ });
+});
diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js
index f13c6d0a73..309a299953 100644
--- a/website/src/controllers/api-v3/user.js
+++ b/website/src/controllers/api-v3/user.js
@@ -477,15 +477,14 @@ api.allocateNow = {
};
/**
- * @api {post} /api/v3/user/buy/:key Buy a content item.
+ * @api {post} /user/buy/:key Buy gear, armoire or potion
* @apiVersion 3.0.0
* @apiName UserBuy
* @apiGroup User
*
* @apiParam {string} key The item to buy.
*
- * @apiSuccess {Object} data `items, achievements, stats, flags`
- * @apiSuccess {object} armoireResp Optional extra item given by the armoire
+ * @apiSuccess {Object} data `items`
* @apiSuccess {string} message
*/
api.buy = {
@@ -501,7 +500,77 @@ api.buy = {
};
/**
- * @api {post} /api/v3/user/buy-mystery-set/:key Buy a mystery set.
+ * @api {post} /user/buy-gear/:key Buy a piece of gear.
+ * @apiVersion 3.0.0
+ * @apiName UserBuyGear
+ * @apiGroup User
+ *
+ * @apiParam {string} key The item to buy.
+ *
+ * @apiSuccess {Object} data `items`
+ * @apiSuccess {string} message
+ */
+api.buyGear = {
+ method: 'POST',
+ middlewares: [authWithHeaders()],
+ url: '/user/buy-gear/:key',
+ async handler (req, res) {
+ let user = res.locals.user;
+ let buyRes = common.ops.buyGear(user, req, res.analytics);
+ await user.save();
+ res.respond(200, buyRes);
+ },
+};
+
+/**
+ * @api {post} /user/buy-armoire Buy an armoire item.
+ * @apiVersion 3.0.0
+ * @apiName UserBuyArmoire
+ * @apiGroup User
+ *
+ * @apiParam {string} key The item to buy.
+ *
+ * @apiSuccess {Object} data `items flags`
+ * @apiSuccess {object} armoireResp Optional extra item given by the armoire
+ * @apiSuccess {string} message
+ */
+api.buyArmoire = {
+ method: 'POST',
+ middlewares: [authWithHeaders()],
+ url: '/user/buy-armoire',
+ async handler (req, res) {
+ let user = res.locals.user;
+ let buyArmoireResponse = common.ops.buyArmoire(user, req, res.analytics);
+ await user.save();
+ res.respond(200, buyArmoireResponse);
+ },
+};
+
+/**
+ * @api {post} /user/buy-potion Buy a potion.
+ * @apiVersion 3.0.0
+ * @apiName UserBuyPotion
+ * @apiGroup User
+ *
+ * @apiParam {string} key The item to buy.
+ *
+ * @apiSuccess {Object} data `stats`
+ * @apiSuccess {string} message
+ */
+api.buyPotion = {
+ method: 'POST',
+ middlewares: [authWithHeaders()],
+ url: '/user/buy-potion',
+ async handler (req, res) {
+ let user = res.locals.user;
+ let buyPotionResponse = common.ops.buyPotion(user, req, res.analytics);
+ await user.save();
+ res.respond(200, buyPotionResponse);
+ },
+};
+
+/**
+ * @api {post} /user/buy-mystery-set/:key Buy a mystery set.
* @apiVersion 3.0.0
* @apiName UserBuyMysterySet
* @apiGroup User