mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 22:27:26 +01:00
Bulk stats (#9260)
* Reorganized stats * Organized allocation common code * Added bulk allocate to common * Added allocate bulk route * Fixed structure and lint * Fixed import and apidoc
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate', () => {
|
||||
let user;
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
generateUser,
|
||||
translate as t,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate-bulk', () => {
|
||||
let user;
|
||||
const statsUpdate = {
|
||||
stats: {
|
||||
con: 1,
|
||||
str: 2,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await generateUser();
|
||||
});
|
||||
|
||||
// More tests in common code unit tests
|
||||
|
||||
it('returns an error if user does not have enough points', async () => {
|
||||
await expect(user.post('/user/allocate-bulk', statsUpdate))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notEnoughAttrPoints'),
|
||||
});
|
||||
});
|
||||
|
||||
it('allocates attribute points', async () => {
|
||||
await user.update({'stats.points': 3});
|
||||
|
||||
await user.post('/user/allocate-bulk', statsUpdate);
|
||||
await user.sync();
|
||||
|
||||
expect(user.stats.con).to.equal(1);
|
||||
expect(user.stats.str).to.equal(2);
|
||||
expect(user.stats.points).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
|
||||
describe('POST /user/allocate-now', () => {
|
||||
// More tests in common code unit tests
|
||||
@@ -1,12 +1,12 @@
|
||||
import allocate from '../../../website/common/script/ops/allocate';
|
||||
import allocate from '../../../../website/common/script/ops/stats/allocate';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocate', () => {
|
||||
let user;
|
||||
98
test/common/ops/stats/allocateBulk.js
Normal file
98
test/common/ops/stats/allocateBulk.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import allocateBulk from '../../../../website/common/script/ops/stats/allocateBulk';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocateBulk', () => {
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
});
|
||||
|
||||
it('throws an error if an invalid attribute is supplied', (done) => {
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
invalid: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidAttribute', {attr: 'invalid'}));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the stats are not supplied', (done) => {
|
||||
try {
|
||||
allocateBulk(user);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('statsObjectRequired'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have attribute points', (done) => {
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughAttrPoints'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('throws an error if the user doesn\'t have enough attribute points', (done) => {
|
||||
user.stats.points = 1;
|
||||
try {
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('notEnoughAttrPoints'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('allocates attribute points', () => {
|
||||
user.stats.points = 3;
|
||||
expect(user.stats.int).to.equal(0);
|
||||
expect(user.stats.str).to.equal(0);
|
||||
|
||||
allocateBulk(user, {
|
||||
body: {
|
||||
stats: {
|
||||
int: 1,
|
||||
str: 2,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(user.stats.str).to.equal(2);
|
||||
expect(user.stats.int).to.equal(1);
|
||||
expect(user.stats.points).to.equal(0);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import allocateNow from '../../../website/common/script/ops/allocateNow';
|
||||
import allocateNow from '../../../../website/common/script/ops/stats/allocateNow';
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../helpers/common.helper';
|
||||
} from '../../../helpers/common.helper';
|
||||
|
||||
describe('shared.ops.allocateNow', () => {
|
||||
let user;
|
||||
@@ -559,7 +559,7 @@ import keys from 'lodash/keys';
|
||||
import { beastMasterProgress, mountMasterProgress } from '../../../common/script/count';
|
||||
import statsComputed from '../../../common/script/libs/statsComputed';
|
||||
import autoAllocate from '../../../common/script/fns/autoAllocate';
|
||||
import allocate from '../../../common/script/ops/allocate';
|
||||
import allocate from '../../../common/script/ops/stats/allocate';
|
||||
|
||||
import MemberDetails from '../memberDetails';
|
||||
import bPopover from 'bootstrap-vue/lib/components/popover';
|
||||
|
||||
@@ -184,7 +184,7 @@ import { beastMasterProgress, mountMasterProgress } from '../../../common/script
|
||||
import statsComputed from '../../../common/script/libs/statsComputed';
|
||||
import autoAllocate from '../../../common/script/fns/autoAllocate';
|
||||
import changeClass from '../../../common/script/ops/changeClass';
|
||||
import allocate from '../../../common/script/ops/allocate';
|
||||
import allocate from '../../../common/script/ops/stats/allocate';
|
||||
|
||||
const DROP_ANIMALS = keys(Content.pets);
|
||||
const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length;
|
||||
|
||||
@@ -222,5 +222,6 @@
|
||||
"allocated": "Allocated",
|
||||
"buffs": "Buffs",
|
||||
"pointsAvailable": "Points Available",
|
||||
"pts": "pts"
|
||||
"pts": "pts",
|
||||
"statsObjectRequired": "Stats update is required"
|
||||
}
|
||||
|
||||
@@ -137,7 +137,9 @@ api.fns = {
|
||||
|
||||
import scoreTask from './ops/scoreTask';
|
||||
import sleep from './ops/sleep';
|
||||
import allocate from './ops/allocate';
|
||||
import allocateNow from './ops/stats/allocateNow';
|
||||
import allocate from './ops/stats/allocate';
|
||||
import allocateBulk from './ops/stats/allocateBulk';
|
||||
import buy from './ops/buy';
|
||||
import buyGear from './ops/buyGear';
|
||||
import buyHealthPotion from './ops/buyHealthPotion';
|
||||
@@ -145,7 +147,6 @@ import buyArmoire from './ops/buyArmoire';
|
||||
import buyMysterySet from './ops/buyMysterySet';
|
||||
import buyQuest from './ops/buyQuest';
|
||||
import buySpecialSpell from './ops/buySpecialSpell';
|
||||
import allocateNow from './ops/allocateNow';
|
||||
import hatch from './ops/hatch';
|
||||
import feed from './ops/feed';
|
||||
import equip from './ops/equip';
|
||||
@@ -176,6 +177,7 @@ api.ops = {
|
||||
scoreTask,
|
||||
sleep,
|
||||
allocate,
|
||||
allocateBulk,
|
||||
buy,
|
||||
buyGear,
|
||||
buyHealthPotion,
|
||||
|
||||
@@ -3,7 +3,9 @@ import revive from './revive';
|
||||
import reset from './reset';
|
||||
import reroll from './reroll';
|
||||
import rebirth from './rebirth';
|
||||
import allocateNow from './allocateNow';
|
||||
import allocate from './stats/allocate';
|
||||
import allocateBulk from './stats/allocateBulk';
|
||||
import allocateNow from './stats/allocateNow';
|
||||
import sortTask from './sortTask';
|
||||
import updateTask from './updateTask';
|
||||
import deleteTask from './deleteTask';
|
||||
@@ -35,7 +37,6 @@ import hatch from './hatch';
|
||||
import unlock from './unlock';
|
||||
import changeClass from './changeClass';
|
||||
import disableClasses from './disableClasses';
|
||||
import allocate from './allocate';
|
||||
import readCard from './readCard';
|
||||
import openMysteryItem from './openMysteryItem';
|
||||
import scoreTask from './scoreTask';
|
||||
@@ -49,6 +50,7 @@ module.exports = {
|
||||
reroll,
|
||||
rebirth,
|
||||
allocateNow,
|
||||
allocateBulk,
|
||||
sortTask,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
ATTRIBUTES,
|
||||
} from '../constants';
|
||||
} from '../../constants';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../libs/errors';
|
||||
import i18n from '../i18n';
|
||||
} from '../../libs/errors';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
module.exports = function allocate (user, req = {}) {
|
||||
let stat = get(req, 'query.stat', 'str');
|
||||
44
website/common/script/ops/stats/allocateBulk.js
Normal file
44
website/common/script/ops/stats/allocateBulk.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
ATTRIBUTES,
|
||||
} from '../../constants';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
module.exports = function allocateBulk (user, req = {}) {
|
||||
const stats = get(req, 'body.stats');
|
||||
if (!stats) throw new BadRequest(i18n.t('statsObjectRequired', req.language));
|
||||
|
||||
const statKeys = Object.keys(stats);
|
||||
const invalidStats = statKeys.filter(statKey => {
|
||||
return ATTRIBUTES.indexOf(statKey) === -1;
|
||||
});
|
||||
if (invalidStats.length > 0) {
|
||||
throw new BadRequest(i18n.t('invalidAttribute', {attr: invalidStats.join(',')}, req.language));
|
||||
}
|
||||
|
||||
if (user.stats.points <= 0) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughAttrPoints', req.language));
|
||||
}
|
||||
|
||||
const newStatValues = Object.values(stats);
|
||||
const totalPointsToAllocate = newStatValues.reduce((sum, value) => {
|
||||
return sum + value;
|
||||
}, 0);
|
||||
if (user.stats.points < totalPointsToAllocate) {
|
||||
throw new NotAuthorized(i18n.t('notEnoughAttrPoints', req.language));
|
||||
}
|
||||
|
||||
for (let [stat, value] of Object.entries(stats)) {
|
||||
user.stats[stat] += value;
|
||||
user.stats.points -= value;
|
||||
if (stat === 'int') user.stats.mp += value;
|
||||
}
|
||||
|
||||
return [
|
||||
user.stats,
|
||||
];
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import times from 'lodash/times';
|
||||
import autoAllocate from '../fns/autoAllocate';
|
||||
import autoAllocate from '../../fns/autoAllocate';
|
||||
|
||||
module.exports = function allocateNow (user) {
|
||||
times(user.stats.points, () => autoAllocate(user));
|
||||
@@ -746,95 +746,6 @@ api.sleep = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate Allocate a single attribute point
|
||||
* @apiName UserAllocate
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam (Body) {String="str","con","int","per"} stat Query parameter - Default ='str'
|
||||
*
|
||||
* @apiParamExample {json} Example request
|
||||
* {"stat":"int"}
|
||||
*
|
||||
* @apiSuccess {Object} data Returns stats from the user profile
|
||||
*
|
||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "You don't have enough attribute points."
|
||||
* }
|
||||
*/
|
||||
api.allocate = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let allocateRes = common.ops.allocate(user, req);
|
||||
await user.save();
|
||||
res.respond(200, ...allocateRes);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @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. Note: will return success, even if there are 0 points to allocate.
|
||||
* @apiName UserAllocateNow
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "hp": 50,
|
||||
* "mp": 38,
|
||||
* "exp": 7,
|
||||
* "gp": 284.8637271160258,
|
||||
* "lvl": 10,
|
||||
* "class": "rogue",
|
||||
* "points": 0,
|
||||
* "str": 2,
|
||||
* "con": 2,
|
||||
* "int": 3,
|
||||
* "per": 3,
|
||||
* "buffs": {
|
||||
* "str": 0,
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "con": 0,
|
||||
* "stealth": 0,
|
||||
* "streaks": false,
|
||||
* "snowball": false,
|
||||
* "spookySparkles": false,
|
||||
* "shinySeed": false,
|
||||
* "seafoam": false
|
||||
* },
|
||||
* "training": {
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "str": 0,
|
||||
* "con": 0
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data user.stats
|
||||
*/
|
||||
api.allocateNow = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate-now',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let allocateNowRes = common.ops.allocateNow(user);
|
||||
await user.save();
|
||||
res.respond(200, ...allocateNowRes);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/buy/:key Buy gear, armoire or potion
|
||||
* @apiDescription Under the hood uses UserBuyGear, UserBuyPotion and UserBuyArmoire
|
||||
|
||||
136
website/server/controllers/api-v3/user/stats.js
Normal file
136
website/server/controllers/api-v3/user/stats.js
Normal file
@@ -0,0 +1,136 @@
|
||||
// @TODO: Can we import only the functions we need instead of the large object?
|
||||
import common from '../../../../common';
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate Allocate a single attribute point
|
||||
* @apiName UserAllocate
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam (Body) {String="str","con","int","per"} stat Query parameter - Default ='str'
|
||||
*
|
||||
* @apiParamExample {json} Example request
|
||||
* {"stat":"int"}
|
||||
*
|
||||
* @apiSuccess {Object} data Returns stats from the user profile
|
||||
*
|
||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "You don't have enough attribute points."
|
||||
* }
|
||||
*/
|
||||
api.allocate = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let allocateRes = common.ops.allocate(user, req);
|
||||
await user.save();
|
||||
res.respond(200, ...allocateRes);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/allocate-bulk Allocate multiple attribute points
|
||||
* @apiName UserAllocateBulk
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiParam (Body) { Object } stats Body parameter
|
||||
*
|
||||
* @apiParamExample {json} Example request
|
||||
* {
|
||||
* stats: {
|
||||
* 'int': int,
|
||||
* 'str': int,
|
||||
* 'con': int,
|
||||
* 'per': int,
|
||||
* },
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data Returns stats from the user profile
|
||||
*
|
||||
* @apiError {NotAuthorized} NoPoints Not enough attribute points to increment a stat.
|
||||
*
|
||||
* @apiErrorExample {json}
|
||||
* {
|
||||
* "success": false,
|
||||
* "error": "NotAuthorized",
|
||||
* "message": "You don't have enough attribute points."
|
||||
* }
|
||||
*/
|
||||
api.allocateBulk = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate-bulk',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let allocateRes = common.ops.allocateBulk(user, req);
|
||||
await user.save();
|
||||
res.respond(200, ...allocateRes);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @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. Note: will return success, even if there are 0 points to allocate.
|
||||
* @apiName UserAllocateNow
|
||||
* @apiGroup User
|
||||
*
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
* {
|
||||
* "success": true,
|
||||
* "data": {
|
||||
* "hp": 50,
|
||||
* "mp": 38,
|
||||
* "exp": 7,
|
||||
* "gp": 284.8637271160258,
|
||||
* "lvl": 10,
|
||||
* "class": "rogue",
|
||||
* "points": 0,
|
||||
* "str": 2,
|
||||
* "con": 2,
|
||||
* "int": 3,
|
||||
* "per": 3,
|
||||
* "buffs": {
|
||||
* "str": 0,
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "con": 0,
|
||||
* "stealth": 0,
|
||||
* "streaks": false,
|
||||
* "snowball": false,
|
||||
* "spookySparkles": false,
|
||||
* "shinySeed": false,
|
||||
* "seafoam": false
|
||||
* },
|
||||
* "training": {
|
||||
* "int": 0,
|
||||
* "per": 0,
|
||||
* "str": 0,
|
||||
* "con": 0
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @apiSuccess {Object} data user.stats
|
||||
*/
|
||||
api.allocateNow = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/allocate-now',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let allocateNowRes = common.ops.allocateNow(user);
|
||||
await user.save();
|
||||
res.respond(200, ...allocateNowRes);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
Reference in New Issue
Block a user