Skip to content

Commit 810d8bf

Browse files
feat: milestone achievements (#3747)
1 parent ac266d3 commit 810d8bf

10 files changed

Lines changed: 1209 additions & 61 deletions

File tree

__tests__/questProgress.ts

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,28 @@ import {
99
User,
1010
} from '../src/entity';
1111
import { UserQuest, UserQuestStatus } from '../src/entity/user';
12-
import { checkQuestProgress } from '../src/common/quest';
12+
import {
13+
checkQuestProgress,
14+
syncMilestoneQuestProgress,
15+
} from '../src/common/quest';
1316
import { createMockLogger, saveFixtures } from './helpers';
1417

1518
const userId = '11111111-1111-4111-8111-111111111111';
1619
const questIds = [
1720
'22222222-2222-4222-8222-222222222222',
1821
'33333333-3333-4333-8333-333333333333',
1922
'44444444-4444-4444-8444-444444444444',
23+
'88888888-8888-4888-8888-888888888881',
24+
'88888888-8888-4888-8888-888888888882',
25+
'88888888-8888-4888-8888-888888888883',
2026
];
2127
const rotationIds = [
2228
'55555555-5555-4555-8555-555555555555',
2329
'66666666-6666-4666-8666-666666666666',
2430
'77777777-7777-4777-8777-777777777777',
31+
'99999999-9999-4999-8999-999999999991',
32+
'99999999-9999-4999-8999-999999999992',
33+
'99999999-9999-4999-8999-999999999993',
2534
];
2635

2736
let con: DataSource;
@@ -196,6 +205,230 @@ describe('checkQuestProgress', () => {
196205
expect(userQuest.status).toBe(UserQuestStatus.InProgress);
197206
});
198207

208+
it('should not advance quest completion milestone on completion alone, only on claim', async () => {
209+
const now = new Date();
210+
const logger = createMockLogger();
211+
const periodStart = new Date(now.getTime() - 60 * 60 * 1000);
212+
const periodEnd = new Date(now.getTime() + 60 * 60 * 1000);
213+
const milestonePeriodStart = new Date('2026-03-25T00:00:00.000Z');
214+
const milestonePeriodEnd = new Date('9999-12-31T23:59:59.000Z');
215+
216+
await saveFixtures(con, Quest, [
217+
{
218+
id: questIds[3],
219+
name: 'Up and comer',
220+
description: 'Claim 2 quests',
221+
type: QuestType.Milestone,
222+
eventType: QuestEventType.QuestComplete,
223+
criteria: { targetCount: 2 },
224+
active: true,
225+
},
226+
{
227+
id: questIds[4],
228+
name: 'Daily upvotes 1',
229+
description: 'Upvote 1 post',
230+
type: QuestType.Daily,
231+
eventType: QuestEventType.PostUpvote,
232+
criteria: { targetCount: 1 },
233+
active: true,
234+
},
235+
{
236+
id: questIds[5],
237+
name: 'Daily upvotes 2',
238+
description: 'Upvote 1 more post',
239+
type: QuestType.Daily,
240+
eventType: QuestEventType.PostUpvote,
241+
criteria: { targetCount: 1 },
242+
active: true,
243+
},
244+
]);
245+
246+
await saveFixtures(con, QuestRotation, [
247+
{
248+
id: rotationIds[3],
249+
questId: questIds[3],
250+
type: QuestType.Milestone,
251+
plusOnly: false,
252+
slot: 1,
253+
periodStart: milestonePeriodStart,
254+
periodEnd: milestonePeriodEnd,
255+
},
256+
{
257+
id: rotationIds[4],
258+
questId: questIds[4],
259+
type: QuestType.Daily,
260+
plusOnly: false,
261+
slot: 1,
262+
periodStart,
263+
periodEnd,
264+
},
265+
{
266+
id: rotationIds[5],
267+
questId: questIds[5],
268+
type: QuestType.Daily,
269+
plusOnly: false,
270+
slot: 2,
271+
periodStart,
272+
periodEnd,
273+
},
274+
]);
275+
276+
const didUpdate = await checkQuestProgress({
277+
con,
278+
logger,
279+
userId,
280+
eventType: QuestEventType.PostUpvote,
281+
incrementBy: 1,
282+
now,
283+
});
284+
285+
expect(didUpdate).toBe(true);
286+
287+
const userQuests = await con.getRepository(UserQuest).find({
288+
where: { userId },
289+
order: { rotationId: 'ASC' },
290+
});
291+
292+
expect(userQuests).toHaveLength(3);
293+
expect(
294+
userQuests
295+
.filter(({ rotationId }) =>
296+
[rotationIds[4], rotationIds[5]].includes(rotationId),
297+
)
298+
.map(({ progress, status }) => ({ progress, status })),
299+
).toEqual([
300+
{
301+
progress: 1,
302+
status: UserQuestStatus.Completed,
303+
},
304+
{
305+
progress: 1,
306+
status: UserQuestStatus.Completed,
307+
},
308+
]);
309+
310+
const milestoneQuest = userQuests.find(
311+
({ rotationId }) => rotationId === rotationIds[3],
312+
);
313+
expect(milestoneQuest).toMatchObject({
314+
progress: 0,
315+
status: UserQuestStatus.InProgress,
316+
});
317+
});
318+
319+
it('should advance quest completion milestone when quests are claimed', async () => {
320+
const now = new Date();
321+
const logger = createMockLogger();
322+
const periodStart = new Date(now.getTime() - 60 * 60 * 1000);
323+
const periodEnd = new Date(now.getTime() + 60 * 60 * 1000);
324+
const milestonePeriodStart = new Date('2026-03-25T00:00:00.000Z');
325+
const milestonePeriodEnd = new Date('9999-12-31T23:59:59.000Z');
326+
327+
await saveFixtures(con, Quest, [
328+
{
329+
id: questIds[3],
330+
name: 'Up and comer',
331+
description: 'Claim 2 quests',
332+
type: QuestType.Milestone,
333+
eventType: QuestEventType.QuestComplete,
334+
criteria: { targetCount: 2 },
335+
active: true,
336+
},
337+
{
338+
id: questIds[4],
339+
name: 'Daily upvotes 1',
340+
description: 'Upvote 1 post',
341+
type: QuestType.Daily,
342+
eventType: QuestEventType.PostUpvote,
343+
criteria: { targetCount: 1 },
344+
active: true,
345+
},
346+
{
347+
id: questIds[5],
348+
name: 'Daily upvotes 2',
349+
description: 'Upvote 1 more post',
350+
type: QuestType.Daily,
351+
eventType: QuestEventType.PostUpvote,
352+
criteria: { targetCount: 1 },
353+
active: true,
354+
},
355+
]);
356+
357+
await saveFixtures(con, QuestRotation, [
358+
{
359+
id: rotationIds[3],
360+
questId: questIds[3],
361+
type: QuestType.Milestone,
362+
plusOnly: false,
363+
slot: 1,
364+
periodStart: milestonePeriodStart,
365+
periodEnd: milestonePeriodEnd,
366+
},
367+
{
368+
id: rotationIds[4],
369+
questId: questIds[4],
370+
type: QuestType.Daily,
371+
plusOnly: false,
372+
slot: 1,
373+
periodStart,
374+
periodEnd,
375+
},
376+
{
377+
id: rotationIds[5],
378+
questId: questIds[5],
379+
type: QuestType.Daily,
380+
plusOnly: false,
381+
slot: 2,
382+
periodStart,
383+
periodEnd,
384+
},
385+
]);
386+
387+
// Complete the daily quests
388+
await checkQuestProgress({
389+
con,
390+
logger,
391+
userId,
392+
eventType: QuestEventType.PostUpvote,
393+
incrementBy: 1,
394+
now,
395+
});
396+
397+
// Simulate claiming both daily quests
398+
const dailyUserQuests = await con.getRepository(UserQuest).find({
399+
where: {
400+
userId,
401+
rotationId: In([rotationIds[4], rotationIds[5]]),
402+
},
403+
});
404+
405+
for (const uq of dailyUserQuests) {
406+
await con
407+
.getRepository(UserQuest)
408+
.update(
409+
{ id: uq.id },
410+
{ status: UserQuestStatus.Claimed, claimedAt: now },
411+
);
412+
}
413+
414+
// Sync milestones after claims
415+
await syncMilestoneQuestProgress({
416+
con,
417+
userId,
418+
eventType: QuestEventType.QuestComplete,
419+
now,
420+
});
421+
422+
const milestoneQuest = await con.getRepository(UserQuest).findOneOrFail({
423+
where: { userId, rotationId: rotationIds[3] },
424+
});
425+
426+
expect(milestoneQuest).toMatchObject({
427+
progress: 2,
428+
status: UserQuestStatus.Completed,
429+
});
430+
});
431+
199432
it('should not update claimed quests', async () => {
200433
const now = new Date();
201434
const logger = createMockLogger();

0 commit comments

Comments
 (0)