111 lines
3.5 KiB
TypeScript
111 lines
3.5 KiB
TypeScript
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);
|
|
})
|
|
}
|
|
}
|
|
};
|