From 52f8ab11f08815783a1c59aa3715d0a8216e7d99 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 15 Jan 2026 15:04:50 +0100 Subject: [PATCH] feat: Implement quest event handling and integrate it into leveling, economy, and inventory services. --- shared/modules/economy/economy.service.ts | 4 ++ shared/modules/inventory/inventory.service.ts | 14 +++++ shared/modules/leveling/leveling.service.ts | 4 ++ shared/modules/quest/quest.service.test.ts | 56 +++++++++++++++++++ shared/modules/quest/quest.service.ts | 31 ++++++++++ 5 files changed, 109 insertions(+) diff --git a/shared/modules/economy/economy.service.ts b/shared/modules/economy/economy.service.ts index 6d026e4..f12eab0 100644 --- a/shared/modules/economy/economy.service.ts +++ b/shared/modules/economy/economy.service.ts @@ -196,6 +196,10 @@ export const economyService = { description: description, }); + // Trigger Quest Event + const { questService } = await import("@shared/modules/quest/quest.service"); + await questService.handleEvent(id, type, 1, txFn); + return user; }, tx); }, diff --git a/shared/modules/inventory/inventory.service.ts b/shared/modules/inventory/inventory.service.ts index 7f702d4..7f7628b 100644 --- a/shared/modules/inventory/inventory.service.ts +++ b/shared/modules/inventory/inventory.service.ts @@ -37,6 +37,11 @@ export const inventoryService = { eq(inventory.itemId, itemId) )) .returning(); + + // Trigger Quest Event + const { questService } = await import("@shared/modules/quest/quest.service"); + await questService.handleEvent(userId, 'ITEM_COLLECT', Number(quantity), txFn); + return entry; } else { // Check Slot Limit @@ -60,6 +65,11 @@ export const inventoryService = { quantity: quantity, }) .returning(); + + // Trigger Quest Event + const { questService } = await import("@shared/modules/quest/quest.service"); + await questService.handleEvent(userId, 'ITEM_COLLECT', Number(quantity), txFn); + return entry; } }, tx); @@ -179,6 +189,10 @@ export const inventoryService = { await inventoryService.removeItem(userId, itemId, 1n, txFn); } + // Trigger Quest Event + const { questService } = await import("@shared/modules/quest/quest.service"); + await questService.handleEvent(userId, 'ITEM_USE', 1, txFn); + return { success: true, results, usageData, item }; }, tx); }, diff --git a/shared/modules/leveling/leveling.service.ts b/shared/modules/leveling/leveling.service.ts index f6af7e5..a009c51 100644 --- a/shared/modules/leveling/leveling.service.ts +++ b/shared/modules/leveling/leveling.service.ts @@ -68,6 +68,10 @@ export const levelingService = { .where(eq(users.id, BigInt(id))) .returning(); + // Trigger Quest Event + const { questService } = await import("@shared/modules/quest/quest.service"); + await questService.handleEvent(id, 'XP_GAIN', Number(amount), txFn); + return { user: updatedUser, levelUp, currentLevel: newLevel }; }, tx); }, diff --git a/shared/modules/quest/quest.service.test.ts b/shared/modules/quest/quest.service.test.ts index 1cd1b73..6ab616d 100644 --- a/shared/modules/quest/quest.service.test.ts +++ b/shared/modules/quest/quest.service.test.ts @@ -148,4 +148,60 @@ describe("questService", () => { expect(result).toEqual(mockData as any); }); }); + + describe("handleEvent", () => { + it("should progress a quest", async () => { + const mockUserQuest = { + userId: 1n, + questId: 101, + progress: 0, + completedAt: null, + quest: { triggerEvent: "TEST_EVENT", requirements: { target: 5 } } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + mockReturning.mockResolvedValue([{ userId: 1n, questId: 101, progress: 1 }]); + + await questService.handleEvent("1", "TEST_EVENT", 1); + + expect(mockUpdate).toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledWith({ progress: 1 }); + }); + + it("should complete a quest when target reached", async () => { + const mockUserQuest = { + userId: 1n, + questId: 101, + progress: 4, + completedAt: null, + quest: { + triggerEvent: "TEST_EVENT", + requirements: { target: 5 }, + rewards: { balance: 100 } + } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + mockFindFirst.mockResolvedValue(mockUserQuest); // For completeQuest + + await questService.handleEvent("1", "TEST_EVENT", 1); + + // Verify completeQuest was called (it will update completedAt) + expect(mockUpdate).toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledWith({ completedAt: expect.any(Date) }); + }); + + it("should ignore irrelevant events", async () => { + const mockUserQuest = { + userId: 1n, + questId: 101, + progress: 0, + completedAt: null, + quest: { triggerEvent: "DIFFERENT_EVENT", requirements: { target: 5 } } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + + await questService.handleEvent("1", "TEST_EVENT", 1); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); + }); }); diff --git a/shared/modules/quest/quest.service.ts b/shared/modules/quest/quest.service.ts index db1199b..4705496 100644 --- a/shared/modules/quest/quest.service.ts +++ b/shared/modules/quest/quest.service.ts @@ -34,6 +34,37 @@ export const questService = { }, tx); }, + handleEvent: async (userId: string, eventName: string, weight: number = 1, tx?: Transaction) => { + return await withTransaction(async (txFn) => { + // 1. Fetch active user quests for this event + const activeUserQuests = await txFn.query.userQuests.findMany({ + where: and( + eq(userQuests.userId, BigInt(userId)), + ), + with: { + quest: true + } + }); + + const relevant = activeUserQuests.filter(uq => + uq.quest.triggerEvent === eventName && !uq.completedAt + ); + + for (const uq of relevant) { + const requirements = uq.quest.requirements as { target?: number }; + const target = requirements?.target || 1; + + const newProgress = (uq.progress || 0) + weight; + + if (newProgress >= target) { + await questService.completeQuest(userId, uq.questId, txFn); + } else { + await questService.updateProgress(userId, uq.questId, newProgress, txFn); + } + } + }, tx); + }, + completeQuest: async (userId: string, questId: number, tx?: Transaction) => { return await withTransaction(async (txFn) => { const userQuest = await txFn.query.userQuests.findFirst({