diff --git a/common/script/content/spells.js b/common/script/content/spells.js index 9a58e85394..7609bd844a 100644 --- a/common/script/content/spells.js +++ b/common/script/content/spells.js @@ -218,10 +218,10 @@ spells.healer = { text: t('spellHealerBrightnessText'), mana: 15, lvl: 12, - target: 'self', + target: 'tasks', notes: t('spellHealerBrightnessNotes'), - cast (user) { - _.each(user.tasks, (task) => { + cast (user, tasks) { + _.each(tasks, (task) => { if (task.type !== 'reward') { task.value += 4 * (user._statsComputed.int / (user._statsComputed.int + 40)); } diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 39efc002f9..bcdad4d9f1 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -1,6 +1,15 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth'; import cron from '../../middlewares/api-v3/cron'; import common from '../../../../common'; +import { + NotFound, + BadRequest, +} from '../../libs/api-v3/errors'; +import * as Tasks from '../../models/task'; +import { model as Group } from '../../models/group'; +import { model as User } from '../../models/user'; +import Q from 'q'; +import _ from 'lodash'; let api = {}; @@ -31,4 +40,117 @@ api.getUser = { }, }; +const partyMembersFields = 'profile.name stats achievements items.special'; + +/** + * @api {post} /user/class/cast/:spell Cast a 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. + * + * @apiSuccess {Object|Array} mixed Will return the modified targets. For party members only the necessary fields will be populated. + */ +api.castSpell = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/class/cast/:spell', + 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', {spell: spellId})); + if (spell.mana > user.stats.mp) throw new BadRequest(res.t('notEnoughMana')); + let targetType = spell.target; + + if (targetType === 'task') { + if (!targetId) throw new BadRequest(res.t('targetIdUUID')); + + // TODO what about challenge tasks? should casting be disabled on them? + let 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')); + + spell.cast(user, task); + await task.save(); + res.respond(200, task); + } else if (targetType === 'self') { + spell.cast(user); + await user.save(); + res.respond(200, user); + } else if (targetType === 'tasks') { // new target type when all the user's tasks are necessary + let tasks = await Tasks.Task.find({ + userId: user._id, + 'challenge.id': {$exists: false}, // exclude challenge tasks + $or: [ // Exclude completed todos + {type: 'todo', completed: false}, + {type: {$in: ['habit', 'daily', 'reward']}}, + ], + }).exec(); + + spell.cast(user, tasks); + + let toSave = tasks.filter(t => t.isModified()); + let isUserModified = user.isModified(); + toSave.unshift(user.save()); + let saved = await Q.all(toSave); + + let response = { + tasks: isUserModified ? _.rest(saved) : saved, + }; + if (isUserModified) res.user = user; + res.respond(200, response); + } else if (targetType === 'party' || targetType === 'user') { + let party = await Group.getGroup({_id: 'party', user}); + + // arrays of users when targetType is 'party' otherwise single users + let partyMembers; + + if (targetType === 'party') { + if (!party) { + partyMembers = [user]; // Act as solo party + } else { + partyMembers = await User.find({'party._id': party._id}).select(partyMembersFields).exec(); + } + + spell.cast(user, partyMembers); + await Q.all(partyMembers.map(m => m.save())); + } else { + if (!party && (!targetId || user._id === targetId)) { + partyMembers = user; + } else { + if (!targetId) throw new BadRequest(res.t('targetIdUUID')); + partyMembers = await User.findOne({_id: targetId, 'party._id': party._id}).select(partyMembersFields).exec(); + } + + if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId})); + spell.cast(user, partyMembers); + await partyMembers.save(); + } + res.respond(200, partyMembers); + + 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(); + } + } + }, +}; + export default api;