diff --git a/src/graphics/studentID.ts b/src/graphics/studentID.ts index 2c6d57d..45a381e 100644 --- a/src/graphics/studentID.ts +++ b/src/graphics/studentID.ts @@ -1,4 +1,5 @@ import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas'; +import { levelingService } from '@/modules/leveling/leveling.service'; import path from 'path'; // Register Fonts @@ -123,5 +124,36 @@ export async function generateStudentIdCard(data: StudentCardData): Promise= 100 + let percentage = 0; + if (requiredXp > 0n) { + percentage = Number(xp) / Number(requiredXp); + } + + const xpFilledWidth = Math.min(Math.max(percentage * xpbarMaxWidth, 0), xpbarMaxWidth); + + const gradient = ctx.createLinearGradient(xpbarX, xpbarY, xpbarX + xpFilledWidth, xpbarY); + gradient.addColorStop(0, '#FFFFFF'); + gradient.addColorStop(1, '#D9B178'); + ctx.save(); + ctx.fillStyle = accentColor; + ctx.fillRect(xpbarX, xpbarY, xpbarMaxWidth, xpbarHeight); + + if (xpFilledWidth > 0) { + ctx.fillStyle = gradient; + ctx.fillRect(xpbarX, xpbarY, xpFilledWidth, xpbarHeight); + } + ctx.restore(); + return canvas.toBuffer('image/png'); } diff --git a/src/modules/leveling/leveling.service.ts b/src/modules/leveling/leveling.service.ts index d6446a5..ecd4d07 100644 --- a/src/modules/leveling/leveling.service.ts +++ b/src/modules/leveling/leveling.service.ts @@ -1,10 +1,13 @@ -import { users } from "@/db/schema"; -import { eq, sql } from "drizzle-orm"; +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 = 1000; -const XP_EXPONENT = 1.5; +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 @@ -12,6 +15,7 @@ export const levelingService = { 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 @@ -45,7 +49,7 @@ export const levelingService = { .returning(); return { user: updatedUser, levelUp, currentLevel }; - } + }; if (tx) { return await execute(tx); @@ -55,4 +59,52 @@ export const levelingService = { }) } }, + + // 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); + }) + } + } }; diff --git a/src/scripts/test-student-id.ts b/src/scripts/test-student-id.ts new file mode 100644 index 0000000..35e70fb --- /dev/null +++ b/src/scripts/test-student-id.ts @@ -0,0 +1,61 @@ + +import { DrizzleClient } from "@/lib/DrizzleClient"; +import { eq } from "drizzle-orm"; +import { userService } from "@/modules/user/user.service"; +import { classService } from "@/modules/class/class.service"; +import { generateStudentIdCard } from "@/graphics/studentID"; +import fs from 'fs'; +import path from 'path'; + +async function main() { + console.log("Fetching first user from database..."); + + // Get the first user + const user = await DrizzleClient.query.users.findFirst({ where: (users) => eq(users.id, BigInt(109998942841765888)) }); + + if (!user) { + console.error("No users found in database. Please ensure the database is seeded or has at least one user."); + process.exit(1); + } + + console.log(`Found user: ${user.username} (${user.id})`); + + // Get user class + const userClass = await userService.getUserClass(user.id.toString()); + const className = userClass?.name || "Unknown"; + console.log(`User Class: ${className}`); + + // Get class balance + const classBalance = await classService.getClassBalance(userClass?.id || BigInt(0)); + console.log(`Class Balance: ${classBalance}`); + + // Placeholder avatar (default discord avatar) + const avatarUrl = "https://cdn.discordapp.com/embed/avatars/0.png"; + + console.log("Generating Student ID Card..."); + + try { + const cardBuffer = await generateStudentIdCard({ + username: user.username, + avatarUrl: avatarUrl, + id: user.id.toString(), + level: user.level || 1, + xp: user.xp || 0n, + au: user.balance || 0n, + cu: classBalance || 0n, + className: 'D' + }); + + const outputPath = path.join(process.cwd(), 'test-student-id.png'); + fs.writeFileSync(outputPath, cardBuffer); + + console.log(`Student ID card generated successfully: ${outputPath}`); + } catch (error) { + console.error("Error generating student ID card:", error); + } finally { + // Exit cleanly + process.exit(0); + } +} + +main().catch(console.error);