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:
Keith Holliday
2017-10-31 12:57:44 -06:00
committed by GitHub
parent 365daba6fc
commit 3e37941e0a
16 changed files with 342 additions and 108 deletions

View File

@@ -1,7 +1,7 @@
import { import {
generateUser, generateUser,
translate as t, translate as t,
} from '../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
describe('POST /user/allocate', () => { describe('POST /user/allocate', () => {
let user; let user;

View File

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

View File

@@ -1,6 +1,6 @@
import { import {
generateUser, generateUser,
} from '../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
describe('POST /user/allocate-now', () => { describe('POST /user/allocate-now', () => {
// More tests in common code unit tests // More tests in common code unit tests

View File

@@ -1,12 +1,12 @@
import allocate from '../../../website/common/script/ops/allocate'; import allocate from '../../../../website/common/script/ops/stats/allocate';
import { import {
BadRequest, BadRequest,
NotAuthorized, NotAuthorized,
} from '../../../website/common/script/libs/errors'; } from '../../../../website/common/script/libs/errors';
import i18n from '../../../website/common/script/i18n'; import i18n from '../../../../website/common/script/i18n';
import { import {
generateUser, generateUser,
} from '../../helpers/common.helper'; } from '../../../helpers/common.helper';
describe('shared.ops.allocate', () => { describe('shared.ops.allocate', () => {
let user; let user;

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

View File

@@ -1,7 +1,7 @@
import allocateNow from '../../../website/common/script/ops/allocateNow'; import allocateNow from '../../../../website/common/script/ops/stats/allocateNow';
import { import {
generateUser, generateUser,
} from '../../helpers/common.helper'; } from '../../../helpers/common.helper';
describe('shared.ops.allocateNow', () => { describe('shared.ops.allocateNow', () => {
let user; let user;

View File

@@ -559,7 +559,7 @@ import keys from 'lodash/keys';
import { beastMasterProgress, mountMasterProgress } from '../../../common/script/count'; import { beastMasterProgress, mountMasterProgress } from '../../../common/script/count';
import statsComputed from '../../../common/script/libs/statsComputed'; import statsComputed from '../../../common/script/libs/statsComputed';
import autoAllocate from '../../../common/script/fns/autoAllocate'; 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 MemberDetails from '../memberDetails';
import bPopover from 'bootstrap-vue/lib/components/popover'; import bPopover from 'bootstrap-vue/lib/components/popover';

View File

@@ -184,7 +184,7 @@ import { beastMasterProgress, mountMasterProgress } from '../../../common/script
import statsComputed from '../../../common/script/libs/statsComputed'; import statsComputed from '../../../common/script/libs/statsComputed';
import autoAllocate from '../../../common/script/fns/autoAllocate'; import autoAllocate from '../../../common/script/fns/autoAllocate';
import changeClass from '../../../common/script/ops/changeClass'; 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 DROP_ANIMALS = keys(Content.pets);
const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length; const TOTAL_NUMBER_OF_DROP_ANIMALS = DROP_ANIMALS.length;

View File

@@ -222,5 +222,6 @@
"allocated": "Allocated", "allocated": "Allocated",
"buffs": "Buffs", "buffs": "Buffs",
"pointsAvailable": "Points Available", "pointsAvailable": "Points Available",
"pts": "pts" "pts": "pts",
"statsObjectRequired": "Stats update is required"
} }

View File

@@ -137,7 +137,9 @@ api.fns = {
import scoreTask from './ops/scoreTask'; import scoreTask from './ops/scoreTask';
import sleep from './ops/sleep'; 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 buy from './ops/buy';
import buyGear from './ops/buyGear'; import buyGear from './ops/buyGear';
import buyHealthPotion from './ops/buyHealthPotion'; import buyHealthPotion from './ops/buyHealthPotion';
@@ -145,7 +147,6 @@ import buyArmoire from './ops/buyArmoire';
import buyMysterySet from './ops/buyMysterySet'; import buyMysterySet from './ops/buyMysterySet';
import buyQuest from './ops/buyQuest'; import buyQuest from './ops/buyQuest';
import buySpecialSpell from './ops/buySpecialSpell'; import buySpecialSpell from './ops/buySpecialSpell';
import allocateNow from './ops/allocateNow';
import hatch from './ops/hatch'; import hatch from './ops/hatch';
import feed from './ops/feed'; import feed from './ops/feed';
import equip from './ops/equip'; import equip from './ops/equip';
@@ -176,6 +177,7 @@ api.ops = {
scoreTask, scoreTask,
sleep, sleep,
allocate, allocate,
allocateBulk,
buy, buy,
buyGear, buyGear,
buyHealthPotion, buyHealthPotion,

View File

@@ -3,7 +3,9 @@ import revive from './revive';
import reset from './reset'; import reset from './reset';
import reroll from './reroll'; import reroll from './reroll';
import rebirth from './rebirth'; 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 sortTask from './sortTask';
import updateTask from './updateTask'; import updateTask from './updateTask';
import deleteTask from './deleteTask'; import deleteTask from './deleteTask';
@@ -35,7 +37,6 @@ import hatch from './hatch';
import unlock from './unlock'; import unlock from './unlock';
import changeClass from './changeClass'; import changeClass from './changeClass';
import disableClasses from './disableClasses'; import disableClasses from './disableClasses';
import allocate from './allocate';
import readCard from './readCard'; import readCard from './readCard';
import openMysteryItem from './openMysteryItem'; import openMysteryItem from './openMysteryItem';
import scoreTask from './scoreTask'; import scoreTask from './scoreTask';
@@ -49,6 +50,7 @@ module.exports = {
reroll, reroll,
rebirth, rebirth,
allocateNow, allocateNow,
allocateBulk,
sortTask, sortTask,
updateTask, updateTask,
deleteTask, deleteTask,

View File

@@ -1,12 +1,12 @@
import get from 'lodash/get'; import get from 'lodash/get';
import { import {
ATTRIBUTES, ATTRIBUTES,
} from '../constants'; } from '../../constants';
import { import {
BadRequest, BadRequest,
NotAuthorized, NotAuthorized,
} from '../libs/errors'; } from '../../libs/errors';
import i18n from '../i18n'; import i18n from '../../i18n';
module.exports = function allocate (user, req = {}) { module.exports = function allocate (user, req = {}) {
let stat = get(req, 'query.stat', 'str'); let stat = get(req, 'query.stat', 'str');

View 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,
];
};

View File

@@ -1,5 +1,5 @@
import times from 'lodash/times'; import times from 'lodash/times';
import autoAllocate from '../fns/autoAllocate'; import autoAllocate from '../../fns/autoAllocate';
module.exports = function allocateNow (user) { module.exports = function allocateNow (user) {
times(user.stats.points, () => autoAllocate(user)); times(user.stats.points, () => autoAllocate(user));

View File

@@ -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 * @api {post} /api/v3/user/buy/:key Buy gear, armoire or potion
* @apiDescription Under the hood uses UserBuyGear, UserBuyPotion and UserBuyArmoire * @apiDescription Under the hood uses UserBuyGear, UserBuyPotion and UserBuyArmoire

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