Merge branch 'api-v3' into Alys-v3-armoire-fix

This commit is contained in:
Blade Barringer
2016-05-16 14:10:39 -05:00
28 changed files with 253 additions and 171 deletions

View File

@@ -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));
},
},
};

View File

@@ -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;

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;
}
}
});

View File

@@ -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);
}
});

View File

@@ -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);
});

View File

@@ -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);

View File

@@ -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);

View File

@@ -31,6 +31,7 @@ describe('POST /user/rebirth', () => {
let daily = await generateDaily({
text: 'test habit',
type: 'daily',
value: 1,
streak: 1,
userId: user._id,
});

View File

@@ -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'));

View File

@@ -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);
});
});

View File

@@ -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;

View File

@@ -176,6 +176,7 @@ habitrpg.controller('SettingsCtrl',
$scope.reset = function(){
User.reset({});
User.sync();
$rootScope.$state.go('tasks');
}

View File

@@ -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});
}
}]);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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);
},

View File

@@ -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

View File

@@ -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;