import { users, cooldowns } from "@/db/schema"; import { eq, sql, and } from "drizzle-orm"; import { DrizzleClient } from "@/lib/DrizzleClient"; // Simple configurable curve: Base * (Level ^ Exponent) const XP_BASE = 100; const XP_EXPONENT = 2.5; const CHAT_XP_COOLDOWN_MS = 60000; // 1 minute const MIN_CHAT_XP = 15; const MAX_CHAT_XP = 25; export const levelingService = { // Calculate XP required for a specific level getXpForLevel: (level: number) => { return Math.floor(XP_BASE * Math.pow(level, XP_EXPONENT)); }, // Pure XP addition - No cooldown checks addXp: async (id: string, amount: bigint, tx?: any) => { const execute = async (txFn: any) => { // Get current state const user = await txFn.query.users.findFirst({ where: eq(users.id, BigInt(id)), }); if (!user) throw new Error("User not found"); let newXp = (user.xp ?? 0n) + amount; let currentLevel = user.level ?? 1; let levelUp = false; // Check for level up loop let xpForNextLevel = BigInt(levelingService.getXpForLevel(currentLevel)); while (newXp >= xpForNextLevel) { newXp -= xpForNextLevel; currentLevel++; levelUp = true; xpForNextLevel = BigInt(levelingService.getXpForLevel(currentLevel)); } // Update user const [updatedUser] = await txFn.update(users) .set({ xp: newXp, level: currentLevel, }) .where(eq(users.id, BigInt(id))) .returning(); return { user: updatedUser, levelUp, currentLevel }; }; if (tx) { return await execute(tx); } else { return await DrizzleClient.transaction(async (t) => { return await execute(t); }) } }, // Handle chat XP with cooldowns processChatXp: async (id: string, tx?: any) => { const execute = async (txFn: any) => { // check if an xp cooldown is in place const cooldown = await txFn.query.cooldowns.findFirst({ where: and( eq(cooldowns.userId, BigInt(id)), eq(cooldowns.actionKey, 'xp') ), }); const now = new Date(); if (cooldown && cooldown.readyAt > now) { return { awarded: false, reason: 'cooldown' }; } // Calculate random XP const amount = BigInt(Math.floor(Math.random() * (MAX_CHAT_XP - MIN_CHAT_XP + 1)) + MIN_CHAT_XP); // Add XP const result = await levelingService.addXp(id, amount, txFn); // Update/Set Cooldown const nextReadyAt = new Date(now.getTime() + CHAT_XP_COOLDOWN_MS); await txFn.insert(cooldowns) .values({ userId: BigInt(id), actionKey: 'xp', readyAt: nextReadyAt, }) .onConflictDoUpdate({ target: [cooldowns.userId, cooldowns.actionKey], set: { readyAt: nextReadyAt }, }); return { awarded: true, amount, ...result }; }; if (tx) { return await execute(tx); } else { return await DrizzleClient.transaction(async (t) => { return await execute(t); }) } } };