import { userQuests, quests } 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"; import { systemEvents, EVENTS } from "@shared/lib/events"; 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; } // Emit completion event for the bot to handle notifications systemEvents.emit(EVENTS.QUEST.COMPLETED, { userId, questId, quest: userQuest.quest, rewards: results }); 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, } }); }, async getAvailableQuests(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 }); }, async createQuest(data: { name: string; description: string; triggerEvent: string; requirements: { target: number }; rewards: { xp: number; balance: number }; }, tx?: Transaction) { return await withTransaction(async (txFn) => { return await txFn.insert(quests) .values({ name: data.name, description: data.description, triggerEvent: data.triggerEvent, requirements: data.requirements, rewards: data.rewards, }) .returning(); }, tx); }, async getAllQuests() { return await DrizzleClient.query.quests.findMany({ orderBy: (quests, { asc }) => [asc(quests.id)], }); }, async deleteQuest(id: number, tx?: Transaction) { return await withTransaction(async (txFn) => { return await txFn.delete(quests) .where(eq(quests.id, id)) .returning(); }, tx); }, async updateQuest(id: number, data: { name?: string; description?: string; triggerEvent?: string; requirements?: { target?: number }; rewards?: { xp?: number; balance?: number }; }, tx?: Transaction) { return await withTransaction(async (txFn) => { return await txFn.update(quests) .set({ ...(data.name !== undefined && { name: data.name }), ...(data.description !== undefined && { description: data.description }), ...(data.triggerEvent !== undefined && { triggerEvent: data.triggerEvent }), ...(data.requirements !== undefined && { requirements: data.requirements }), ...(data.rewards !== undefined && { rewards: data.rewards }), }) .where(eq(quests.id, id)) .returning(); }, tx); } };