feat: Implement chat XP with cooldowns and display an XP progress bar on the student ID card.
This commit is contained in:
@@ -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<Buff
|
||||
ctx.fillText((data.level + 1).toString(), 431, 255);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Draw xp bar
|
||||
const xpbarX = 2;
|
||||
const xpbarY = 268;
|
||||
const xpbarMaxWidth = 400; // in pixels
|
||||
const xpbarHeight = 4;
|
||||
|
||||
const xp = data.xp;
|
||||
const requiredXp = BigInt(levelingService.getXpForLevel(data.level));
|
||||
|
||||
// Check to avoid division by zero, though requiredXp should be >= 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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
61
src/scripts/test-student-id.ts
Normal file
61
src/scripts/test-student-id.ts
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user