import { userQuests } from "@db/schema"; import { eq, and } from "drizzle-orm"; 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 { withTransaction } from "@/lib/db"; import type { Transaction } from "@shared/lib/types"; import { TransactionType } from "@shared/lib/constants"; export const questService = { assignQuest: async (userId: string, questId: number, tx?: Transaction) => { return await withTransaction(async (txFn) => { return await txFn.insert(userQuests) .values({ userId: BigInt(userId), questId: questId, progress: 0, }) .onConflictDoNothing() // Ignore if already assigned .returning(); }, tx); }, updateProgress: async (userId: string, questId: number, progress: number, tx?: Transaction) => { return await withTransaction(async (txFn) => { return await txFn.update(userQuests) .set({ progress: progress }) .where(and( eq(userQuests.userId, BigInt(userId)), eq(userQuests.questId, questId) )) .returning(); }, 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 => { 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 }; 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({ where: and( eq(userQuests.userId, BigInt(userId)), eq(userQuests.questId, questId) ), with: { quest: true, } }); if (!userQuest) throw new UserError("Quest not assigned"); if (userQuest.completedAt) throw new UserError("Quest already completed"); // Mark completed await txFn.update(userQuests) .set({ completedAt: new Date() }) .where(and( eq(userQuests.userId, BigInt(userId)), eq(userQuests.questId, questId) )); // Distribute Rewards const rewards = userQuest.quest.rewards as { xp?: number, balance?: number }; const results = { xp: 0n, balance: 0n }; if (rewards?.balance) { const bal = BigInt(rewards.balance); await economyService.modifyUserBalance(userId, bal, TransactionType.QUEST_REWARD, `Reward for quest ${questId}`, null, txFn); results.balance = bal; } if (rewards?.xp) { const xp = BigInt(rewards.xp); await levelingService.addXp(userId, xp, txFn); results.xp = xp; } return { success: true, rewards: results }; }, tx); }, getUserQuests: async (userId: string) => { return await DrizzleClient.query.userQuests.findMany({ where: eq(userQuests.userId, BigInt(userId)), with: { quest: true, } }); }, getAvailableQuests: async (userId: string) => { const userQuestIds = (await DrizzleClient.query.userQuests.findMany({ where: eq(userQuests.userId, BigInt(userId)), columns: { questId: true } })).map(uq => uq.questId); return await DrizzleClient.query.quests.findMany({ where: (quests, { notInArray }) => userQuestIds.length > 0 ? notInArray(quests.id, userQuestIds) : undefined }); } };