diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index fbf7f140ca..cad4c68a88 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -158,5 +158,7 @@ "pathRequired": "Path string is required", "unlocked": "Items have been unlocked", "alreadyUnlocked": "Item already unlocked", - "cannotRevive": "Cannot revive if not dead" + "cannotRevive": "Cannot revive if not dead", + "rebirthComplete": "You have been reborn!", + "petNotOwned": "You do not own this pet." } diff --git a/common/script/index.js b/common/script/index.js index 23ef5f95f5..c043af6a0f 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -124,6 +124,7 @@ import releaseMounts from './ops/releaseMounts'; import sell from './ops/sell'; import unlock from './ops/unlock'; import revive from './ops/revive'; +import rebirth from './ops/rebirth'; api.ops = { scoreTask, @@ -149,6 +150,7 @@ api.ops = { sell, unlock, revive, + rebirth, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/rebirth.js b/common/script/ops/rebirth.js index 1ddb75aba1..3739f2c80b 100644 --- a/common/script/ops/rebirth.js +++ b/common/script/ops/rebirth.js @@ -1,21 +1,30 @@ -import content from '../content/index'; import i18n from '../i18n'; import _ from 'lodash'; import { capByLevel } from '../statHelpers'; import { MAX_LEVEL } from '../constants'; +import { + NotAuthorized, +} from '../libs/errors'; +import resetGear from './resetGear'; +import equip from './equip'; + +const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp']; + +module.exports = function rebirth (user, tasks = [], req = {}, analytics) { + let analyticsData; + let flags; + let lvl; + let stats; -module.exports = function(user, req, cb, analytics) { - var analyticsData, flags, gear, lvl, stats; if (user.balance < 2 && user.stats.lvl < MAX_LEVEL) { - return typeof cb === "function" ? cb({ - code: 401, - message: i18n.t('notEnoughGems', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t('notEnoughGems', req.language)); } + analyticsData = { uuid: user._id, - category: 'behavior' + category: 'behavior', }; + if (user.stats.lvl < MAX_LEVEL) { user.balance -= 2; analyticsData.acquireMethod = 'Gems'; @@ -24,62 +33,52 @@ module.exports = function(user, req, cb, analytics) { analyticsData.gemCost = 0; analyticsData.acquireMethod = '> 100'; } - if (analytics != null) { + + if (analytics) { analytics.track('Rebirth', analyticsData); } + lvl = capByLevel(user.stats.lvl); - _.each(user.tasks, function(task) { + + _.each(tasks, function resetTasks (task) { if (task.type !== 'reward') { task.value = 0; } if (task.type === 'daily') { - return task.streak = 0; + task.streak = 0; } }); + stats = user.stats; stats.buffs = {}; stats.hp = 50; stats.lvl = 1; - stats["class"] = 'warrior'; - _.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) { - return stats[value] = 0; - }); - // TODO during refactoring: move all gear code from rebirth() to its own function and then call it in reset() as well - gear = user.items.gear; - _.each(['equipped', 'costume'], function(type) { - gear[type] = {}; - gear[type].armor = 'armor_base_0'; - gear[type].weapon = 'weapon_warrior_0'; - gear[type].head = 'head_base_0'; - return gear[type].shield = 'shield_base_0'; + stats.class = 'warrior'; + + _.each(USERSTATSLIST, function resetStats (value) { + stats[value] = 0; }); + + resetGear(user); + if (user.items.currentPet) { - user.ops.equip({ + equip(user, { params: { type: 'pet', - key: user.items.currentPet - } + key: user.items.currentPet, + }, }); } + if (user.items.currentMount) { - user.ops.equip({ + equip(user, { params: { type: 'mount', - key: user.items.currentMount - } + key: user.items.currentMount, + }, }); } - _.each(gear.owned, function(v, k) { - if (gear.owned[k] && content.gear.flat[k].value) { - gear.owned[k] = false; - return true; - } - }); - gear.owned.weapon_warrior_0 = true; - if (typeof user.markModified === "function") { - user.markModified('items.gear.owned'); - } - user.preferences.costume = false; + flags = user.flags; if (!user.achievements.beastMaster) { flags.rebirthEnabled = false; @@ -88,13 +87,21 @@ module.exports = function(user, req, cb, analytics) { flags.dropsEnabled = false; flags.classSelected = false; flags.levelDrops = {}; + if (!user.achievements.rebirths) { user.achievements.rebirths = 1; user.achievements.rebirthLevel = lvl; - } else if (lvl > user.achievements.rebirthLevel || lvl === 100) { + } else if (lvl > user.achievements.rebirthLevel || lvl === MAX_LEVEL) { user.achievements.rebirths++; user.achievements.rebirthLevel = lvl; } + user.stats.buffs = {}; - return typeof cb === "function" ? cb(null, user) : void 0; + + let response = { + data: user, + message: i18n.t('rebirthComplete'), + }; + + return response; }; diff --git a/common/script/ops/resetGear.js b/common/script/ops/resetGear.js new file mode 100644 index 0000000000..2625f5c9b8 --- /dev/null +++ b/common/script/ops/resetGear.js @@ -0,0 +1,25 @@ +import _ from 'lodash'; +import content from '../content/index'; + +module.exports = function resetGear (user) { + let gear = user.items.gear; + + _.each(['equipped', 'costume'], function resetUserGear (type) { + gear[type] = {}; + gear[type].armor = 'armor_base_0'; + gear[type].weapon = 'weapon_warrior_0'; + gear[type].head = 'head_base_0'; + gear[type].shield = 'shield_base_0'; + }); + + // Gear.owned is a Mongo object so the _.each function iterates over hidden properties. + // The content.gear.flat[k] check should prevent this causing an error + _.each(gear.owned, function resetOwnedGear (v, k) { + if (gear.owned[k] && content.gear.flat[k] && content.gear.flat[k].value) { + gear.owned[k] = false; + } + }); + + gear.owned.weapon_warrior_0 = true; // eslint-disable-line camelcase + user.preferences.costume = false; +}; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 2ea777a0d5..ed79dfc879 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -23,7 +23,6 @@ const COMMON_FILES = [ '!./common/script/ops/deleteWebhook.js', '!./common/script/ops/getTag.js', '!./common/script/ops/getTags.js', - '!./common/script/ops/rebirth.js', '!./common/script/ops/releaseBoth.js', '!./common/script/ops/releaseMounts.js', '!./common/script/ops/releasePets.js', diff --git a/test/api/v3/integration/user/POST-user_rebirth.test.js b/test/api/v3/integration/user/POST-user_rebirth.test.js new file mode 100644 index 0000000000..3591844c3c --- /dev/null +++ b/test/api/v3/integration/user/POST-user_rebirth.test.js @@ -0,0 +1,56 @@ +import { + generateUser, + generateDaily, + generateReward, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/rebirth', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('returns an error when user balance is too low', async () => { + await expect(user.post('/user/rebirth')) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notEnoughGems'), + }); + }); + + // More tests in common code unit tests + + it('resets user\'s tasks', async () => { + await user.update({ + balance: 2, + }); + + let daily = await generateDaily({ + text: 'test habit', + type: 'daily', + streak: 1, + userId: user._id, + }); + + let reward = await generateReward({ + text: 'test reward', + type: 'reward', + value: 1, + userId: user._id, + }); + + let response = await user.post('/user/rebirth'); + await user.sync(); + + let updatedDaily = await user.get(`/tasks/${daily._id}`); + let updatedReward = await user.get(`/tasks/${reward._id}`); + + expect(response.message).to.equal(t('rebirthComplete')); + expect(updatedDaily.streak).to.equal(0); + expect(updatedDaily.value).to.equal(0); + expect(updatedReward.value).to.equal(1); + }); +}); diff --git a/test/common/ops/rebirth.js b/test/common/ops/rebirth.js new file mode 100644 index 0000000000..61d1334569 --- /dev/null +++ b/test/common/ops/rebirth.js @@ -0,0 +1,188 @@ +import rebirth from '../../../common/script/ops/rebirth'; +import i18n from '../../../common/script/i18n'; +import { MAX_LEVEL } from '../../../common/script/constants'; +import { + generateUser, + generateDaily, + generateReward, +} from '../../helpers/common.helper'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; + +describe('shared.ops.rebirth', () => { + let user; + let animal = 'Wolf-Base'; + let userStats = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp']; + let tasks = []; + + beforeEach(() => { + user = generateUser(); + user.balance = 2; + tasks = [generateDaily(), generateReward()]; + }); + + it('returns an error when user balance is too low and user is less than max level', (done) => { + user.balance = 0; + + try { + rebirth(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('notEnoughGems')); + done(); + } + }); + + it('rebirths a user with enough gems', () => { + let response = rebirth(user); + + expect(response.message).to.equal(i18n.t('rebirthComplete')); + }); + + it('rebirths a user with not enough gems but max level', () => { + user.balance = 0; + user.stats.lvl = MAX_LEVEL; + + let response = rebirth(user); + + expect(response.message).to.equal(i18n.t('rebirthComplete')); + }); + + it('resets user\'s taks values except for rewards to 0', () => { + tasks[0].value = 1; + tasks[1].value = 1; + + rebirth(user, tasks); + + expect(tasks[0].value).to.equal(0); + expect(tasks[1].value).to.equal(1); + }); + + it('resets user\'s daily streaks to 0', () => { + tasks[0].streak = 1; + + rebirth(user, tasks); + + expect(tasks[0].streak).to.equal(0); + }); + + it('resets a user\'s buffs', () => { + user.stats.buffs = {test: 'test'}; + + rebirth(user); + + expect(user.stats.buffs).to.be.empty; + }); + + it('resets a user\'s health points', () => { + user.stats.hp = 40; + + rebirth(user); + + expect(user.stats.hp).to.equal(50); + }); + + it('resets a user\'s class', () => { + user.stats.class = 'rouge'; + + rebirth(user); + + expect(user.stats.class).to.equal('warrior'); + }); + + it('resets a user\'s stats', () => { + user.stats.class = 'rouge'; + _.each(userStats, function setUsersStats (value) { + user.stats[value] = 10; + }); + + rebirth(user); + + _.each(userStats, function resetUserStats (value) { + user.stats[value] = 0; + }); + }); + + it('resets a user\'s gear', () => { + let gearReset = { + armor: 'armor_base_0', + weapon: 'weapon_warrior_0', + head: 'head_base_0', + shield: 'shield_base_0', + }; + + rebirth(user); + + expect(user.items.gear.equipped).to.deep.equal(gearReset); + expect(user.items.gear.costume).to.deep.equal(gearReset); + expect(user.preferences.costume).to.be.false; + }); + + it('resets a user\'s gear owned', () => { + user.items.gear.owned.weapon_warrior_1 = true; // eslint-disable-line camelcase + rebirth(user); + + expect(user.items.gear.owned.weapon_warrior_1).to.be.false; + expect(user.items.gear.owned.weapon_warrior_0).to.be.true; + }); + + it('resets a user\'s current pet', () => { + user.items.pets[animal] = true; + user.items.currentPet = animal; + rebirth(user); + + expect(user.items.currentPet).to.be.empty; + }); + + it('resets a user\'s current mount', () => { + user.items.mounts[animal] = true; + user.items.currentMount = animal; + rebirth(user); + + expect(user.items.currentMount).to.be.empty; + }); + + it('resets a user\'s flags', () => { + user.flags.itemsEnabled = true; + user.flags.dropsEnabled = true; + user.flags.classSelected = true; + user.flags.rebirthEnabled = true; + user.flags.levelDrops = {test: 'test'}; + + rebirth(user); + + expect(user.flags.itemsEnabled).to.be.false; + expect(user.flags.dropsEnabled).to.be.false; + expect(user.flags.classSelected).to.be.false; + expect(user.flags.rebirthEnabled).to.be.false; + expect(user.flags.levelDrops).to.be.emtpy; + }); + + it('does not reset rebirthEnabled if user has beastMaster', () => { + user.achievements.beastMaster = 1; + user.flags.rebirthEnabled = true; + + rebirth(user); + + expect(user.flags.rebirthEnabled).to.be.true; + }); + + it('sets rebirth achievement', () => { + rebirth(user); + + expect(user.achievements.rebirths).to.equal(1); + expect(user.achievements.rebirthLevel).to.equal(user.stats.lvl); + }); + + it('increments rebirth achievemnts', () => { + user.stats.lvl = 2; + user.achievements.rebirths = 1; + user.achievements.rebirthLevel = 1; + + rebirth(user); + + expect(user.achievements.rebirths).to.equal(2); + expect(user.achievements.rebirthLevel).to.equal(2); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 3cc462d444..827e73c34e 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -888,4 +888,33 @@ api.userRevive = { }, }; +/* +* @api {post} /user/rebirth Resets a user. +* @apiVersion 3.0.0 +* @apiName UserRebirth +* @apiGroup User +* +* @apiSuccess {Object} data `user` +*/ +api.userRebirth = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/rebirth', + async handler (req, res) { + let user = res.locals.user; + let query = { + userId: user._id, + type: {$in: ['daily', 'habit', 'todo']}, + }; + let tasks = await Tasks.Task.find(query).exec(); + let rebirthResponse = common.ops.rebirth(user, tasks, req, res.analytics); + + await user.save(); + + await Q.all(tasks.map(task => task.save())); + + res.respond(200, rebirthResponse); + }, +}; + module.exports = api;