Merge remote-tracking branch 'origin/develop' into negue/ci-cache

This commit is contained in:
negue
2023-05-30 23:10:38 +02:00
78 changed files with 2442 additions and 1164 deletions
@@ -0,0 +1,158 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230522_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Parrot-Base']
&& pets['Parrot-CottonCandyBlue']
&& pets['Parrot-CottonCandyPink']
&& pets['Parrot-Desert']
&& pets['Parrot-Golden']
&& pets['Parrot-Red']
&& pets['Parrot-Shade']
&& pets['Parrot-Skeleton']
&& pets['Parrot-White']
&& pets['Parrot-Zombie']
&& pets['Rooster-Base']
&& pets['Rooster-CottonCandyBlue']
&& pets['Rooster-CottonCandyPink']
&& pets['Rooster-Desert']
&& pets['Rooster-Golden']
&& pets['Rooster-Red']
&& pets['Rooster-Shade']
&& pets['Rooster-Skeleton']
&& pets['Rooster-White']
&& pets['Rooster-Zombie']
&& pets['Triceratops-Base']
&& pets['Triceratops-CottonCandyBlue']
&& pets['Triceratops-CottonCandyPink']
&& pets['Triceratops-Desert']
&& pets['Triceratops-Golden']
&& pets['Triceratops-Red']
&& pets['Triceratops-Shade']
&& pets['Triceratops-Skeleton']
&& pets['Triceratops-White']
&& pets['Triceratops-Zombie']
&& pets['TRex-Base']
&& pets['TRex-CottonCandyBlue']
&& pets['TRex-CottonCandyPink']
&& pets['TRex-Desert']
&& pets['TRex-Golden']
&& pets['TRex-Red']
&& pets['TRex-Shade']
&& pets['TRex-Skeleton']
&& pets['TRex-White']
&& pets['TRex-Zombie']
&& pets['Pterodactyl-Base']
&& pets['Pterodactyl-CottonCandyBlue']
&& pets['Pterodactyl-CottonCandyPink']
&& pets['Pterodactyl-Desert']
&& pets['Pterodactyl-Golden']
&& pets['Pterodactyl-Red']
&& pets['Pterodactyl-Shade']
&& pets['Pterodactyl-Skeleton']
&& pets['Pterodactyl-White']
&& pets['Pterodactyl-Zombie']
&& pets['Owl-Base']
&& pets['Owl-CottonCandyBlue']
&& pets['Owl-CottonCandyPink']
&& pets['Owl-Desert']
&& pets['Owl-Golden']
&& pets['Owl-Red']
&& pets['Owl-Shade']
&& pets['Owl-Skeleton']
&& pets['Owl-White']
&& pets['Owl-Zombie']
&& pets['Velociraptor-Base']
&& pets['Velociraptor-CottonCandyBlue']
&& pets['Velociraptor-CottonCandyPink']
&& pets['Velociraptor-Desert']
&& pets['Velociraptor-Golden']
&& pets['Velociraptor-Red']
&& pets['Velociraptor-Shade']
&& pets['Velociraptor-Skeleton']
&& pets['Velociraptor-White']
&& pets['Velociraptor-Zombie']
&& pets['Penguin-Base']
&& pets['Penguin-CottonCandyBlue']
&& pets['Penguin-CottonCandyPink']
&& pets['Penguin-Desert']
&& pets['Penguin-Golden']
&& pets['Penguin-Red']
&& pets['Penguin-Shade']
&& pets['Penguin-Skeleton']
&& pets['Penguin-White']
&& pets['Penguin-Zombie']
&& pets['Falcon-Base']
&& pets['Falcon-CottonCandyBlue']
&& pets['Falcon-CottonCandyPink']
&& pets['Falcon-Desert']
&& pets['Falcon-Golden']
&& pets['Falcon-Red']
&& pets['Falcon-Shade']
&& pets['Falcon-Skeleton']
&& pets['Falcon-White']
&& pets['Falcon-Zombie']
&& pets['Peacock-Base']
&& pets['Peacock-CottonCandyBlue']
&& pets['Peacock-CottonCandyPink']
&& pets['Peacock-Desert']
&& pets['Peacock-Golden']
&& pets['Peacock-Red']
&& pets['Peacock-Shade']
&& pets['Peacock-Skeleton']
&& pets['Peacock-White']
&& pets['Peacock-Zombie']) {
set['achievements.dinosaurDynasty'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};
+578 -524
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -1,11 +1,11 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.269.0",
"version": "4.272.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.21.4",
"@babel/preset-env": "^7.20.2",
"@babel/core": "^7.21.8",
"@babel/preset-env": "^7.21.5",
"@babel/register": "^7.21.0",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
@@ -42,7 +42,7 @@
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"jwks-rsa": "^2.1.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
@@ -67,8 +67,8 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^11.10.0",
"superagent": "^8.0.6",
"stripe": "^12.6.0",
"superagent": "^8.0.9",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
@@ -122,7 +122,7 @@
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.0.4",
"sinon": "^15.1.0",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
},
+11 -1
View File
@@ -748,9 +748,19 @@ describe('payments/index', () => {
});
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('resets plans.consecutive.offset if 1 month subscription', async () => {
user.purchased.plan.consecutive.offset = 1;
await user.save();
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
+1 -1
View File
@@ -242,7 +242,7 @@ describe('cron middleware', () => {
sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'update')
sandbox.stub(User, 'updateOne')
.withArgs({
_id: user._id,
$or: [
+18 -18
View File
@@ -1732,7 +1732,7 @@ describe('Group Model', () => {
});
it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
await party.startQuest(nonParticipatingMember);
@@ -1740,7 +1740,7 @@ describe('Group Model', () => {
questLeader._id, participatingMember._id, sleepingParticipatingMember._id,
];
expect(User.update).to.be.calledWith(
expect(User.updateMany).to.be.calledWith(
{ _id: { $in: members } },
{
$set: {
@@ -1753,11 +1753,11 @@ describe('Group Model', () => {
});
it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateOne');
await party.startQuest(participatingMember);
expect(User.update).to.be.calledWith(
expect(User.updateOne).to.be.calledWith(
{ _id: questLeader._id },
{
$inc: {
@@ -1819,29 +1819,29 @@ describe('Group Model', () => {
};
it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'update').returns(successfulMock);
sandbox.stub(User, 'updateOne').returns(successfulMock);
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.updateOne).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
const updateStub = sandbox.stub(User, 'update');
const updateStub = sandbox.stub(User, 'updateOne');
updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock);
await party.finishQuest(quest);
expect(User.update.callCount).to.equal(4);
expect(User.updateOne.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'update').returns(failedMock);
sandbox.stub(User, 'updateOne').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(15); // for 3 users
expect(User.updateOne.callCount).to.eql(15); // for 3 users
});
});
@@ -2088,17 +2088,17 @@ describe('Group Model', () => {
context('Party quests', () => {
it('updates participating members with rewards', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateOne');
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
expect(User.updateOne).to.be.calledThrice;
expect(User.updateOne).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
expect(User.updateOne).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
expect(User.updateOne).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
@@ -2173,11 +2173,11 @@ describe('Group Model', () => {
});
it('updates all users with rewards', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
await party.finishQuest(tavernQuest);
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({});
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({});
});
it('sets quest completed to the world quest key', async () => {
@@ -2,7 +2,6 @@ import { v4 as generateUUID } from 'uuid';
import {
generateUser,
createAndPopulateGroup,
checkExistence,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -626,7 +626,8 @@ describe('Post /groups/:groupId/invite', () => {
});
describe('party size limits', () => {
let party, partyLeader;
let party;
let partyLeader;
beforeEach(async () => {
group = await createAndPopulateGroup({
@@ -202,18 +202,86 @@ describe('POST /user/class/cast/:spellId', () => {
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.groupLeader.sync());
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.groupLeader.stats.mp).to.be.equal(170); // spell caster
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
});
const spellList = [
{
className: 'warrior',
spells: [['smash', 'task'], ['defensiveStance'], ['valorousPresence'], ['intimidate']],
},
{
className: 'wizard',
spells: [['fireball', 'task'], ['mpheal'], ['earth'], ['frost']],
},
{
className: 'healer',
spells: [['heal'], ['brightness'], ['protectAura'], ['healAll']],
},
{
className: 'rogue',
spells: [['pickPocket', 'task'], ['backStab', 'task'], ['toolsOfTrade'], ['stealth']],
},
];
spellList.forEach(async habitClass => {
describe(`For a ${habitClass.className}`, async () => {
habitClass.spells.forEach(async spell => {
describe(`Using ${spell[0]}`, async () => {
it('Deducts MP from spell caster', async () => {
const { groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 3,
});
await groupLeader.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await groupLeader.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await groupLeader.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await groupLeader.post(`/user/class/cast/${spell[0]}`);
}
await groupLeader.sync();
expect(groupLeader.stats.mp).to.be.lessThan(200);
});
it('works without a party', async () => {
await user.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await user.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await user.post(`/user/class/cast/${spell[0]}`);
}
await user.sync();
expect(user.stats.mp).to.be.lessThan(200);
});
});
});
});
});
it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const
groupDetails: { type: 'party', privacy: 'private' },
+39 -32
View File
@@ -16901,9 +16901,9 @@
}
},
"core-js": {
"version": "3.30.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz",
"integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ=="
"version": "3.30.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.2.tgz",
"integrity": "sha512-uBJiDmwqsbJCWHAwjrx3cvjbMXP7xD72Dmsn5LOJpiRmE3WbBbN5rCqQ2Qh6Ek6/eOrjlWngEynBWo4VxerQhg=="
},
"core-js-compat": {
"version": "3.11.0",
@@ -17952,9 +17952,9 @@
}
},
"dompurify": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.3.tgz",
"integrity": "sha512-q6QaLcakcRjebxjg8/+NP+h0rPfatOgOzc46Fst9VAA3jF2ApfKBNKMzdP4DYTqtUMXSCd5pRS/8Po/OmoCHZQ=="
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
},
"domutils": {
"version": "1.7.0",
@@ -21071,6 +21071,11 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
},
"immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -21431,9 +21436,9 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
"intro.js": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-6.0.0.tgz",
"integrity": "sha512-ZUiR6BoLSvPSlLG0boewnWVgji1fE1gBvP/pyw5pgCKXEDQz1mMeUxarggClPNs71UTq364LwSk9zxz17A9gaQ=="
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.0.1.tgz",
"integrity": "sha512-1oqz6aOz9cGQ3CrtVYhCSo6AkjnXUn302kcIWLaZ3TI4kKssRXDwDSz4VRoGcfC1jN+WfaSJXRBrITz+QVEBzg=="
},
"invariant": {
"version": "2.2.4",
@@ -22013,9 +22018,9 @@
}
},
"jquery": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz",
"integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ=="
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
},
"js-message": {
"version": "1.0.5",
@@ -27361,17 +27366,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz",
"integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==",
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz",
"integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"dependencies": {
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -27391,18 +27398,18 @@
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"requires": {
"anymatch": "~3.1.1",
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
"readdirp": "~3.6.0"
}
},
"fill-range": {
@@ -27441,9 +27448,9 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"requires": {
"picomatch": "^2.2.1"
}
@@ -30265,9 +30272,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"uuid-browser": {
"version": "3.1.0",
+6 -6
View File
@@ -32,8 +32,8 @@
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.30.1",
"dompurify": "^2.4.3",
"core-js": "^3.30.2",
"dompurify": "^3.0.3",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
@@ -41,12 +41,12 @@
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^6.0.0",
"jquery": "^3.6.4",
"intro.js": "^7.0.1",
"jquery": "^3.7.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nconf": "^0.12.0",
"sass": "^1.34.0",
"sass": "^1.62.1",
"sass-loader": "^8.0.2",
"smartbanner.js": "^1.19.2",
"stopword": "^2.0.8",
@@ -54,7 +54,7 @@
"svg-url-loader": "^7.1.1",
"svgo": "^1.3.2",
"svgo-loader": "^2.2.1",
"uuid": "^8.3.2",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
@@ -138,6 +138,11 @@
width: 48px;
height: 52px;
}
.achievement-dinosaurDynasty2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-dinosaurDynasty2x.png');
width: 68px;
height: 68px;
}
.achievement-domesticated2x {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-domesticated2x.png');
width: 60px;
@@ -820,6 +825,11 @@
width: 141px;
height: 147px;
}
.background_cretaceous_forest {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_cretaceous_forest.png');
width: 141px;
height: 147px;
}
.background_crosscountry_ski_trail {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_crosscountry_ski_trail.png');
width: 141px;
@@ -1054,6 +1064,11 @@
width: 141px;
height: 147px;
}
.background_flying_over_hedge_maze {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_flying_over_hedge_maze.png');
width: 141px;
height: 147px;
}
.background_flying_over_icy_steppes {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_flying_over_icy_steppes.png');
width: 141px;
@@ -1329,6 +1344,11 @@
width: 141px;
height: 147px;
}
.background_in_a_painting {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_in_a_painting.png');
width: 141px;
height: 147px;
}
.background_in_an_ancient_tomb {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_in_an_ancient_tomb.png');
width: 141px;
@@ -2506,6 +2526,11 @@
width: 68px;
height: 68px;
}
.icon_background_cretaceous_forest {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_cretaceous_forest.png');
width: 68px;
height: 68px;
}
.icon_background_crosscountry_ski_trail {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_crosscountry_ski_trail.png');
width: 68px;
@@ -2740,6 +2765,11 @@
width: 68px;
height: 68px;
}
.icon_background_flying_over_hedge_maze {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_flying_over_hedge_maze.png');
width: 68px;
height: 68px;
}
.icon_background_flying_over_icy_steppes {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_flying_over_icy_steppes.png');
width: 68px;
@@ -3015,6 +3045,11 @@
width: 68px;
height: 68px;
}
.icon_background_in_a_painting {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_in_a_painting.png');
width: 68px;
height: 68px;
}
.icon_background_in_an_ancient_tomb {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/icon_background_in_an_ancient_tomb.png');
width: 68px;
@@ -18640,6 +18675,11 @@
width: 90px;
height: 90px;
}
.broad_armor_armoire_paintersApron {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_paintersApron.png');
width: 114px;
height: 90px;
}
.broad_armor_armoire_pirateOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_pirateOutfit.png');
width: 114px;
@@ -19130,6 +19170,11 @@
width: 90px;
height: 90px;
}
.head_armoire_paintersBeret {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_paintersBeret.png');
width: 114px;
height: 90px;
}
.head_armoire_paperBag {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_paperBag.png');
width: 90px;
@@ -19470,6 +19515,11 @@
width: 90px;
height: 90px;
}
.shield_armoire_paintersPalette {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_paintersPalette.png');
width: 114px;
height: 90px;
}
.shield_armoire_perchingFalcon {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_perchingFalcon.png');
width: 90px;
@@ -19910,6 +19960,11 @@
width: 68px;
height: 68px;
}
.shop_armor_armoire_paintersApron {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_armoire_paintersApron.png');
width: 68px;
height: 68px;
}
.shop_armor_armoire_pirateOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_armoire_pirateOutfit.png');
width: 68px;
@@ -20415,6 +20470,11 @@
width: 68px;
height: 68px;
}
.shop_head_armoire_paintersBeret {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_paintersBeret.png');
width: 68px;
height: 68px;
}
.shop_head_armoire_paperBag {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_head_armoire_paperBag.png');
width: 68px;
@@ -20755,6 +20815,11 @@
width: 68px;
height: 68px;
}
.shop_shield_armoire_paintersPalette {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_paintersPalette.png');
width: 68px;
height: 68px;
}
.shop_shield_armoire_perchingFalcon {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_shield_armoire_perchingFalcon.png');
width: 68px;
@@ -21200,6 +21265,11 @@
width: 68px;
height: 68px;
}
.shop_weapon_armoire_paintbrush {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_paintbrush.png');
width: 68px;
height: 68px;
}
.shop_weapon_armoire_paperCutter {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_armoire_paperCutter.png');
width: 68px;
@@ -21655,6 +21725,11 @@
width: 90px;
height: 90px;
}
.slim_armor_armoire_paintersApron {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_paintersApron.png');
width: 114px;
height: 90px;
}
.slim_armor_armoire_pirateOutfit {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_pirateOutfit.png');
width: 114px;
@@ -22110,6 +22185,11 @@
width: 114px;
height: 90px;
}
.weapon_armoire_paintbrush {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_paintbrush.png');
width: 114px;
height: 90px;
}
.weapon_armoire_paperCutter {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_paperCutter.png');
width: 114px;
@@ -27930,6 +28010,36 @@
width: 68px;
height: 68px;
}
.broad_armor_mystery_202306 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202306.png');
width: 114px;
height: 90px;
}
.shop_armor_mystery_202306 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_mystery_202306.png');
width: 68px;
height: 68px;
}
.shop_set_mystery_202306 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202306.png');
width: 68px;
height: 68px;
}
.shop_weapon_mystery_202306 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_mystery_202306.png');
width: 68px;
height: 68px;
}
.slim_armor_mystery_202306 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202306.png');
width: 114px;
height: 90px;
}
.weapon_mystery_202306 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202306.png');
width: 114px;
height: 90px;
}
.broad_armor_mystery_301404 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
width: 90px;
@@ -17,10 +17,18 @@
Payment schedule ("basic-earned" is monthly):
<strong>{{ hero.purchased.plan.planId }}</strong>
</div>
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
Group plan ID:
<strong>{{ hero.purchased.plan.owner }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCreated">
Creation date:
<strong>{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCurrentTypeCreated">
Start date for current subscription type:
<strong>{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}</strong>
</div>
<div>
Termination date:
<strong
@@ -46,9 +54,16 @@
Perk offset months:
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
</div>
<div>
<div class="form-inline">
Perk month count:
<strong>{{ hero.purchased.plan.perkMonthCount }}</strong>
<input
v-model="hero.purchased.plan.perkMonthCount"
class="form-control"
type="number"
min="0"
max="2"
step="1"
>
</div>
<div>
Next Mystic Hourglass:
@@ -264,12 +264,6 @@ export default {
},
mounted () {
this.seeking = Boolean(this.user.party.seeking);
Analytics.track({
eventName: 'Start a Party button',
eventAction: 'Start a Party button',
eventCategory: 'behavior',
hitType: 'event',
}, { trackOnClient: true });
},
methods: {
async createParty () {
@@ -243,8 +243,6 @@ import rogueIcon from '@/assets/svg/rogue.svg';
import healerIcon from '@/assets/svg/healer.svg';
import wizardIcon from '@/assets/svg/wizard.svg';
import * as Analytics from '@/libs/analytics';
export default {
components: {
Avatar,
@@ -287,12 +285,6 @@ export default {
section: this.$t('lookingForPartyTitle'),
});
this.seekers = await this.$store.dispatch('party:lookingForParty');
await Analytics.track({
hitType: 'event',
eventName: 'View Find Members',
eventAction: 'View Find Members',
eventCategory: 'behavior',
}, { trackOnClient: true });
this.canLoadMore = this.seekers.length === 30;
this.loading = false;
}
+16 -1
View File
@@ -122,6 +122,7 @@
<script>
import orderBy from 'lodash/orderBy';
import * as Analytics from '@/libs/analytics';
import { mapGetters, mapActions } from '@/libs/store';
import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal';
@@ -232,10 +233,24 @@ export default {
this.expandedMember = memberId;
}
},
createOrInviteParty () {
async createOrInviteParty () {
if (this.user.party._id) {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Find Party Members',
});
this.$router.push('/looking-for-party');
} else {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Get Started',
});
this.$root.$emit('bv::show::modal', 'create-party-modal');
}
},
@@ -126,7 +126,7 @@
<!-- the word "total" -->
<div class="buy-gem-total">
{{ $t('sendGiftTotal') }}
{{ $t('sendTotal') }}
</div>
<!-- the actual dollar amount -->
@@ -144,22 +144,6 @@
>{{ $t('startAdvCollapsed') }}</span>
</label>
</div>
<div class="checkbox">
<label>
<input
v-model="user.preferences.dailyDueDefaultView"
type="checkbox"
class="mr-2"
@change="set('dailyDueDefaultView')"
>
<span
class="hint"
popover-trigger="mouseenter"
popover-placement="right"
:popover="$t('dailyDueDefaultViewPop')"
>{{ $t('dailyDueDefaultView') }}</span>
</label>
</div>
<div
v-if="party.memberCount === 1"
class="checkbox"
@@ -0,0 +1,93 @@
<template>
<div class="item-cost">
<span
class="cost"
:class="getPriceClass()"
>
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
>
</span>
<span
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
</div>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
@import '~@/assets/scss/mixins.scss';
.item-cost {
padding-bottom: 16px;
}
.cost {
height: 40px;
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
vertical-align: middle;
&.gems {
color: $gems-color;
border-radius: 20px;
padding: 8px 20px 8px 20px;
margin-top: 16px;
margin-bottom: 16px;
background-color: rgba(36, 204, 143, 0.15);
}
&.gold {
color: $gold-color;
border-radius: 20px;
padding: 8px 20px 8px 20px;
margin-top: 16px;
margin-bottom: 16px;
background-color: rgba(255, 190, 93, 0.15);
}
&.hourglasses {
color: $hourglass-color;
border-radius: 20px;
padding: 8px 20px 8px 20px;
margin-top: 16px;
margin-bottom: 16px;
background-color: rgba(41, 149, 205, 0.15);
}
}
</style>
<script>
import svgClose from '@/assets/svg/close.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgGem from '@/assets/svg/gem.svg';
export default {
data () {
return {
icons: Object.freeze({
close: svgClose,
gold: svgGold,
gems: svgGem,
}),
selectedAmountToBuy: 1,
selectedAmount: 1,
};
},
methods: {
getPriceClass () {
if (this.priceType && this.icons[this.priceType]) {
return this.priceType;
} if (this.item.currency && this.icons[this.item.currency]) {
return this.item.currency;
}
return 'gold';
},
},
};
</script>
@@ -0,0 +1,137 @@
<template>
<div class="d-flex flex-row align-items-center justify-content-center number-increment">
<!-- buy modal -->
<div
class="gray-circle"
@click="quantity <= 0
? quantity = 0
: quantity--"
>
<div
class="icon-negative"
v-html="icons.svgNegative"
></div>
</div>
<div class="input-group">
<div class="align-items-center">
</div>
<input
v-model="quantity"
class="form-control alignment"
step="1"
type="number"
>
</div>
<div
class="gray-circle"
@click="quantity++"
>
<div
class="icon-positive"
v-html="icons.svgPositive"
></div>
</div>
</div>
</template>
<style scoped lang="scss">
@import '~@/assets/scss/colors.scss';
.number-increment {
padding-bottom: 0px;
}
.alignment {
text-align: center;
}
.input-group {
width: 94px;
height: 32px;
width: 48px;
margin: 0px 16px 0px 16px;
padding: 0;
border-radius: 2px;
border: solid 1px $gray-400;
background-color: $white;
}
.gray-circle {
border-radius: 100%;
border: solid 2px $gray-300;
width: 32px;
height: 32px;
cursor: pointer;
&:hover {
border-color: $purple-300;
}
}
.gray-circle:hover{
.icon-positive, .icon-negative {
& ::v-deep svg path {
fill: $purple-300;
}
}
}
.icon-positive, .icon-negative {
width: 10px;
height: 10px;
margin: 4px auto;
& ::v-deep svg path {
fill: $gray-300;
}
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
</style>
<script>
// icons
import svgGem from '@/assets/svg/gem.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
export default {
data () {
return {
icons: Object.freeze({
svgGem,
svgGold,
svgPositive,
svgNegative,
}),
item: { },
quantity: 1,
};
},
computed: {
},
watch: {
quantity () {
this.$emit('updateQuantity', this.quantity);
},
},
methods: {
setDefaults () {
this.input = 1;
},
},
};
</script>
@@ -22,10 +22,11 @@
@import '~@/assets/scss/colors.scss';
span {
font-weight: normal;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.33;
color: $gray-200;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
display: inline-block;
}
+363 -102
View File
@@ -17,7 +17,7 @@
</span>
<div>
<span
class="svg-icon icon-12 close-icon"
class="svg-icon close-icon icon-16 color"
aria-hidden="true"
tabindex="0"
@click="hideDialog()"
@@ -45,6 +45,13 @@
:sprites-margin="'0px auto 0px -24px'"
/>
</div>
<item
v-else-if="item.key === 'gem'"
class="flat bordered-item"
:item="item"
:item-content-class="item.class"
:show-popover="false"
/>
<item
v-else-if="item.key != 'gem'"
class="flat bordered-item"
@@ -53,10 +60,20 @@
:show-popover="false"
/>
</slot>
<div
v-if="!showAvatar && user.items[item.purchaseType]"
class="owned"
:class="totalOwned"
>
<!-- eslint-disable-next-line max-len -->
<span class="owned-text">{{ $t('owned') }}: <span class="user-amount">{{ totalOwned }}</span></span>
</div>
<h4 class="title">
{{ itemText }}
</h4>
<div v-html="itemNotes"></div>
<div class="item-notes">
{{ itemNotes }}
</div>
<slot
name="additionalInfo"
:item="item"
@@ -69,60 +86,61 @@
/>
</slot>
<div
v-if="item.value > 0"
v-if="item.value > 0 && !(item.key === 'gem' && gemsLeft < 1)"
class="purchase-amount"
>
<div
v-if="showAmountToBuy(item)"
class="how-many-to-buy"
>
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
<div v-if="showAmountToBuy(item)">
<div class="box">
<input
v-model.number="selectedAmountToBuy"
class="form-control"
type="number"
min="0"
step="1"
>
</div>
<span :class="{'notEnough': notEnoughCurrency}">
<!-- this is where the pretty item cost element lives -->
<div class="item-cost">
<span
class="cost"
:class="getPriceClass()"
>
<span
class="svg-icon inline icon-32"
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
>
</span>
<span
class="cost"
:class="getPriceClass()"
>{{ item.value }}</span>
</span>
</div>
<div
v-else
class="d-flex align-items-middle"
v-if="showAmountToBuy(item)"
class="how-many-to-buy"
>
<span
class="svg-icon inline icon-32 ml-auto my-auto"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
<span
class="cost mr-auto my-auto"
:class="getPriceClass()"
>{{ item.value }}</span>
{{ $t('howManyToBuy') }}
</div>
<div
v-if="showAmountToBuy(item)"
>
<number-increment
class="number-increment"
@updateQuantity="selectedAmountToBuy = $event"
/>
<div
:class="{'notEnough': notEnoughCurrency}"
class="total"
>
<span class="total-text">{{ $t('sendTotal') }}</span>
<span
class="svg-icon total icon-24"
aria-hidden="true"
v-html="icons[getPriceClass()]"
></span>
<span
class="total-text"
:class="getPriceClass()"
>{{ item.value * selectedAmountToBuy }}</span>
</div>
</div>
</div>
<div
v-if="item.key === 'gem'"
class="gems-left"
v-if="item.key === 'gem' && gemsLeft < 1"
class="no-more-gems"
>
<strong v-if="gemsLeft > 0">{{ gemsLeft }} {{ $t('gemsRemaining') }}</strong>
<strong v-if="gemsLeft === 0">{{ $t('maxBuyGems') }}</strong>
</div>
<div v-if="attemptingToPurchaseMoreGemsThanAreLeft">
{{ $t('notEnoughGemsToBuy') }}
</div>
<div
@@ -147,7 +165,7 @@
{{ $t('viewSubscriptions') }}
</button>
<button
v-else
v-else-if="!(item.key === 'gem' && gemsLeft < 1)"
class="btn btn-primary"
:disabled="item.key === 'gem' && gemsLeft === 0 ||
attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid || item.locked ||
@@ -165,6 +183,7 @@
<countdown-banner
v-if="item.event && item.owned == null"
:end-date="endDate"
class="limitedTime available"
/>
<div
v-if="item.key === 'rebirth_orb' && item.value > 0 && user.stats.lvl >= 100"
@@ -179,12 +198,31 @@
</div>
</div>
<div
slot="modal-footer"
class="d-flex"
v-if="item.key === 'gem'"
class="d-flex justify-content-center align-items-center"
>
<span class="balance mr-auto">{{ $t('yourBalance') }}</span>
<div
v-if="gemsLeft > 0"
class="gems-left d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
<div
v-if="gemsLeft === 0"
class="out-of-gems-banner d-flex justify-content-center align-items-center"
>
<strong>{{ $t('monthlyGems') }} &nbsp;</strong>
{{ gemsLeft }} / {{ totalGems }} {{ $t('gemsRemaining') }}
</div>
</div>
<div
slot="modal-footer"
class="clearfix"
>
<span class="user-balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo
class="ml-auto"
class="currency-totals"
:currency-needed="getPriceClass()"
:amount-needed="item.value"
/>
@@ -200,11 +238,47 @@
@include centeredModal();
.modal-body {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
margin: 24px 0 0 0;
padding: 16px 24px;
align-content: center;
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
.modal-dialog {
width: 330px;
width: 448px;
box-sizing: border-box;
}
.badge-dialog {
left: -8px;
top: -8px;
}
.avatar {
@@ -212,8 +286,71 @@
margin: 0 auto;
}
.owned {
height: 32px;
width: 141px;
margin-top: -36px;
margin-left: 153px;
padding-top: 6px;
background-color: $gray-600;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
display: block;
text-align: center;
position: relative;
z-index: 1;
.owned-text {
font-size: 0.75rem;
font-weight: bold;
line-height: 1.71;
}
.user-amount {
font-weight: normal !important;
}
}
.item {
width: 141px;
height: 147px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
cursor: default;
}
.item-content {
transform: scale(1.45, 1.45);
top: -25.67px;
left: 1px;
&.shop_gem {
transform: scale(1.45, 1.45);
top: -2px;
left: 0px;
}
}
.title {
height: 28px;
color: $gray-10;
font-size: 1.25rem;
margin-top: 25px;
}
.item-notes {
margin-top: 8px;
padding-left: 48.5px;
padding-right: 48.5px;
line-height: 1.71;
font-size: 0.875rem;
}
.content {
text-align: center;
width: 448px;
}
.item-wrapper {
@@ -221,15 +358,22 @@
}
.inner-content {
margin: 33px auto auto;
width: 282px;
margin: 32px auto auto;
}
.btn-primary {
margin-top: 16px;
}
.purchase-amount {
margin-top: 24px;
margin-top: 0px;
.how-many-to-buy {
margin-bottom: 16px;
font-weight: bold !important;
}
.number-increment {
margin-top: 16px;
}
.box {
@@ -255,31 +399,105 @@
}
}
}
.no-more-gems {
color: $yellow-5;
font-size: 0.875em;
line-height: 1.33;
margin: 16px 48px 0 48px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
// for cost icon of a single item
span.svg-icon.inline.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-right: 4px;
padding-top: 4px;
}
// for the total user cost
span.svg-icon.total.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-left: 6px;
margin-right: 8px;
padding-top: 6px;
}
vertical-align: middle;
span.svg-icon.icon-16 {
height: 16px;
width: 16px;
}
.close-icon {
color: $gray-200;
stroke-width: 0px;
&:hover {
color: $gray-100;
}
}
.attributes-group {
margin: 32px;
border-radius: 4px;
line-height: 1.71;
font-size: 0.875;
}
.attributesGrid {
margin-top: 28px;
border-radius: 2px;
background-color: $gray-500;
}
.item-cost {
display: inline-flex;
margin: 16px 0;
align-items: center;
height: 40px;
}
.cost {
width: 28px;
height: 32px;
font-size: 24px;
display: inline-block;
font-family: sans-serif;
font-size: 1.25rem;
font-weight: bold;
line-height: 1.33;
vertical-align: middle;
padding: 6px 20px;
line-height: 1.4;
border-radius: 20px;
&.gems {
color: $gems-color;
color: $green-10;
background-color: rgba(36, 204, 143, 0.15);
align-items: center;
}
&.gold {
color: $gold-color;
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
align-items: center;
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba(41, 149, 205, 0.15);
align-items: center;
}
}
.total {
font-weight: bold;
font-size: 0.875rem;
padding-top: 2px;
margin-top: 4px;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
@@ -287,62 +505,84 @@
}
}
.total-text {
color: $gray-50;
font-weight: bold;
font-size: 0.875rem;
line-height: 1.71;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
color: $hourglass-color;
}
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
min-width: 6rem;
margin-top: 16px;
padding: 4px 16px;
height: 32px;
&:focus {
border: 2px solid black;
}
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
.notEnough {
pointer-events: none;
opacity: 0.55;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
}
.free-rebirth {
background-color: $yellow-5;
color: $white;
height: 2rem;
line-height: 16px;
margin: auto -1rem -1rem;
}
.notEnough {
pointer-events: none;
opacity: 0.55;
}
// .pt-015 {
// padding-top: 0.15rem;
// }
.attributesGrid {
margin-top: 8px;
border-radius: 2px;
background-color: $gray-500;
margin: 10px 0 24px;
}
.gems-left {
margin-top: .5em;
height: 32px;
background-color: $green-100;
font-size: 0.75rem;
margin-top: 24px;
color: $green-1;
width: 100%;
margin-bottom: -24px;
}
.free-rebirth {
background-color: $yellow-5;
.out-of-gems-banner {
height: 32px;
font-size: 0.75rem;
margin-top: 24px;
background-color: $yellow-100;
color: $yellow-1;
width: 100%;
margin-bottom: -24px;
}
.limitedTime {
height: 32px;
width: 446px;
font-size: 0.75rem;
margin: 24px 0 0 0;
background-color: $purple-300;
color: $white;
height: 2rem;
line-height: 16px;
margin: auto -1rem -1rem;
}
.pt-015 {
padding-top: 0.15rem;
margin-bottom: -24px;
}
}
</style>
<style lang="scss" scoped>
@@ -370,6 +610,8 @@ import svgGem from '@/assets/svg/gem.svg';
import svgHourglasses from '@/assets/svg/hourglass.svg';
import svgClock from '@/assets/svg/clock.svg';
import svgWhiteClock from '@/assets/svg/clock-white.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
import BalanceInfo from './balanceInfo.vue';
import PinBadge from '@/components/ui/pinBadge';
@@ -377,6 +619,7 @@ import CountdownBanner from './countdownBanner';
import currencyMixin from './_currencyMixin';
import notifications from '@/mixins/notifications';
import buyMixin from '@/mixins/buy';
import numberIncrement from '@/components/shared/numberIncrement';
import { mapState } from '@/libs/store';
@@ -407,14 +650,17 @@ export default {
Avatar,
PinBadge,
CountdownBanner,
numberIncrement,
},
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
props: {
// eslint-disable-next-line vue/require-default-prop
item: {
type: Object,
},
priceType: {
type: String,
default: '',
},
withPin: {
type: Boolean,
@@ -433,10 +679,14 @@ export default {
hourglasses: svgHourglasses,
clock: svgClock,
whiteClock: svgWhiteClock,
positive: svgPositive,
negative: svgNegative,
}),
selectedAmountToBuy: 1,
selectedAmount: 1,
isPinned: false,
quantity: 1,
};
},
computed: {
@@ -474,6 +724,11 @@ export default {
return planGemLimits.convCap
+ this.user.purchased.plan.consecutive.gemCapExtra - this.user.purchased.plan.gemsBought;
},
totalGems () {
if (!this.user.purchased.plan) return 0;
return planGemLimits.convCap
+ this.user.purchased.plan.consecutive.gemCapExtra;
},
attemptingToPurchaseMoreGemsThanAreLeft () {
if (this.item && this.item.key && this.item.key === 'gem' && this.selectedAmountToBuy > this.gemsLeft) return true;
return false;
@@ -490,6 +745,9 @@ export default {
endDate () {
return moment(this.item.event.end);
},
totalOwned () {
return this.user.items[this.item.purchaseType][this.item.key] || 0;
},
},
watch: {
item: function itemChanged () {
@@ -500,7 +758,9 @@ export default {
methods: {
onChange ($event) {
this.$emit('change', $event);
this.selectedAmountToBuy = 1;
},
buyItem () {
// @TODO: I think we should buying to the items.
// Turn the items into classes, and use polymorphism
@@ -597,6 +857,7 @@ export default {
}
},
hideDialog () {
this.selectedAmountToBuy = 1;
this.$root.$emit('bv::hide::modal', 'buy-modal');
},
getPriceClass () {
@@ -4,9 +4,9 @@
:hide-header="true"
@change="onChange($event)"
>
<div class="close">
<div>
<span
class="svg-icon inline icon-10"
class="svg-icon close-icon icon-16 color"
aria-hidden="true"
@click="hideDialog()"
v-html="icons.close"
@@ -14,60 +14,73 @@
</div>
<div
v-if="item"
class="content"
class="content bordered-item"
>
<div class="inner-content">
<item
class="flat"
class="flat bordered-item"
:item="item"
:item-content-class="itemContextToSell.itemClass"
:show-popover="false"
>
<countBadge
slot="itemBadge"
:show="true"
:count="itemContextToSell.itemCount"
/>
</item>
/>
<span class="owned">
{{ $t('owned') }}: <span class="user-amount">{{ itemContextToSell.itemCount }}</span>
</span>
<h4 class="title">
{{ itemContextToSell.itemName }}
</h4>
<div v-if="item.key === 'Saddle'">
<div class="text">
<div class="item-notes">
{{ item.sellWarningNote() }}
</div>
<br>
</div>
<div v-else>
<div>
<div class="text">
<div class="item-notes">
{{ item.notes() }}
</div>
<div>
<b class="how-many-to-sell">{{ $t('howManyToSell') }}</b>
<div class="item-cost">
<span class="cost gold">
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons.gold"
></span>
<span>{{ item.value }}</span>
</span>
</div>
<div>
<b-input
v-model="selectedAmountToSell"
class="itemsToSell"
type="number"
:max="itemContextToSell.itemCount"
min="1"
step="1"
@keyup.native="preventNegative($event)"
/>
<span
class="svg-icon inline icon-32"
class="how-many-to-sell"
>
{{ $t('howManyToSell') }}
</span>
</div>
<div>
<number-increment
@updateQuantity="selectedAmountToSell = $event"
/>
</div>
<div class="total-row">
<span class="total-text">
{{ $t('sendTotal') }}
</span>
<span
class="svg-icon total icon-24"
aria-hidden="true"
v-html="icons.gold"
></span>
<span class="value">{{ item.value }}</span>
<span class="total-text gold">
{{ item.value * selectedAmountToSell }}
</span>
</div>
<button
class="btn btn-primary"
:disabled="selectedAmountToSell > itemContextToSell.itemCount"
@click="sellItems()"
>
{{ $t('sell') }}
{{ $t('sellItems') }}
</button>
</div>
</div>
@@ -77,8 +90,10 @@
slot="modal-footer"
class="clearfix"
>
<span class="balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo class="float-right" />
<span class="user-balance float-left">{{ $t('yourBalance') }}</span>
<balanceInfo
class="float-right currency-totals"
/>
</div>
</b-modal>
</template>
@@ -95,51 +110,13 @@
}
.modal-dialog {
width: 330px;
width: 448px;
}
.content {
text-align: center;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-left: 24px;
margin-right: 8px;
vertical-align: middle;
}
.value {
width: 28px;
height: 32px;
font-size: 24px;
font-weight: bold;
line-height: 1.33;
color: #df911e;
vertical-align: middle;
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
.modal-body {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
.modal-footer {
@@ -148,29 +125,215 @@
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
margin: 24px 0 0;
padding: 16px 24px;
align-content: center;
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
.how-many-to-sell {
margin-bottom: 16px;
.content {
text-align: center;
}
.inner-content {
margin: 33px auto auto;
width: 282px;
}
.owned {
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
background-color: $gray-600;
padding: 8px 8px;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
display: block;
width: 141px;
margin-left: 71px;
margin-top: -48px;
position: relative;
z-index: 1;
.user-amount {
font-weight: normal !important;
}
}
.item-wrapper {
margin-top: -10px;
}
.item {
width: 141px;
height: 147px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 0px;
border-bottom-left-radius: 0px;
cursor: default;
margin-top: 8px;
}
.item-content {
transform: scale(1.45, 1.45);
top: -25px;
left: 1px;
}
.title {
color: $gray-10;
font-size: 1.25rem;
margin-top: 26px;
margin-bottom: 0px;
}
.item-notes {
margin-top: 12px;
line-height: 1.71;
font-size: 0.875rem;
}
// for cost icon of a single item
span.svg-icon.inline.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-right: 4px;
padding-top: 4px;
}
// for the total user cost
span.svg-icon.total.icon-24 {
display: inline-block;
height: 24px;
width: 24px;
margin-left: 6px;
margin-right: 8px;
padding-top: 6px;
}
span.svg-icon.icon-16 {
height: 16px;
width: 16px;
}
.close-icon {
color: $gray-200;
stroke-width: 0px;
cursor: pointer;
&:hover {
color: $gray-100;
}
}
.item-cost {
display: inline-flex;
margin: 16px 0;
align-items: center;
height: 40px;
}
.cost {
display: inline-block;
font-family: sans-serif;
font-size: 1.25rem;
font-weight: bold;
padding: 6px 20px;
line-height: 1.4;
border-radius: 20px;
&.gold {
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
align-items: center;
}
}
}
.how-many-to-sell {
font-weight: bold !important;
}
.number-increment {
margin-top: 16px;
}
.total-row {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
&.gold {
color: $yellow-5;
}
}
.total-text {
color: $gray-50;
font-weight: bold;
font-size: 0.875rem;
line-height: 1.71;
&.gold {
color: $yellow-5;
}
}
button.btn.btn-primary {
margin-top: 16px;
padding: 4px 16px;
height: 32px;
&:focus {
border: 2px solid black;
}
.balance {
width: 74px;
height: 16px;
font-size: 12px;
font-weight: bold;
line-height: 1.33;
color: $gray-200;
}
}
</style>
<script>
import svgClose from '@/assets/svg/close.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgGem from '@/assets/svg/gem.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
import BalanceInfo from '../balanceInfo.vue';
import Item from '@/components/inventory/item';
import CountBadge from '@/components/ui/countBadge';
import numberIncrement from '@/components/shared/numberIncrement';
export default {
components: {
BalanceInfo,
Item,
CountBadge,
numberIncrement,
},
data () {
return {
@@ -181,6 +344,8 @@ export default {
close: svgClose,
gold: svgGold,
gem: svgGem,
svgPositive,
svgNegative,
}),
};
},
@@ -211,6 +376,10 @@ export default {
this.selectedAmountToSell = 0;
}
},
maxOwned () {
const maxOwned = this.itemContextToSell.itemCount;
return maxOwned;
},
sellItems () {
if (!Number.isInteger(Number(this.selectedAmountToSell))) {
this.selectedAmountToSell = 0;
@@ -33,6 +33,22 @@
v-if="!item.locked"
class="purchase-amount"
>
<div class="item-cost">
<span
class="cost"
:class="priceType"
>
<span
class="svg-icon inline icon-24"
aria-hidden="true"
v-html="icons[priceType]"
>
</span>
<span
:class="priceType"
>{{ item.value }}</span>
</span>
</div>
<div class="how-many-to-buy">
<strong>{{ $t('howManyToBuy') }}</strong>
</div>
@@ -42,24 +58,25 @@
>
{{ item.addlNotes }}
</div>
<div class="box">
<input
v-model.number="selectedAmountToBuy"
class="form-control"
type="number"
min="0"
step="1"
>
<div>
<number-increment
@updateQuantity="selectedAmountToBuy = $event"
/>
</div>
<div class="total-row">
<span class="total-text">
{{ $t('sendTotal') }}
</span>
<span
class="svg-icon inline icon-20"
aria-hidden="true"
v-html="currencyIcon"
></span>
<span
class="total"
:class="priceType"
>{{ item.value * selectedAmountToBuy }}</span>
</div>
<span
class="svg-icon inline icon-32"
aria-hidden="true"
v-html="currencyIcon"
></span>
<span
class="value"
:class="priceType"
>{{ item.value }}</span>
</div>
<button
v-if="priceType === 'gems'
@@ -112,6 +129,40 @@
margin-top: 1rem;
}
.modal-body {
padding-left: 0px;
padding-right: 0px;
padding-bottom: 0px;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
margin-top: 24px;
padding: 16px 24px;
align-content: center;
.user-balance {
width: 150px;
height: 16px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.33;
color: $gray-100;
margin-bottom: 16px;
margin-top: -4px;
margin-left: -4px;
}
.currency-totals {
margin-right: -8px;
float: right;
}
}
.modal-dialog {
margin-top: 8%;
width: 448px !important;
@@ -129,8 +180,13 @@
margin: 33px auto auto;
}
.modal-body {
padding-bottom: 0px;
.item-notes {
height: 48px;
margin-top: 8px;
padding-left: 48.5px;
padding-right: 48.5px;
line-height: 1.71;
font-size: 0.875rem;
}
.questInfo {
@@ -152,16 +208,14 @@
height: 100%;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
vertical-align: middle;
}
button.btn.btn-primary {
margin-top: 24px;
margin-bottom: 24px;
margin-top: 14px;
padding: 4px 16px;
height: 32px;
&:focus {
border: 2px solid black;
}
}
.balance {
@@ -173,19 +227,6 @@
color: $gray-200;
}
.modal-footer {
height: 48px;
background-color: $gray-700;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
display: block;
padding: 1rem 1.5rem;
&> * {
margin: 0;
}
}
.notEnough {
pointer-events: none;
opacity: 0.55;
@@ -198,30 +239,108 @@
margin-bottom: 16px;
}
.box {
display: inline-block;
width: 74px;
height: 40px;
border-radius: 2px;
background-color: #ffffff;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
margin-right: 24px;
input {
width: 100%;
border: none;
.item-cost {
padding-bottom: 16px;
}
input::-webkit-contacts-auto-fill-button {
visibility: hidden;
display: none !important;
pointer-events: none;
position: absolute;
right: 0;
}
.cost {
height: 40px;
font-size: 1.25rem;
font-weight: bold;
vertical-align: middle;
padding: 8px 20px 8px 20px;
&.gems {
color: $green-10;
background-color: rgba(36, 204, 143, 0.15);
line-height: 1.4;
margin: 0 0 0 -4px;
border-radius: 20px;
}
&.gold {
color: $yellow-5;
background-color: rgba(255, 190, 93, 0.15);
line-height: 1.4;
margin: 0 0 0 -4px;
border-radius: 20px;
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba(41, 149, 205, 0.15);
line-height: 1.4;
margin: 0 0 0 -4px;
border-radius: 20px;
}
}
.total-row {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
}
.total {
font-weight: bold;
font-size: 0.875rem;
margin-top: 16px;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
color: $hourglass-color;
}
}
.total-text {
color: $gray-50;
font-weight: bold;
font-size: 0.875rem;
height: 24px;
line-height: 1.71;
padding-right: 4px;
&.gems {
color: $green-10;
}
&.gold {
color: $yellow-5;
}
&.hourglasses {
color: $hourglass-color;
}
}
span.svg-icon.inline.icon-20 {
height: 20px;
width: 20px;
margin-right: 4px;
vertical-align: middle;
}
span.svg-icon.inline.icon-24 {
height: 24px;
width: 24px;
margin-right: 8px;
vertical-align: middle;
}
span.svg-icon.inline.icon-32 {
height: 32px;
width: 32px;
margin-right: 8px;
vertical-align: middle;
}
@media only screen and (max-width: 1000px) {
.modal-dialog {
max-width: 80%;
@@ -234,9 +353,10 @@
}
}
}
}
</style>
<style lang="scss" scoped>
<!-- <style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.value {
@@ -260,7 +380,7 @@
color: $hourglass-color;
}
}
</style>
</style> -->
<script>
import moment from 'moment';
@@ -272,6 +392,8 @@ import svgExperience from '@/assets/svg/experience.svg';
import svgGem from '@/assets/svg/gem.svg';
import svgGold from '@/assets/svg/gold.svg';
import svgHourglasses from '@/assets/svg/hourglass.svg';
import svgPositive from '@/assets/svg/positive.svg';
import svgNegative from '@/assets/svg/negative.svg';
import BalanceInfo from '../balanceInfo.vue';
import currencyMixin from '../_currencyMixin';
@@ -280,6 +402,7 @@ import buyMixin from '@/mixins/buy';
import numberInvalid from '@/mixins/numberInvalid';
import PinBadge from '@/components/ui/pinBadge';
import CountdownBanner from '../countdownBanner';
import numberIncrement from '@/components/shared/numberIncrement';
import questDialogContent from './questDialogContent';
import QuestRewards from './questRewards';
@@ -293,6 +416,7 @@ export default {
PinBadge,
questDialogContent,
CountdownBanner,
numberIncrement,
},
mixins: [buyMixin, currencyMixin, notifications, numberInvalid],
props: {
@@ -301,6 +425,7 @@ export default {
},
priceType: {
type: String,
default: '',
},
withPin: {
type: Boolean,
@@ -312,9 +437,11 @@ export default {
clock: svgClock,
close: svgClose,
experience: svgExperience,
gem: svgGem,
gems: svgGem,
gold: svgGold,
hourglass: svgHourglasses,
hourglasses: svgHourglasses,
positive: svgPositive,
negative: svgNegative,
}),
isPinned: false,
@@ -339,8 +466,8 @@ export default {
},
currencyIcon () {
if (this.priceType === 'gold') return this.icons.gold;
if (this.priceType === 'hourglasses') return this.icons.hourglass;
return this.icons.gem;
if (this.priceType === 'hourglasses') return this.icons.hourglasses;
return this.icons.gems;
},
endDate () {
return moment(this.item.event.end);
@@ -33,17 +33,17 @@
h3 {
color: $gray-10;
margin-bottom: 0.25rem;
margin-bottom: 4pxrem;
}
.quest-image {
margin: 0 auto;
margin-bottom: 1em;
margin-top: 1.5em;
margin-bottom: 16px;
margin-top: 24px;
}
.text {
margin-bottom: 1rem;
margin: 16px 16px;
overflow-y: auto;
text-overflow: ellipsis;
}
@@ -54,10 +54,10 @@
line-height: 1.71;
color: $gray-50;
text-align: center;
margin-bottom: 0.5rem;
margin-bottom: 8px;
::v-deep .user-label {
font-size: 14px;
font-size: 0.875rem;
}
}
@@ -177,9 +177,6 @@ export default {
@import '~@/assets/scss/colors.scss';
.quest-rewards {
margin-left: -1rem;
margin-right: -1rem;
background-color: $gray-700;
}
+13 -6
View File
@@ -521,7 +521,12 @@ export default {
// Get Category Filter Labels
this.typeFilters = getFilterLabels(this.type, this.challenge);
// Set default filter for task column
this.activateFilter(this.type);
if (this.challenge) {
this.activateFilter(this.type);
} else {
this.activateFilter(this.type, this.user.preferences.tasks.activeFilter[this.type], true);
}
},
mounted () {
this.setColumnBackgroundVisibility();
@@ -661,7 +666,7 @@ export default {
taskSummary (task) {
this.$emit('taskSummary', task);
},
activateFilter (type, filter = '') {
activateFilter (type, filter = '', skipSave = false) {
// Needs a separate API call as this data may not reside in store
if (type === 'todo' && filter === 'complete2') {
if (this.group && this.group._id) {
@@ -677,14 +682,16 @@ export default {
// as default filter for daily
// and set the filter as 'due' only when the component first
// loads and not on subsequent reloads.
if (
type === 'daily' && filter === '' && !this.challenge
&& this.user.preferences.dailyDueDefaultView
) {
if (type === 'daily' && filter === '' && !this.challenge) {
filter = 'due'; // eslint-disable-line no-param-reassign
}
this.activeFilter = getActiveFilter(type, filter, this.challenge);
if (!skipSave && !this.challenge) {
const propertyToUpdate = `preferences.tasks.activeFilter.${type}`;
this.$store.dispatch('user:set', { [propertyToUpdate]: filter });
}
},
setColumnBackgroundVisibility () {
this.$nextTick(() => {
+11 -10
View File
@@ -6,6 +6,7 @@
'groupTask': task.group.id,
'task-not-editable': !teamManagerAccess,
'task-not-scoreable': showTaskLockIcon,
'link-exempt': !isChallengeTask && !isGroupTask,
}, `type_${task.type}`
]"
@click="castEnd($event, task)"
@@ -240,10 +241,10 @@
>
<div
v-b-tooltip.hover.bottom="$t('dueDate')"
class="svg-icon calendar"
class="svg-icon calendar my-auto"
v-html="icons.calendar"
></div>
<span>{{ formatDueDate() }}</span>
<span>{{ $t('due') }} {{ formatDueDate() }}</span>
</div>
<div class="icons-right d-flex justify-content-end">
<div
@@ -700,7 +701,7 @@
.icons {
margin-top: 4px;
color: $gray-300;
color: $gray-100;
font-style: normal;
&-right {
@@ -759,7 +760,7 @@
}
.due-overdue {
color: $red-50;
color: $maroon-10;
}
.calendar.svg-icon {
@@ -898,7 +899,7 @@
}
</style>
<!-- eslint-enable max-len -->
<!-- eslint-disable-next-line vue/component-tags-order -->
<script>
import moment from 'moment';
import { v4 as uuid } from 'uuid';
@@ -1125,13 +1126,13 @@ export default {
return moment.duration(endOfDueDate.diff(endOfToday));
},
checkIfOverdue () {
return this.calculateTimeTillDue().asDays() <= 0;
return this.calculateTimeTillDue().asDays() < 0;
},
formatDueDate () {
const timeTillDue = this.calculateTimeTillDue();
const dueIn = timeTillDue.asDays() === 0 ? this.$t('today') : timeTillDue.humanize(true);
return this.task.date && this.$t('dueIn', { dueIn });
if (moment().isSame(this.task.date, 'day')) {
return this.$t('today');
}
return moment(this.task.date).format(this.user.preferences.dateFormat.toUpperCase());
},
edit (e, task) {
if (this.isRunningYesterdailies) return;
@@ -110,7 +110,9 @@ export default {
};
},
updated () {
this.handleExternalLinks();
window.setTimeout(() => {
this.handleExternalLinks();
}, 500);
},
computed: {
...mapState({ user: 'user.data' }),
@@ -16,6 +16,7 @@ export default {
}
if ((link.classList.value.indexOf('external-link') === -1)
&& (!link.offsetParent || link.offsetParent.classList.value.indexOf('link-exempt') === -1)
&& domainIndex !== 1
&& !some(TRUSTED_DOMAINS.split(','), domain => link.href.indexOf(domain) === domainIndex)) {
link.classList.add('external-link');
+3 -1
View File
@@ -1,7 +1,9 @@
export default {
computed: {
numberInvalid () {
return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy);
const inputNumber = Number(this.selectedAmountToBuy);
return inputNumber < 1
|| !Number.isInteger(inputNumber);
},
},
};
+1 -1
View File
@@ -114,7 +114,7 @@ export default {
this.castCancel();
// the selected member doesn't have the flags property which sets `cardReceived`
if (spell.pinType !== 'card') {
if (spell.pinType !== 'card' && spell.bulk !== true) {
try {
spell.cast(this.user, target, {});
} catch (e) {
+10
View File
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import * as Analytics from '@/libs/analytics';
import getStore from '@/store';
import handleRedirect from './handleRedirect';
@@ -439,6 +440,15 @@ router.beforeEach(async (to, from, next) => {
router.app.$root.$emit('update-party');
}
if (to.name === 'lookingForParty') {
Analytics.track({
hitType: 'event',
eventName: 'View Find Members',
eventAction: 'View Find Members',
eventCategory: 'behavior',
}, { trackOnClient: true });
}
// Redirect old guild urls
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
const splits = to.hash.split('/');
@@ -21,6 +21,18 @@ describe('Task Column', () => {
getters: {
'tasks:getFilteredTaskList': () => [],
},
state: {
user: {
data: {
preferences: {
tasks: {
activeFilter: {},
},
},
},
},
},
},
mocks,
stubs,
@@ -76,7 +88,20 @@ describe('Task Column', () => {
'tasks:getFilteredTaskList': () => () => habits,
};
const store = new Store({ getters });
const store = new Store({
getters,
state: {
user: {
data: {
preferences: {
tasks: {
activeFilter: {},
},
},
},
},
},
});
wrapper = makeWrapper({ store });
});
@@ -1,88 +0,0 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import moment from 'moment';
import Task from '@/components/tasks/task.vue';
import Store from '@/libs/store';
const localVue = createLocalVue();
localVue.use(Store);
describe('Task', () => {
let wrapper;
function makeWrapper (additionalTaskData = {}, additionalUserData = {}) {
return shallowMount(Task, {
propsData: {
task: {
group: {},
...additionalTaskData,
},
},
store: {
state: {
user: {
data: {
preferences: {},
...additionalUserData,
},
},
},
getters: {
'tasks:getTaskClasses': () => ({}),
'tasks:canEdit': () => ({}),
'tasks:canDelete': () => ({}),
},
},
mocks: { $t: (key, params) => key + (params ? JSON.stringify(params) : '') },
directives: { 'b-tooltip': {} },
localVue,
});
}
it('returns a vue instance', () => {
wrapper = makeWrapper();
expect(wrapper.isVueInstance()).to.be.true;
});
describe('Due date calculation', () => {
let clock;
function setClockTo (time) {
const now = moment(time);
clock = sinon.useFakeTimers(now.toDate());
return now;
}
afterEach(() => {
clock.restore();
});
it('formats due date to today if due today', () => {
const now = setClockTo('2019-09-17T17:57:00+02:00');
wrapper = makeWrapper({ date: now });
expect(wrapper.vm.formatDueDate()).to.equal('dueIn{"dueIn":"today"}');
});
it('formats due date to tomorrow if due tomorrow', () => {
const now = setClockTo('2012-06-12T14:17:28Z');
wrapper = makeWrapper({ date: now.add(1, 'day') });
expect(wrapper.vm.formatDueDate()).to.equal('dueIn{"dueIn":"in a day"}');
});
it('formats due date to 5 days if due in 5 days', () => {
const now = setClockTo();
wrapper = makeWrapper({ date: now.add(5, 'days') });
expect(wrapper.vm.formatDueDate()).to.equal('dueIn{"dueIn":"in 5 days"}');
});
it('formats due date to tomorrow if today but before dayStart', () => {
const now = setClockTo('2019-06-12T04:23:37+02:00');
wrapper = makeWrapper({ date: now.add(8, 'hours') }, { preferences: { dayStart: 7 } });
expect(wrapper.vm.formatDueDate()).to.equal('dueIn{"dueIn":"in a day"}');
});
});
});
+1 -1
View File
@@ -132,5 +132,5 @@
"subscriptionBenefit6": "Earn Mystic Hourglasses for use in the Time Travelers' Shop!",
"purchaseAll": "Purchase Set",
"gemsRemaining": "gems remaining",
"notEnoughGemsToBuy": "You are unable to buy that amount of gems"
"notEnoughGemsToBuy": "You are unable to buy that amount of gems",
}
+4 -1
View File
@@ -147,5 +147,8 @@
"achievementPolarProModalText": "You collected all the Polar Pets!",
"achievementPlantParent": "Plant Parent",
"achievementPlantParentText": "Has hatched all standard colors of Plant pets: Cactus and Treeling!",
"achievementPlantParentModalText": "You collected all the Plant Pets!"
"achievementPlantParentModalText": "You collected all the Plant Pets!",
"achievementDinosaurDynasty": "Dinosaur Dynasty",
"achievementDinosaurDynastyText": "Has hatched all standard colors of bird and dinosaur pets: Falcon, Owl, Parrot, Peacock, Penguin, Rooster, Pterodactyl, T-Rex, Triceratops, and Velociraptor!",
"achievementDinosaurDynastyModalText": "You collected all the bird and dinosaur pets!"
}
@@ -875,6 +875,14 @@
"backgroundUnderWisteriaText": "Under Wisteria",
"backgroundUnderWisteriaNotes": "Relax Under Wisteria.",
"backgrounds052023": "SET 108: Released May 2023",
"backgroundInAPaintingText": "In A Painting",
"backgroundInAPaintingNotes": "Enjoy creative pursuits Inside a Painting.",
"backgroundFlyingOverHedgeMazeText": "Flying Over Hedge Maze",
"backgroundFlyingOverHedgeMazeNotes": "Marvel while Flying over a Hedge Maze.",
"backgroundCretaceousForestText": "Cretaceous Forest",
"backgroundCretaceousForestNotes": "Take in the ancient greenery of a Cretaceous Forest.",
"timeTravelBackgrounds": "Steampunk Backgrounds",
"backgroundAirshipText": "Airship",
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",
+30 -30
View File
@@ -48,9 +48,9 @@
"webFaqAnswer8": "The blue bar that appeared when you hit level 10 and chose a Class is your Mana bar. As you continue to level up, you will unlock special Skills that cost Mana to use. Each Class has different Skills, which appear after level 11 in the action bar at the bottom of the screen. Unlike your Health bar, your Mana bar does not reset when you gain a level. Instead, Mana is gained when you complete good Habits, Dailies, and To Do's, and lost when you indulge bad Habits. You'll also regain some Mana overnight -- the more Dailies you completed, the more you will gain.",
"faqQuestion9": "How do I take on Quests?",
"iosFaqAnswer9": "First, you need to join or start a Party by selecting \"Party\" in the navigation. Although you can take on Quests alone, we recommend playing with others to make the Quest faster and boost your motivation with extra accountability.\n\nOnce youre in a Party, you can invite Party members to any Quest scrolls in your Inventory. After everyone accepts, the Quest will begin. To progress, complete your tasks as you normally would! Youll either build up damage against a monster if youre taking on a Boss Quest, or have a chance to find items if youre taking on a Collection Quest. All pending progress is applied the next day.\n\nWhen your Party members do enough damage or collect all items, the Quest ends and everyone will receive their rewards!",
"androidFaqAnswer9": "First, you need to join or start a Party by selecting \"Party\" in the navigation. Although you can take on Quests alone, we recommend playing with others to make the Quest faster and boost your motivation with extra accountability.\n\nOnce youre in a Party, you can invite Party members to any Quest scrolls in your Inventory. After everyone accepts, the Quest will begin. To progress, complete your tasks as you normally would! Youll either build up damage against a monster if youre taking on a Boss Quest, or have a chance to find items if youre taking on a Collection Quest. All pending progress is applied the next day.\n\nWhen your Party members do enough damage or collect all items, the Quest ends and everyone will receive their rewards!",
"webFaqAnswer9": "First, you need to join or start a Party by selecting \"Party\" in the navigation. Although you can take on Quests alone, we recommend playing with others to make the Quest faster and boost your motivation with extra accountability.\n\nOnce youre in a Party, you can invite Party members to any Quest scrolls in your Inventory. After everyone accepts, the Quest will begin. To progress, complete your tasks as you normally would! Youll either build up damage against a monster if youre taking on a Boss Quest, or have a chance to find items if youre taking on a Collection Quest. All pending progress is applied the next day.\n\nWhen your Party members do enough damage or collect all items, the Quest ends and everyone will receive their rewards!",
"iosFaqAnswer9": "First, you need to join or start a Party by selecting \"Party\" in the navigation menu. Although you can take on Quests alone, we recommend playing with others to make the Quest faster and boost your motivation with extra accountability.\n\nOnce youre in a Party, you can invite Party members to any Quest scrolls in your Inventory. After everyone accepts, the Quest will begin. To progress, complete your tasks as you normally would! Youll either build up damage against a monster if youre taking on a Boss Quest, or have a chance to find items if youre taking on a Collection Quest. All pending progress is applied the next day.\n\nWhen your Party members do enough damage or collect all items, the Quest ends and everyone will receive their rewards!",
"androidFaqAnswer9": "First, you need to join or start a Party by selecting \"Party\" in the navigation menu. Although you can take on Quests alone, we recommend playing with others to make the Quest faster and boost your motivation with extra accountability.\n\nOnce youre in a Party, you can invite Party members to any Quest scrolls in your Inventory. After everyone accepts, the Quest will begin. To progress, complete your tasks as you normally would! Youll either build up damage against a monster if youre taking on a Boss Quest, or have a chance to find items if youre taking on a Collection Quest. All pending progress is applied the next day.\n\nWhen your Party members do enough damage or collect all items, the Quest ends and everyone will receive their rewards!",
"webFaqAnswer9": "First, you need to join or start a Party by selecting \"Party\" in the navigation menu. Although you can take on Quests alone, we recommend playing with others to make the Quest faster and boost your motivation with extra accountability.\n\nOnce youre in a Party, you can invite Party members to any Quest scrolls in your Inventory. After everyone accepts, the Quest will begin. To progress, complete your tasks as you normally would! Youll either build up damage against a monster if youre taking on a Boss Quest, or have a chance to find items if youre taking on a Collection Quest. All pending progress is applied the next day.\n\nWhen your Party members do enough damage or collect all items, the Quest ends and everyone will receive their rewards!",
"faqQuestion10": "What are Gems, and how do I get them?",
"iosFaqAnswer10": "Gems are purchased with real money from Menu > Purchase Gems. When you buy Gems, you are helping us to keep Habitica running. Were very grateful for every bit of support!\n\n In addition to buying Gems directly, there are three other ways players can gain Gems:\n\n * Win a Challenge that has been set up by another player. Go to Menu > Challenges to join some.\n * Subscribe and unlock the ability to buy a certain number of Gems per month.\n * Contribute your skills to the Habitica project. See this wiki page for more details: [Contributing to Habitica](https://habitica.fandom.com/wiki/Contributing_to_Habitica).\n\n Keep in mind that items purchased with Gems do not offer any statistical advantages, so players can still make use of the app without them!",
@@ -75,29 +75,29 @@
"parties": "Parties",
"faqQuestion14": "How do I find a Party when I'm not in one?",
"iosFaqAnswer14": "If you want to experience Habitica with others but dont know other players, searching for a Party is your best option! If you already know other players that have a Party, you can share your @username with them to be invited. Alternatively, you can create a new Party and invite them with their @username or email address.\n\nTo create or search for a Party, select “Party” in the navigation then choose the option that works for you.",
"androidFaqAnswer14": "If you want to experience Habitica with others but dont know other players, searching for a Party is your best option! If you already know other players that have a Party, you can share your @username with them to be invited. Alternatively, you can create a new Party and invite them with their @username or email address.\n\nTo create or search for a Party, select “Party” in the navigation then choose the option that works for you.",
"webFaqAnswer14": "If you want to experience Habitica with others but dont know other players, searching for a Party is your best option! If you already know other players that have a Party, you can share your @username with them to be invited. Alternatively, you can create a new Party and invite them with their @username or email address.\n\nTo create or search for a Party, select “Party” in the navigation then choose the option that works for you.",
"iosFaqAnswer14": "If you want to experience Habitica with others but dont know other players, searching for a Party is your best option! If you already know other players that have a Party, you can share your @username with them to be invited. Alternatively, you can create a new Party and invite them with their @username or email address.\n\nTo create or search for a Party, select “Party” in the navigation menu, then choose the option that works for you.",
"androidFaqAnswer14": "If you want to experience Habitica with others but dont know other players, searching for a Party is your best option! If you already know other players that have a Party, you can share your @username with them to be invited. Alternatively, you can create a new Party and invite them with their @username or email address.\n\nTo create or search for a Party, select “Party” in the navigation menu, then choose the option that works for you.",
"webFaqAnswer14": "If you want to experience Habitica with others but dont know other players, searching for a Party is your best option! If you already know other players that have a Party, you can share your @username with them to be invited. Alternatively, you can create a new Party and invite them with their @username or email address.\n\nTo create or search for a Party, select “Party” in the navigation menu, then choose the option that works for you.",
"faqQuestion15": "How does searching for a Party work?",
"iosFaqAnswer15": "After selecting “Look for a Party” in the Party pop-up, youll be added to a list of players that want to join a Party. Party leaders can view this list and send invitations. Once you receive an invitation, you can accept it from your notifications to join the Party of your choosing!\n\nYou may get multiple invitations to different Parties. However, you can only be a member of one Party at a time.",
"androidFaqAnswer15": "After selecting “Look for a Party” in the Party pop-up, youll be added to a list of players that want to join a Party. Party leaders can view this list and send invitations. Once you receive an invitation, you can accept it from your notifications to join the Party of your choosing!\n\nYou may get multiple invitations to different Parties. However, you can only be a member of one Party at a time.",
"webFaqAnswer15": "After selecting “Look for a Party” in the Party pop-up, youll be added to a list of players that want to join a Party. Party leaders can view this list and send invitations. Once you receive an invitation, you can accept it from your notifications to join the Party of your choosing!\n\nYou may get multiple invitations to different Parties. However, you can only be a member of one Party at a time.",
"iosFaqAnswer15": "After selecting “Look for a Party”, youll be added to a list of players that want to join a Party. Party leaders can view this list and send invitations. Once you receive an invitation, you can accept it from your notifications to join the Party of your choosing!\n\nYou may get multiple invitations to different Parties. However, you can only be a member of one Party at a time.",
"androidFaqAnswer15": "After selecting “Look for a Party”, youll be added to a list of players that want to join a Party. Party leaders can view this list and send invitations. Once you receive an invitation, you can accept it from your notifications to join the Party of your choosing!\n\nYou may get multiple invitations to different Parties. However, you can only be a member of one Party at a time.",
"webFaqAnswer15": "After selecting “Look for a Party”, youll be added to a list of players that want to join a Party. Party leaders can view this list and send invitations. Once you receive an invitation, you can accept it from your notifications to join the Party of your choosing!\n\nYou may get multiple invitations to different Parties. However, you can only be a member of one Party at a time.",
"faqQuestion16": "How long can I search for a Party after joining the list?",
"iosFaqAnswer16": "You will remain in the list until you accept an invite to a Party or dont login for 7 days, whichever comes first.",
"androidFaqAnswer16": "You will remain in the list until you accept an invite to a Party or dont login for 7 days, whichever comes first.",
"webFaqAnswer16": "You will remain in the list until you accept an invite to a Party or dont login for 7 days, whichever comes first.",
"iosFaqAnswer16": "You will remain in the list until you accept an invite to a Party or dont login for 7 days, whichever comes first. If you log in after being inactive for 7 days, well automatically add you back to the list as long as you don't have a pending invite.",
"androidFaqAnswer16": "You will remain in the list until you accept an invite to a Party or dont login for 7 days, whichever comes first. If you log in after being inactive for 7 days, well automatically add you back to the list as long as you don't have a pending invite.",
"webFaqAnswer16": "You will remain in the list until you accept an invite to a Party or dont login for 7 days, whichever comes first. If you log in after being inactive for 7 days, well automatically add you back to the list as long as you don't have a pending invite.",
"faqQuestion17": "Can I stop searching for a Party?",
"iosFaqAnswer17": "You can leave the list at any time. To stop searching for Party on the website, select the “Party” link in the navigation then click “Leave” in the pop-up.",
"androidFaqAnswer17": "You can leave the list at any time. To stop searching for Party on the website, select the “Party” link in the navigation then click “Leave” in the pop-up.",
"webFaqAnswer17": "You can leave the list at any time. To stop searching for Party on the website, select the “Party” link in the navigation then click “Leave” in the pop-up.",
"iosFaqAnswer17": "If you no longer want to find a Party, you can stop searching at any time.\n\nTo stop searching for a Party on Habiticas website:\n\n1. Select the “Party” link in the navigation.\n2. Click “Leave” in the pop-up.\n\nTo stop searching for a Party on the Android app:\n1. Tap on “Party” from the menu.\n2. Tap “Leave” at the bottom of the screen.",
"androidFaqAnswer17": "If you no longer want to find a Party, you can stop searching at any time.\n\nTo stop searching for a Party on Habiticas website:\n\n1. Select the “Party” link in the navigation.\n2. Click “Leave” in the pop-up.\n\nTo stop searching for a Party on the Android app:\n1. Tap on “Party” from the menu.\n2. Tap “Leave” at the bottom of the screen.",
"webFaqAnswer17": "If you no longer want to find a Party, you can stop searching at any time.\n\nTo stop searching for a Party on Habiticas website:\n\n1. Select the “Party” link in the navigation.\n2. Click “Leave” in the pop-up.\n\nTo stop searching for a Party on the Android app:\n1. Tap on “Party” from the menu.\n2. Tap “Leave” at the bottom of the screen.",
"faqQuestion18": "I have a Party, how do I find more members?",
"iosFaqAnswer18": "If you are using Habiticas website, select “Find Members” from the Party dropdown. This will display a list of Habitica players that are actively looking for a Party and can be invited to join.\n\nYou'll see some information, such as language, class, level, and how many days they have used Habitica to help find a good fit for your Party. If youd like to chat with someone before sending an invite, you can view their Profile and send a message.",
"androidFaqAnswer18": "If you are using Habiticas website, select “Find Members” from the Party dropdown. This will display a list of Habitica players that are actively looking for a Party and can be invited to join.\n\nYou'll see some information, such as language, class, level, and how many days they have used Habitica to help find a good fit for your Party. If youd like to chat with someone before sending an invite, you can view their Profile and send a message.",
"webFaqAnswer18": "If you are using Habiticas website, select “Find Members” from the Party dropdown. This will display a list of Habitica players that are actively looking for a Party and can be invited to join.\n\nYou'll see some information, such as language, class, level, and how many days they have used Habitica to help find a good fit for your Party. If youd like to chat with someone before sending an invite, you can view their Profile and send a message.",
"iosFaqAnswer18": "If you are using Habiticas website, select “Find Members” from the Party dropdown. If youre using the Android app, tap “Find Members” above your Partys member list. This will display a list of players that are actively looking for a Party and can be invited to join.\n\nTo help find a good fit for your Party, you'll see some information, such as language, class, level, and how many days they have used Habitica. If youd like to chat with someone before sending an invite, you can view their Profile and send a message.",
"androidFaqAnswer18": "If you are using Habiticas website, select “Find Members” from the Party dropdown. If youre using the Android app, tap “Find Members” above your Partys member list. This will display a list of players that are actively looking for a Party and can be invited to join.\n\nTo help find a good fit for your Party, you'll see some information, such as language, class, level, and how many days they have used Habitica. If youd like to chat with someone before sending an invite, you can view their Profile and send a message.",
"webFaqAnswer18": "If you are using Habiticas website, select “Find Members” from the Party dropdown. If youre using the Android app, tap “Find Members” above your Partys member list. This will display a list of players that are actively looking for a Party and can be invited to join.\n\nTo help find a good fit for your Party, you'll see some information, such as language, class, level, and how many days they have used Habitica. If youd like to chat with someone before sending an invite, you can view their Profile and send a message.",
"faqQuestion19": "How many members can I invite to my Party?",
"iosFaqAnswer19": "Parties have a maximum limit of 30 members and a minimum of 1 member. Pending invites count towards the member count. For example, 29 members and 1 pending invite would count as 30 members. To clear a pending invite, the invited player must accept or decline, or the Party leader must cancel the invite.",
@@ -105,19 +105,19 @@
"webFaqAnswer19": "Parties have a maximum limit of 30 members and a minimum of 1 member. Pending invites count towards the member count. For example, 29 members and 1 pending invite would count as 30 members. To clear a pending invite, the invited player must accept or decline, or the Party leader must cancel the invite.",
"faqQuestion20": "Can I invite someone I already know?",
"iosFaqAnswer20": "Yes! If you already have a Habitica players username or email address, you can invite them to join your Party. Heres how to send an invite on the different platforms:\n\n## On Habiticas website\n\nNavigate to your Party and click “Invite to Party” on the right-hand side of the page.\n\n## On iOS and Android\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Invite a Member”.",
"androidFaqAnswer20": "Yes! If you already have a Habitica players username or email address, you can invite them to join your Party. Heres how to send an invite on the different platforms:\n\n## On Habiticas website\n\nNavigate to your Party and click “Invite to Party” on the right-hand side of the page.\n\n## On iOS and Android\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Invite a Member”.",
"webFaqAnswer20": "Yes! If you already have a Habitica players username or email address, you can invite them to join your Party. Heres how to send an invite on the different platforms:\n\n## On Habiticas website\n\nNavigate to your Party and click “Invite to Party” on the right-hand side of the page.\n\n## On iOS and Android\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Invite a Member”.",
"iosFaqAnswer20": "Yes! If you already have a Habitica players username or email address, you can invite them to join your Party. Heres how to send an invite on the different platforms:\n\nOn Habiticas website:\n\nNavigate to your Party and click “Invite to Party” on the right-hand side of the page.\n\nOn the Android app:\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Find Members” then tap the “By Invite” tab.\n\nOn the iOS app:\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Invite a Member”.",
"androidFaqAnswer20": "Yes! If you already have a Habitica players username or email address, you can invite them to join your Party. Heres how to send an invite on the different platforms:\n\nOn Habiticas website:\n\nNavigate to your Party and click “Invite to Party” on the right-hand side of the page.\n\nOn the Android app:\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Find Members” then tap the “By Invite” tab.\n\nOn the iOS app:\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Invite a Member”.",
"webFaqAnswer20": "Yes! If you already have a Habitica players username or email address, you can invite them to join your Party. Heres how to send an invite on the different platforms:\n\nOn Habiticas website:\n\nNavigate to your Party and click “Invite to Party” on the right-hand side of the page.\n\nOn the Android app:\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Find Members” then tap the “By Invite” tab.\n\nOn the iOS app:\n\nTap “Party” from the Menu. Scroll to the Members section and tap “Invite a Member”.",
"faqQuestion21": "How do I cancel a pending invitation to my Party?",
"iosFaqAnswer21": "To cancel a pending invitation on Habiticas website:\n\n1. Click on Member list when viewing your Party\n\n2. Click the “Invites” tab\n\n3. Click the three dots beside the users invite you wish to cancel\n\n4. Choose “Cancel Invite”\n\n\n\nYoull be able to cancel a pending invitation from the mobile apps soon as well!",
"androidFaqAnswer21": "To cancel a pending invitation on Habiticas website:\n\n1. Click on Member list when viewing your Party\n\n2. Click the “Invites” tab\n\n3. Click the three dots beside the users invite you wish to cancel\n\n4. Choose “Cancel Invite”\n\n\n\nYoull be able to cancel a pending invitation from the mobile apps soon as well!",
"webFaqAnswer21": "To cancel a pending invitation on Habiticas website:\n\n1. Click on Member list when viewing your Party\n\n2. Click the “Invites” tab\n\n3. Click the three dots beside the users invite you wish to cancel\n\n4. Choose “Cancel Invite”\n\n\n\nYoull be able to cancel a pending invitation from the mobile apps soon as well!",
"iosFaqAnswer21": "To cancel a pending invitation on Habiticas website:\n\n1. Click on Member list when viewing your Party.\n2. Click the “Invites” tab.\n3. Click the three dots beside the users invite you wish to cancel.\n4. Choose “Cancel Invite”\n\nTo cancel a pending invitation on the Android app:\n\n1. Scroll down to your Member list when viewing your Party.\n2. At the bottom of the list, youll see your pending invites.\n3. Tap the “Cancel invitation” button.\n\nYoull be able to cancel a pending invitation from the iOS app soon as well!",
"androidFaqAnswer21": "To cancel a pending invitation on Habiticas website:\n\n1. Click on Member list when viewing your Party.\n2. Click the “Invites” tab.\n3. Click the three dots beside the users invite you wish to cancel.\n4. Choose “Cancel Invite”\n\nTo cancel a pending invitation on the Android app:\n\n1. Scroll down to your Member list when viewing your Party.\n2. At the bottom of the list, youll see your pending invites.\n3. Tap the “Cancel invitation” button.\n\nYoull be able to cancel a pending invitation from the iOS app soon as well!",
"webFaqAnswer21": "To cancel a pending invitation on Habiticas website:\n\n1. Click on Member list when viewing your Party.\n2. Click the “Invites” tab.\n3. Click the three dots beside the users invite you wish to cancel.\n4. Choose “Cancel Invite”\n\nTo cancel a pending invitation on the Android app:\n\n1. Scroll down to your Member list when viewing your Party.\n2. At the bottom of the list, youll see your pending invites.\n3. Tap the “Cancel invitation” button.\n\nYoull be able to cancel a pending invitation from the iOS app soon as well!",
"faqQuestion22": "How do I stop unwanted invitations?",
"iosFaqAnswer22": "Once you join a Party youll stop receiving any more invitations. If you want to prevent invites and future communications from a specific player, view their profile and click the Block button.\n\nIf you encounter a situation where you believe another player has broken our Community Guidelines in their name, profile, or a message they sent, please report any messages or reach out to us at admin@habitica.com.",
"androidFaqAnswer22": "Once you join a Party youll stop receiving any more invitations. If you want to prevent invites and future communications from a specific player, view their profile and click the Block button.\n\nIf you encounter a situation where you believe another player has broken our Community Guidelines in their name, profile, or a message they sent, please report any messages or reach out to us at admin@habitica.com.",
"webFaqAnswer22": "Once you join a Party youll stop receiving any more invitations. If you want to prevent invites and future communications from a specific player, view their profile and click the Block button.\n\nIf you encounter a situation where you believe another player has broken our Community Guidelines in their name, profile, or a message they sent, please report any messages or reach out to us at admin@habitica.com.",
"iosFaqAnswer22": "Once you join a Party youll stop receiving any more invitations. If you want to prevent invites and future communications from a specific player, view their profile and click the Block button. On mobile profiles, tap the three dots in the top corner then select “Block”.\n\nIf you encounter a situation where you believe another player has broken our Community Guidelines in their name, profile, or a message they sent, please report any messages or reach out to us at admin@habitica.com.",
"androidFaqAnswer22": "Once you join a Party youll stop receiving any more invitations. If you want to prevent invites and future communications from a specific player, view their profile and click the Block button. On mobile profiles, tap the three dots in the top corner then select “Block”.\n\nIf you encounter a situation where you believe another player has broken our Community Guidelines in their name, profile, or a message they sent, please report any messages or reach out to us at admin@habitica.com.",
"webFaqAnswer22": "Once you join a Party youll stop receiving any more invitations. If you want to prevent invites and future communications from a specific player, view their profile and click the Block button. On mobile profiles, tap the three dots in the top corner then select “Block”.\n\nIf you encounter a situation where you believe another player has broken our Community Guidelines in their name, profile, or a message they sent, please report any messages or reach out to us at admin@habitica.com.",
"faqQuestion23": "How do I filter the list of members searching for a Party?",
"iosFaqAnswer23": "At the moment there is no way to filter the list of members searching for a Party. However, we have plans to introduce filters in the future, such as class, level, and language.",
@@ -125,9 +125,9 @@
"webFaqAnswer23": "At the moment there is no way to filter the list of members searching for a Party. However, we have plans to introduce filters in the future, such as class, level, and language.",
"faqQuestion24": "How do I search for a Party on Android or iOS?",
"iosFaqAnswer24": "Were working on adding the ability to search for a Party and find Party members on mobile soon! Keep an eye out for Android version 4.2 and iOS version 3.8.",
"androidFaqAnswer24": "Were working on adding the ability to search for a Party and find Party members on mobile soon! Keep an eye out for Android version 4.2 and iOS version 3.8.",
"webFaqAnswer24": "Were working on adding the ability to search for a Party and find Party members on mobile soon! Keep an eye out for Android version 4.2 and iOS version 3.8.",
"iosFaqAnswer24": "We added the ability to look for a Party and find Party members in Android version 4.2! Be sure youre updated to the latest version to check it out. Solo players can look for a Party from the Party screen when they arent in one. Party leaders can browse the list of players looking for an invite by tapping “Find Members” above their Partys member list.\n\nSupport for the feature is coming to iOS in version 3.8, so keep an eye out soon!",
"androidFaqAnswer24": "We added the ability to look for a Party and find Party members in Android version 4.2! Be sure youre updated to the latest version to check it out. Solo players can look for a Party from the Party screen when they arent in one. Party leaders can browse the list of players looking for an invite by tapping “Find Members” above their Partys member list.\n\nSupport for the feature is coming to iOS in version 3.8, so keep an eye out soon!",
"webFaqAnswer24": "We added the ability to look for a Party and find Party members in Android version 4.2! Be sure youre updated to the latest version to check it out. Solo players can look for a Party from the Party screen when they arent in one. Party leaders can browse the list of players looking for an invite by tapping “Find Members” above their Partys member list.\n\nSupport for the feature is coming to iOS in version 3.8, so keep an eye out soon!",
"iosFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
"androidFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
+14 -2
View File
@@ -506,7 +506,9 @@
"weaponMystery202211Text": "Electromancer Staff",
"weaponMystery202211Notes": "Harness the awesome power of a lightning storm with this staff. Confers no benefit. November 2022 Subscriber Item.",
"weaponMystery202212Text": "Glacial Wand",
"weaponMystery202212Notes": "The glowing snowflake in this wand holds the power to warm hearts on even the coldest winter night! Confers no benefit. December 2022 Subscriber Item.",
"weaponMystery202212Notes": "The glowing snowflake in this wand holds the power to warm hearts on even the coldest winter night! Confers no benefit. December 2022 Subscriber Item.",
"weaponMystery202306Text": "Rainbow Umbrella",
"weaponMystery202306Notes": "Shine proud and bring a shimmering prism of color wherever you go! Confers no benefit. June 2023 Subscriber Item.",
"weaponMystery301404Text": "Steampunk Cane",
"weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.",
@@ -694,6 +696,8 @@
"weaponArmoireMagicSpatulaNotes": "Watch your food fly and flip in the air. You get good luck for the day if it magically flips over three times and then lands back on your spatula. Increases Perception by <%= per %>. Enchanted Armoire: Cooking Implements Set (Item 1 of 2).",
"weaponArmoireFinelyCutGemText": "Finely Cut Gem",
"weaponArmoireFinelyCutGemNotes": "What a find! This stunning, precision-cut gem will be the prize of your collection. And it might contain some special magic, just waiting for you to tap into it. Increases Constitution by <%= con %>. Enchanted Armoire: Jeweler Set (Item 4 of 4).",
"weaponArmoirePaintbrushText": "Paintbrush",
"weaponArmoirePaintbrushNotes": "A jolt of pure inspiration rushes through you when you pick up this brush, allowing you to paint anything you can imagine. Increases Intelligence by <%= int %>. Enchanted Armoire: Painter Set (Item 3 of 4).",
"armor": "armor",
"armorCapitalized": "Armor",
@@ -1265,6 +1269,8 @@
"armorMystery202212Notes": "The universe can be cold, but this charming dress will keep you cozy as you fly. Confers no benefit. December 2022 Subscriber Item.",
"armorMystery202304Text": "Tiptop Teapot Armor",
"armorMystery202304Notes": "Here is your handle and here is your spout! Confers no benefit. April 2023 Subscriber Item.",
"armorMystery202306Text": "Rainbow Parka",
"armorMystery202306Notes": "No ones going to rain on your parade! And if they try, youll stay colorful and dry! Confers no benefit. June 2023 Subscriber Item.",
"armorMystery301404Text": "Steampunk Suit",
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
@@ -1453,6 +1459,8 @@
"armorArmoireTeaGownNotes": "Youre resilient, creative, brilliant, and so fashionable! Increases Strength and Intelligence by <%= attrs %> each. Enchanted Armoire: Tea Party Set (Item 1 of 3).",
"armorArmoireBasketballUniformText": "Basketball Uniform",
"armorArmoireBasketballUniformNotes": "Wondering whats printed on the back of this uniform? Its your lucky number, of course! Increases Perception by <% per %>. Enchanted Armoire: Old Timey Basketball Set (Item 1 of 2).",
"armorArmoirePaintersApronText": "Painter's Apron",
"armorArmoirePaintersApronNotes": "This apron can protect your clothes from paint and your creative projects from harsh critiques. Increases Constitution by <%= con %>. Enchanted Armoire: Painter Set (Item 1 of 4).",
"headgear": "helm",
"headgearCapitalized": "Headgear",
@@ -2232,6 +2240,8 @@
"headArmoireTeaHatNotes": "This elegant hat is both fancy and functional. Increases Perception by <%= per %>. Enchanted Armoire: Tea Party Set (Item 2 of 3).",
"headArmoireBeaniePropellerHatText": "Beanie Propeller Hat",
"headArmoireBeaniePropellerHatNotes": "This isnt the time to keep your feet on the ground! Spin this little propeller and rise as high as your ambitions will take you. Increases all stats by <%= attrs %>. Enchanted Armoire: Independent Item.",
"headArmoirePaintersBeretText": "Painter's Beret",
"headArmoirePaintersBeretNotes": "See the world with a more artistic eye when you wear this jaunty beret. Increases Perception by <%= per %>. Enchanted Armoire: Painter Set (Item 2 of 4).",
"offhand": "off-hand item",
"offHandCapitalized": "Off-Hand Item",
@@ -2646,7 +2656,9 @@
"shieldArmoireTeaKettleText": "Tea Kettle",
"shieldArmoireTeaKettleNotes": "All your favorite, flavorful teas can be brewed in this kettle. Are you in the mood for black tea, green tea, oolong, or perhaps an herbal infusion? Increases Constitution by <%= con %>. Enchanted Armoire: Tea Party Set (Item 3 of 3).",
"shieldArmoireBasketballText": "Basketball",
"shieldArmoireBasketballNotes": "Swish! Whenever you shoot this magic basketball, there will be nothing but net. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Old Timey Basketball Set (Item 2 of 2).",
"shieldArmoireBasketballNotes": "Swish! Whenever you shoot this magic basketball, there will be nothing but net. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Old Timey Basketball Set (Item 2 of 2).",
"shieldArmoirePaintersPaletteText": "Painter's Palette",
"shieldArmoirePaintersPaletteNotes": "Paints in all colors of the rainbow are at your disposal. Is it magic that makes them so vivid when you use them, or is it your talent? Increases Strength by <%= str %>. Enchanted Armoire: Painter Set (Item 4 of 4).",
"back": "Back Accessory",
"backBase0Text": "No Back Accessory",
+1 -1
View File
@@ -208,7 +208,7 @@
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> sent you a message",
"letsgo": "Let's Go!",
"selected": "Selected",
"howManyToBuy": "How many would you like to buy?",
"howManyToBuy": "How many would you like to purchase?",
"contactForm": "Contact the Moderation Team",
"loadEarlierMessages": "Load Earlier Messages",
"askQuestion": "Ask a Question",
+1 -1
View File
@@ -130,7 +130,7 @@
"sendGiftHeading": "Send Gift to <%= name %>",
"sendGiftGemsBalance": "From <%= number %> Gems",
"sendGiftCost": "Total: $<%= cost %> USD",
"sendGiftTotal": "Total:",
"sendTotal": "Total:",
"sendGiftFromBalance": "From Balance",
"sendGiftPurchase": "Purchase",
"sendGiftLabel": "Would you like to send a gift message?",
+1
View File
@@ -31,6 +31,7 @@
"howManyToSell": "How many would you like to sell?",
"yourBalance": "Your balance:",
"sell": "Sell",
"sellItems": "Sell Items",
"buyNow": "Buy Now",
"sortByNumber": "Number",
"featuredItems": "Featured Items!",
-2
View File
@@ -5,8 +5,6 @@
"helpWithTranslation": "Would you like to help with the translation of Habitica? Great! Then visit <a href=\"/groups/guild/7732f64c-33ee-4cce-873c-fc28f147a6f7\">the Aspiring Linguists Guild</a>!",
"stickyHeader": "Sticky header",
"newTaskEdit": "Open new tasks in edit mode",
"dailyDueDefaultView": "Set Dailies default to 'due' tab",
"dailyDueDefaultViewPop": "With this option set, the Dailies tasks will default to 'due' instead of 'all'",
"reverseChatOrder": "Show chat messages in reverse order",
"startAdvCollapsed": "Advanced Settings in tasks start collapsed",
"startAdvCollapsedPop": "With this option set, Advanced Settings will be hidden when you first open a task for editing.",
+5 -3
View File
@@ -150,6 +150,7 @@
"mysterySet202303": "Mane Character Set",
"mysterySet202304": "Tiptop Teapot Set",
"mysterySet202305": "Eventide Dragon Set",
"mysterySet202306": "Razzle Dazzle Rainbow Set",
"mysterySet301404": "Steampunk Standard Set",
"mysterySet301405": "Steampunk Accessories Set",
"mysterySet301703": "Peacock Steampunk Set",
@@ -194,8 +195,8 @@
"subscriptionBenefit5": "Receive the Royal Purple Jackalope pet when you become a new subscriber.",
"subscriptionBenefit6": "Earn Mystic Hourglasses to purchase items in the Time Travelers Shop!",
"purchaseAll": "Purchase Set",
"gemsRemaining": "Gems remaining",
"notEnoughGemsToBuy": "You are unable to buy that amount of Gems",
"gemsRemaining": "remaining",
"notEnoughGemsToBuy": "No more Gems available for purchase this month. More will become available within the first 3 days of each month.",
"subscribersReceiveBenefits": "Subscribers receive these useful benefits!",
"monthlyMysteryItems": "Monthly Mystery Items",
"doubleDropCap": "Double the Drops",
@@ -217,5 +218,6 @@
"haveNonRecurringSub": "You have a non-recurring gift subscription.",
"switchToRecurring": "Switch to a recurring subscription?",
"continueGiftSubBenefits": "Want to continue your benefits? You can start a new subscription before your gifted one runs out to keep your benefits active.",
"subscriptionCreditConversion": "Starting a new subscription will convert any remaining months to credit that will be used after the recurring subscription is canceled."
"subscriptionCreditConversion": "Starting a new subscription will convert any remaining months to credit that will be used after the recurring subscription is canceled.",
"monthlyGems": "Monthly Gems:"
}
@@ -178,6 +178,11 @@ const animalSetAchievs = {
titleKey: 'achievementBirdsOfAFeather',
textKey: 'achievementBirdsOfAFeatherText',
},
dinosaurDynasty: {
icon: 'achievement-dinosaurDynasty',
titleKey: 'achievementDinosaurDynasty',
textKey: 'achievementDinosaurDynastyText',
},
domesticated: {
icon: 'achievement-domesticated',
titleKey: 'achievementDomesticated',
@@ -555,6 +555,11 @@ const backgrounds = {
springtime_shower: { },
under_wisteria: { },
},
backgrounds052023: {
in_a_painting: { },
flying_over_hedge_maze: { },
cretaceous_forest: { },
},
eventBackgrounds: {
birthday_bash: {
price: 0,
+2 -2
View File
@@ -178,9 +178,9 @@ const bundles = {
'penguin',
'rooster',
],
event: EVENTS.bundle202109,
event: EVENTS.bundle202305,
canBuy () {
return moment().isBefore(EVENTS.bundle202109.end);
return moment().isBefore(EVENTS.bundle202305.end);
},
type: 'quests',
value: 7,
@@ -26,6 +26,23 @@ const ANIMAL_SET_ACHIEVEMENTS = {
achievementKey: 'birdsOfAFeather',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
dinosaurDynasty: {
type: 'pet',
species: [
'Falcon',
'Owl',
'Parrot',
'Peacock',
'Penguin',
'Rooster',
'Pterodactyl',
'TRex',
'Triceratops',
'Velociraptor',
],
achievementKey: 'dinosaurDynasty',
notificationType: 'ACHIEVEMENT_ANIMAL_SET',
},
domesticated: {
type: 'pet',
species: [
@@ -10,11 +10,19 @@ const gemsPromo = {
export const EVENTS = {
noEvent: {
start: '2023-05-01T23:59-04:00',
start: '2023-05-31T23:59-04:00',
end: '2023-06-22T08:00-04:00',
season: 'normal',
npcImageSuffix: '',
},
bundle202305: {
start:'2023-05-23T08:00-04:00',
end:'2023-05-31T23:59-04:00',
},
potions202305: {
start:'2023-05-16T08:00-04:00',
end:'2023-05-31T23:59-04:00',
},
aprilFools2023: {
start: '2023-04-01T08:00-04:00',
end: '2023-04-02T08:00-04:00',
@@ -420,6 +420,10 @@ const armor = {
per: 10,
set: 'oldTimeyBasketball',
},
paintersApron: {
con: 10,
set: 'painters',
},
};
const body = {
@@ -851,6 +855,10 @@ const head = {
str: 3,
int: 3,
},
paintersBeret: {
per: 9,
set: 'painters',
},
};
const shield = {
@@ -1161,6 +1169,10 @@ const shield = {
str: 5,
set: 'oldTimeyBasketball',
},
paintersPalette: {
str: 7,
set: 'painters',
},
};
const headAccessory = {
@@ -1603,6 +1615,10 @@ const weapon = {
con: 10,
set: 'jewelers',
},
paintbrush: {
int: 8,
set: 'painters',
},
};
forEach({
@@ -61,6 +61,7 @@ const armor = {
202210: { },
202212: { },
202304: { },
202306: { },
301404: { },
301703: { },
301704: { },
@@ -267,6 +268,7 @@ const weapon = {
202201: { },
202212: { },
202209: { },
202306: { },
301404: { },
};
@@ -88,26 +88,26 @@ const premium = {
value: 2,
text: t('hatchingPotionFairy'),
limited: true,
event: EVENTS.potions202105,
event: EVENTS.potions202305,
_addlNotes: t('eventAvailabilityReturning', {
availableDate: t('dateEndMay'),
previousDate: t('mayYYYY', { year: 2020 }),
previousDate: t('mayYYYY', { year: 2021 }),
}),
canBuy () {
return moment().isBefore(EVENTS.potions202105.end);
return moment().isBefore(EVENTS.potions202305.end);
},
},
Floral: {
value: 2,
text: t('hatchingPotionFloral'),
limited: true,
event: EVENTS.potions202205,
event: EVENTS.potions202305,
_addlNotes: t('eventAvailabilityReturning', {
availableDate: t('dateEndMay'),
previousDate: t('mayYYYY', { year: 2021 }),
previousDate: t('mayYYYY', { year: 2022 }),
}),
canBuy () {
return moment().isBefore(EVENTS.potions202205.end);
return moment().isBefore(EVENTS.potions202305.end);
},
},
Aquatic: {
@@ -5,7 +5,7 @@ import { EVENTS } from './constants';
// path: 'premiumHatchingPotions.Rainbow',
const featuredItems = {
market () {
if (moment().isBetween(EVENTS.spring2023.start, EVENTS.spring2023.end)) {
if (moment().isBetween(EVENTS.potions202305.start, EVENTS.potions202305.end)) {
return [
{
type: 'armoire',
@@ -13,15 +13,15 @@ const featuredItems = {
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.PolkaDot',
path: 'premiumHatchingPotions.Fairy',
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.BirchBark',
path: 'premiumHatchingPotions.Floral',
},
{
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Rainbow',
type: 'hatchingPotions',
path: 'hatchingPotions.Golden',
},
];
}
@@ -45,19 +45,19 @@ const featuredItems = {
];
},
quests () { // start date is 3/28
if (moment().isBetween(EVENTS.bundle202303.start, EVENTS.bundle202303.end)) {
if (moment().isBetween(EVENTS.bundle202305.start, EVENTS.bundle202305.end)) {
return [
{
type: 'bundles',
path: 'bundles.jungleBuddies',
path: 'bundles.birdBuddies',
},
{
type: 'quests',
path: 'quests.kraken',
path: 'quests.harpy',
},
{
type: 'quests',
path: 'quests.nudibranch',
path: 'quests.owl',
},
];
}
+25 -37
View File
@@ -77,13 +77,11 @@ spells.wizard = {
lvl: 12,
target: 'party',
notes: t('spellWizardMPHealNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).int;
if (user._id !== member._id && member.stats.class !== 'wizard') {
member.stats.mp += Math.ceil(diminishingReturns(bonus, 25, 125));
}
});
bulk: true,
cast (user, data) {
const bonus = statsComputed(user).int;
data.query['stats.class'] = { $ne: 'wizard' };
data.update = { $inc: { 'stats.mp': Math.ceil(diminishingReturns(bonus, 25, 125)) } };
},
},
earth: { // Earthquake
@@ -92,12 +90,10 @@ spells.wizard = {
lvl: 13,
target: 'party',
notes: t('spellWizardEarthNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).int - user.stats.buffs.int;
if (!member.stats.buffs.int) member.stats.buffs.int = 0;
member.stats.buffs.int += Math.ceil(diminishingReturns(bonus, 30, 200));
});
bulk: true,
cast (user, data) {
const bonus = statsComputed(user).int - user.stats.buffs.int;
data.update = { $inc: { 'stats.buffs.int': Math.ceil(diminishingReturns(bonus, 30, 200)) } };
},
},
frost: { // Chilling Frost
@@ -147,12 +143,10 @@ spells.warrior = {
lvl: 13,
target: 'party',
notes: t('spellWarriorValorousPresenceNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).str - user.stats.buffs.str;
if (!member.stats.buffs.str) member.stats.buffs.str = 0;
member.stats.buffs.str += Math.ceil(diminishingReturns(bonus, 20, 200));
});
bulk: true,
cast (user, data) {
const bonus = statsComputed(user).str - user.stats.buffs.str;
data.update = { $inc: { 'stats.buffs.str': Math.ceil(diminishingReturns(bonus, 20, 200)) } };
},
},
intimidate: { // Intimidating Gaze
@@ -161,12 +155,10 @@ spells.warrior = {
lvl: 14,
target: 'party',
notes: t('spellWarriorIntimidateNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!member.stats.buffs.con) member.stats.buffs.con = 0;
member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 24, 200));
});
bulk: true,
cast (user, data) {
const bonus = statsComputed(user).con - user.stats.buffs.con;
data.update = { $inc: { 'stats.buffs.con': Math.ceil(diminishingReturns(bonus, 24, 200)) } };
},
},
};
@@ -203,12 +195,10 @@ spells.rogue = {
lvl: 13,
target: 'party',
notes: t('spellRogueToolsOfTradeNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).per - user.stats.buffs.per;
if (!member.stats.buffs.per) member.stats.buffs.per = 0;
member.stats.buffs.per += Math.ceil(diminishingReturns(bonus, 100, 50));
});
bulk: true,
cast (user, data) {
const bonus = statsComputed(user).per - user.stats.buffs.per;
data.update = { $inc: { 'stats.buffs.per': Math.ceil(diminishingReturns(bonus, 100, 50)) } };
},
},
stealth: { // Stealth
@@ -257,12 +247,10 @@ spells.healer = {
lvl: 13,
target: 'party',
notes: t('spellHealerProtectAuraNotes'),
cast (user, target) {
each(target, member => {
const bonus = statsComputed(user).con - user.stats.buffs.con;
if (!member.stats.buffs.con) member.stats.buffs.con = 0;
member.stats.buffs.con += Math.ceil(diminishingReturns(bonus, 200, 200));
});
bulk: true,
cast (user, data) {
const bonus = statsComputed(user).con - user.stats.buffs.con;
data.update = { $inc: { 'stats.buffs.con': Math.ceil(diminishingReturns(bonus, 200, 200)) } };
},
},
healAll: { // Blessing
@@ -222,6 +222,7 @@ function _getBasicAchievements (user, language) {
_addSimple(result, user, { path: 'boneToPick', language });
_addSimple(result, user, { path: 'polarPro', language });
_addSimple(result, user, { path: 'plantParent', language });
_addSimple(result, user, { path: 'dinosaurDynasty', language });
_addSimpleWithMasterCount(result, user, { path: 'beastMaster', language });
_addSimpleWithMasterCount(result, user, { path: 'mountMaster', language });
@@ -282,6 +282,8 @@ api.postChat = {
analyticsObject.groupName = group.name;
}
res.analytics.track('group chat', analyticsObject);
if (chatUpdated) {
res.respond(200, { chat: chatRes.chat });
} else {
+6 -8
View File
@@ -93,7 +93,7 @@ api.inviteToQuest = {
user.party.quest.RSVPNeeded = false;
user.party.quest.key = questKey;
await User.update({
await User.updateMany({
'party._id': group._id,
_id: { $ne: user._id },
}, {
@@ -101,7 +101,7 @@ api.inviteToQuest = {
'party.quest.RSVPNeeded': true,
'party.quest.key': questKey,
},
}, { multi: true }).exec();
}).exec();
_.each(members, member => {
group.quest.members[member._id] = null;
@@ -409,10 +409,9 @@ api.cancelQuest = {
const [savedGroup] = await Promise.all([
group.save(),
newChatMessage.save(),
User.update(
User.updateMany(
{ 'party._id': groupId },
Group.cleanQuestParty(),
{ multi: true },
).exec(),
]);
@@ -467,12 +466,11 @@ api.abortQuest = {
});
await newChatMessage.save();
const memberUpdates = User.update({
const memberUpdates = User.updateMany({
'party._id': groupId,
}, Group.cleanQuestParty(),
{ multi: true }).exec();
}, Group.cleanQuestParty()).exec();
const questLeaderUpdate = User.update({
const questLeaderUpdate = User.updateOne({
_id: group.quest.leader,
}, {
$inc: {
+3 -3
View File
@@ -227,7 +227,7 @@ api.deleteTag = {
const tagFound = find(user.tags, tag => tag.id === req.params.tagId);
if (!tagFound) throw new NotFound(res.t('tagNotFound'));
await user.update({
await user.updateOne({
$pull: { tags: { id: tagFound.id } },
}).exec();
@@ -237,13 +237,13 @@ api.deleteTag = {
user._v += 1;
// Remove from all the tasks TODO test
await Tasks.Task.update({
await Tasks.Task.updateMany({
userId: user._id,
}, {
$pull: {
tags: tagFound.id,
},
}, { multi: true }).exec();
}).exec();
res.respond(200, {});
},
+3 -3
View File
@@ -840,7 +840,7 @@ api.moveTask = {
// Cannot send $pull and $push on same field in one single op
const pullQuery = { $pull: {} };
pullQuery.$pull[`tasksOrder.${task.type}s`] = task.id;
await owner.update(pullQuery).exec();
await owner.updateOne(pullQuery).exec();
let position = to;
if (to === -1) position = order.length - 1; // push to bottom
@@ -850,7 +850,7 @@ api.moveTask = {
$each: [task._id],
$position: position,
};
await owner.update(updateQuery).exec();
await owner.updateOne(updateQuery).exec();
// Update the user version field manually,
// it cannot be updated in the pre update hook
@@ -1434,7 +1434,7 @@ api.deleteTask = {
const pullQuery = { $pull: {} };
pullQuery.$pull[`tasksOrder.${task.type}s`] = task._id;
const taskOrderUpdate = (challenge || user).update(pullQuery).exec();
const taskOrderUpdate = (challenge || user).updateOne(pullQuery).exec();
// Update the user version field manually,
// it cannot be updated in the pre update hook
+9 -1
View File
@@ -37,7 +37,15 @@ export default function baseModel (schema, options = {}) {
});
schema.pre('update', function preUpdateModel () {
this.update({}, { $set: { updatedAt: new Date() } });
this.set({}, { $set: { updatedAt: new Date() } });
});
schema.pre('updateOne', function preUpdateModel () {
this.set({}, { $set: { updatedAt: new Date() } });
});
schema.pre('updateMany', function preUpdateModel () {
this.set({}, { $set: { updatedAt: new Date() } });
});
}
@@ -180,6 +180,7 @@ async function addSubToGroupUser (member, group) {
}
// save unused hourglass and mystery items
plan.perkMonthCount = memberPlan.perkMonthCount;
plan.consecutive.trinkets = memberPlan.consecutive.trinkets;
plan.mysteryItems = memberPlan.mysteryItems;
@@ -253,13 +253,18 @@ async function createSubscription (data) {
} = await prepareSubscriptionValues(data);
// Block sub perks
if (months > 0 && (!data.gift || !isNewSubscription)) {
if (months > 1 && (!data.gift || !isNewSubscription)) {
if (!data.gift && !groupId) {
plan.consecutive.offset = block.months;
}
} else if (months === 1) {
plan.consecutive.offset = 0;
}
if (months > 1 || data.gift) {
await plan.incrementPerkCounterAndReward(recipient._id, months);
} else {
// Make sure the perkMonthCount field is initialized.
await plan.incrementPerkCounterAndReward(recipient._id, 0);
}
if (recipient !== group) {
+1 -1
View File
@@ -21,7 +21,7 @@ const apnProvider = APN_ENABLED ? new apn.Provider({
}) : undefined;
function removePushDevice (user, pushDevice) {
return User.update({ _id: user._id }, {
return User.updateOne({ _id: user._id }, {
$pull: { pushDevices: { regId: pushDevice.regId } },
}).exec().catch(err => {
logger.error(err, `Error removing pushDevice ${pushDevice.regId} for user ${user._id}`);
+2 -1
View File
@@ -24,7 +24,7 @@ export function readController (router, controller, overrides = []) {
// If an authentication middleware is used run getUserLanguage after it, otherwise before
// for cron instead use it only if an authentication middleware is present
const authMiddlewareIndex = _.findIndex(middlewares, middleware => {
let authMiddlewareIndex = _.findIndex(middlewares, middleware => {
if (middleware.name.indexOf('authWith') === 0) { // authWith{Headers|Session|Url|...}
return true;
}
@@ -36,6 +36,7 @@ export function readController (router, controller, overrides = []) {
// disable caching for all routes with mandatory or optional authentication
if (authMiddlewareIndex !== -1) {
middlewares.unshift(disableCache);
authMiddlewareIndex += 1;
}
if (action.noLanguage !== true) { // unless getting the language is explictly disabled
+34 -15
View File
@@ -11,7 +11,7 @@ import {
} from '../models/group';
import apiError from './apiError';
const partyMembersFields = 'profile.name stats achievements items.special notifications flags pinnedItems';
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';
@@ -74,12 +74,13 @@ async function castSelfSpell (req, user, spell, quantity = 1) {
await user.save();
}
async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) {
async function getPartyMembers (user, party) {
let partyMembers;
if (!party) {
// Act as solo party
partyMembers = [user]; // eslint-disable-line no-param-reassign
partyMembers = [user];
} else {
partyMembers = await User // eslint-disable-line no-param-reassign
partyMembers = await User
.find({
'party._id': party._id,
_id: { $ne: user._id }, // add separately
@@ -89,22 +90,40 @@ async function castPartySpell (req, party, partyMembers, user, spell, quantity =
partyMembers.unshift(user);
}
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, partyMembers, targetId, user, spell, quantity = 1) {
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; // eslint-disable-line no-param-reassign
partyMembers = user;
} else {
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
if (!party) throw new NotFound(res.t('partyNotFound'));
partyMembers = await User // eslint-disable-line no-param-reassign
partyMembers = await User
.findOne({ _id: targetId, 'party._id': party._id })
.select(partyMembersFields)
.exec();
@@ -195,10 +214,10 @@ async function castSpell (req, res, { isV3 = false }) {
let partyMembers;
if (targetType === 'party') {
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
partyMembers = await castPartySpell(req, party, user, spell, quantity);
} else {
partyMembers = await castUserSpell(
res, req, party, partyMembers,
res, req, party,
targetId, user, spell, quantity,
);
}
+1 -1
View File
@@ -114,7 +114,7 @@ async function createTasks (req, res, options = {}) {
};
}
await owner.update(taskOrderUpdateQuery).exec();
await owner.updateOne(taskOrderUpdateQuery).exec();
// tasks with aliases need to be validated asynchronously
await validateTaskAlias(toSave, res);
+6 -3
View File
@@ -121,7 +121,7 @@ async function checkNewInputForProfanity (user, res, newValue) {
export async function update (req, res, { isV3 = false }) {
const { user } = res.locals;
const promisesForTagsRemoval = [];
let promisesForTagsRemoval = [];
if (req.body['party.seeking'] !== undefined && req.body['party.seeking'] !== null) {
user.invitations.party = {};
@@ -130,6 +130,7 @@ export async function update (req, res, { isV3 = false }) {
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
}
@@ -187,6 +188,7 @@ export async function update (req, res, { isV3 = false }) {
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
} else if (key === 'tags') {
if (!Array.isArray(val)) throw new BadRequest('Tag list must be an array.');
@@ -218,13 +220,13 @@ export async function update (req, res, { isV3 = false }) {
// Remove from all the tasks
// NOTE each tag to remove requires a query
promisesForTagsRemoval.push(removedTagsIds.map(tagId => Tasks.Task.update({
promisesForTagsRemoval = removedTagsIds.map(tagId => Tasks.Task.updateMany({
userId: user._id,
}, {
$pull: {
tags: tagId,
},
}, { multi: true }).exec()));
}).exec());
} else if (key === 'flags.newStuff' && val === false) {
// flags.newStuff was removed from the user schema and is only returned for compatibility
// reasons but we're keeping the ability to set it in API v3
@@ -274,6 +276,7 @@ export async function reset (req, res, { isV3 = false }) {
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
res.respond(200, ...resetRes);
+1 -1
View File
@@ -61,7 +61,7 @@ function sendWebhook (webhook, body, user) {
};
}
return User.update({
return User.updateOne({
_id: user._id,
'webhooks.id': webhook.id,
}, update).exec();
+5 -5
View File
@@ -16,7 +16,7 @@ async function checkForActiveCron (user, now) {
// To avoid double cron we first set _cronSignature
// and then check that it's not changed while processing
const userUpdateResult = await User.update({
const userUpdateResult = await User.updateOne({
_id: user._id,
$or: [ // Make sure last cron was successful or failed before cronRetryTime
{ _cronSignature: 'NOT_RUNNING' },
@@ -36,7 +36,7 @@ async function checkForActiveCron (user, now) {
}
async function updateLastCron (user, now) {
await User.update({
await User.updateOne({
_id: user._id,
}, {
lastCron: now, // setting lastCron now so we don't risk re-running parts of cron if it fails
@@ -44,7 +44,7 @@ async function updateLastCron (user, now) {
}
async function unlockUser (user) {
await User.update({
await User.updateOne({
_id: user._id,
}, {
_cronSignature: 'NOT_RUNNING',
@@ -125,7 +125,7 @@ async function cronAsync (req, res) {
await Group.processQuestProgress(user, progress);
// Set _cronSignature, lastCron and auth.timestamps.loggedin to signal end of cron
await User.update({
await User.updateOne({
_id: user._id,
}, {
$set: {
@@ -153,7 +153,7 @@ async function cronAsync (req, res) {
// For any other error make sure to reset _cronSignature
// so that it doesn't prevent cron from running
// at the next request
await User.update({
await User.updateOne({
_id: user._id,
}, {
_cronSignature: 'NOT_RUNNING',
+16 -15
View File
@@ -102,7 +102,7 @@ schema.methods.addToUser = async function addChallengeToUser (user) {
// Add challenge to users challenges atomically (with a condition that checks that it
// is not there already) to prevent multiple concurrent requests from passing through
// see https://github.com/HabitRPG/habitica/issues/11295
const result = await User.update(
const result = await User.updateOne(
{
_id: user._id,
challenges: { $nin: [this._id] },
@@ -249,7 +249,7 @@ async function _addTaskFn (challenge, tasks, memberId) {
},
};
const updateUserParams = { ...updateTasksOrderQ, ...addToChallengeTagSet };
toSave.unshift(User.update({ _id: memberId }, updateUserParams).exec());
toSave.unshift(User.updateOne({ _id: memberId }, updateUserParams).exec());
return Promise.all(toSave);
}
@@ -278,11 +278,11 @@ schema.methods.updateTask = async function challengeUpdateTask (task) {
const taskSchema = Tasks[task.type];
// Updating instead of loading and saving for performances,
// risks becoming a problem if we introduce more complexity in tasks
await taskSchema.update({
await taskSchema.updateMany({
userId: { $exists: true },
'challenge.id': challenge.id,
'challenge.taskId': task._id,
}, updateCmd, { multi: true }).exec();
}, updateCmd).exec();
};
// Remove a task from challenge members
@@ -290,13 +290,13 @@ schema.methods.removeTask = async function challengeRemoveTask (task) {
const challenge = this;
// Set the task as broken
await Tasks.Task.update({
await Tasks.Task.updateMany({
userId: { $exists: true },
'challenge.id': challenge.id,
'challenge.taskId': task._id,
}, {
$set: { 'challenge.broken': 'TASK_DELETED' },
}, { multi: true }).exec();
}).exec();
};
// Unlink challenges tasks (and the challenge itself) from user. TODO rename to 'leave'
@@ -311,9 +311,9 @@ schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep, sa
this.memberCount -= 1;
if (keep === 'keep-all') {
await Tasks.Task.update(findQuery, {
await Tasks.Task.updateMany(findQuery, {
$set: { challenge: {} },
}, { multi: true }).exec();
}).exec();
const promises = [this.save()];
@@ -356,11 +356,12 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
// Refund the leader if the challenge is deleted (no winner chosen)
if (brokenReason === 'CHALLENGE_DELETED') {
await User.update({ _id: challenge.leader }, { $inc: { balance: challenge.prize / 4 } }).exec();
await User.updateOne({ _id: challenge.leader }, { $inc: { balance: challenge.prize / 4 } })
.exec();
}
// Update the challengeCount on the group
await Group.update({ _id: challenge.group }, { $inc: { challengeCount: -1 } }).exec();
await Group.updateOne({ _id: challenge.group }, { $inc: { challengeCount: -1 } }).exec();
// Award prize to winner and notify
if (winner) {
@@ -370,7 +371,7 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
// reimburse the leader
const winnerCanGetGems = await winner.canGetGems();
if (!winnerCanGetGems) {
await User.update(
await User.updateOne(
{ _id: challenge.leader },
{ $inc: { balance: challenge.prize / 4 } },
).exec();
@@ -408,22 +409,22 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
Tasks.Task.remove({ 'challenge.id': challenge._id, userId: { $exists: false } }).exec(),
// Set the challenge tag to non-challenge status
// and remove the challenge from the user's challenges
User.update({
User.updateMany({
challenges: challenge._id,
'tags.id': challenge._id,
}, {
$set: { 'tags.$.challenge': false },
$pull: { challenges: challenge._id },
}, { multi: true }).exec(),
}).exec(),
// Break users' tasks
Tasks.Task.update({
Tasks.Task.updateMany({
'challenge.id': challenge._id,
}, {
$set: {
'challenge.broken': brokenReason,
'challenge.winner': winner && winner.profile.name,
},
}, { multi: true }).exec(),
}).exec(),
];
Promise.all(backgroundTasks);
+17 -15
View File
@@ -268,10 +268,13 @@ schema.statics.getGroup = async function getGroup (options = {}) {
if (groupId === user.party._id) {
// reset party object to default state
user.party = {};
await user.save();
} else {
removeFromArray(user.guilds, groupId);
const item = removeFromArray(user.guilds, groupId);
if (item) {
await user.save();
}
}
await user.save();
}
return group;
@@ -659,7 +662,7 @@ schema.methods.handleQuestInvitation = async function handleQuestInvitation (use
// to prevent multiple concurrent requests overriding updates
// see https://github.com/HabitRPG/habitica/issues/11398
const Group = this.constructor;
const result = await Group.update(
const result = await Group.updateOne(
{
_id: this._id,
[`quest.members.${user._id}`]: { $type: 10 }, // match BSON Type Null (type number 10)
@@ -707,7 +710,7 @@ schema.methods.startQuest = async function startQuest (user) {
// Persist quest.members early to avoid simultaneous handling of accept/reject
// while processing the rest of this script
await this.update({ $set: { 'quest.members': this.quest.members } }).exec();
await this.updateOne({ $set: { 'quest.members': this.quest.members } }).exec();
const nonUserQuestMembers = _.keys(this.quest.members);
removeFromArray(nonUserQuestMembers, user._id);
@@ -747,7 +750,7 @@ schema.methods.startQuest = async function startQuest (user) {
user.markModified('items.quests');
promises.push(user.save());
} else { // another user is starting the quest, update the leader separately
promises.push(User.update({ _id: this.quest.leader }, {
promises.push(User.updateOne({ _id: this.quest.leader }, {
$inc: {
[`items.quests.${this.quest.key}`]: -1,
},
@@ -755,7 +758,7 @@ schema.methods.startQuest = async function startQuest (user) {
}
// update the remaining users
promises.push(User.update({
promises.push(User.updateMany({
_id: { $in: nonUserQuestMembers },
}, {
$set: {
@@ -763,16 +766,15 @@ schema.methods.startQuest = async function startQuest (user) {
'party.quest.progress.down': 0,
'party.quest.completed': null,
},
}, { multi: true }).exec());
}).exec());
await Promise.all(promises);
// update the users who are not participating
// Do not block updates
User.update({
User.updateMany({
_id: { $in: nonMembers },
}, _cleanQuestParty(),
{ multi: true }).exec();
}, _cleanQuestParty()).exec();
const newMessage = this.sendChat({
message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``,
@@ -903,7 +905,7 @@ function _getUserUpdateForQuestReward (itemToAward, allAwardedItems) {
async function _updateUserWithRetries (userId, updates, numTry = 1, query = {}) {
query._id = userId;
try {
return await User.update(query, updates).exec();
return await User.updateOne(query, updates).exec();
} catch (err) {
if (numTry < MAX_UPDATE_RETRIES) {
numTry += 1; // eslint-disable-line no-param-reassign
@@ -949,7 +951,7 @@ schema.methods.finishQuest = async function finishQuest (quest) {
this.markModified('quest');
if (this._id === TAVERN_ID) {
return User.update({}, updates, { multi: true }).exec();
return User.updateMany({}, updates).exec();
}
const promises = participants.map(userId => {
@@ -1389,10 +1391,10 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
const userUpdate = { $pull: { 'preferences.tasks.mirrorGroupTasks': group._id } };
if (group.type === 'guild') {
userUpdate.$pull.guilds = group._id;
promises.push(User.update({ _id: user._id }, userUpdate).exec());
promises.push(User.updateOne({ _id: user._id }, userUpdate).exec());
} else {
userUpdate.$set = { party: {} };
promises.push(User.update({ _id: user._id }, userUpdate).exec());
promises.push(User.updateOne({ _id: user._id }, userUpdate).exec());
update.$unset = { [`quest.members.${user._id}`]: 1 };
}
@@ -1508,7 +1510,7 @@ schema.methods.unlinkTask = async function groupUnlinkTask (
const promises = [unlinkingTask.save()];
if (keep === 'keep-all') {
await Tasks.Task.update(findQuery, {
await Tasks.Task.updateOne(findQuery, {
$set: { group: {} },
}).exec();
+8 -1
View File
@@ -56,13 +56,20 @@ schema.methods.incrementPerkCounterAndReward = async function incrementPerkCount
if (typeof adding === 'string' || adding instanceof String) {
addingNumber = parseInt(adding, 10);
}
const isSingleMonthPlan = this.planId === 'basic_earned' || this.planId === 'group_plan_auto' || this.planId === 'group_monthly';
// if perkMonthCount wasn't used before, initialize it.
if (this.perkMonthCount === undefined || this.perkMonthCount === -1) {
if (this.planId === 'basic_earned') {
if (isSingleMonthPlan && this.consecutive.count > 0) {
this.perkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH;
} else {
this.perkMonthCount = 0;
}
} else if (isSingleMonthPlan) {
const expectedPerkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH;
if (this.perkMonthCount === (expectedPerkMonthCount - 1)) {
// User was affected by a bug that makes their perkMonthCount off by one
this.perkMonthCount += 1;
}
}
this.perkMonthCount += addingNumber;
+7
View File
@@ -392,6 +392,13 @@ schema.pre('update', function preUpdateUser () {
this.update({}, { $inc: { _v: 1 } });
});
schema.pre('updateOne', function preUpdateUser () {
this.updateOne({}, { $inc: { _v: 1 } });
});
schema.pre('updateMany', function preUpdateUser () {
this.updateMany({}, { $inc: { _v: 1 } });
});
schema.post('save', function postSaveUser () {
// Send a webhook notification when the user has leveled up
if (this._tmp && this._tmp.leveledUp && this._tmp.leveledUp.length > 0) {
+2 -4
View File
@@ -225,10 +225,9 @@ schema.statics.pushNotification = async function pushNotification (
throw validationResult;
}
await this.update(
await this.updateMany(
query,
{ $push: { notifications: newNotification.toObject() } },
{ multi: true },
).exec();
};
@@ -274,13 +273,12 @@ schema.statics.addAchievementUpdate = async function addAchievementUpdate (query
const validationResult = newNotification.validateSync();
if (validationResult) throw validationResult;
await this.update(
await this.updateMany(
query,
{
$push: { notifications: newNotification.toObject() },
$set: { [`achievements.${achievement}`]: true },
},
{ multi: true },
).exec();
};
+8
View File
@@ -154,6 +154,7 @@ export default new Schema({
boneToPick: Boolean,
polarPro: Boolean,
plantParent: Boolean,
dinosaurDynasty: Boolean,
// Onboarding Guide
createdTask: Boolean,
completedTask: Boolean,
@@ -534,6 +535,7 @@ export default new Schema({
stickyHeader: { $type: Boolean, default: true },
disableClasses: { $type: Boolean, default: false },
newTaskEdit: { $type: Boolean, default: false },
// not used anymore, now the current filter is saved in preferences.activeFilter
dailyDueDefaultView: { $type: Boolean, default: false },
advancedCollapsed: { $type: Boolean, default: false },
toolbarCollapsed: { $type: Boolean, default: false },
@@ -594,6 +596,12 @@ export default new Schema({
mirrorGroupTasks: [
{ $type: String, validate: [v => validator.isUUID(v), 'Invalid group UUID.'], ref: 'Group' },
],
activeFilter: {
habit: { $type: String, default: 'all' },
daily: { $type: String, default: 'all' },
todo: { $type: String, default: 'remaining' },
reward: { $type: String, default: 'all' },
},
},
improvementCategories: {
$type: Array,