Files
AuroraBot-discord/src/commands/economy/exam.ts
2025-12-18 17:39:46 +01:00

187 lines
7.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 { userTimers, users } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import { config } from "@lib/config";
const EXAM_TIMER_TYPE = '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);
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 nextWeek = new Date(now);
nextWeek.setDate(now.getDate() + 7);
const metadata: ExamMetadata = {
examDay: currentDay,
lastXp: user.xp.toString()
};
await DrizzleClient.insert(userTimers).values({
userId: user.id,
type: EXAM_TIMER_TYPE,
key: EXAM_TIMER_KEY,
expiresAt: nextWeek,
metadata: metadata
});
await interaction.editReply({
embeds: [createSuccessEmbed(
`You have registered for the exam! Your exam day is **${DAYS[currentDay]}**.\n` +
`Come back next week on **${DAYS[currentDay]}** to take your first exam and earn rewards based on your XP gain!`,
"Exam Registration Successful"
)]
});
return;
}
const metadata = timer.metadata as unknown as ExamMetadata;
const examDay = metadata.examDay;
// 3. Cooldown Check
if (now < new Date(timer.expiresAt)) {
// Calculate time remaining
const expiresAt = new Date(timer.expiresAt);
// Simple formatting
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}:R> (${DAYS[examDay]})`
)]
});
return;
}
// 4. Day Check
if (currentDay !== examDay) {
// "If not executed on same weekday... we do not reward"
// Consume the attempt (reset timer) but give 0 reward.
const nextWeek = new Date(now);
nextWeek.setDate(now.getDate() + 7);
const newMetadata: ExamMetadata = {
examDay: examDay,
lastXp: user.xp.toString() // Reset tracking
};
await DrizzleClient.update(userTimers)
.set({
expiresAt: nextWeek,
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 is on **${DAYS[examDay]}**, but today is ${DAYS[currentDay]}.\n` +
`You verify your attendance but score a **0**. Come back next **${DAYS[examDay]}**!`,
"Exam Failed"
)]
});
return;
}
// 5. Reward Calculation
const lastXp = BigInt(metadata.lastXp || "0"); // Fallback just in case
const currentXp = user.xp;
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 nextWeek = new Date(now);
nextWeek.setDate(now.getDate() + 7);
const newMetadata: ExamMetadata = {
examDay: examDay,
lastXp: currentXp.toString()
};
await DrizzleClient.transaction(async (tx) => {
// Update Timer
await tx.update(userTimers)
.set({
expiresAt: nextWeek,
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 on **${DAYS[examDay]}**!`,
"Exam Passed!"
)]
});
} catch (error: any) {
console.error("Exam command error:", error);
await interaction.editReply({ embeds: [createErrorEmbed("An error occurred while processing your exam.")] });
}
}
});