167 lines
6.1 KiB
TypeScript
167 lines
6.1 KiB
TypeScript
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);
|
|
}
|
|
};
|