forked from syntaxbullet/AuroraBot-discord
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 { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas';
|
||||||
|
import { levelingService } from '@/modules/leveling/leveling.service';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
// Register Fonts
|
// Register Fonts
|
||||||
@@ -123,5 +124,36 @@ export async function generateStudentIdCard(data: StudentCardData): Promise<Buff
|
|||||||
ctx.fillText((data.level + 1).toString(), 431, 255);
|
ctx.fillText((data.level + 1).toString(), 431, 255);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
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');
|
return canvas.toBuffer('image/png');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { users } from "@/db/schema";
|
import { users, cooldowns } from "@/db/schema";
|
||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql, and } from "drizzle-orm";
|
||||||
import { DrizzleClient } from "@/lib/DrizzleClient";
|
import { DrizzleClient } from "@/lib/DrizzleClient";
|
||||||
|
|
||||||
// Simple configurable curve: Base * (Level ^ Exponent)
|
// Simple configurable curve: Base * (Level ^ Exponent)
|
||||||
const XP_BASE = 1000;
|
const XP_BASE = 100;
|
||||||
const XP_EXPONENT = 1.5;
|
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 = {
|
export const levelingService = {
|
||||||
// Calculate XP required for a specific level
|
// Calculate XP required for a specific level
|
||||||
@@ -12,6 +15,7 @@ export const levelingService = {
|
|||||||
return Math.floor(XP_BASE * Math.pow(level, XP_EXPONENT));
|
return Math.floor(XP_BASE * Math.pow(level, XP_EXPONENT));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Pure XP addition - No cooldown checks
|
||||||
addXp: async (id: string, amount: bigint, tx?: any) => {
|
addXp: async (id: string, amount: bigint, tx?: any) => {
|
||||||
const execute = async (txFn: any) => {
|
const execute = async (txFn: any) => {
|
||||||
// Get current state
|
// Get current state
|
||||||
@@ -45,7 +49,7 @@ export const levelingService = {
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return { user: updatedUser, levelUp, currentLevel };
|
return { user: updatedUser, levelUp, currentLevel };
|
||||||
}
|
};
|
||||||
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
return await execute(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