Skip to content

Commit 51cae64

Browse files
authored
some more achievements (#1319)
* some more achievements * update on update
1 parent 1e89379 commit 51cae64

File tree

6 files changed

+109
-11
lines changed

6 files changed

+109
-11
lines changed

constants/achievements/AchievementRulesSocial.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IAchievementInfo } from './achievementInfo';
22
import AchievementType from './achievementType';
33

44
interface IAchievementInfoSocial extends IAchievementInfo {
5-
unlocked: ({ commentCount, hasWelcomed, hasSharedToSocial }: { commentCount: number, hasWelcomed: boolean, hasSharedToSocial: boolean }) => boolean;
5+
unlocked: ({ commentCount, hasWelcomed, hasSharedToSocial, user }: { commentCount: number, hasWelcomed: boolean, hasSharedToSocial: boolean, user: User }) => boolean;
66
}
77

88
const AchievementRulesSocial: { [achievementType: string]: IAchievementInfoSocial } = {};
@@ -57,6 +57,26 @@ AchievementRulesSocial[AchievementType.WELCOME] = {
5757
return hasWelcomed;
5858
},
5959
};
60+
AchievementRulesSocial[AchievementType.UPLOAD_AVATAR] = {
61+
getDescription: () => 'Uploaded an avatar to personalize your profile',
62+
name: 'Avatar Awakened',
63+
emoji: '🖼️',
64+
discordNotification: false,
65+
secret: true,
66+
unlocked: ({ user }) => {
67+
return !!(user?.avatarUpdatedAt);
68+
},
69+
};
70+
AchievementRulesSocial[AchievementType.UPDATE_BIO] = {
71+
getDescription: () => 'Added a bio to your profile',
72+
name: 'Autobiographer',
73+
emoji: '✍️',
74+
discordNotification: false,
75+
secret: true,
76+
unlocked: ({ user }) => {
77+
return !!(user?.bio && user.bio.length > 0);
78+
},
79+
};
6080
AchievementRulesSocial[AchievementType.SOCIAL_SHARE] = {
6181
getDescription: () => 'Shared a level to a social network',
6282
name: 'Social Sharer',

constants/achievements/achievementType.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ enum AchievementType {
4848
CHAPTER_1_COMPLETED = 'CHAPTER_1_COMPLETED',
4949
CHAPTER_2_COMPLETED = 'CHAPTER_2_COMPLETED',
5050
CHAPTER_3_COMPLETED = 'CHAPTER_3_COMPLETED',
51+
UPLOAD_AVATAR = 'UPLOAD_AVATAR',
52+
UPDATE_BIO = 'UPDATE_BIO',
5153
SOCIAL_SHARE = 'SOCIAL_SHARE',
5254
}
5355

helpers/refreshAchievements.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import { getRecordsByUserId } from './getRecordsByUserId';
2121

2222
const AchievementCategoryFetch = {
2323
// no game ID as this is a global
24-
[AchievementCategory.SOCIAL]: async (gameId: GameId, userId: Types.ObjectId) => {
25-
const [commentCount, welcomeComments, socialShareCount] = await Promise.all([
24+
[AchievementCategory.SOCIAL]: async (_gameId: GameId, userId: Types.ObjectId) => {
25+
const [commentCount, welcomeComments, socialShareCount, user] = await Promise.all([
2626
CommentModel.countDocuments({
2727
author: userId,
2828
deletedAt: null,
@@ -34,23 +34,22 @@ const AchievementCategoryFetch = {
3434
target: { $ne: userId },
3535
text: { $regex: /welcome/i },
3636
}).populate('target').lean<Comment[]>(),
37-
GraphModel.countDocuments({ source: userId, type: GraphType.SHARE })
37+
GraphModel.countDocuments({ source: userId, type: GraphType.SHARE }),
38+
UserModel.findById(userId).select('+bio +avatarUpdatedAt').lean<User>()
3839
]);
3940

4041
const hasWelcomed = welcomeComments.some((comment) => {
41-
const user = comment.target as unknown as User;
42+
const targetUser = comment.target as unknown as User;
4243

43-
if (!user?.ts) {
44+
if (!targetUser?.ts) {
4445
return false;
4546
}
4647

4748
// if the comment was made in the first 24 hrs since account creation
48-
return comment.createdAt.getTime() - 1000 * user.ts < 24 * 60 * 60 * 1000;
49+
return comment.createdAt.getTime() - 1000 * targetUser.ts < 24 * 60 * 60 * 1000;
4950
});
5051

51-
const hasSharedToSocial = socialShareCount > 0;
52-
53-
return { commentCount: commentCount, hasWelcomed: hasWelcomed, hasSharedToSocial: hasSharedToSocial };
52+
return { commentCount: commentCount, hasWelcomed: hasWelcomed, hasSharedToSocial: socialShareCount > 0, user: user };
5453
},
5554
[AchievementCategory.PROGRESS]: async (gameId: GameId, userId: Types.ObjectId) => {
5655
const userConfig = await UserConfigModel.findOne({ userId: userId, gameId: gameId }).lean<User>();

pages/api/user/image.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { Types } from 'mongoose';
22
import { NextApiResponse } from 'next';
33
import sharp from 'sharp';
4+
import AchievementCategory from '../../../constants/achievements/achievementCategory';
45
import Dimensions from '../../../constants/dimensions';
6+
import { GameId } from '../../../constants/GameId';
57
import { TimerUtil } from '../../../helpers/getTs';
68
import { logger } from '../../../helpers/logger';
79
import dbConnect from '../../../lib/dbConnect';
810
import withAuth, { NextApiRequestWithAuth } from '../../../lib/withAuth';
911
import { ImageModel, UserModel } from '../../../models/mongoose';
12+
import { queueRefreshAchievements } from '../internal-jobs/worker/queueFunctions';
1013

1114
export const config = {
1215
api: {
@@ -96,6 +99,7 @@ export default withAuth({ PUT: {} }, async (req: NextApiRequestWithAuth, res: Ne
9699
ts: ts,
97100
} }),
98101
UserModel.updateOne({ _id: req.userId }, { $set: { avatarUpdatedAt: ts } }),
102+
queueRefreshAchievements(GameId.THINKY, req.userId, [AchievementCategory.SOCIAL]),
99103
]);
100104

101105
return res.status(200).send({ updated: true });

pages/api/user/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import User from '@root/models/db/user';
99
import bcrypt from 'bcryptjs';
1010
import mongoose from 'mongoose';
1111
import type { NextApiResponse } from 'next';
12+
import AchievementCategory from '../../../constants/achievements/achievementCategory';
13+
import { GameId } from '../../../constants/GameId';
1214
import PrivateTagType from '../../../constants/privateTagType';
1315
import { ValidType } from '../../../helpers/apiWrapper';
1416
import { generateCollectionSlug, generateLevelSlug } from '../../../helpers/generateSlug';
@@ -19,6 +21,7 @@ import dbConnect from '../../../lib/dbConnect';
1921
import withAuth, { NextApiRequestWithAuth } from '../../../lib/withAuth';
2022
import Level from '../../../models/db/level';
2123
import { CollectionModel, LevelModel, MultiplayerProfileModel, UserModel } from '../../../models/mongoose';
24+
import { queueRefreshAchievements } from '../internal-jobs/worker/queueFunctions';
2225
import { getSubscriptions, SubscriptionData } from '../subscription';
2326
import { getUserConfig } from '../user-config';
2427

@@ -177,6 +180,11 @@ export default withAuth({
177180
return res.status(error.status).json({ error: error.message });
178181
}
179182
}
183+
184+
// Queue achievement refresh if bio was updated
185+
if (bio !== undefined) {
186+
await queueRefreshAchievements(GameId.THINKY, req.user._id, [AchievementCategory.SOCIAL]);
187+
}
180188
// eslint-disable-next-line @typescript-eslint/no-explicit-any
181189
} catch (err: any) {
182190
logger.error(err);

tests/helpers/refreshAchievements.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import GraphType from '@root/constants/graphType';
55
import TestId from '@root/constants/testId';
66
import { refreshAchievements } from '@root/helpers/refreshAchievements';
77
import { getTokenCookieValue } from '@root/lib/getTokenCookie';
8-
import { AchievementModel, GraphModel, LevelModel, UserConfigModel } from '@root/models/mongoose';
8+
import { AchievementModel, GraphModel, LevelModel, UserConfigModel, UserModel } from '@root/models/mongoose';
99
import { processQueueMessages } from '@root/pages/api/internal-jobs/worker';
1010
import socialShareHandler from '@root/pages/api/social-share/index';
1111
import { Types } from 'mongoose';
@@ -83,6 +83,71 @@ describe('helpers/refreshAchievements.ts', () => {
8383
expect(achievements.some(a => a.type === AchievementType.CHAPTER_3_COMPLETED)).toBe(true);
8484
});
8585

86+
test('test avatar upload achievement', async () => {
87+
// Clean up any existing achievements
88+
await AchievementModel.deleteMany({ userId: TestId.USER });
89+
90+
// Test without avatar
91+
await UserModel.updateOne(
92+
{ _id: TestId.USER },
93+
{ $unset: { avatarUpdatedAt: 1 } }
94+
);
95+
96+
await refreshAchievements(GameId.THINKY, new Types.ObjectId(TestId.USER), [AchievementCategory.SOCIAL]);
97+
let achievements = await AchievementModel.find({ userId: TestId.USER }).lean();
98+
99+
expect(achievements.some(a => a.type === AchievementType.UPLOAD_AVATAR)).toBe(false);
100+
101+
// Test with avatar
102+
await UserModel.updateOne(
103+
{ _id: TestId.USER },
104+
{ $set: { avatarUpdatedAt: Date.now() } }
105+
);
106+
107+
await refreshAchievements(GameId.THINKY, new Types.ObjectId(TestId.USER), [AchievementCategory.SOCIAL]);
108+
achievements = await AchievementModel.find({ userId: TestId.USER }).lean();
109+
110+
expect(achievements.some(a => a.type === AchievementType.UPLOAD_AVATAR)).toBe(true);
111+
});
112+
113+
test('test bio update achievement', async () => {
114+
// Clean up any existing achievements
115+
await AchievementModel.deleteMany({ userId: TestId.USER });
116+
117+
// Test without bio
118+
await UserModel.updateOne(
119+
{ _id: TestId.USER },
120+
{ $unset: { bio: 1 } }
121+
);
122+
123+
await refreshAchievements(GameId.THINKY, new Types.ObjectId(TestId.USER), [AchievementCategory.SOCIAL]);
124+
let achievements = await AchievementModel.find({ userId: TestId.USER }).lean();
125+
126+
expect(achievements.some(a => a.type === AchievementType.UPDATE_BIO)).toBe(false);
127+
128+
// Test with empty bio
129+
await UserModel.updateOne(
130+
{ _id: TestId.USER },
131+
{ $set: { bio: '' } }
132+
);
133+
134+
await refreshAchievements(GameId.THINKY, new Types.ObjectId(TestId.USER), [AchievementCategory.SOCIAL]);
135+
achievements = await AchievementModel.find({ userId: TestId.USER }).lean();
136+
137+
expect(achievements.some(a => a.type === AchievementType.UPDATE_BIO)).toBe(false);
138+
139+
// Test with non-empty bio
140+
await UserModel.updateOne(
141+
{ _id: TestId.USER },
142+
{ $set: { bio: 'This is my bio!' } }
143+
);
144+
145+
await refreshAchievements(GameId.THINKY, new Types.ObjectId(TestId.USER), [AchievementCategory.SOCIAL]);
146+
achievements = await AchievementModel.find({ userId: TestId.USER }).lean();
147+
148+
expect(achievements.some(a => a.type === AchievementType.UPDATE_BIO)).toBe(true);
149+
});
150+
86151
describe('SOCIAL achievements', () => {
87152
beforeEach(async () => {
88153
await AchievementModel.deleteMany({ userId: TestId.USER });

0 commit comments

Comments
 (0)