feat: Implement quest event handling and integrate it into leveling, economy, and inventory services.

This commit is contained in:
syntaxbullet
2026-01-15 15:04:50 +01:00
parent f8436e9755
commit 52f8ab11f0
5 changed files with 109 additions and 0 deletions

View File

@@ -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();
});
});
});

View File

@@ -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({