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 index c56be573c3..8b384abdf9 100644 --- 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 @@ -41,6 +41,29 @@ describe('POST /user/feed/:pet/:food', () => { expect(user.items.pets['Wolf-Base']).to.equal(7); }); + it('bulk feeding pet with non-preferred food', async () => { + await user.update({ + 'items.pets.Wolf-Base': 5, + 'items.food.Milk': 3, + }); + + const food = content.food.Milk; + const pet = content.petInfo['Wolf-Base']; + + const res = await user.post('/user/feed/Wolf-Base/Milk?amount=2'); + await user.sync(); + expect(res).to.eql({ + data: user.items.pets['Wolf-Base'], + message: t('messageDontEnjoyFood', { + egg: pet.text(), + foodText: food.textThe(), + }), + }); + + expect(user.items.food.Milk).to.eql(1); + expect(user.items.pets['Wolf-Base']).to.equal(9); + }); + context('sending user activity webhooks', () => { before(async () => { await server.start(); @@ -77,5 +100,33 @@ describe('POST /user/feed/:pet/:food', () => { expect(body.pet).to.eql('Wolf-Base'); expect(body.message).to.eql(res.message); }); + + it('sends user activity webhook (mount raised after full bulk feeding)', async () => { + const uuid = generateUUID(); + + await user.post('/user/webhook', { + url: `http://localhost:${server.port}/webhooks/${uuid}`, + type: 'userActivity', + enabled: true, + options: { + mountRaised: true, + }, + }); + + await user.update({ + 'items.pets.Wolf-Base': 47, + 'items.food.Milk': 3, + }); + const res = await user.post('/user/feed/Wolf-Base/Milk?amount=2'); + + await sleep(); + + const body = server.getWebhookData(uuid); + + expect(user.achievements.allYourBase).to.not.equal(true); + expect(body.type).to.eql('mountRaised'); + expect(body.pet).to.eql('Wolf-Base'); + expect(body.message).to.eql(res.message); + }); }); }); diff --git a/test/common/ops/feed.js b/test/common/ops/feed.js index c19e22bde6..98fbb01aba 100644 --- a/test/common/ops/feed.js +++ b/test/common/ops/feed.js @@ -113,6 +113,30 @@ describe('shared.ops.feed', () => { done(); } }); + + it('does not allow bulk-feeding query amount above food owned', done => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Meat = 6; + try { + feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' }, query: { amount: 8 } }); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('notEnoughFood')); + done(); + } + }); + + it('does not allow bulk-over-feeding pet', done => { + user.items.pets['Wolf-Base'] = 45; + user.items.food.Meat = 3; + try { + feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' }, query: { amount: 2 } }); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('tooMuchFood')); + done(); + } + }); }); context('successful feeding', () => { @@ -188,6 +212,61 @@ describe('shared.ops.feed', () => { expect(user.items.pets['Wolf-Base']).to.equal(7); }); + it('evolves the pet into a mount when feeding user.items.pets[pet] >= 50 preferred food (bulk)', () => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Meat = 10; + user.items.currentPet = 'Wolf-Base'; + + const pet = content.petInfo['Wolf-Base']; + + const [data, message] = feed(user, { params: { pet: 'Wolf-Base', food: 'Meat' }, query: { amount: 9 } }); + expect(data).to.eql(user.items.pets['Wolf-Base']); + expect(message).to.eql(i18n.t('messageEvolve', { + egg: pet.text(), + })); + + expect(user.items.food.Meat).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('evolves the pet into a mount when feeding user.items.pets[pet] >= 50 wrong food (bulk)', () => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Milk = 25; + user.items.currentPet = 'Wolf-Base'; + + const pet = content.petInfo['Wolf-Base']; + + const [data, message] = feed(user, { params: { pet: 'Wolf-Base', food: 'Milk' }, query: { amount: 23 } }); + expect(data).to.eql(user.items.pets['Wolf-Base']); + expect(message).to.eql(i18n.t('messageEvolve', { + egg: pet.text(), + })); + expect(user.items.food.Milk).to.equal(2); + 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('does not like the food (bulk low food) ', () => { + user.items.pets['Wolf-Base'] = 5; + user.items.food.Milk = 5; + + const food = content.food.Milk; + const pet = content.petInfo['Wolf-Base']; + + const [data, message] = feed(user, { params: { pet: 'Wolf-Base', food: 'Milk' }, query: { amount: 5 } }); + expect(data).to.eql(user.items.pets['Wolf-Base']); + expect(message).to.eql(i18n.t('messageDontEnjoyFood', { + egg: pet.text(), + foodText: food.textThe(), + })); + + expect(user.items.food.Milk).to.equal(0); + expect(user.items.pets['Wolf-Base']).to.equal(15); + }); + it('awards All Your Base achievement', () => { user.items.pets['Wolf-Spooky'] = 5; user.items.food.Milk = 2; diff --git a/website/common/locales/en/pets.json b/website/common/locales/en/pets.json index 228c399d59..cbf5be2a0e 100644 --- a/website/common/locales/en/pets.json +++ b/website/common/locales/en/pets.json @@ -141,5 +141,8 @@ "clickOnPotionToHatch": "Click on a hatching potion to use it on your <%= eggName %> and hatch a new pet!", "notEnoughPets": "You have not collected enough pets", "notEnoughMounts": "You have not collected enough mounts", - "notEnoughPetsMounts": "You have not collected enough pets and mounts" + "notEnoughPetsMounts": "You have not collected enough pets and mounts", + "notEnoughFood": "You don't have enough food", + "tooMuchFood": "You're trying to feed too much food to your pet, action cancelled", + "invalidAmount": "Invalid amount of food, must be a positive integer" } diff --git a/website/common/script/ops/feed.js b/website/common/script/ops/feed.js index 9d83962d2a..79bd7489cb 100644 --- a/website/common/script/ops/feed.js +++ b/website/common/script/ops/feed.js @@ -38,6 +38,8 @@ function evolve (user, pet, req) { export default function feed (user, req = {}, analytics) { let pet = get(req, 'params.pet'); const foodK = get(req, 'params.food'); + let amount = Number(get(req.query, 'amount', 1)); + let foodFactor; if (!pet || !foodK) throw new BadRequest(errorMessage('missingPetFoodFeed')); @@ -68,9 +70,28 @@ export default function feed (user, req = {}, analytics) { throw new NotAuthorized(i18n.t('messageAlreadyMount', req.language)); } + if (!Number.isInteger(amount) || amount < 0) { + throw new BadRequest(i18n.t('invalidAmount', req.language)); + } + + if (amount > user.items.food[food.key]) { + throw new NotAuthorized(i18n.t('notEnoughFood', req.language)); + } + + if (food.target === pet.potion || pet.type === 'premium') { + foodFactor = 5; + } else { + foodFactor = 2; + } + + if ((user.items.pets[pet.key] + (amount * foodFactor)) >= (50 + foodFactor)) { + throw new NotAuthorized(i18n.t('tooMuchFood', req.language)); + } + let message; if (food.key === 'Saddle') { + amount = 1; message = evolve(user, pet, req); } else { const messageParams = { @@ -79,10 +100,10 @@ export default function feed (user, req = {}, analytics) { }; if (food.target === pet.potion || pet.type === 'premium') { - user.items.pets[pet.key] += 5; + user.items.pets[pet.key] += foodFactor * amount; message = i18n.t('messageLikesFood', messageParams, req.language); } else { - user.items.pets[pet.key] += 2; + user.items.pets[pet.key] += foodFactor * amount; message = i18n.t('messageDontEnjoyFood', messageParams, req.language); } @@ -98,7 +119,7 @@ export default function feed (user, req = {}, analytics) { } } - user.items.food[food.key] -= 1; + user.items.food[food.key] -= 1 * amount; if (user.markModified) user.markModified('items.food'); forEach(content.animalColorAchievements, achievement => { diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index 9da35f4365..debb823294 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -862,9 +862,14 @@ api.equip = { * * @apiParam (Path) {String} pet * @apiParam (Path) {String} food + * @apiParam (Query) {Number} [amount] The amount of food to feed. + * Note: Pet can eat 50 units. + * Preferred food offers 5 units per food, + * other food 2 units. * * @apiParamExample {url} Example-URL * https://habitica.com/api/v3/user/feed/Armadillo-Shade/Chocolate + * https://habitica.com/api/v3/user/feed/Armadillo-Shade/Chocolate?amount=9 * * @apiSuccess {Number} data The pet value * @apiSuccess {String} message Success message @@ -877,6 +882,8 @@ api.equip = { * @apiError {BadRequest} InvalidPet Invalid pet name supplied. * @apiError {NotFound} FoodNotOwned :food not found in user.items.food * Note: also sent if food name is invalid. + * @apiError {NotAuthorized} notEnoughFood :Not enough food to feed the pet as requested. + * @apiError {NotAuthorized} tooMuchFood :You try to feed too much food. Action ancelled. * * */