diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index a0049ec3a5..e9dcd2d86d 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -110,7 +110,12 @@ "invalidAttribute": "\"<%= attr %>\" is not a valid attribute.", "notEnoughAttrPoints": "You don't have enough attribute points.", "missingKeyParam": "\"req.params.key\" is required.", - "mysterySetNotFound": "Mystery set not found, or set already owned", + "mysterySetNotFound": "Mystery set not found, or set already owned.", "itemNotFound": "Item \"<%= key %>\" not found.", - "cannoyBuyItem": "You can't buy this item" + "cannoyBuyItem": "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'." } diff --git a/common/script/index.js b/common/script/index.js index 9d2245e662..2e05604be3 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -109,6 +109,9 @@ import buyMysterySet from './ops/buyMysterySet'; import buyQuest from './ops/buyQuest'; import buySpecialSpell from './ops/buySpecialSpell'; import allocateNow from './ops/allocateNow'; +import hatch from './ops/hatch'; +import feed from './ops/feed'; +import equip from './ops/equip'; api.ops = { scoreTask, @@ -119,6 +122,9 @@ api.ops = { buySpecialSpell, buyQuest, allocateNow, + hatch, + feed, + equip, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/equip.js b/common/script/ops/equip.js index 8c07a364b1..e402b614ff 100644 --- a/common/script/ops/equip.js +++ b/common/script/ops/equip.js @@ -1,52 +1,66 @@ import content from '../content/index'; import i18n from '../i18n'; +import handleTwoHanded from '../fns/handleTwoHanded'; +import { + NotFound, + BadRequest, +} from '../libs/errors'; +import _ from 'lodash'; + +module.exports = function equip (user, req = {}) { + // Being type a parameter followed by another parameter + // when using the API it must be passes specifically in the URL, it's won't default to equipped + let type = _.get(req, 'params.type', 'equipped'); + let key = _.get(req, 'params.key'); + + if (!key || !type) throw new BadRequest(i18n.t('missingTypeKeyEquip', req.language)); + if (['mount', 'pet', 'costume', 'equipped'].indexOf(type) === -1) { + throw new BadRequest(i18n.t('invalidTypeEquip', req.language)); + } + + let message; -module.exports = function(user, req, cb) { - var item, key, message, ref, type; - ref = [req.params.type || 'equipped', req.params.key], type = ref[0], key = ref[1]; switch (type) { case 'mount': if (!user.items.mounts[key]) { - return typeof cb === "function" ? cb({ - code: 404, - message: ":You do not own this mount." - }) : void 0; + throw new NotFound(i18n.t('mountNotOwned', req.language)); } + user.items.currentMount = user.items.currentMount === key ? '' : key; break; + case 'pet': if (!user.items.pets[key]) { - return typeof cb === "function" ? cb({ - code: 404, - message: ":You do not own this pet." - }) : void 0; + throw new NotFound(i18n.t('petNotOwned', req.language)); } + user.items.currentPet = user.items.currentPet === key ? '' : key; break; + case 'costume': case 'equipped': - item = content.gear.flat[key]; if (!user.items.gear.owned[key]) { - return typeof cb === "function" ? cb({ - code: 404, - message: ":You do not own this gear." - }) : void 0; + throw new NotFound(i18n.t('gearNotOwned', req.language)); } + + let item = content.gear.flat[key]; + if (user.items.gear[type][item.type] === key) { - user.items.gear[type][item.type] = item.type + "_base_0"; + user.items.gear[type][item.type] = `${item.type}_base_0`; message = i18n.t('messageUnEquipped', { - itemText: item.text(req.language) + itemText: item.text(req.language), }, req.language); } else { user.items.gear[type][item.type] = item.key; - message = user.fns.handleTwoHanded(item, type, req); - } - if (typeof user.markModified === "function") { - user.markModified("items.gear." + type); + message = handleTwoHanded(user, item, type, req); } + break; } - return typeof cb === "function" ? cb((message ? { - code: 200, - message: message - } : null), user.items) : void 0; + + let res = { + data: user.items, + }; + if (message) res.message = message; + + return res; }; diff --git a/common/script/ops/feed.js b/common/script/ops/feed.js index 3c9b0f87fd..e4f2fa849f 100644 --- a/common/script/ops/feed.js +++ b/common/script/ops/feed.js @@ -1,78 +1,96 @@ import content from '../content/index'; import i18n from '../i18n'; +import _ from 'lodash'; +import { + BadRequest, + NotAuthorized, + NotFound, +} from '../libs/errors'; + +function evolve (user, pet, petDisplayName, req) { + user.items.pets[pet] = -1; + user.items.mounts[pet] = true; + + if (pet === user.items.currentPet) { + user.items.currentPet = ''; + } + + return i18n.t('messageEvolve', { + egg: petDisplayName, + }, req.language); +} + +module.exports = function feed (user, req = {}) { + let pet = _.get(req, 'params.pet'); + let foodK = _.get(req, 'params.food'); + + if (!pet || !foodK) throw new BadRequest(i18n.t('missingPetFoodFeed')); + + if (pet.indexOf('-') === -1) { + throw new BadRequest(i18n.t('invalidPetName', req.language)); + } + + let food = content.food[foodK]; + if (!food) { + throw new NotFound(i18n.t('messageFoodNotFound', req.language)); + } + + let userPets = user.items.pets; -module.exports = function(user, req, cb) { - var egg, eggText, evolve, food, message, pet, petDisplayName, potion, potionText, ref, ref1, ref2, userPets; - ref = req.params, pet = ref.pet, food = ref.food; - food = content.food[food]; - ref1 = pet.split('-'), egg = ref1[0], potion = ref1[1]; - userPets = user.items.pets; - potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; - eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; - petDisplayName = i18n.t('petName', { - potion: potionText, - egg: eggText - }); if (!userPets[pet]) { - return typeof cb === "function" ? cb({ - code: 404, - message: i18n.t('messagePetNotFound', req.language) - }) : void 0; + throw new NotFound(i18n.t('messagePetNotFound', req.language)); } - if (!((ref2 = user.items.food) != null ? ref2[food.key] : void 0)) { - return typeof cb === "function" ? cb({ - code: 404, - message: i18n.t('messageFoodNotFound', req.language) - }) : void 0; + + let [egg, potion] = pet.split('-'); + + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let petDisplayName = i18n.t('petName', { + potion: potionText, + egg: eggText, + }, req.language); + + if (!user.items.food[food.key]) { + throw new NotFound(i18n.t('messageFoodNotFound', req.language)); } + if (content.specialPets[pet]) { - return typeof cb === "function" ? cb({ - code: 401, - message: i18n.t('messageCannotFeedPet', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t('messageCannotFeedPet', req.language)); } + if (user.items.mounts[pet]) { - return typeof cb === "function" ? cb({ - code: 401, - message: i18n.t('messageAlreadyMount', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t('messageAlreadyMount', req.language)); } - message = ''; - evolve = function() { - userPets[pet] = -1; - user.items.mounts[pet] = true; - if (pet === user.items.currentPet) { - user.items.currentPet = ""; - } - return message = i18n.t('messageEvolve', { - egg: petDisplayName - }, req.language); - }; + + let message; + if (food.key === 'Saddle') { - evolve(); + message = evolve(user, pet, petDisplayName, req); } else { if (food.target === potion || content.hatchingPotions[potion].premium) { userPets[pet] += 5; message = i18n.t('messageLikesFood', { egg: petDisplayName, - foodText: food.text(req.language) + foodText: food.text(req.language), }, req.language); } else { userPets[pet] += 2; message = i18n.t('messageDontEnjoyFood', { egg: petDisplayName, - foodText: food.text(req.language) + foodText: food.text(req.language), }, req.language); } + if (userPets[pet] >= 50 && !user.items.mounts[pet]) { - evolve(); + message = evolve(user, pet, petDisplayName, req); } } + user.items.food[food.key]--; - return typeof cb === "function" ? cb({ - code: 200, - message: message - }, { - value: userPets[pet] - }) : void 0; + + return { + data: userPets[pet], + message, + }; }; diff --git a/common/script/ops/hatch.js b/common/script/ops/hatch.js index 6292e24bcb..fee84fd4a8 100644 --- a/common/script/ops/hatch.js +++ b/common/script/ops/hatch.js @@ -1,39 +1,40 @@ import content from '../content/index'; import i18n from '../i18n'; +import _ from 'lodash'; +import { + BadRequest, + NotAuthorized, + NotFound, +} from '../libs/errors'; + +module.exports = function hatch (user, req = {}) { + let egg = _.get(req, 'params.egg'); + let hatchingPotion = _.get(req, 'params.hatchingPotion'); -module.exports = function(user, req, cb) { - var egg, hatchingPotion, pet, ref; - ref = req.params, egg = ref.egg, hatchingPotion = ref.hatchingPotion; if (!(egg && hatchingPotion)) { - return typeof cb === "function" ? cb({ - code: 400, - message: "Please specify query.egg & query.hatchingPotion" - }) : void 0; + throw new BadRequest(i18n.t('missingEggHatchingPotionHatch', req.language)); } + if (!(user.items.eggs[egg] > 0 && user.items.hatchingPotions[hatchingPotion] > 0)) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t('messageMissingEggPotion', req.language) - }) : void 0; + throw new NotFound(i18n.t('messageMissingEggPotion', req.language)); } + if (content.hatchingPotions[hatchingPotion].premium && !content.dropEggs[egg]) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t('messageInvalidEggPotionCombo', req.language) - }) : void 0; + throw new BadRequest(i18n.t('messageInvalidEggPotionCombo', req.language)); } - pet = egg + "-" + hatchingPotion; + + let pet = `${egg}-${hatchingPotion}`; + if (user.items.pets[pet] && user.items.pets[pet] > 0) { - return typeof cb === "function" ? cb({ - code: 403, - message: i18n.t('messageAlreadyPet', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t('messageAlreadyPet', req.language)); } + user.items.pets[pet] = 5; user.items.eggs[egg]--; user.items.hatchingPotions[hatchingPotion]--; - return typeof cb === "function" ? cb({ - code: 200, - message: i18n.t('messageHatched', req.language) - }, user.items) : void 0; + + return { + message: i18n.t('messageHatched', req.language), + data: user.items, + }; }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 25a0f6a978..057e0c0fc0 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -28,11 +28,8 @@ const COMMON_FILES = [ '!./common/script/ops/deleteTask.js', '!./common/script/ops/deleteWebhook.js', '!./common/script/ops/disableClasses.js', - '!./common/script/ops/equip.js', - '!./common/script/ops/feed.js', '!./common/script/ops/getTag.js', '!./common/script/ops/getTags.js', - '!./common/script/ops/hatch.js', '!./common/script/ops/hourglassPurchase.js', '!./common/script/ops/openMysteryItem.js', '!./common/script/ops/purchase.js', diff --git a/test/api/v3/integration/user/POST-user_equip_type_key.test.js b/test/api/v3/integration/user/POST-user_equip_type_key.test.js new file mode 100644 index 0000000000..53e8dbfe1f --- /dev/null +++ b/test/api/v3/integration/user/POST-user_equip_type_key.test.js @@ -0,0 +1,42 @@ +/* eslint-disable camelcase */ + +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/equip/:type/:key', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('equip an item', async () => { + await user.update({ + 'items.gear.owned': { + weapon_warrior_0: true, + weapon_warrior_1: true, + weapon_warrior_2: true, + weapon_wizard_1: true, + weapon_wizard_2: true, + shield_base_0: true, + shield_warrior_1: true, + }, + 'items.gear.equipped': { + weapon: 'weapon_warrior_0', + shield: 'shield_base_0', + }, + 'stats.gp': 200, + }); + + await user.post(`/user/equip/equipped/weapon_warrior_1`); + let res = await user.post(`/user/equip/equipped/weapon_warrior_2`); + await user.sync(); + + expect(res).to.eql({ + data: JSON.parse(JSON.stringify(user.items)), + }); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_feed_pet_food.test.js b/test/api/v3/integration/user/POST-user_feed_pet_food.test.js new file mode 100644 index 0000000000..6fa3275196 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_feed_pet_food.test.js @@ -0,0 +1,45 @@ +/* eslint-disable camelcase */ + +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; +import content from '../../../../../common/script/content'; + +describe('POST /user/feed/:pet/:food', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('does not enjoy the food', async () => { + await user.update({ + 'items.pets.Wolf-Base': 5, + 'items.food.Milk': 2, + }); + + let food = content.food.Milk; + let [egg, potion] = 'Wolf-Base'.split('-'); + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let res = await user.post(`/user/feed/Wolf-Base/Milk`); + await user.sync(); + expect(res).to.eql({ + data: user.items.pets['Wolf-Base'], + message: t('messageDontEnjoyFood', { + egg: t('petName', { + potion: potionText, + egg: eggText, + }), + foodText: food.text(), + }), + }); + + expect(user.items.food.Milk).to.equal(1); + expect(user.items.pets['Wolf-Base']).to.equal(7); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_hatch_egg_hatchingPotion.test.js b/test/api/v3/integration/user/POST-user_hatch_egg_hatchingPotion.test.js new file mode 100644 index 0000000000..e2a5e330f5 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_hatch_egg_hatchingPotion.test.js @@ -0,0 +1,31 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/hatch/:egg/:hatchingPotion', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('hatch a new pet', async () => { + await user.update({ + 'items.eggs.Wolf': 1, + 'items.hatchingPotions.Base': 1, + }); + let res = await user.post(`/user/hatch/Wolf/Base`); + await user.sync(); + expect(user.items.pets['Wolf-Base']).to.equal(5); + expect(user.items.eggs.Wolf).to.equal(0); + expect(user.items.hatchingPotions.Base).to.equal(0); + + expect(res).to.eql({ + message: t('messageHatched'), + data: JSON.parse(JSON.stringify(user.items)), + }); + }); +}); diff --git a/test/common/ops/equip.js b/test/common/ops/equip.js new file mode 100644 index 0000000000..77110f7f40 --- /dev/null +++ b/test/common/ops/equip.js @@ -0,0 +1,85 @@ +/* eslint-disable camelcase */ +import equip from '../../../common/script/ops/equip'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; +import content from '../../../common/script/content/index'; + +describe('shared.ops.equip', () => { + let user; + + beforeEach(() => { + user = generateUser({ + items: { + gear: { + owned: { + weapon_warrior_0: true, + weapon_warrior_1: true, + weapon_warrior_2: true, + weapon_wizard_1: true, + weapon_wizard_2: true, + shield_base_0: true, + shield_warrior_1: true, + }, + equipped: { + weapon: 'weapon_warrior_0', + shield: 'shield_base_0', + }, + }, + }, + stats: {gp: 200}, + }); + }); + + context('Gear', () => { + it('should not send a message if a weapon is equipped while only having zero or one weapons equipped', () => { + equip(user, {params: {key: 'weapon_warrior_1'}}); + + // one-handed to one-handed + let res = equip(user, {params: {key: 'weapon_warrior_2'}}); + expect(res.message).to.not.exists; + + // one-handed to two-handed + res = equip(user, {params: {key: 'weapon_wizard_1'}}); + expect(res.message).to.not.exists; + + // two-handed to two-handed + res = equip(user, {params: {key: 'weapon_wizard_2'}}); + expect(res.message).to.not.exists; + + // two-handed to one-handed + res = equip(user, {params: {key: 'weapon_warrior_2'}}); + expect(res.message).to.not.exists; + }); + + it('should send messages if equipping a two-hander causes the off-hander to be unequipped', () => { + equip(user, {params: {key: 'weapon_warrior_1'}}); + equip(user, {params: {key: 'shield_warrior_1'}}); + + // equipping two-hander + let res = equip(user, {params: {key: 'weapon_wizard_1'}}); + let weapon = content.gear.flat.weapon_wizard_1; + let item = content.gear.flat.shield_warrior_1; + + expect(res).to.eql({ + message: i18n.t('messageTwoHandedEquip', {twoHandedText: weapon.text(), offHandedText: item.text()}), + data: user.items, + }); + }); + + it('should send messages if equipping an off-hand item causes a two-handed weapon to be unequipped', () => { + // equipping two-hander + equip(user, {params: {key: 'weapon_wizard_1'}}); + let weapon = content.gear.flat.weapon_wizard_1; + let shield = content.gear.flat.shield_warrior_1; + + let res = equip(user, {params: {key: 'shield_warrior_1'}}); + + expect(res).to.eql({ + message: i18n.t('messageTwoHandedUnequip', {twoHandedText: weapon.text(), offHandedText: shield.text()}), + data: user.items, + }); + }); + }); +}); diff --git a/test/common/ops/feed.js b/test/common/ops/feed.js new file mode 100644 index 0000000000..a2cf8395b8 --- /dev/null +++ b/test/common/ops/feed.js @@ -0,0 +1,219 @@ +import feed from '../../../common/script/ops/feed'; +import content from '../../../common/script/content'; +import { + BadRequest, + NotAuthorized, + NotFound, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.feed', () => { + let user; + + beforeEach(() => { + user = generateUser(); + }); + + context('failure conditions', () => { + it('does not allow feeding without specifying pet and food', () => { + try { + feed(user); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('missingPetFoodFeed')); + } + }); + + it('does not allow feeding if pet name format is invalid', () => { + try { + feed(user, {params: {pet: 'invalid', food: 'food'}}); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidPetName')); + } + }); + + it('does not allow feeding if food does not exists', () => { + try { + feed(user, {params: {pet: 'valid-pet', food: 'invalid food name'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('messageFoodNotFound')); + } + }); + + it('does not allow feeding if pet is not owned', () => { + try { + feed(user, {params: {pet: 'not-owned', food: 'Meat'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('messagePetNotFound')); + } + }); + + it('does not allow feeding if food is not owned', () => { + user.items.pets['Wolf-Base'] = 5; + try { + feed(user, {params: {pet: 'Wolf-Base', food: 'Meat'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('messageFoodNotFound')); + } + }); + + it('does not allow feeding of special pets', () => { + user.items.pets['Wolf-Veteran'] = 5; + user.items.food.Meat = 1; + try { + feed(user, {params: {pet: 'Wolf-Veteran', food: 'Meat'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageCannotFeedPet')); + } + }); + + it('does not allow feeding of mounts', () => { + user.items.pets['Wolf-Base'] = -1; + user.items.mounts['Wolf-Base'] = true; + user.items.food.Meat = 1; + try { + feed(user, {params: {pet: 'Wolf-Base', food: 'Meat'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageAlreadyMount')); + } + }); + }); + + context('successful feeding', () => { + it('evolves the pet if the food is a Saddle', () => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Saddle = 2; + user.items.currentPet = 'Wolf-Base'; + let [egg, potion] = 'Wolf-Base'.split('-'); + + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let res = feed(user, {params: {pet: 'Wolf-Base', food: 'Saddle'}}); + expect(res).to.eql({ + data: user.items.pets['Wolf-Base'], + message: i18n.t('messageEvolve', { + egg: i18n.t('petName', { + potion: potionText, + egg: eggText, + }), + }), + }); + + expect(user.items.food.Saddle).to.equal(1); + expect(user.items.pets['Wolf-Base']).to.equal(-1); + expect(user.items.mounts['Wolf-Base']).to.equal(true); + expect(user.items.currentPet).to.equal(''); + }); + + it('enjoys the food', () => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Meat = 2; + + let food = content.food.Meat; + let [egg, potion] = 'Wolf-Base'.split('-'); + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let res = feed(user, {params: {pet: 'Wolf-Base', food: 'Meat'}}); + expect(res).to.eql({ + data: user.items.pets['Wolf-Base'], + message: i18n.t('messageLikesFood', { + egg: i18n.t('petName', { + potion: potionText, + egg: eggText, + }), + foodText: food.text(), + }), + }); + + expect(user.items.food.Meat).to.equal(1); + expect(user.items.pets['Wolf-Base']).to.equal(10); + }); + + it('enjoys the food (premium potion)', () => { + user.items.pets['Wolf-Spooky'] = 5; + user.items.food.Milk = 2; + + let food = content.food.Milk; + let [egg, potion] = 'Wolf-Spooky'.split('-'); + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let res = feed(user, {params: {pet: 'Wolf-Spooky', food: 'Milk'}}); + expect(res).to.eql({ + data: user.items.pets['Wolf-Spooky'], + message: i18n.t('messageLikesFood', { + egg: i18n.t('petName', { + potion: potionText, + egg: eggText, + }), + foodText: food.text(), + }), + }); + + expect(user.items.food.Milk).to.equal(1); + expect(user.items.pets['Wolf-Spooky']).to.equal(10); + }); + + it('does not like the food', () => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Milk = 2; + + let food = content.food.Milk; + let [egg, potion] = 'Wolf-Base'.split('-'); + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let res = feed(user, {params: {pet: 'Wolf-Base', food: 'Milk'}}); + expect(res).to.eql({ + data: user.items.pets['Wolf-Base'], + message: i18n.t('messageDontEnjoyFood', { + egg: i18n.t('petName', { + potion: potionText, + egg: eggText, + }), + foodText: food.text(), + }), + }); + + expect(user.items.food.Milk).to.equal(1); + expect(user.items.pets['Wolf-Base']).to.equal(7); + }); + + it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50', () => { + user.items.pets['Wolf-Base'] = 49; + user.items.food.Milk = 2; + user.items.currentPet = 'Wolf-Base'; + + let [egg, potion] = 'Wolf-Base'.split('-'); + let potionText = content.hatchingPotions[potion] ? content.hatchingPotions[potion].text() : potion; + let eggText = content.eggs[egg] ? content.eggs[egg].text() : egg; + + let res = feed(user, {params: {pet: 'Wolf-Base', food: 'Milk'}}); + expect(res).to.eql({ + data: user.items.pets['Wolf-Base'], + message: i18n.t('messageEvolve', { + egg: i18n.t('petName', { + potion: potionText, + egg: eggText, + }), + }), + }); + + expect(user.items.food.Milk).to.equal(1); + expect(user.items.pets['Wolf-Base']).to.equal(-1); + expect(user.items.mounts['Wolf-Base']).to.equal(true); + expect(user.items.currentPet).to.equal(''); + }); + }); +}); diff --git a/test/common/ops/hatch.js b/test/common/ops/hatch.js new file mode 100644 index 0000000000..ead2a397f0 --- /dev/null +++ b/test/common/ops/hatch.js @@ -0,0 +1,143 @@ +import hatch from '../../../common/script/ops/hatch'; +import { + BadRequest, + NotAuthorized, + NotFound, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.hatch', () => { + let user; + + beforeEach(() => { + user = generateUser(); + }); + + context('Pet Hatching', () => { + context('failure conditions', () => { + it('does not allow hatching without specifying egg and potion', () => { + user.items.pets = {}; + try { + hatch(user); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('missingEggHatchingPotionHatch')); + expect(user.items.pets).to.be.empty; + } + }); + + it('does not allow hatching if user lacks specified egg', () => { + user.items.eggs.Wolf = 1; + user.items.hatchingPotions.Base = 1; + user.items.pets = {}; + try { + hatch(user, {params: {egg: 'Dragon', hatchingPotion: 'Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('messageMissingEggPotion')); + expect(user.items.pets).to.be.empty; + expect(user.items.eggs.Wolf).to.equal(1); + expect(user.items.hatchingPotions.Base).to.equal(1); + } + }); + + it('does not allow hatching if user lacks specified hatching potion', () => { + user.items.eggs.Wolf = 1; + user.items.hatchingPotions.Base = 1; + user.items.pets = {}; + try { + hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Golden'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('messageMissingEggPotion')); + expect(user.items.pets).to.be.empty; + expect(user.items.eggs.Wolf).to.equal(1); + expect(user.items.hatchingPotions.Base).to.equal(1); + } + }); + + it('does not allow hatching if user already owns target pet', () => { + user.items.eggs = {Wolf: 1}; + user.items.hatchingPotions = {Base: 1}; + user.items.pets = {'Wolf-Base': 10}; + try { + hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Base'}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageAlreadyPet')); + expect(user.items.pets).to.eql({'Wolf-Base': 10}); + expect(user.items.eggs).to.eql({Wolf: 1}); + expect(user.items.hatchingPotions).to.eql({Base: 1}); + } + }); + + it('does not allow hatching quest pet egg using premium potion', () => { + user.items.eggs = {Cheetah: 1}; + user.items.hatchingPotions = {Spooky: 1}; + user.items.pets = {}; + try { + hatch(user, {params: {egg: 'Cheetah', hatchingPotion: 'Spooky'}}); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('messageInvalidEggPotionCombo')); + expect(user.items.pets).to.be.empty; + expect(user.items.eggs).to.eql({Cheetah: 1}); + expect(user.items.hatchingPotions).to.eql({Spooky: 1}); + } + }); + }); + + context('successful hatching', () => { + it('hatches a basic pet', () => { + user.items.eggs = {Wolf: 1}; + user.items.hatchingPotions = {Base: 1}; + user.items.pets = {}; + let res = hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Base'}}); + expect(res.message).to.equal(i18n.t('messageHatched')); + expect(res.data).to.eql(user.items); + expect(user.items.pets).to.eql({'Wolf-Base': 5}); + expect(user.items.eggs).to.eql({Wolf: 0}); + expect(user.items.hatchingPotions).to.eql({Base: 0}); + }); + + it('hatches a quest pet', () => { + user.items.eggs = {Cheetah: 1}; + user.items.hatchingPotions = {Base: 1}; + user.items.pets = {}; + let res = hatch(user, {params: {egg: 'Cheetah', hatchingPotion: 'Base'}}); + expect(res.message).to.equal(i18n.t('messageHatched')); + expect(res.data).to.eql(user.items); + expect(user.items.pets).to.eql({'Cheetah-Base': 5}); + expect(user.items.eggs).to.eql({Cheetah: 0}); + expect(user.items.hatchingPotions).to.eql({Base: 0}); + }); + + it('hatches a premium pet', () => { + user.items.eggs = {Wolf: 1}; + user.items.hatchingPotions = {Spooky: 1}; + user.items.pets = {}; + let res = hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Spooky'}}); + expect(res.message).to.equal(i18n.t('messageHatched')); + expect(res.data).to.eql(user.items); + expect(user.items.pets).to.eql({'Wolf-Spooky': 5}); + expect(user.items.eggs).to.eql({Wolf: 0}); + expect(user.items.hatchingPotions).to.eql({Spooky: 0}); + }); + + it('hatches a pet previously raised to a mount', () => { + user.items.eggs = {Wolf: 1}; + user.items.hatchingPotions = {Base: 1}; + user.items.pets = {'Wolf-Base': -1}; + let res = hatch(user, {params: {egg: 'Wolf', hatchingPotion: 'Base'}}); + expect(res.message).to.eql(i18n.t('messageHatched')); + expect(res.data).to.eql(user.items); + expect(user.items.pets).to.eql({'Wolf-Base': 5}); + expect(user.items.eggs).to.eql({Wolf: 0}); + expect(user.items.hatchingPotions).to.eql({Base: 0}); + }); + }); + }); +}); diff --git a/test/common_old/user.ops.equip.test.js b/test/common_old/user.ops.equip.test.js deleted file mode 100644 index 62f7956e02..0000000000 --- a/test/common_old/user.ops.equip.test.js +++ /dev/null @@ -1,102 +0,0 @@ -/* eslint-disable camelcase */ - -import sinon from 'sinon'; // eslint-disable-line no-shadow -import {assert} from 'sinon'; -import i18n from '../../common/script/i18n'; -import shared from '../../common/script/index.js'; -import content from '../../common/script/content/index'; - -describe('user.ops.equip', () => { - let user; - let spy; - - beforeEach(() => { - user = { - items: { - gear: { - owned: { - weapon_warrior_0: true, - weapon_warrior_1: true, - weapon_warrior_2: true, - weapon_wizard_1: true, - weapon_wizard_2: true, - shield_base_0: true, - shield_warrior_1: true, - }, - equipped: { - weapon: 'weapon_warrior_0', - shield: 'shield_base_0', - }, - }, - }, - preferences: {}, - stats: {gp: 200}, - achievements: {}, - flags: {}, - }; - - shared.wrap(user); - spy = sinon.spy(); - }); - - context('Gear', () => { - it('should not send a message if a weapon is equipped while only having zero or one weapons equipped', () => { - // user.ops.equip always calls the callback, even if it isn't sending a message - // so we need to check to see if a single null message was sent. - user.ops.equip({params: {key: 'weapon_warrior_1'}}); - - // one-handed to one-handed - user.ops.equip({params: {key: 'weapon_warrior_2'}}, spy); - - assert.calledOnce(spy); - assert.calledWith(spy, null); - spy.reset(); - - // one-handed to two-handed - user.ops.equip({params: {key: 'weapon_wizard_1'}}, spy); - assert.calledOnce(spy); - assert.calledWith(spy, null); - spy.reset(); - - // two-handed to two-handed - user.ops.equip({params: {key: 'weapon_wizard_2'}}, spy); - assert.calledOnce(spy); - assert.calledWith(spy, null); - spy.reset(); - - // two-handed to one-handed - user.ops.equip({params: {key: 'weapon_warrior_2'}}, spy); - assert.calledOnce(spy); - assert.calledWith(spy, null); - spy.reset(); - }); - - it('should send messages if equipping a two-hander causes the off-hander to be unequipped', () => { - user.ops.equip({params: {key: 'weapon_warrior_1'}}); - user.ops.equip({params: {key: 'shield_warrior_1'}}); - - // equipping two-hander - user.ops.equip({params: {key: 'weapon_wizard_1'}}, spy); - let weapon = content.gear.flat.weapon_wizard_1; - let item = content.gear.flat.shield_warrior_1; - let message = i18n.t('messageTwoHandedEquip', {twoHandedText: weapon.text(null), offHandedText: item.text(null)}); - - assert.calledOnce(spy); - assert.calledWith(spy, {code: 200, message}); - }); - - it('should send messages if equipping an off-hand item causes a two-handed weapon to be unequipped', () => { - // equipping two-hander - user.ops.equip({params: {key: 'weapon_wizard_1'}}); - let weapon = content.gear.flat.weapon_wizard_1; - let shield = content.gear.flat.shield_warrior_1; - - user.ops.equip({params: {key: 'shield_warrior_1'}}, spy); - - let message = i18n.t('messageTwoHandedUnequip', {twoHandedText: weapon.text(null), offHandedText: shield.text(null)}); - - assert.calledOnce(spy); - assert.calledWith(spy, {code: 200, message}); - }); - }); -}); diff --git a/test/common_old/user.ops.hatch.js b/test/common_old/user.ops.hatch.js deleted file mode 100644 index 573103a360..0000000000 --- a/test/common_old/user.ops.hatch.js +++ /dev/null @@ -1,129 +0,0 @@ -let shared = require('../../common/script/index.js'); - -describe('user.ops.hatch', () => { - let user; - - beforeEach(() => { - user = { - items: { - eggs: {}, - hatchingPotions: {}, - pets: {}, - }, - }; - - shared.wrap(user); - }); - - context('Pet Hatching', () => { - context('failure conditions', () => { - it('does not allow hatching without specifying egg and potion', (done) => { - user.ops.hatch({params: {}}, (response) => { - expect(response.message).to.eql('Please specify query.egg & query.hatchingPotion'); - expect(user.items.pets).to.be.empty; - done(); - }); - }); - - it('does not allow hatching if user lacks specified egg', (done) => { - user.items.eggs = {Wolf: 1}; - user.items.hatchingPotions = {Base: 1}; - user.ops.hatch({params: {egg: 'Dragon', hatchingPotion: 'Base'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageMissingEggPotion')); - expect(user.items.pets).to.be.empty; - expect(user.items.eggs).to.eql({Wolf: 1}); - expect(user.items.hatchingPotions).to.eql({Base: 1}); - done(); - }); - }); - - it('does not allow hatching if user lacks specified hatching potion', (done) => { - user.items.eggs = {Wolf: 1}; - user.items.hatchingPotions = {Base: 1}; - user.ops.hatch({params: {egg: 'Wolf', hatchingPotion: 'Golden'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageMissingEggPotion')); - expect(user.items.pets).to.be.empty; - expect(user.items.eggs).to.eql({Wolf: 1}); - expect(user.items.hatchingPotions).to.eql({Base: 1}); - done(); - }); - }); - - it('does not allow hatching if user already owns target pet', (done) => { - user.items.eggs = {Wolf: 1}; - user.items.hatchingPotions = {Base: 1}; - user.items.pets = {'Wolf-Base': 10}; - user.ops.hatch({params: {egg: 'Wolf', hatchingPotion: 'Base'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageAlreadyPet')); - expect(user.items.pets).to.eql({'Wolf-Base': 10}); - expect(user.items.eggs).to.eql({Wolf: 1}); - expect(user.items.hatchingPotions).to.eql({Base: 1}); - done(); - }); - }); - - it('does not allow hatching quest pet egg using premium potion', (done) => { - user.items.eggs = {Cheetah: 1}; - user.items.hatchingPotions = {Spooky: 1}; - user.ops.hatch({params: {egg: 'Cheetah', hatchingPotion: 'Spooky'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageInvalidEggPotionCombo')); - expect(user.items.pets).to.be.empty; - expect(user.items.eggs).to.eql({Cheetah: 1}); - expect(user.items.hatchingPotions).to.eql({Spooky: 1}); - done(); - }); - }); - }); - - context('successful hatching', () => { - it('hatches a basic pet', (done) => { - user.items.eggs = {Wolf: 1}; - user.items.hatchingPotions = {Base: 1}; - user.ops.hatch({params: {egg: 'Wolf', hatchingPotion: 'Base'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageHatched')); - expect(user.items.pets).to.eql({'Wolf-Base': 5}); - expect(user.items.eggs).to.eql({Wolf: 0}); - expect(user.items.hatchingPotions).to.eql({Base: 0}); - done(); - }); - }); - - it('hatches a quest pet', (done) => { - user.items.eggs = {Cheetah: 1}; - user.items.hatchingPotions = {Base: 1}; - user.ops.hatch({params: {egg: 'Cheetah', hatchingPotion: 'Base'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageHatched')); - expect(user.items.pets).to.eql({'Cheetah-Base': 5}); - expect(user.items.eggs).to.eql({Cheetah: 0}); - expect(user.items.hatchingPotions).to.eql({Base: 0}); - done(); - }); - }); - - it('hatches a premium pet', (done) => { - user.items.eggs = {Wolf: 1}; - user.items.hatchingPotions = {Spooky: 1}; - user.ops.hatch({params: {egg: 'Wolf', hatchingPotion: 'Spooky'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageHatched')); - expect(user.items.pets).to.eql({'Wolf-Spooky': 5}); - expect(user.items.eggs).to.eql({Wolf: 0}); - expect(user.items.hatchingPotions).to.eql({Spooky: 0}); - done(); - }); - }); - - it('hatches a pet previously raised to a mount', (done) => { - user.items.eggs = {Wolf: 1}; - user.items.hatchingPotions = {Base: 1}; - user.items.pets = {'Wolf-Base': -1}; - user.ops.hatch({params: {egg: 'Wolf', hatchingPotion: 'Base'}}, (response) => { - expect(response.message).to.eql(shared.i18n.t('messageHatched')); - expect(user.items.pets).to.eql({'Wolf-Base': 5}); - expect(user.items.eggs).to.eql({Wolf: 0}); - expect(user.items.hatchingPotions).to.eql({Base: 0}); - done(); - }); - }); - }); - }); -}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 11bd137fa5..d00abcb8fb 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -309,4 +309,76 @@ api.buySpecialSpell = { }, }; +/** + * @api {post} /user/hatch/:egg/:hatchingPotion Hatch a pet. + * @apiVersion 3.0.0 + * @apiName UserHatch + * @apiGroup User + * + * @apiParam {string} egg The egg to use. + * @apiParam {string} hatchingPotion The hatching potion to use. + * + * @apiSuccess {Object} data `user.items` + * @apiSuccess {string} message + */ +api.hatch = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/hatch/:egg/:hatchingPotion', + async handler (req, res) { + let user = res.locals.user; + let hatchRes = common.ops.hatch(user, req); + await user.save(); + res.respond(200, hatchRes); + }, +}; + +/** + * @api {post} /user/equip/:type/:key Equip an item + * @apiVersion 3.0.0 + * @apiName UserEquip + * @apiGroup User + * + * @apiParam {string} type + * @apiParam {string} key + * + * @apiSuccess {Object} data `user.items` + * @apiSuccess {string} message Optional + */ +api.equip = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/equip/:type/:key', + async handler (req, res) { + let user = res.locals.user; + let equipRes = common.ops.equip(user, req); + await user.save(); + res.respond(200, equipRes); + }, +}; + +/** + * @api {post} /user/equip/:pet/:food Feed a pet + * @apiVersion 3.0.0 + * @apiName UserFeed + * @apiGroup User + * + * @apiParam {string} pet + * @apiParam {string} food + * + * @apiSuccess {Object} data The fed pet + * @apiSuccess {string} message + */ +api.feed = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/feed/:pet/:food', + async handler (req, res) { + let user = res.locals.user; + let feedRes = common.ops.feed(user, req); + await user.save(); + res.respond(200, feedRes); + }, +}; + module.exports = api;