diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index a4fd69f0a3..cc8dbfc655 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -154,5 +154,6 @@ "mountsReleased": "Mounts released", "typeNotSellable": "Type is not sellable. Must be one of the following <%= acceptedTypes %>", "userItemsKeyNotFound": "Key not found for user.items <%= type %>", - "sold": "You sold a <%= key %> <%= type %>" + "sold": "You sold a <%= key %> <%= type %>", + "cannotRevive": "Cannot revive if not dead" } diff --git a/common/script/index.js b/common/script/index.js index c55ac24c9f..9e1b8a1496 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -122,6 +122,7 @@ import releasePets from './ops/releasePets'; import releaseBoth from './ops/releaseBoth'; import releaseMounts from './ops/releaseMounts'; import sell from './ops/sell'; +import revive from './ops/revive'; api.ops = { scoreTask, @@ -145,6 +146,7 @@ api.ops = { releaseBoth, releaseMounts, sell, + revive, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/revive.js b/common/script/ops/revive.js index 7b6fe8445f..ce4bbc2fd6 100644 --- a/common/script/ops/revive.js +++ b/common/script/ops/revive.js @@ -1,72 +1,108 @@ import content from '../content/index'; import i18n from '../i18n'; import _ from 'lodash'; +import { + NotAuthorized, +} from '../libs/errors'; +import splitWhitespace from '../libs/splitWhitespace'; +import randomVal from '../fns/randomVal'; -module.exports = function(user, req, cb, analytics) { - var analyticsData, base, cl, gearOwned, item, losableItems, lostItem, lostStat; - if (!(user.stats.hp <= 0)) { - return typeof cb === "function" ? cb({ - code: 400, - message: "Cannot revive if not dead" - }) : void 0; +module.exports = function revive (user, req = {}, analytics) { + if (user.stats.hp > 0) { + throw new NotAuthorized(i18n.t('cannotRevive', req.language)); } + _.merge(user.stats, { hp: 50, exp: 0, - gp: 0 + gp: 0, }); + if (user.stats.lvl > 1) { user.stats.lvl--; } - lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) { + + let lostStat = randomVal(user, _.reduce(['str', 'con', 'per', 'int'], function findRandomStat (m, k) { if (user.stats[k]) { m[k] = k; } return m; - }), {})); + }, {})); + if (lostStat) { user.stats[lostStat]--; } - cl = user.stats["class"]; - gearOwned = (typeof (base = user.items.gear.owned).toObject === "function" ? base.toObject() : void 0) || user.items.gear.owned; - losableItems = {}; - _.each(gearOwned, function(v, k) { - var itm; - if (v) { - itm = content.gear.flat['' + k]; + + let base = user.items.gear.owned; + let gearOwned; + + if (typeof base.toObject === 'function') { + gearOwned = base.toObject(); + } else { + gearOwned = user.items.gear.owned; + } + + let losableItems = {}; + let userClass = user.stats.class; + + _.each(gearOwned, function findLosableItems (value, key) { + let itm; + if (value) { + itm = content.gear.flat[key]; + if (itm) { - if ((itm.value > 0 || k === 'weapon_warrior_0') && (itm.klass === cl || (itm.klass === 'special' && (!itm.specialClass || itm.specialClass === cl)) || itm.klass === 'armoire')) { - return losableItems['' + k] = '' + k; + let itemHasValueOrWarrior0 = itm.value > 0 || key === 'weapon_warrior_0'; + + let itemClassEqualsUserClass = itm.klass === userClass; + + let itemClassSpecial = itm.klass === 'special'; + let itemNotSpecialOrUserClassIsSpecial = !itm.specialClass || itm.specialClass === userClass; + let itemIsSpecial = itemNotSpecialOrUserClassIsSpecial && itemClassSpecial; + + let itemIsArmoire = itm.klass === 'armoire'; + + if (itemHasValueOrWarrior0 && (itemClassEqualsUserClass || itemIsSpecial || itemIsArmoire)) { + losableItems[key] = key; + return losableItems[key]; } } } }); - lostItem = user.fns.randomVal(losableItems); - if (item = content.gear.flat[lostItem]) { + + let lostItem = randomVal(user, losableItems); + + let message = ''; + let item = content.gear.flat[lostItem]; + + if (item) { user.items.gear.owned[lostItem] = false; + if (user.items.gear.equipped[item.type] === lostItem) { - user.items.gear.equipped[item.type] = item.type + "_base_0"; + user.items.gear.equipped[item.type] = `${item.type}_base_0`; } + if (user.items.gear.costume[item.type] === lostItem) { - user.items.gear.costume[item.type] = item.type + "_base_0"; + user.items.gear.costume[item.type] = `${item.type}_base_0`; } + + message = i18n.t('messageLostItem', { itemText: item.text(req.language)}, req.language); } - if (typeof user.markModified === "function") { - user.markModified('items.gear'); + + user.markModified('items.gear'); + + if (analytics) { + analytics.track('Death', { + uuid: user._id, + lostItem, + gaLabel: lostItem, + category: 'behavior', + }); } - analyticsData = { - uuid: user._id, - lostItem: lostItem, - gaLabel: lostItem, - category: 'behavior' + + let response = { + data: _.pick(user, splitWhitespace('user.items')), + message, }; - if (analytics != null) { - analytics.track('Death', analyticsData); - } - return typeof cb === "function" ? cb((item ? { - code: 200, - message: i18n.t('messageLostItem', { - itemText: item.text(req.language) - }, req.language) - } : null), user) : void 0; + + return response; }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 6ed2b567a9..191dce6a5d 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -29,7 +29,6 @@ const COMMON_FILES = [ '!./common/script/ops/releasePets.js', '!./common/script/ops/reroll.js', '!./common/script/ops/reset.js', - '!./common/script/ops/revive.js', '!./common/script/ops/sortTag.js', '!./common/script/ops/sortTask.js', '!./common/script/ops/unlock.js', diff --git a/test/api/v3/integration/user/POST-user_revive.test.js b/test/api/v3/integration/user/POST-user_revive.test.js new file mode 100644 index 0000000000..6ba85ac87f --- /dev/null +++ b/test/api/v3/integration/user/POST-user_revive.test.js @@ -0,0 +1,37 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/revive', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'user.items.gear.owned': {weaponKey: true}, + }); + }); + + it('returns an error when user is not dead', async () => { + await expect(user.post('/user/revive')) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('cannotRevive'), + }); + }); + + // More tests in common code unit tests + + it('decreases a stat', async () => { + await user.update({ + 'stats.str': 2, + 'stats.hp': 0, + }); + + await user.post('/user/revive'); + await user.sync(); + + expect(user.stats.str).to.equal(1); + }); +}); diff --git a/test/common/ops/revive.js b/test/common/ops/revive.js new file mode 100644 index 0000000000..42e1915b07 --- /dev/null +++ b/test/common/ops/revive.js @@ -0,0 +1,91 @@ +import revive from '../../../common/script/ops/revive'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; +import content from '../../../common/script/content/index'; + +describe('shared.ops.revive', () => { + let user; + + beforeEach(() => { + user = generateUser(); + user.stats.hp = 0; + }); + + it('returns an error when user is not dead', (done) => { + user.stats.hp = 10; + + try { + revive(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('cannotRevive')); + done(); + } + }); + + it('resets user\'s hp, exp and gp', () => { + user.stats.exp = 100; + user.stats.gp = 100; + + revive(user); + + expect(user.stats.hp).to.equal(50); + expect(user.stats.exp).to.equal(0); + expect(user.stats.gp).to.equal(0); + }); + + it('decreases user\'s level', () => { + user.stats.lvl = 2; + revive(user); + + expect(user.stats.lvl).to.equal(1); + }); + + it('decreases a stat', () => { + user.stats.str = 2; + revive(user); + + expect(user.stats.str).to.equal(1); + }); + + it('removes a random item from user gear owned', () => { + let weaponKey = 'weapon_warrior_0'; + user.items.gear.owned[weaponKey] = true; + + let reviveRequest = revive(user); + + expect(reviveRequest.message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()})); + expect(user.items.gear.owned[weaponKey]).to.be.false; + }); + + it('removes a random item from user gear equipped', () => { + let weaponKey = 'weapon_warrior_0'; + let itemToLose = content.gear.flat[weaponKey]; + + user.items.gear.owned[weaponKey] = true; + user.items.gear.equipped[itemToLose.type] = itemToLose.key; + + let reviveRequest = revive(user); + + expect(reviveRequest.message).to.equal(i18n.t('messageLostItem', { itemText: itemToLose.text()})); + expect(user.items.gear.equipped[itemToLose.type]).to.equal(`${itemToLose.type}_base_0`); + }); + + it('removes a random item from user gear costume', () => { + let weaponKey = 'weapon_warrior_0'; + let itemToLose = content.gear.flat[weaponKey]; + + user.items.gear.owned[weaponKey] = true; + user.items.gear.costume[itemToLose.type] = itemToLose.key; + + let reviveRequest = revive(user); + + expect(reviveRequest.message).to.equal(i18n.t('messageLostItem', { itemText: itemToLose.text()})); + expect(user.items.gear.costume[itemToLose.type]).to.equal(`${itemToLose.type}_base_0`); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index c60b2a3eef..16a281d727 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -848,4 +848,24 @@ api.userSell = { }, }; +/** +* @api {post} /user/revive Revives user from death. +* @apiVersion 3.0.0 +* @apiName UserRevive +* @apiGroup User +* +* @apiSuccess {Object} data `user.items` +*/ +api.userRevive = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/revive', + async handler (req, res) { + let user = res.locals.user; + let reviveResponse = common.ops.revive(user, req, res.analytics); + await user.save(); + res.respond(200, reviveResponse); + }, +}; + module.exports = api;