diff --git a/common/script/content/spells.js b/common/script/content/spells.js index e7a198ec97..9381ba89d5 100644 --- a/common/script/content/spells.js +++ b/common/script/content/spells.js @@ -194,7 +194,7 @@ spells.rogue = { notes: t('spellRogueStealthNotes'), cast (user) { if (!user.stats.buffs.stealth) user.stats.buffs.stealth = 0; - user.stats.buffs.stealth += Math.ceil(diminishingReturns(user._statsComputed.per, user.dailys.length * 0.64, 55)); + user.stats.buffs.stealth += Math.ceil(diminishingReturns(user._statsComputed.per, user.tasksOrder.dailys.length * 0.64, 55)); }, }, }; diff --git a/common/script/fns/autoAllocate.js b/common/script/fns/autoAllocate.js index 85fe01b78d..71a4898031 100644 --- a/common/script/fns/autoAllocate.js +++ b/common/script/fns/autoAllocate.js @@ -10,17 +10,19 @@ import splitWhitespace from '../libs/splitWhitespace'; function getStatToAllocate (user) { let suggested; + let statsObj = user.stats.toObject ? user.stats.toObject() : user.stats; + switch (user.preferences.allocationMode) { case 'flat': { - let stats = _.pick(user.stats, splitWhitespace('con str per int')); + let stats = _.pick(statsObj, splitWhitespace('con str per int')); return _.invert(stats)[_.min(stats)]; } case 'classbased': { - let lvlDiv7 = user.stats.lvl / 7; + let lvlDiv7 = statsObj.lvl / 7; let ideal = [lvlDiv7 * 3, lvlDiv7 * 2, lvlDiv7, lvlDiv7]; let preference; - switch (user.stats.class) { + switch (statsObj.class) { case 'wizard': { preference = ['int', 'per', 'con', 'str']; break; @@ -39,10 +41,10 @@ function getStatToAllocate (user) { } let diff = [ - user.stats[preference[0]] - ideal[0], - user.stats[preference[1]] - ideal[1], - user.stats[preference[2]] - ideal[2], - user.stats[preference[3]] - ideal[3], + statsObj[preference[0]] - ideal[0], + statsObj[preference[1]] - ideal[1], + statsObj[preference[2]] - ideal[2], + statsObj[preference[3]] - ideal[3], ]; suggested = _.findIndex(diff, (val) => { @@ -52,9 +54,9 @@ function getStatToAllocate (user) { return suggested !== -1 ? preference[suggested] : 'str'; } case 'taskbased': { - suggested = _.invert(user.stats.training)[_.max(user.stats.training)]; + suggested = _.invert(statsObj.training)[_.max(statsObj.training)]; - let training = user.stats.training; + let training = statsObj.training; training.str = 0; training.int = 0; training.con = 0; diff --git a/common/script/index.js b/common/script/index.js index 3b87ac1a7c..e3a0e0a0de 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -114,7 +114,7 @@ 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 buyHealthPotion from './ops/buyHealthPotion'; import buyArmoire from './ops/buyArmoire'; import buyMysterySet from './ops/buyMysterySet'; import buyQuest from './ops/buyQuest'; @@ -155,7 +155,7 @@ api.ops = { allocate, buy, buyGear, - buyPotion, + buyHealthPotion, buyArmoire, buyMysterySet, buySpecialSpell, @@ -274,7 +274,7 @@ 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), + buyHealthPotion: _.partial(importedOps.buyHealthPotion, user), buyArmoire: _.partial(importedOps.buyArmoire, user), buyGear: _.partial(importedOps.buyGear, user), buyQuest: _.partial(importedOps.buyQuest, user), diff --git a/common/script/ops/buy.js b/common/script/ops/buy.js index c79bd65e0d..ded5b034d2 100644 --- a/common/script/ops/buy.js +++ b/common/script/ops/buy.js @@ -3,7 +3,7 @@ import _ from 'lodash'; import { BadRequest, } from '../libs/errors'; -import buyPotion from './buyPotion'; +import buyHealthPotion from './buyHealthPotion'; import buyArmoire from './buyArmoire'; import buyGear from './buyGear'; @@ -12,8 +12,8 @@ module.exports = function buy (user, req = {}, analytics) { if (!key) throw new BadRequest(i18n.t('missingKeyParam', req.language)); let buyRes; - if (key === 'potion') { // health potion - buyRes = buyPotion(user, req, analytics); + if (key === 'potion') { + buyRes = buyHealthPotion(user, req, analytics); } else if (key === 'armoire') { buyRes = buyArmoire(user, req, analytics); } else { diff --git a/common/script/ops/buyPotion.js b/common/script/ops/buyHealthPotion.js similarity index 91% rename from common/script/ops/buyPotion.js rename to common/script/ops/buyHealthPotion.js index 0a15777d56..1a6c8b0e18 100644 --- a/common/script/ops/buyPotion.js +++ b/common/script/ops/buyHealthPotion.js @@ -4,7 +4,7 @@ import { NotAuthorized, } from '../libs/errors'; -module.exports = function buyPotion (user, req = {}, analytics) { // health potion +module.exports = function buyHealthPotion (user, req = {}, analytics) { let item = content.potion; if (user.stats.gp < item.value) { diff --git a/common/script/ops/index.js b/common/script/ops/index.js index 42e77d8718..2e8bca246d 100644 --- a/common/script/ops/index.js +++ b/common/script/ops/index.js @@ -31,7 +31,7 @@ import releaseMounts from './releaseMounts'; import releaseBoth from './releaseBoth'; import buy from './buy'; import buyGear from './buyGear'; -import buyPotion from './buyPotion'; +import buyHealthPotion from './buyHealthPotion'; import buyArmoire from './buyArmoire'; import buyQuest from './buyQuest'; import buyMysterySet from './buyMysterySet'; @@ -83,7 +83,7 @@ module.exports = { releaseBoth, buy, buyGear, - buyPotion, + buyHealthPotion, buyArmoire, buyQuest, buyMysterySet, diff --git a/common/script/ops/rebirth.js b/common/script/ops/rebirth.js index 40920fb0c7..54f9533fc5 100644 --- a/common/script/ops/rebirth.js +++ b/common/script/ops/rebirth.js @@ -11,16 +11,11 @@ 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; - if (user.balance < 2 && user.stats.lvl < MAX_LEVEL) { throw new NotAuthorized(i18n.t('notEnoughGems', req.language)); } - analyticsData = { + let analyticsData = { uuid: user._id, category: 'behavior', }; @@ -38,18 +33,20 @@ module.exports = function rebirth (user, tasks = [], req = {}, analytics) { analytics.track('Rebirth', analyticsData); } - lvl = capByLevel(user.stats.lvl); + let lvl = capByLevel(user.stats.lvl); _.each(tasks, function resetTasks (task) { - if (task.type !== 'reward') { - task.value = 0; - } - if (task.type === 'daily') { - task.streak = 0; + if (!task.challenge || !task.challenge.id || task.challenge.broken) { + if (task.type !== 'reward') { + task.value = 0; + } + if (task.type === 'daily') { + task.streak = 0; + } } }); - stats = user.stats; + let stats = user.stats; stats.buffs = {}; stats.hp = 50; stats.lvl = 1; @@ -79,7 +76,7 @@ module.exports = function rebirth (user, tasks = [], req = {}, analytics) { }); } - flags = user.flags; + let flags = user.flags; if (!user.achievements.beastMaster) { flags.rebirthEnabled = false; } diff --git a/common/script/ops/reroll.js b/common/script/ops/reroll.js index fee26fb88b..72414173f7 100644 --- a/common/script/ops/reroll.js +++ b/common/script/ops/reroll.js @@ -13,8 +13,10 @@ module.exports = function reroll (user, tasks = [], req = {}, analytics) { user.stats.hp = 50; _.each(tasks, function resetTaskValues (task) { - if (task.type !== 'reward') { - task.value = 0; + if (!task.challenge || !task.challenge.id || task.challenge.broken) { + if (task.type !== 'reward') { + task.value = 0; + } } }); diff --git a/common/script/ops/reset.js b/common/script/ops/reset.js index eb8d25033e..3e48fa4f2d 100644 --- a/common/script/ops/reset.js +++ b/common/script/ops/reset.js @@ -13,7 +13,6 @@ module.exports = function reset (user, tasks = [], req = {}) { tasksToRemove.push(task._id); let i = user.tasksOrder[`${task.type}s`].indexOf(task._id); if (i !== -1) user.tasksOrder[`${task.type}s`].splice(i, 1); - tasksToRemove.push(task._id); } }); diff --git a/test/api/v3/integration/tasks/POST-tasks_user.test.js b/test/api/v3/integration/tasks/POST-tasks_user.test.js index b39e640480..623d9bb4a4 100644 --- a/test/api/v3/integration/tasks/POST-tasks_user.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_user.test.js @@ -108,7 +108,7 @@ describe('POST /tasks/user', () => { }); it(`ignores setting userId, history, createdAt, - updatedAt, challenge, completed, streak, + updatedAt, challenge, completed, dateCompleted fields`, async () => { let task = await user.post('/tasks/user', { text: 'test daily', @@ -119,7 +119,6 @@ describe('POST /tasks/user', () => { updatedAt: 'tomorrow', challenge: 'no', completed: true, - streak: 25, dateCompleted: 'never', value: 324, // ignored because not a reward }); @@ -130,7 +129,6 @@ describe('POST /tasks/user', () => { expect(task.updatedAt).not.to.equal('tomorrow'); expect(task.challenge).not.to.equal('no'); expect(task.completed).to.equal(false); - expect(task.streak).to.equal(0); expect(task.streak).not.to.equal('never'); expect(task.value).not.to.equal(324); }); 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 index 0db5f5cec3..32b8134647 100644 --- a/test/api/v3/integration/user/POST-user_buy_armoire.test.js +++ b/test/api/v3/integration/user/POST-user_buy_armoire.test.js @@ -18,6 +18,7 @@ describe('POST /user/buy-armoire', () => { await user.update({ 'stats.gp': 5, }); + await expect(user.post('/user/buy-armoire')) .to.eventually.be.rejected.and.eql({ code: 401, @@ -28,6 +29,7 @@ describe('POST /user/buy-armoire', () => { it('reduces gold when buying from the armoire', async () => { await user.post('/user/buy-armoire'); + await user.sync(); expect(user.stats.gp).to.equal(300); diff --git a/test/api/v3/integration/user/POST-user_buy_potion.test.js b/test/api/v3/integration/user/POST-user_buy_health_potion.test.js similarity index 84% rename from test/api/v3/integration/user/POST-user_buy_potion.test.js rename to test/api/v3/integration/user/POST-user_buy_health_potion.test.js index e37f908e3e..835e893bd7 100644 --- a/test/api/v3/integration/user/POST-user_buy_potion.test.js +++ b/test/api/v3/integration/user/POST-user_buy_health_potion.test.js @@ -6,7 +6,7 @@ import shared from '../../../../../common/script'; let content = shared.content; -describe('POST /user/buy-potion', () => { +describe('POST /user/buy-health-potion', () => { let user; beforeEach(async () => { @@ -18,7 +18,7 @@ describe('POST /user/buy-potion', () => { // 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')) + await expect(user.post('/user/buy-health-potion')) .to.eventually.be.rejected.and.eql({ code: 401, error: 'NotAuthorized', @@ -32,7 +32,7 @@ describe('POST /user/buy-potion', () => { }); let potion = content.potion; - let res = await user.post('/user/buy-potion'); + let res = await user.post('/user/buy-health-potion'); await user.sync(); expect(user.stats.hp).to.equal(50); diff --git a/test/api/v3/integration/user/POST-user_rebirth.test.js b/test/api/v3/integration/user/POST-user_rebirth.test.js index 3591844c3c..21fbed0b8d 100644 --- a/test/api/v3/integration/user/POST-user_rebirth.test.js +++ b/test/api/v3/integration/user/POST-user_rebirth.test.js @@ -31,6 +31,7 @@ describe('POST /user/rebirth', () => { let daily = await generateDaily({ text: 'test habit', type: 'daily', + value: 1, streak: 1, userId: user._id, }); diff --git a/test/common/ops/buyPotion.js b/test/common/ops/buyHealthPotion.js similarity index 84% rename from test/common/ops/buyPotion.js rename to test/common/ops/buyHealthPotion.js index 5230040a35..6a70f71b62 100644 --- a/test/common/ops/buyPotion.js +++ b/test/common/ops/buyHealthPotion.js @@ -2,13 +2,13 @@ import { generateUser, } from '../../helpers/common.helper'; -import buyPotion from '../../../common/script/ops/buyPotion'; +import buyHealthPotion from '../../../common/script/ops/buyHealthPotion'; import { NotAuthorized, } from '../../../common/script/libs/errors'; import i18n from '../../../common/script/i18n'; -describe('shared.ops.buyPotion', () => { +describe('shared.ops.buyHealthPotion', () => { let user; beforeEach(() => { @@ -30,19 +30,19 @@ describe('shared.ops.buyPotion', () => { context('Potion', () => { it('recovers 15 hp', () => { user.stats.hp = 30; - buyPotion(user); + buyHealthPotion(user); expect(user.stats.hp).to.eql(45); }); it('does not increase hp above 50', () => { user.stats.hp = 45; - buyPotion(user); + buyHealthPotion(user); expect(user.stats.hp).to.eql(50); }); it('deducts 25 gp', () => { user.stats.hp = 45; - buyPotion(user); + buyHealthPotion(user); expect(user.stats.gp).to.eql(175); }); @@ -51,7 +51,7 @@ describe('shared.ops.buyPotion', () => { user.stats.hp = 45; user.stats.gp = 5; try { - buyPotion(user); + buyHealthPotion(user); } catch (err) { expect(err).to.be.an.instanceof(NotAuthorized); expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); diff --git a/test/common/ops/rebirth.js b/test/common/ops/rebirth.js index 0ab66674b3..9916144669 100644 --- a/test/common/ops/rebirth.js +++ b/test/common/ops/rebirth.js @@ -3,7 +3,9 @@ import i18n from '../../../common/script/i18n'; import { MAX_LEVEL } from '../../../common/script/constants'; import { generateUser, + generateHabit, generateDaily, + generateTodo, generateReward, } from '../../helpers/common.helper'; import { @@ -19,7 +21,7 @@ describe('shared.ops.rebirth', () => { beforeEach(() => { user = generateUser(); user.balance = 2; - tasks = [generateDaily(), generateReward()]; + tasks = [generateHabit(), generateDaily(), generateTodo(), generateReward()]; }); it('returns an error when user balance is too low and user is less than max level', (done) => { @@ -49,22 +51,35 @@ describe('shared.ops.rebirth', () => { expect(message).to.equal(i18n.t('rebirthComplete')); }); - it('resets user\'s taks values except for rewards to 0', () => { + it('rebirths a user with not enough gems but more than max level', () => { + user.balance = 0; + user.stats.lvl = MAX_LEVEL + 1; + + let [, message] = rebirth(user); + + expect(message).to.equal(i18n.t('rebirthComplete')); + }); + + it('resets user\'s tasks values except for rewards to 0', () => { tasks[0].value = 1; tasks[1].value = 1; + tasks[2].value = 1; + tasks[3].value = 1; // Reward rebirth(user, tasks); expect(tasks[0].value).to.equal(0); - expect(tasks[1].value).to.equal(1); + expect(tasks[1].value).to.equal(0); + expect(tasks[2].value).to.equal(0); + expect(tasks[3].value).to.equal(1); // Reward }); it('resets user\'s daily streaks to 0', () => { - tasks[0].streak = 1; + tasks[1].streak = 1; // Daily rebirth(user, tasks); - expect(tasks[0].streak).to.equal(0); + expect(tasks[1].streak).to.equal(0); }); it('resets a user\'s buffs', () => { @@ -156,7 +171,7 @@ describe('shared.ops.rebirth', () => { 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; + expect(user.flags.levelDrops).to.be.empty; }); it('does not reset rebirthEnabled if user has beastMaster', () => { @@ -175,7 +190,7 @@ describe('shared.ops.rebirth', () => { expect(user.achievements.rebirthLevel).to.equal(user.stats.lvl); }); - it('increments rebirth achievemnts', () => { + it('increments rebirth achievements', () => { user.stats.lvl = 2; user.achievements.rebirths = 1; user.achievements.rebirthLevel = 1; @@ -185,4 +200,37 @@ describe('shared.ops.rebirth', () => { expect(user.achievements.rebirths).to.equal(2); expect(user.achievements.rebirthLevel).to.equal(2); }); + + it('does not increment rebirth achievements when level is lower than previous', () => { + user.stats.lvl = 2; + user.achievements.rebirths = 1; + user.achievements.rebirthLevel = 3; + + rebirth(user); + + expect(user.achievements.rebirths).to.equal(1); + expect(user.achievements.rebirthLevel).to.equal(3); + }); + + it('always increments rebirth achievements when level is MAX_LEVEL', () => { + user.stats.lvl = MAX_LEVEL; + user.achievements.rebirths = 1; + user.achievements.rebirthLevel = MAX_LEVEL + 1; // this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test + + rebirth(user); + + expect(user.achievements.rebirths).to.equal(2); + expect(user.achievements.rebirthLevel).to.equal(MAX_LEVEL); + }); + + it('always increments rebirth achievements when level is greater than MAX_LEVEL', () => { + user.stats.lvl = MAX_LEVEL + 1; + user.achievements.rebirths = 1; + user.achievements.rebirthLevel = MAX_LEVEL + 2; // this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test + + rebirth(user); + + expect(user.achievements.rebirths).to.equal(2); + expect(user.achievements.rebirthLevel).to.equal(MAX_LEVEL); + }); }); diff --git a/website/client/js/controllers/rootCtrl.js b/website/client/js/controllers/rootCtrl.js index f7eb1f4041..1cd06e865d 100644 --- a/website/client/js/controllers/rootCtrl.js +++ b/website/client/js/controllers/rootCtrl.js @@ -289,7 +289,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ } }); } else if (spell.target == 'tasks') { - var tasks = User.user.habits.concat(User.user.dailys).concat(User.user.rewards); + var tasks = User.user.habits.concat(User.user.dailys).concat(User.user.rewards).concat(User.user.todos); // exclude challenge tasks tasks = tasks.filter(function (t) { if (!t.challenge) return true; @@ -316,7 +316,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$ if (targetId) spellUrl += '?targetId=' + targetId; $http.post(spellUrl) - .success(function(){ + .success(function(){ // TODO response will always include the modified data, no need to sync! var msg = window.env.t('youCast', {spell: spell.text()}); switch (type) { case 'task': msg = window.env.t('youCastTarget', {spell: spell.text(), target: target.text});break; diff --git a/website/client/js/controllers/settingsCtrl.js b/website/client/js/controllers/settingsCtrl.js index a93dde0fb3..d778fadc54 100644 --- a/website/client/js/controllers/settingsCtrl.js +++ b/website/client/js/controllers/settingsCtrl.js @@ -176,6 +176,7 @@ habitrpg.controller('SettingsCtrl', $scope.reset = function(){ User.reset({}); + User.sync(); $rootScope.$state.go('tasks'); } diff --git a/website/client/js/services/chatServices.js b/website/client/js/services/chatServices.js index ed812e08ad..a79de22b85 100644 --- a/website/client/js/services/chatServices.js +++ b/website/client/js/services/chatServices.js @@ -81,8 +81,7 @@ angular.module('habitrpg') clearCards: clearCards, } - //@TOOD: Port when User service is updated function clearCards() { - User.user.ops.update && User.set({'flags.cardReceived':false}); + User.user._wrapped && User.set({'flags.cardReceived':false}); } }]); diff --git a/website/client/js/services/guideServices.js b/website/client/js/services/guideServices.js index 08b51732c8..7f3ce9e499 100644 --- a/website/client/js/services/guideServices.js +++ b/website/client/js/services/guideServices.js @@ -264,8 +264,8 @@ function($rootScope, User, $timeout, $state, Analytics) { } //Init and show the welcome tour (only after user is pulled from server & wrapped). - var watcher = $rootScope.$watch('User.user.ops.update', function(updateFn){ - if (!updateFn) return; // only run after user has been wrapped + var watcher = $rootScope.$watch('User.user._wrapped', function(wrapped){ + if (!wrapped) return; // only run after user has been wrapped watcher(); // deregister watcher if (window.env.IS_MOBILE) return; // Don't show tour immediately on mobile devices if (User.user.flags.welcomed == false) { diff --git a/website/client/js/services/userServices.js b/website/client/js/services/userServices.js index 206052c8ad..a25584e5fb 100644 --- a/website/client/js/services/userServices.js +++ b/website/client/js/services/userServices.js @@ -61,21 +61,13 @@ angular.module('habitrpg') // replicated. We need to wrap each op to provide a callback to send that operation $window.habitrpgShared.wrap(user); _.each(user.ops, function(op,k){ - user.ops[k] = function(req,cb){ - if (cb) return op(req,cb); - op(req,function(err,response) { - for(var updatedItem in req.body) { - var itemUpdateResponse = userNotifications[updatedItem]; - if(itemUpdateResponse) Notification.text(itemUpdateResponse); - } - if (err) { - var message = err.code ? err.message : err; - Notification.text(message); - // In the case of 200s, they're friendly alert messages like "Your pet has hatched!" - still send the op - if ((err.code && err.code >= 400) || !err.code) return; - } - userServices.log({op:k, params: req.params, query:req.query, body:req.body}); - }); + user.ops[k] = function(req){ + try { + op(req); + } catch (err) { + Notification.text(err.message); + return; + } } }); } @@ -106,13 +98,27 @@ angular.module('habitrpg') function callOpsFunctionAndRequest (opName, endPoint, method, paramString, opData) { if (!opData) opData = {}; + var clientResponse; + try { - $window.habitrpgShared.ops[opName](user, opData); - } catch(err) { + var args = [user]; + if (opName === 'rebirth' || opName === 'reroll' || opName === 'reset') { + args.push(user.habits.concat(user.dailys).concat(user.rewards).concat(user.todos)); + } + + args.push(opData); + clientResponse = $window.habitrpgShared.ops[opName].apply(null, args); + } catch (err) { Notification.text(err.message); return; } + var clientMessage = clientResponse[1]; + + if (clientMessage) { + Notification.text(clientMessage); + } + var url = '/api/v3/user/' + endPoint; if (paramString) { url += '/' + paramString @@ -130,7 +136,7 @@ angular.module('habitrpg') body: body, }) .then(function (response) { - if (response.data.message) Notification.text(response.data.message); + if (response.data.message && response.data.message !== clientMessage) Notification.text(response.data.message); save(); }) } @@ -182,7 +188,12 @@ angular.module('habitrpg') }, score: function (data) { - $window.habitrpgShared.ops.scoreTask({user: user, task: data.params.task, direction: data.params.direction}, data.params); + try { + $window.habitrpgShared.ops.scoreTask({user: user, task: data.params.task, direction: data.params.direction}, data.params); + } catch (err) { + Notification.text(err.message); + return; + } save(); Tasks.scoreTask(data.params.task._id, data.params.direction).then(function (res) { var tmp = res.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses diff --git a/website/server/controllers/api-v3/content.js b/website/server/controllers/api-v3/content.js index 7522a59096..0235f61645 100644 --- a/website/server/controllers/api-v3/content.js +++ b/website/server/controllers/api-v3/content.js @@ -61,7 +61,7 @@ async function saveContentToDisk (language, content) { } /** - * @api {get} /api/v3/content Get all available content objects. + * @api {get} /api/v3/content Get all available content objects * @apiDescription Does not require authentication. * @apiVersion 3.0.0 * @apiName ContentGet diff --git a/website/server/controllers/api-v3/debug.js b/website/server/controllers/api-v3/debug.js index 9f6d344546..9949421513 100644 --- a/website/server/controllers/api-v3/debug.js +++ b/website/server/controllers/api-v3/debug.js @@ -7,7 +7,7 @@ import _ from 'lodash'; let api = {}; /** - * @api {post} /api/v3/debug/add-ten-gems Add ten gems to the current user. + * @api {post} /api/v3/debug/add-ten-gems Add ten gems to the current user * @apiDescription Only available in development mode. * @apiVersion 3.0.0 * @apiName AddTenGems @@ -31,7 +31,7 @@ api.addTenGems = { }; /** - * @api {post} /api/v3/debug/add-hourglass Add Hourglass to the current user. + * @api {post} /api/v3/debug/add-hourglass Add Hourglass to the current user * @apiDescription Only available in development mode. * @apiVersion 3.0.0 * @apiName AddHourglass diff --git a/website/server/controllers/api-v3/hall.js b/website/server/controllers/api-v3/hall.js index c8f11fdfa8..077b1ef74b 100644 --- a/website/server/controllers/api-v3/hall.js +++ b/website/server/controllers/api-v3/hall.js @@ -9,8 +9,8 @@ import _ from 'lodash'; let api = {}; /** - * @api {get} /api/v3/hall/patrons Get all Patrons. - * @apiDescription Only the first 50 patrons are returned. More can be accessed passing ?page=n. + * @api {get} /api/v3/hall/patrons Get all patrons + * @apiDescription Only the first 50 patrons are returned. More can be accessed passing ?page=n * @apiVersion 3.0.0 * @apiName GetPatrons * @apiGroup Hall @@ -79,13 +79,13 @@ api.getHeroes = { const heroAdminFields = 'contributor balance profile.name purchased items auth'; /** - * @api {get} /api/v3/hall/heroes/:heroId Get an hero given his _id. - * @apiDescription Must be an admin to make this request + * @api {get} /api/v3/hall/heroes/:heroId Get any user ("hero") given the UUID + * @apiDescription Must be an admin to make this request. * @apiVersion 3.0.0 * @apiName GetHero * @apiGroup Hall * - * @apiSuccess {Object} data The hero object + * @apiSuccess {Object} data The user object */ api.getHero = { method: 'GET', @@ -117,13 +117,13 @@ api.getHero = { const gemsPerTier = {1: 3, 2: 3, 3: 3, 4: 4, 5: 4, 6: 4, 7: 4, 8: 0, 9: 0}; /** - * @api {put} /api/v3/hall/heroes/:heroId Update an hero. - * @apiDescription Must be an admin to make this request + * @api {put} /api/v3/hall/heroes/:heroId Update any user ("hero") + * @apiDescription Must be an admin to make this request. * @apiVersion 3.0.0 * @apiName UpdateHero * @apiGroup Hall * - * @apiSuccess {Object} data The updated hero object + * @apiSuccess {Object} data The updated user object */ api.updateHero = { method: 'PUT', diff --git a/website/server/controllers/api-v3/modelsPaths.js b/website/server/controllers/api-v3/modelsPaths.js index dc168b98b4..927087df01 100644 --- a/website/server/controllers/api-v3/modelsPaths.js +++ b/website/server/controllers/api-v3/modelsPaths.js @@ -6,7 +6,7 @@ let tasksModels = ['habit', 'daily', 'todo', 'reward']; let allModels = ['user', 'tag', 'challenge', 'group'].concat(tasksModels); /** - * @api {get} /api/v3/models/:model/paths Get all paths for the specified model. + * @api {get} /api/v3/models/:model/paths Get all paths for the specified model * @apiDescription Doesn't require authentication * @apiVersion 3.0.0 * @apiName GetUserModelPaths diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 3ed454c666..50e32220bc 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -54,7 +54,7 @@ async function _createTasks (req, res, user, challenge) { } /** - * @api {post} /api/v3/tasks/user Create a new task belonging to the user. + * @api {post} /api/v3/tasks/user Create a new task belonging to the user * @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks. * @apiVersion 3.0.0 * @apiName CreateUserTasks @@ -73,13 +73,13 @@ api.createUserTasks = { }; /** - * @api {post} /api/v3/tasks/challenge/:challengeId Create a new task belonging to a challenge. + * @api {post} /api/v3/tasks/challenge/:challengeId Create a new task belonging to a challenge * @apiDescription Can be passed an object to create a single task or an array of objects to create multiple tasks. * @apiVersion 3.0.0 * @apiName CreateChallengeTasks * @apiGroup Task * - * @apiParam {UUID} challengeId The id of the challenge the new task(s) will belong to. + * @apiParam {UUID} challengeId The id of the challenge the new task(s) will belong to * * @apiSuccess data An object if a single task was created, otherwise an array of tasks */ @@ -171,7 +171,7 @@ async function _getTasks (req, res, user, challenge) { * @apiName GetUserTasks * @apiGroup Task * - * @apiParam {string="habits","dailys","todos","rewards","completedTodos"} type Optional query parameter to return just a type of tasks. By default all types will be returned except completed todos that requested separately. + * @apiParam {string="habits","dailys","todos","rewards","completedTodos"} type Optional query parameter to return just a type of tasks. By default all types will be returned except completed todos that must be requested separately. * * @apiSuccess {Array} data An array of tasks */ @@ -197,7 +197,7 @@ api.getUserTasks = { * @apiName GetChallengeTasks * @apiGroup Task * - * @apiParam {UUID} challengeId The id of the challenge from which to retrieve the tasks. + * @apiParam {UUID} challengeId The id of the challenge from which to retrieve the tasks * @apiParam {string="habits","dailys","todos","rewards"} type Optional query parameter to return just a type of tasks * * @apiSuccess {Array} data An array of tasks @@ -427,9 +427,9 @@ api.scoreTask = { }, }; -// completed todos cannot be moved, they'll be returned ordered by date of completion /** * @api {post} /api/v3/tasks/:taskId/move/to/:position Move a task to a new position + * @apiDescription Note: completed To-Dos are not sortable, do not appear in user.tasksOrder.todos, and are ordered by date of completion. * @apiVersion 3.0.0 * @apiName MoveTask * @apiGroup Task diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index f477631b58..a96bd43453 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -142,7 +142,7 @@ let checkPreferencePurchase = (user, path, item) => { }; /** - * @api {put} /api/v3/user Update the user. + * @api {put} /api/v3/user Update the user * @apiDescription Example body: {'stats.hp':50, 'preferences.background': 'beach'} * @apiVersion 3.0.0 * @apiName UserUpdate @@ -305,13 +305,13 @@ api.getUserAnonymized = { const partyMembersFields = 'profile.name stats achievements items.special'; /** - * @api {post} /api/v3/user/class/cast/:spellId Cast a spell on a target. + * @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target * @apiVersion 3.0.0 * @apiName UserCast * @apiGroup User * - * @apiParam {string} spellId The spell to cast. - * @apiParam {UUID} targetId Optional query parameter, the id of the target when casting a spell on a party member or a task. + * @apiParam {string} spellId The skill to cast + * @apiParam {UUID} targetId Optional query parameter, the id of the target when casting a skill on a party member or a task * * @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned. */ @@ -443,7 +443,7 @@ api.castSpell = { }; /** - * @api {post} /api/v3/user/sleep Put the user in the inn. + * @api {post} /api/v3/user/sleep Make the user start / stop sleeping (resting in the Inn) * @apiVersion 3.0.0 * @apiName UserSleep * @apiGroup User @@ -463,7 +463,7 @@ api.sleep = { }; /** - * @api {post} /api/v3/user/allocate Allocate an attribute point. + * @api {post} /api/v3/user/allocate Allocate an attribute point * @apiVersion 3.0.0 * @apiName UserAllocate * @apiGroup User @@ -485,7 +485,8 @@ api.allocate = { }; /** - * @api {post} /api/v3/user/allocate-now Allocate all attribute points. + * @api {post} /api/v3/user/allocate-now Allocate all attribute points + * @apiDescription Uses the user's chosen automatic allocation method, or if none, assigns all to STR. * @apiVersion 3.0.0 * @apiName UserAllocateNow * @apiGroup User @@ -511,7 +512,7 @@ api.allocateNow = { * @apiName UserBuy * @apiGroup User * - * @apiParam {string} key The item to buy. + * @apiParam {string} key The item to buy */ api.buy = { method: 'POST', @@ -526,12 +527,12 @@ api.buy = { }; /** - * @api {post} /user/buy-gear/:key Buy a piece of gear. + * @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. + * @apiParam {string} key The item to buy * * @apiSuccess {object} data.items user.items * @apiSuccess {object} data.flags user.flags @@ -552,7 +553,7 @@ api.buyGear = { }; /** - * @api {post} /user/buy-armoire Buy an armoire item. + * @api {post} /user/buy-armoire Buy an armoire item * @apiVersion 3.0.0 * @apiName UserBuyArmoire * @apiGroup User @@ -575,7 +576,7 @@ api.buyArmoire = { }; /** - * @api {post} /user/buy-potion Buy a health potion + * @api {post} /user/buy-health-potion Buy a health potion * @apiVersion 3.0.0 * @apiName UserBuyPotion * @apiGroup User @@ -583,25 +584,25 @@ api.buyArmoire = { * @apiSuccess {Object} data user.stats * @apiSuccess {string} message Success message */ -api.buyPotion = { +api.buyHealthPotion = { method: 'POST', middlewares: [authWithHeaders()], - url: '/user/buy-potion', + url: '/user/buy-health-potion', async handler (req, res) { let user = res.locals.user; - let buyPotionResponse = common.ops.buyPotion(user, req, res.analytics); + let buyHealthPotionResponse = common.ops.buyHealthPotion(user, req, res.analytics); await user.save(); - res.respond(200, ...buyPotionResponse); + res.respond(200, ...buyHealthPotionResponse); }, }; /** - * @api {post} /user/buy-mystery-set/:key Buy a mystery set. + * @api {post} /user/buy-mystery-set/:key Buy a mystery set * @apiVersion 3.0.0 * @apiName UserBuyMysterySet * @apiGroup User * - * @apiParam {string} key The mystery set to buy. + * @apiParam {string} key The mystery set to buy * * @apiSuccess {Object} data.items user.items * @apiSuccess {Object} data.purchasedPlanConsecutive user.purchased.plan.consecutive @@ -620,12 +621,12 @@ api.buyMysterySet = { }; /** - * @api {post} /api/v3/user/buy-quest/:key Buy a quest with gold. + * @api {post} /api/v3/user/buy-quest/:key Buy a quest with gold * @apiVersion 3.0.0 * @apiName UserBuyQuest * @apiGroup User * - * @apiParam {string} key The quest spell to buy. + * @apiParam {string} key The quest scroll to buy * * @apiSuccess {Object} data `user.items.quests` * @apiSuccess {string} message Success message @@ -643,12 +644,13 @@ api.buyQuest = { }; /** - * @api {post} /api/v3/user/buy-special-spell/:key Buy special spell. + * @api {post} /api/v3/user/buy-special-spell/:key Buy special "spell" item + * @apiDescription Includes gift cards (e.g., birthday card), and avatar Transformation Items and their antidotes (e.g., Snowball item and Salt reward). * @apiVersion 3.0.0 * @apiName UserBuySpecialSpell * @apiGroup User * - * @apiParam {string} key The special spell to buy. + * @apiParam {string} key The special item to buy. Must be one of the keys from "content.special", such as birthday, snowball, salt. * * @apiSuccess {Object} data.stats user.stats * @apiSuccess {Object} data.items user.items @@ -667,13 +669,13 @@ api.buySpecialSpell = { }; /** - * @api {post} /api/v3/user/hatch/:egg/:hatchingPotion Hatch a pet. + * @api {post} /api/v3/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. + * @apiParam {string} egg The egg to use + * @apiParam {string} hatchingPotion The hatching potion to use * * @apiSuccess {Object} data user.items * @apiSuccess {string} message @@ -739,13 +741,13 @@ api.feed = { }; /** -* @api {post} /api/v3/user/change-class Change class. +* @api {post} /api/v3/user/change-class Change class * @apiDescription User must be at least level 10. If ?class is defined and user.flags.classSelected is false it'll change the class. If user.preferences.disableClasses it'll enable classes, otherwise it sets user.flags.classSelected to false (costs 3 gems) * @apiVersion 3.0.0 * @apiName UserChangeClass * @apiGroup User * -* @apiParam {string} class Query parameter - ?class={warrior|rogue|wizard|healer}. +* @apiParam {string} class Query parameter - ?class={warrior|rogue|wizard|healer} * * @apiSuccess {object} data.flags user.flags * @apiSuccess {object} data.stats user.stats @@ -765,7 +767,7 @@ api.changeClass = { }; /** -* @api {post} /api/v3/user/disable-classes Disable classes. +* @api {post} /api/v3/user/disable-classes Disable classes * @apiVersion 3.0.0 * @apiName UserDisableClasses * @apiGroup User @@ -787,13 +789,13 @@ api.disableClasses = { }; /** -* @api {post} /api/v3/user/purchase/:type/:key Purchase Gem Items. +* @api {post} /api/v3/user/purchase/:type/:key Purchase Gem or Gem-purchasable item * @apiVersion 3.0.0 * @apiName UserPurchase * @apiGroup User * -* @apiParam {string} type Type of item to purchase. Must be one of: gem, gems, eggs, hatchingPotions, food, quests or gear -* @apiParam {string} key Item's key +* @apiParam {string} type Type of item to purchase. Must be one of: gems, eggs, hatchingPotions, food, quests, or gear +* @apiParam {string} key Item's key (use "gem" for purchasing gems) * * @apiSuccess {object} data.items user.items * @apiSuccess {number} data.balance user.balance @@ -812,7 +814,7 @@ api.purchase = { }; /** -* @api {post} /api/v3/user/purchase-hourglass/:type/:key Purchase Hourglass. +* @api {post} /api/v3/user/purchase-hourglass/:type/:key Purchase Hourglass-purchasable item * @apiVersion 3.0.0 * @apiName UserPurchaseHourglass * @apiGroup User @@ -837,7 +839,7 @@ api.userPurchaseHourglass = { }; /** -* @api {post} /api/v3/user/read-card/:cardType Reads a card. +* @api {post} /api/v3/user/read-card/:cardType Reads a card * @apiVersion 3.0.0 * @apiName UserReadCard * @apiGroup User @@ -861,7 +863,7 @@ api.readCard = { }; /** -* @api {post} /api/v3/user/open-mystery-item Open the mystery item. +* @api {post} /api/v3/user/open-mystery-item Open the Mystery Item box * @apiVersion 3.0.0 * @apiName UserOpenMysteryItem * @apiGroup User @@ -881,7 +883,7 @@ api.userOpenMysteryItem = { }, }; -/* +/** * @api {post} /api/v3/user/webhook Create a new webhook * @apiVersion 3.0.0 * @apiName UserAddWebhook @@ -904,7 +906,7 @@ api.addWebhook = { }, }; -/* +/** * @api {put} /api/v3/user/webhook/:id Edit a webhook * @apiVersion 3.0.0 * @apiName UserUpdateWebhook @@ -928,7 +930,7 @@ api.updateWebhook = { }, }; -/* +/** * @api {delete} /api/v3/user/webhook/:id Delete a webhook * @apiVersion 3.0.0 * @apiName UserDeleteWebhook @@ -951,7 +953,7 @@ api.deleteWebhook = { }; -/* @api {post} /api/v3/user/release-pets Releases pets. +/* @api {post} /api/v3/user/release-pets Release pets * @apiVersion 3.0.0 * @apiName UserReleasePets * @apiGroup User @@ -971,8 +973,8 @@ api.userReleasePets = { }, }; -/* -* @api {post} /api/v3/user/release-both Releases Pets and Mounts and grants Triad Bingo. +/** +* @api {post} /api/v3/user/release-both Release pets and mounts and grants Triad Bingo * @apiVersion 3.0.0 * @apiName UserReleaseBoth * @apiGroup User @@ -994,8 +996,8 @@ api.userReleaseBoth = { }, }; -/* -* @api {post} /api/v3/user/release-mounts Released mounts. +/** +* @api {post} /api/v3/user/release-mounts Release mounts * @apiVersion 3.0.0 * @apiName UserReleaseMounts * @apiGroup User @@ -1015,13 +1017,13 @@ api.userReleaseMounts = { }, }; -/* -* @api {post} /api/v3/user/sell/:type/:key Sells a gold item owned by the user. +/** +* @api {post} /api/v3/user/sell/:type/:key Sell a gold-sellable item owned by the user * @apiVersion 3.0.0 * @apiName UserSell * @apiGroup User * -* @apiParam {string} type The type of item to sell. Acceptable types are eggs, hatchingPotions, food +* @apiParam {string} type The type of item to sell. Must be one of: eggs, hatchingPotions, or food * @apiParam {string} key The key of the item * * @apiSuccess {Object} data.stats @@ -1040,8 +1042,8 @@ api.userSell = { }, }; -/* -* @api {post} /api/v3/user/unlock Unlocks items by purchase. +/** +* @api {post} /api/v3/user/unlock Unlock item or set of items by purchase * @apiVersion 3.0.0 * @apiName UserUnlock * @apiGroup User @@ -1049,9 +1051,9 @@ api.userSell = { * @apiParam {string} path Query parameter. The path to unlock * * @apiSuccess {Object} data.purchased -* @apiSuccess {Object} data.items` -* @apiSuccess {Object} data.preferences` -* @apiSuccess {string} message` +* @apiSuccess {Object} data.items +* @apiSuccess {Object} data.preferences +* @apiSuccess {string} message */ api.userUnlock = { method: 'POST', @@ -1066,7 +1068,7 @@ api.userUnlock = { }; /** -* @api {post} /api/v3/user/revive Revives user from death. +* @api {post} /api/v3/user/revive Revive user from death * @apiVersion 3.0.0 * @apiName UserRevive * @apiGroup User @@ -1086,13 +1088,13 @@ api.userRevive = { }, }; -/* -* @api {post} /api/v3/user/rebirth Resets a user. +/** +* @api {post} /api/v3/user/rebirth Use Orb of Rebirth on user * @apiVersion 3.0.0 * @apiName UserRebirth * @apiGroup User * -* @apiSuccess {Object} data.userr +* @apiSuccess {Object} data.user * @apiSuccess {array} data.tasks User's modified tasks (no rewards) * @apiSuccess {string} message Success message */ @@ -1102,23 +1104,30 @@ api.userRebirth = { url: '/user/rebirth', async handler (req, res) { let user = res.locals.user; - let query = { + let tasks = await Tasks.Task.find({ userId: user._id, type: {$in: ['daily', 'habit', 'todo']}, - }; - let tasks = await Tasks.Task.find(query).exec(); + $or: [ // exclude challenge tasks + {'challenge.id': {$exists: false}}, + {'challenge.broken': {$exists: true}}, + ], + }).exec(); + let rebirthRes = common.ops.rebirth(user, tasks, req, res.analytics); - await user.save(); + let toSave = tasks.map(task => task.save()); - await Bluebird.all(tasks.map(task => task.save())); + toSave.push(user.save()); + + await Bluebird.all(toSave); res.respond(200, ...rebirthRes); }, }; /** - * @api {post} /api/v3/user/block/:uuid Blocks and unblocks a user + * @api {post} /api/v3/user/block/:uuid Block and unblock a user + * @apiDescription Must be an admin to make this request. * @apiVersion 3.0.0 * @apiName BlockUser * @apiGroup User @@ -1201,8 +1210,8 @@ api.markPmsRead = { }, }; -/* -* @api {post} /api/v3/user/reroll Rerolls a user. +/** +* @api {post} /api/v3/user/reroll Reroll a user using the Fortify Potion * @apiVersion 3.0.0 * @apiName UserReroll * @apiGroup User @@ -1220,6 +1229,10 @@ api.userReroll = { let query = { userId: user._id, type: {$in: ['daily', 'habit', 'todo']}, + $or: [ // exclude challenge tasks + {'challenge.id': {$exists: false}}, + {'challenge.broken': {$exists: true}}, + ], }; let tasks = await Tasks.Task.find(query).exec(); let rerollRes = common.ops.reroll(user, tasks, req, res.analytics); @@ -1233,8 +1246,8 @@ api.userReroll = { }, }; -/* -* @api {post} /api/v3/user/addPushDevice Adds a push device to a user. +/** +* @api {post} /api/v3/user/addPushDevice Add a push device to a user * @apiVersion 3.0.0 * @apiName UserAddPushDevice * @apiGroup User @@ -1259,8 +1272,8 @@ api.userAddPushDevice = { }, }; -/* -* @api {post} /api/v3/user/reset Resets a user. +/** +* @api {post} /api/v3/user/reset Reset user * @apiVersion 3.0.0 * @apiName UserReset * @apiGroup User @@ -1276,11 +1289,20 @@ api.userReset = { async handler (req, res) { let user = res.locals.user; - let tasks = await Tasks.Task.find({userId: user._id}).select('_id type challenge').exec(); + let tasks = await Tasks.Task.find({ + userId: user._id, + $or: [ // exclude challenge tasks + {'challenge.id': {$exists: false}}, + {'challenge.broken': {$exists: true}}, + ], + }).select('_id type challenge').exec(); - let resetRes = common.ops.reset(user, tasks); + let resetRes = common.ops.reset(user, tasks, req); - await Bluebird.all([Tasks.Task.remove({_id: {$in: resetRes[0].tasksToRemove}, userId: user._id}), user.save()]); + await Bluebird.all([ + Tasks.Task.remove({_id: {$in: resetRes[0].tasksToRemove}, userId: user._id}), + user.save(), + ]); res.respond(200, ...resetRes); }, diff --git a/website/server/controllers/top-level/dataexport.js b/website/server/controllers/top-level/dataexport.js index 10519807e3..477c8ea829 100644 --- a/website/server/controllers/top-level/dataexport.js +++ b/website/server/controllers/top-level/dataexport.js @@ -26,8 +26,8 @@ const BASE_URL = nconf.get('BASE_URL'); let api = {}; /** - * @api {get} /export/history.csv Export user tasks history in CSV format. - * @apiDescription History is only available for habits and dailys so todos and rewards won't be included NOTE: Part of the private API that may change at any time. + * @api {get} /export/history.csv Export user tasks history in CSV format + * @apiDescription History is only available for habits and dailys so todos and rewards won't be included. NOTE: Part of the private API that may change at any time. * @apiVersion 3.0.0 * @apiName ExportUserHistory * @apiGroup DataExport @@ -94,7 +94,7 @@ async function _getUserDataForExport (user) { } /** - * @api {get} /export/userdata.json Export user data in JSON format. + * @api {get} /export/userdata.json Export user data in JSON format * @apiVersion 3.0.0 * @apiName ExportUserDataJson * @apiGroup DataExport diff --git a/website/server/models/task.js b/website/server/models/task.js index 0459a74580..27f356efa3 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -56,7 +56,7 @@ export let TaskSchema = new Schema({ }, discriminatorOptions)); TaskSchema.plugin(baseModel, { - noSet: ['challenge', 'userId', 'completed', 'history', 'streak', 'dateCompleted', 'completed'], + noSet: ['challenge', 'userId', 'completed', 'history', 'dateCompleted', 'completed'], sanitizeTransform (taskObj) { if (taskObj.type && taskObj.type !== 'reward') { // value should be settable directly only for rewards delete taskObj.value;