feat: Add /sell command, enhance inventory service, and refactor student ID card generation with new constellation graphics and dynamic backgrounds.

This commit is contained in:
syntaxbullet
2025-12-12 13:41:13 +01:00
parent 209340c06e
commit 8262eb8f02
14 changed files with 179 additions and 106 deletions

View File

@@ -7,35 +7,50 @@ const fontDir = path.join(process.cwd(), 'src', 'assets', 'fonts');
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexSansCondensed-SemiBold.ttf'), 'IBMPlexSansCondensed-SemiBold');
GlobalFonts.registerFromPath(path.join(fontDir, 'IBMPlexMono-Bold.ttf'), 'IBMPlexMono-Bold');
const textColor = '#FFE7C5';
const secondaryTextColor = '#9C7D53';
const accentColor = '#3F2923';
interface StudentCardData {
username: string;
avatarUrl: string;
id: string;
level: number;
au: bigint; // Astral Units
cu: bigint; // Constellation Units
xp: bigint; // Experience
xp: bigint;
className?: string | null;
}
export async function generateStudentIdCard(data: StudentCardData): Promise<Buffer> {
const templatePath = path.join(process.cwd(), 'src', 'assets', 'student-id-template.png');
const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'studentID', 'template.png');
const classTemplatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'studentID', `Constellation-${data.className}.png`);
const template = await loadImage(templatePath);
const classTemplate = await loadImage(classTemplatePath);
const canvas = createCanvas(template.width, template.height);
const ctx = canvas.getContext('2d');
// Draw Background Gradient with random hue
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
const hue = Math.random() * 360;
const saturation = 40 + Math.random() * 20; // 40-60%
const lightness = 20 + Math.random() * 20; // 20-40%
const color2 = `hsl(${Math.random() * 360}, ${saturation}%, ${lightness}%)`;
const color = `hsl(${Math.random() * 360}, ${saturation}%, ${lightness - 20}%)`;
gradient.addColorStop(0, color);
gradient.addColorStop(1, color2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw Template
ctx.drawImage(template, 0, 0);
// Draw Class Template
ctx.drawImage(classTemplate, 0, 0);
// Draw Avatar
const avatarSize = 111;
const avatarX = 18;
const avatarY = 64;
const avatarSize = 140;
const avatarX = 19;
const avatarY = 76;
try {
const avatar = await loadImage(data.avatarUrl);
@@ -53,106 +68,43 @@ export async function generateStudentIdCard(data: StudentCardData): Promise<Buff
// Draw ID
ctx.save();
ctx.font = '12px IBMPlexMono-Bold';
ctx.fillStyle = secondaryTextColor;
ctx.fillStyle = '#DAC7A1';
ctx.textAlign = 'left';
ctx.fillText(`ID: ${data.id}`, 302, 30);
ctx.fillText(`ID: ${data.id}`, 314, 30);
ctx.restore();
// Draw Username
ctx.save();
ctx.font = '32px IBMPlexSansCondensed-SemiBold';
ctx.fillStyle = textColor;
ctx.font = '24px IBMPlexSansCondensed-SemiBold';
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'left';
ctx.fillText(data.username, 140, 107);
ctx.fillText(data.username, 181, 122);
ctx.restore();
// Draw Class
try {
const classBadge = await loadImage(path.join(process.cwd(), 'src', 'assets', 'student-id-class-badge.png'));
ctx.save();
ctx.beginPath();
ctx.drawImage(classBadge, 100, 126, 58, 58);
ctx.font = '32px IBMPlexMono-Bold';
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
ctx.fillText(data.className || 'Unknown', 120, 162);
ctx.restore();
} catch (e) {
console.error("Failed to load class badge", e);
}
// Draw Astral Units
// Draw AU
ctx.save();
ctx.font = '32px IBMPlexMono-Bold';
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
ctx.fillText(data.au.toString(), 163, 218);
ctx.restore();
// Draw Constellation Units
ctx.save();
ctx.font = '32px IBMPlexMono-Bold';
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
ctx.fillText(data.cu.toString(), 163, 153);
ctx.font = '24px IBMPlexMono-Bold';
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'right';
ctx.fillText(`${data.au}`, 270, 183);
ctx.restore();
// Draw Level
ctx.save();
// check how many digits the level has and adjust the position accordingly
const levelDigits = data.level.toString().length;
ctx.font = '12px IBMPlexMono-Bold';
ctx.fillStyle = secondaryTextColor;
ctx.textAlign = 'left';
if (levelDigits === 1) {
ctx.fillText(data.level.toString(), 406, 265);
} else {
ctx.fillText(data.level.toString(), 400, 265);
}
ctx.restore();
// Draw Next Level
ctx.save();
// check how many digits the next level has and adjust the position accordingly
const nextLevelDigits = (data.level + 1).toString().length;
ctx.font = '24px IBMPlexMono-Bold';
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
if (nextLevelDigits === 1) {
ctx.fillText((data.level + 1).toString(), 438, 255);
} else {
ctx.fillText((data.level + 1).toString(), 431, 255);
}
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'center';
ctx.fillText(`${data.level}`, 445, 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');
// Draw XP Bar
const xpForNextLevel = levelingService.getXpForLevel(data.level);
const xpBarMaxWidth = 382;
const xpBarWidth = xpBarMaxWidth * Number(data.xp) / Number(xpForNextLevel);
const xpBarHeight = 3;
ctx.save();
ctx.fillStyle = accentColor;
ctx.fillRect(xpbarX, xpbarY, xpbarMaxWidth, xpbarHeight);
if (xpFilledWidth > 0) {
ctx.fillStyle = gradient;
ctx.fillRect(xpbarX, xpbarY, xpFilledWidth, xpbarHeight);
}
ctx.fillStyle = '#B3AD93';
ctx.fillRect(32, 244, xpBarWidth, xpBarHeight);
ctx.restore();
return canvas.toBuffer('image/png');