From eb108695d3a16b1757e56a9985967dbb23b46d8b Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 15 Jan 2026 15:22:20 +0100 Subject: [PATCH] feat: Implement flexible quest event matching to allow generic triggers to match specific event instances. --- shared/modules/quest/quest.service.test.ts | 62 ++++++++++++++++++++++ shared/modules/quest/quest.service.ts | 9 ++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/shared/modules/quest/quest.service.test.ts b/shared/modules/quest/quest.service.test.ts index 9f1917c..220523d 100644 --- a/shared/modules/quest/quest.service.test.ts +++ b/shared/modules/quest/quest.service.test.ts @@ -189,6 +189,68 @@ describe("questService", () => { expect(mockSet).toHaveBeenCalledWith({ completedAt: expect.any(Date) }); }); + it("should progress a quest with generic events", async () => { + const mockUserQuest = { + userId: 1n, + questId: 102, + progress: 0, + completedAt: null, + quest: { triggerEvent: "ITEM_COLLECT", requirements: { target: 5 } } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + mockReturning.mockResolvedValue([{ userId: 1n, questId: 102, progress: 1 }]); + + await questService.handleEvent("1", "ITEM_COLLECT:505", 1); + + expect(mockUpdate).toHaveBeenCalled(); + expect(mockSet).toHaveBeenCalledWith({ progress: 1 }); + }); + + it("should ignore events that are not prefix matches", async () => { + const mockUserQuest = { + userId: 1n, + questId: 103, + progress: 0, + completedAt: null, + quest: { triggerEvent: "ITEM_COLLECT", requirements: { target: 5 } } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + + await questService.handleEvent("1", "ITEM_COLLECT_UNRELATED", 1); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it("should not progress a specific quest with a different specific event", async () => { + const mockUserQuest = { + userId: 1n, + questId: 104, + progress: 0, + completedAt: null, + quest: { triggerEvent: "ITEM_COLLECT:101", requirements: { target: 5 } } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + + await questService.handleEvent("1", "ITEM_COLLECT:202", 1); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); + + it("should not progress a specific quest with a generic event", async () => { + const mockUserQuest = { + userId: 1n, + questId: 105, + progress: 0, + completedAt: null, + quest: { triggerEvent: "ITEM_COLLECT:101", requirements: { target: 5 } } + }; + mockFindMany.mockResolvedValue([mockUserQuest]); + + await questService.handleEvent("1", "ITEM_COLLECT", 1); + + expect(mockUpdate).not.toHaveBeenCalled(); + }); + it("should ignore irrelevant events", async () => { const mockUserQuest = { userId: 1n, diff --git a/shared/modules/quest/quest.service.ts b/shared/modules/quest/quest.service.ts index 4705496..1bf8ed6 100644 --- a/shared/modules/quest/quest.service.ts +++ b/shared/modules/quest/quest.service.ts @@ -46,9 +46,12 @@ export const questService = { } }); - const relevant = activeUserQuests.filter(uq => - uq.quest.triggerEvent === eventName && !uq.completedAt - ); + const relevant = activeUserQuests.filter(uq => { + const trigger = uq.quest.triggerEvent; + // Exact match or prefix match (e.g. ITEM_COLLECT matches ITEM_COLLECT:101) + const isMatch = eventName === trigger || eventName.startsWith(trigger + ":"); + return isMatch && !uq.completedAt; + }); for (const uq of relevant) { const requirements = uq.quest.requirements as { target?: number };