diff --git a/shared/modules/quest/quest.service.test.ts b/shared/modules/quest/quest.service.test.ts index 96cadc6..cef36f1 100644 --- a/shared/modules/quest/quest.service.test.ts +++ b/shared/modules/quest/quest.service.test.ts @@ -75,6 +75,12 @@ describe("questService", () => { describe("assignQuest", () => { it("should assign quest", async () => { + const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service"); + const mockGetSettings = spyOn(gameSettingsService, 'getSettings').mockResolvedValue({ + quest: { maxActiveQuests: 3 }, + } as any); + + mockFindMany.mockResolvedValue([]); // no active quests mockReturning.mockResolvedValue([{ userId: 1n, questId: 101 }]); const result = await questService.assignQuest("1", 101); @@ -86,6 +92,47 @@ describe("questService", () => { questId: 101, progress: 0 }); + + mockGetSettings.mockRestore(); + }); + + it("should throw when user has reached active quest limit", async () => { + // Mock gameSettingsService to return maxActiveQuests: 2 + const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service"); + const mockGetSettings = spyOn(gameSettingsService, 'getSettings').mockResolvedValue({ + quest: { maxActiveQuests: 2 }, + } as any); + + // User has 2 incomplete quests + mockFindMany.mockResolvedValue([ + { userId: 1n, questId: 1, completedAt: null }, + { userId: 1n, questId: 2, completedAt: null }, + ]); + + expect(questService.assignQuest("1", 3)).rejects.toThrow( + "You can only have 2 active quests at a time. Complete a quest before accepting a new one." + ); + + mockGetSettings.mockRestore(); + }); + + it("should allow assignment when completed quests don't count toward limit", async () => { + const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service"); + const mockGetSettings = spyOn(gameSettingsService, 'getSettings').mockResolvedValue({ + quest: { maxActiveQuests: 2 }, + } as any); + + // User has 2 quests but 1 is completed + mockFindMany.mockResolvedValue([ + { userId: 1n, questId: 1, completedAt: null }, + { userId: 1n, questId: 2, completedAt: new Date() }, + ]); + mockReturning.mockResolvedValue([{ userId: 1n, questId: 3 }]); + + const result = await questService.assignQuest("1", 3); + expect(result).toEqual([{ userId: 1n, questId: 3 }]); + + mockGetSettings.mockRestore(); }); }); diff --git a/shared/modules/quest/quest.service.ts b/shared/modules/quest/quest.service.ts index c9efe65..06eda87 100644 --- a/shared/modules/quest/quest.service.ts +++ b/shared/modules/quest/quest.service.ts @@ -4,6 +4,7 @@ import { UserError } from "@shared/lib/errors"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { economyService } from "@shared/modules/economy/economy.service"; import { levelingService } from "@shared/modules/leveling/leveling.service"; +import { gameSettingsService } from "@shared/modules/game-settings/game-settings.service"; import { withTransaction } from "@/lib/db"; import type { Transaction } from "@shared/lib/types"; import { TransactionType } from "@shared/lib/constants"; @@ -12,13 +13,29 @@ import { systemEvents, EVENTS } from "@shared/lib/events"; export const questService = { assignQuest: async (userId: string, questId: number, tx?: Transaction) => { return await withTransaction(async (txFn) => { + // Check active quest limit + const settings = await gameSettingsService.getSettings(); + const maxActive = settings?.quest?.maxActiveQuests ?? 3; + + const userQuestList = await txFn.query.userQuests.findMany({ + where: eq(userQuests.userId, BigInt(userId)), + }); + + const activeCount = userQuestList.filter(uq => !uq.completedAt).length; + + if (activeCount >= maxActive) { + throw new UserError( + `You can only have ${maxActive} active quests at a time. Complete a quest before accepting a new one.` + ); + } + return await txFn.insert(userQuests) .values({ userId: BigInt(userId), questId: questId, progress: 0, }) - .onConflictDoNothing() // Ignore if already assigned + .onConflictDoNothing() .returning(); }, tx); },