Files
habitica/website/server/libs/spells.js
Phillip Thelen 8150fef993 Database Access optimisations (#14544)
* Optimize database access during spell casting

* load less data when casting spells

* Begin migrating update calls to updateOne and updateMany

* Only update user objects that don’t have notification yet

* fix test

* fix spy

* Don’t unnecessarily update user when requesting invalid guild

* fix sort order for middlewares to not load user twice every request

* fix tests

* fix integration test

* fix skill usage not always deducting mp

* addtest case for blessing spell

* fix healAll

* fix lint

* Fix error for when some spells are used outside of party

* Add check to not run bulk spells in web client

* fix(tags): change const to let

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-05-16 12:21:45 -05:00

280 lines
8.1 KiB
JavaScript

import { model as User } from '../models/user';
import * as Tasks from '../models/task';
import {
NotFound,
BadRequest,
NotAuthorized,
} from './errors';
import common from '../../common';
import {
model as Group,
} from '../models/group';
import apiError from './apiError';
const partyMembersFields = 'profile.name stats achievements items.special pinnedItems notifications flags';
// Excluding notifications and flags from the list of public fields to return.
const partyMembersPublicFields = 'profile.name stats achievements items.special';
// @TODO: After refactoring individual spells, move quantity to the calculations
async function castTaskSpell (res, req, targetId, user, spell, quantity = 1) {
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
const task = await Tasks.Task.findOne({
userId: user._id,
_id: targetId,
}).exec();
if (!task) throw new NotFound(res.t('messageTaskNotFound'));
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
if (task.group.id) throw new BadRequest(res.t('groupTasksNoCast'));
for (let i = 0; i < quantity; i += 1) {
spell.cast(user, task, req);
}
const results = await Promise.all([
user.save(),
task.save(),
]);
return results;
}
async function castMultiTaskSpell (req, user, spell, quantity = 1) {
const tasks = await Tasks.Task.find({
userId: user._id,
...Tasks.taskIsGroupOrChallengeQuery,
}).exec();
for (let i = 0; i < quantity; i += 1) {
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, quantity = 1) {
for (let i = 0; i < quantity; i += 1) {
spell.cast(user, null, req);
}
common.setDebuffPotionItems(user);
await user.save();
}
async function getPartyMembers (user, party) {
let partyMembers;
if (!party) {
// Act as solo party
partyMembers = [user];
} else {
partyMembers = await User
.find({
'party._id': party._id,
_id: { $ne: user._id }, // add separately
})
.select(partyMembersFields)
.exec();
partyMembers.unshift(user);
}
return partyMembers;
}
async function castPartySpell (req, party, user, spell, quantity = 1) {
let partyMembers;
if (spell.bulk) {
const data = { };
if (party) {
data.query = { 'party._id': party._id };
} else {
data.query = { _id: user._id };
}
spell.cast(user, data);
await User.updateMany(data.query, data.update);
await user.save();
partyMembers = await getPartyMembers(user, party);
} else {
partyMembers = await getPartyMembers(user, party);
for (let i = 0; i < quantity; i += 1) {
spell.cast(user, partyMembers, req);
}
await Promise.all(partyMembers.map(m => m.save()));
}
return partyMembers;
}
async function castUserSpell (res, req, party, targetId, user, spell, quantity = 1) {
let partyMembers;
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)
.exec();
}
if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', { userId: targetId }));
for (let i = 0; i < quantity; i += 1) {
spell.cast(user, partyMembers, req);
}
common.setDebuffPotionItems(partyMembers);
if (partyMembers !== user) {
await Promise.all([
user.save(),
partyMembers.save(),
]);
} else {
await partyMembers.save(); // partyMembers is user
}
return partyMembers;
}
async function castSpell (req, res, { isV3 = false }) {
const { user } = res.locals;
const { spellId } = req.params;
const { targetId } = req.query;
const quantity = req.body.quantity || 1;
// optional because not required by all targetTypes, presence is checked later if necessary
req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID();
const reqValidationErrors = req.validationErrors();
if (reqValidationErrors) throw reqValidationErrors;
const klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
const spell = common.content.spells[klass][spellId];
if (!spell) throw new NotFound(apiError('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 }));
const targetType = spell.target;
if (targetType === 'task') {
const results = await castTaskSpell(res, req, targetId, user, spell, quantity);
let userToJson = results[0];
if (isV3) userToJson = await userToJson.toJSONWithInbox();
res.respond(200, {
user: userToJson,
task: results[1],
});
} else if (targetType === 'self') {
const spellName = spell.key;
// Check if stealth skill has been previously casted or not.
// See #12361 for more details.
if (spellName === 'stealth') {
const incompleteDailiesDue = await Tasks.Task.countDocuments({
userId: user._id,
type: 'daily',
completed: false,
isDue: true,
}).exec();
if (user.stats.buffs.stealth >= incompleteDailiesDue) {
throw new BadRequest(res.t('spellAlreadyCast'));
}
}
await castSelfSpell(req, user, spell, quantity);
let userToJson = user;
if (isV3) userToJson = await userToJson.toJSONWithInbox();
res.respond(200, {
user: userToJson,
});
} else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary
const response = await castMultiTaskSpell(req, user, spell, quantity);
if (isV3) response.user = await response.user.toJSONWithInbox();
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, user, spell, quantity);
} else {
partyMembers = await castUserSpell(
res, req, party,
targetId, user, spell, quantity,
);
}
let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];
// Only return some fields.
// We can't just return the selected fields because they're private
partyMembersRes = partyMembersRes
.map(partyMember => common.pickDeep(
partyMember.toJSON(),
common.$w(partyMembersPublicFields),
));
let userToJson = user;
if (isV3) userToJson = await userToJson.toJSONWithInbox();
res.respond(200, {
partyMembers: partyMembersRes,
user: userToJson,
});
if (party && !spell.silent) {
if (targetType === 'user') {
const newChatMessage = party.sendChat({
message: `\`${common.i18n.t('chatCastSpellUser', { username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name }, 'en')}\``,
info: {
type: 'spell_cast_user',
user: user.profile.name,
class: klass,
spell: spellId,
target: partyMembers.profile.name,
},
});
await newChatMessage.save();
} else {
const newChatMessage = party.sendChat({
message: `\`${common.i18n.t('chatCastSpellParty', { username: user.profile.name, spell: spell.text() }, 'en')}\``,
info: {
type: 'spell_cast_party',
user: user.profile.name,
class: klass,
spell: spellId,
},
});
await newChatMessage.save();
}
}
}
}
export {
castTaskSpell,
castMultiTaskSpell,
castSelfSpell,
castPartySpell,
castUserSpell,
castSpell,
};