Skip to content

Commit

Permalink
Merge branch 'develop' into node8
Browse files Browse the repository at this point in the history
  • Loading branch information
paglias committed Feb 24, 2018
2 parents fdbd63c + b1bd243 commit e460bd1
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 225 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [![Open Source Helpers](https://www.codetriage.com/habitrpg/habitica/badges/users.svg)](https://www.codetriage.com/habitrpg/habitica)
===============

[![Greenkeeper badge](https://badges.greenkeeper.io/HabitRPG/habitica.svg)](https://greenkeeper.io/)

[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.

We need more programmers! Your assistance will be greatly appreciated.
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.28.0",
"main": "./website/server/index.js",
"greenkeeper": {
"ignore": [
"mongoose"
]
},
"dependencies": {
"@slack/client": "^3.8.1",
"accepts": "^1.3.2",
Expand Down
16 changes: 16 additions & 0 deletions test/api/v3/integration/user/POST-user_class_cast_spellId.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,22 @@ describe('POST /user/class/cast/:spellId', () => {
expect(group.chat[0].uuid).to.equal('system');
});

it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});

await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth', {quantity: 2});

await sleep(1);
await group.sync();

expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
});

it('searing brightness does not affect challenge or group tasks', async () => {
let guild = await generateGroup(user);
let challenge = await generateChallenge(user, guild);
Expand Down
2 changes: 2 additions & 0 deletions website/common/locales/en/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
"taskAliasAlreadyUsed": "Task alias already used on another task.",
"taskNotFound": "Task not found.",
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
"invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".",
"invalidTasksTypeExtra": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\", \"completedTodos\".",
"cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.",
"checklistOnlyDailyTodo": "Checklists are supported only on Dailies and To-Dos",
"checklistItemNotFound": "No checklist item was found with given id.",
Expand Down
4 changes: 2 additions & 2 deletions website/server/controllers/api-v3/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ api.getUserTasks = {
async handler (req, res) {
let types = Tasks.tasksTypes.map(type => `${type}s`);
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
req.checkQuery('type', res.t('invalidTasksTypeExtra')).optional().isIn(types);

let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
Expand Down Expand Up @@ -324,7 +324,7 @@ api.getChallengeTasks = {
async handler (req, res) {
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
let types = Tasks.tasksTypes.map(type => `${type}s`);
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);

let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
Expand Down
2 changes: 1 addition & 1 deletion website/server/controllers/api-v3/tasks/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ api.getGroupTasks = {
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);

let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
Expand Down
222 changes: 1 addition & 221 deletions website/server/controllers/api-v3/user.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { authWithHeaders } from '../../middlewares/auth';
import common from '../../../common';
import {
NotFound,
BadRequest,
NotAuthorized,
} from '../../libs/errors';
import * as Tasks from '../../models/task';
import {
basicFields as basicGroupFields,
model as Group,
} from '../../models/group';
import { model as User } from '../../models/user';
import * as Tasks from '../../models/task';
import _ from 'lodash';
import * as passwordUtils from '../../libs/password';
import {
Expand Down Expand Up @@ -523,224 +521,6 @@ api.getUserAnonymized = {
},
};

const partyMembersFields = 'profile.name stats achievements items.special';

async function castTaskSpell (res, req, targetId, user, spell) {
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));

const task = await Tasks.Task.findOne({
_id: targetId,
userId: user._id,
}).exec();
if (!task) throw new NotFound(res.t('taskNotFound'));
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
if (task.group.id) throw new BadRequest(res.t('groupTasksNoCast'));

spell.cast(user, task, req);

const results = await Promise.all([
user.save(),
task.save(),
]);

return results;
}

async function castMultiTaskSpell (req, user, spell) {
const tasks = await Tasks.Task.find({
userId: user._id,
...Tasks.taskIsGroupOrChallengeQuery,
}).exec();

spell.cast(user, tasks, req);

const toSave = tasks
.filter(t => t.isModified())
.map(t => t.save());
toSave.unshift(user.save());
const saved = await Promise.all(toSave);

const response = {
tasks: saved,
user,
};

return response;
}

async function castSelfSpell (req, user, spell) {
spell.cast(user, null, req);
await user.save();
}

async function castPartySpell (req, party, partyMembers, user, spell) {
if (!party) {
partyMembers = [user]; // Act as solo party
} else {
partyMembers = await User
.find({
'party._id': party._id,
_id: { $ne: user._id }, // add separately
})
// .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save
// default values for non-selected fields and pre('save') will mess up thinking some values are missing
.exec();

partyMembers.unshift(user);
}

spell.cast(user, partyMembers, req);
await Promise.all(partyMembers.map(m => m.save()));

return partyMembers;
}

async function castUserSpell (res, req, party, partyMembers, targetId, user, spell) {
if (!party && (!targetId || user._id === targetId)) {
partyMembers = user;
} else {
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
if (!party) throw new NotFound(res.t('partyNotFound'));
partyMembers = await User
.findOne({_id: targetId, 'party._id': party._id})
// .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save
// default values for non-selected fields and pre('save') will mess up thinking some values are missing
.exec();
}

if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId}));

spell.cast(user, partyMembers, req);

if (partyMembers !== user) {
await Promise.all([
user.save(),
partyMembers.save(),
]);
} else {
await partyMembers.save(); // partyMembers is user
}

return partyMembers;
}

/**
* @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target
* @apiName UserCast
* @apiGroup User
*
* @apiParam (Path) {String=fireball, mpheal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast.
* @apiParam (Query) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on the user or the user's current party.
* @apiParamExample {json} Query example:
* Cast "Pickpocket" on a task:
* https://habitica.com/api/v3/user/class/cast/pickPocket?targetId=fd427623...
*
* Cast "Tools of the Trade" on the party:
* https://habitica.com/api/v3/user/class/cast/toolsOfTrade
*
* @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned.
*
* @apiDescription Skill Key to Name Mapping
* Mage
* fireball: "Burst of Flames"
* mpheal: "Ethereal Surge"
* earth: "Earthquake"
* frost: "Chilling Frost"
*
* Warrior
* smash: "Brutal Smash"
* defensiveStance: "Defensive Stance"
* valorousPresence: "Valorous Presence"
* intimidate: "Intimidating Gaze"
*
* Rogue
* pickPocket: "Pickpocket"
* backStab: "Backstab"
* toolsOfTrade: "Tools of the Trade"
* stealth: "Stealth"
*
* Healer
* heal: "Healing Light"
* protectAura: "Protective Aura"
* brightness: "Searing Brightness"
* healAll: "Blessing"
*
* @apiError (400) {NotAuthorized} Not enough mana.
* @apiUse TaskNotFound
* @apiUse PartyNotFound
* @apiUse UserNotFound
*/
api.castSpell = {
method: 'POST',
middlewares: [authWithHeaders()],
url: '/user/class/cast/:spellId',
async handler (req, res) {
let user = res.locals.user;
let spellId = req.params.spellId;
let targetId = req.query.targetId;

// optional because not required by all targetTypes, presence is checked later if necessary
req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID();

let reqValidationErrors = req.validationErrors();
if (reqValidationErrors) throw reqValidationErrors;

let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
let spell = common.content.spells[klass][spellId];

if (!spell) throw new NotFound(res.t('spellNotFound', {spellId}));
if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana'));
if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold'));
if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl}));

let targetType = spell.target;

if (targetType === 'task') {
const results = await castTaskSpell(res, req, targetId, user, spell);
res.respond(200, {
user: results[0],
task: results[1],
});
} else if (targetType === 'self') {
await castSelfSpell(req, user, spell);
res.respond(200, { user });
} else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary
const response = await castMultiTaskSpell(req, user, spell);
res.respond(200, response);
} else if (targetType === 'party' || targetType === 'user') {
const party = await Group.getGroup({groupId: 'party', user});
// arrays of users when targetType is 'party' otherwise single users
let partyMembers;

if (targetType === 'party') {
partyMembers = await castPartySpell(req, party, partyMembers, user, spell);
} else {
partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell);
}

let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];

// Only return some fields.
// See comment above on why we can't just select the necessary fields when querying
partyMembersRes = partyMembersRes.map(partyMember => {
return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields));
});

res.respond(200, {
partyMembers: partyMembersRes,
user,
});

if (party && !spell.silent) {
let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``;
party.sendChat(message);
await party.save();
}
}
},
};

/**
* @api {post} /api/v3/user/sleep Make the user start / stop sleeping (resting in the Inn)
* @apiName UserSleep
Expand Down
Loading

0 comments on commit e460bd1

Please sign in to comment.