206 lines
8.1 KiB
TypeScript
206 lines
8.1 KiB
TypeScript
import { createCommand } from "@/lib/utils";
|
|
import { SlashCommandBuilder } from "discord.js";
|
|
import { userService } from "@/modules/user/user.service";
|
|
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
|
import { UserError } from "@/lib/errors";
|
|
import { userTimers, users } from "@/db/schema";
|
|
import { eq, and, sql } from "drizzle-orm";
|
|
import { DrizzleClient } from "@/lib/DrizzleClient";
|
|
import { config } from "@lib/config";
|
|
import { TimerType } from "@/lib/constants";
|
|
|
|
const EXAM_TIMER_TYPE = TimerType.EXAM_SYSTEM;
|
|
const EXAM_TIMER_KEY = 'default';
|
|
|
|
interface ExamMetadata {
|
|
examDay: number;
|
|
lastXp: string;
|
|
}
|
|
|
|
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
|
|
export const exam = createCommand({
|
|
data: new SlashCommandBuilder()
|
|
.setName("exam")
|
|
.setDescription("Take your weekly exam to earn rewards based on your XP progress."),
|
|
execute: async (interaction) => {
|
|
await interaction.deferReply();
|
|
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
|
if (!user) {
|
|
await interaction.editReply({ embeds: [createErrorEmbed("Failed to retrieve user data.")] });
|
|
return;
|
|
}
|
|
const now = new Date();
|
|
const currentDay = now.getDay();
|
|
|
|
try {
|
|
// 1. Fetch existing timer/exam data
|
|
const timer = await DrizzleClient.query.userTimers.findFirst({
|
|
where: and(
|
|
eq(userTimers.userId, user.id),
|
|
eq(userTimers.type, EXAM_TIMER_TYPE),
|
|
eq(userTimers.key, EXAM_TIMER_KEY)
|
|
)
|
|
});
|
|
|
|
// 2. First Run Logic
|
|
if (!timer) {
|
|
// Set exam day to today
|
|
const nextExamDate = new Date(now);
|
|
nextExamDate.setDate(now.getDate() + 7);
|
|
nextExamDate.setHours(0, 0, 0, 0);
|
|
const nextExamTimestamp = Math.floor(nextExamDate.getTime() / 1000);
|
|
|
|
const metadata: ExamMetadata = {
|
|
examDay: currentDay,
|
|
lastXp: (user.xp ?? 0n).toString()
|
|
};
|
|
|
|
await DrizzleClient.insert(userTimers).values({
|
|
userId: user.id,
|
|
type: EXAM_TIMER_TYPE,
|
|
key: EXAM_TIMER_KEY,
|
|
expiresAt: nextExamDate,
|
|
metadata: metadata
|
|
});
|
|
|
|
await interaction.editReply({
|
|
embeds: [createSuccessEmbed(
|
|
`You have registered for the exam! Your exam day is **${DAYS[currentDay]}** (Server Time).\n` +
|
|
`Come back on <t:${nextExamTimestamp}:D> (<t:${nextExamTimestamp}:R>) to take your first exam!`,
|
|
"Exam Registration Successful"
|
|
)]
|
|
});
|
|
return;
|
|
}
|
|
|
|
const metadata = timer.metadata as unknown as ExamMetadata;
|
|
const examDay = metadata.examDay;
|
|
|
|
// 3. Cooldown Check
|
|
const expiresAt = new Date(timer.expiresAt);
|
|
expiresAt.setHours(0, 0, 0, 0);
|
|
|
|
if (now < expiresAt) {
|
|
// Calculate time remaining
|
|
const timestamp = Math.floor(expiresAt.getTime() / 1000);
|
|
|
|
await interaction.editReply({
|
|
embeds: [createErrorEmbed(
|
|
`You have already taken your exam for this week (or are waiting for your first week to pass).\n` +
|
|
`Next exam available: <t:${timestamp}:D> (<t:${timestamp}:R>)`
|
|
)]
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 4. Day Check
|
|
if (currentDay !== examDay) {
|
|
// Calculate next correct exam day to correct the schedule
|
|
let daysUntil = (examDay - currentDay + 7) % 7;
|
|
if (daysUntil === 0) daysUntil = 7;
|
|
|
|
const nextExamDate = new Date(now);
|
|
nextExamDate.setDate(now.getDate() + daysUntil);
|
|
nextExamDate.setHours(0, 0, 0, 0);
|
|
const nextExamTimestamp = Math.floor(nextExamDate.getTime() / 1000);
|
|
|
|
const newMetadata: ExamMetadata = {
|
|
examDay: examDay,
|
|
lastXp: (user.xp ?? 0n).toString()
|
|
};
|
|
|
|
await DrizzleClient.update(userTimers)
|
|
.set({
|
|
expiresAt: nextExamDate,
|
|
metadata: newMetadata
|
|
})
|
|
.where(and(
|
|
eq(userTimers.userId, user.id),
|
|
eq(userTimers.type, EXAM_TIMER_TYPE),
|
|
eq(userTimers.key, EXAM_TIMER_KEY)
|
|
));
|
|
|
|
await interaction.editReply({
|
|
embeds: [createErrorEmbed(
|
|
`You missed your exam day! Your exam day is **${DAYS[examDay]}** (Server Time).\n` +
|
|
`You verify your attendance but score a **0**.\n` +
|
|
`Your next exam opportunity is: <t:${nextExamTimestamp}:D> (<t:${nextExamTimestamp}:R>)`,
|
|
"Exam Failed"
|
|
)]
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 5. Reward Calculation
|
|
const lastXp = BigInt(metadata.lastXp || "0"); // Fallback just in case
|
|
const currentXp = user.xp ?? 0n;
|
|
const diff = currentXp - lastXp;
|
|
|
|
// Calculate Reward
|
|
const multMin = config.economy.exam.multMin;
|
|
const multMax = config.economy.exam.multMax;
|
|
const multiplier = Math.random() * (multMax - multMin) + multMin;
|
|
|
|
// Allow negative reward? existing description implies "difference", usually gain.
|
|
// If diff is negative (lost XP?), reward might be 0.
|
|
let reward = 0n;
|
|
if (diff > 0n) {
|
|
reward = BigInt(Math.floor(Number(diff) * multiplier));
|
|
}
|
|
|
|
// 6. Update State
|
|
const nextExamDate = new Date(now);
|
|
nextExamDate.setDate(now.getDate() + 7);
|
|
nextExamDate.setHours(0, 0, 0, 0);
|
|
const nextExamTimestamp = Math.floor(nextExamDate.getTime() / 1000);
|
|
|
|
const newMetadata: ExamMetadata = {
|
|
examDay: examDay,
|
|
lastXp: currentXp.toString()
|
|
};
|
|
|
|
await DrizzleClient.transaction(async (tx) => {
|
|
// Update Timer
|
|
await tx.update(userTimers)
|
|
.set({
|
|
expiresAt: nextExamDate,
|
|
metadata: newMetadata
|
|
})
|
|
.where(and(
|
|
eq(userTimers.userId, user.id),
|
|
eq(userTimers.type, EXAM_TIMER_TYPE),
|
|
eq(userTimers.key, EXAM_TIMER_KEY)
|
|
));
|
|
|
|
// Add Currency
|
|
if (reward > 0n) {
|
|
await tx.update(users)
|
|
.set({
|
|
balance: sql`${users.balance} + ${reward}`
|
|
})
|
|
.where(eq(users.id, user.id));
|
|
}
|
|
});
|
|
|
|
await interaction.editReply({
|
|
embeds: [createSuccessEmbed(
|
|
`**XP Gained:** ${diff.toString()}\n` +
|
|
`**Multiplier:** x${multiplier.toFixed(2)}\n` +
|
|
`**Reward:** ${reward.toString()} Currency\n\n` +
|
|
`See you next week: <t:${nextExamTimestamp}:D>`,
|
|
"Exam Passed!"
|
|
)]
|
|
});
|
|
|
|
} catch (error: any) {
|
|
if (error instanceof UserError) {
|
|
await interaction.reply({ embeds: [createErrorEmbed(error.message)], ephemeral: true });
|
|
} else {
|
|
console.error("Error in exam command:", error);
|
|
await interaction.reply({ embeds: [createErrorEmbed("An unexpected error occurred.")], ephemeral: true });
|
|
}
|
|
}
|
|
}
|
|
});
|