diff --git a/src/assets/graphics/studentID/Constellation-A.png b/src/assets/graphics/studentID/Constellation-A.png new file mode 100644 index 0000000..4b6ef5f Binary files /dev/null and b/src/assets/graphics/studentID/Constellation-A.png differ diff --git a/src/assets/graphics/studentID/Constellation-B.png b/src/assets/graphics/studentID/Constellation-B.png new file mode 100644 index 0000000..438c62b Binary files /dev/null and b/src/assets/graphics/studentID/Constellation-B.png differ diff --git a/src/assets/graphics/studentID/Constellation-C.png b/src/assets/graphics/studentID/Constellation-C.png new file mode 100644 index 0000000..2a04e15 Binary files /dev/null and b/src/assets/graphics/studentID/Constellation-C.png differ diff --git a/src/assets/graphics/studentID/Constellation-D.png b/src/assets/graphics/studentID/Constellation-D.png new file mode 100644 index 0000000..9b70cd5 Binary files /dev/null and b/src/assets/graphics/studentID/Constellation-D.png differ diff --git a/src/assets/graphics/studentID/Constellation-S.png b/src/assets/graphics/studentID/Constellation-S.png new file mode 100644 index 0000000..b313da7 Binary files /dev/null and b/src/assets/graphics/studentID/Constellation-S.png differ diff --git a/src/assets/graphics/studentID/template.png b/src/assets/graphics/studentID/template.png new file mode 100644 index 0000000..0624408 Binary files /dev/null and b/src/assets/graphics/studentID/template.png differ diff --git a/src/assets/student-id-class-badge.png b/src/assets/student-id-class-badge.png deleted file mode 100644 index c0b0bf2..0000000 Binary files a/src/assets/student-id-class-badge.png and /dev/null differ diff --git a/src/assets/student-id-template.png b/src/assets/student-id-template.png deleted file mode 100644 index 0076c8d..0000000 Binary files a/src/assets/student-id-template.png and /dev/null differ diff --git a/src/commands/economy/sell.ts b/src/commands/economy/sell.ts new file mode 100644 index 0000000..930ca7d --- /dev/null +++ b/src/commands/economy/sell.ts @@ -0,0 +1,123 @@ +import { createCommand } from "@/lib/utils"; +import { + SlashCommandBuilder, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + type BaseGuildTextChannel, + type ButtonInteraction, + PermissionFlagsBits +} from "discord.js"; +import { userService } from "@/modules/user/user.service"; +import { inventoryService } from "@/modules/inventory/inventory.service"; +import type { items } from "@db/schema"; + +export const sell = createCommand({ + data: new SlashCommandBuilder() + .setName("sell") + .setDescription("Post an item for sale in the current channel so regular users can buy it") + .addNumberOption(option => + option.setName("itemid") + .setDescription("The ID of the item to sell") + .setRequired(true) + ) + .addChannelOption(option => + option.setName("channel") + .setDescription("The channel to post the item in") + .setRequired(false) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction) => { + await interaction.deferReply({ ephemeral: true }); + + const itemId = interaction.options.getNumber("itemid", true); + const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel; + + if (!targetChannel || !targetChannel.isSendable()) { + await interaction.editReply({ content: "Target channel is invalid or not sendable." }); + return; + } + + const item = await inventoryService.getItem(itemId); + if (!item) { + await interaction.editReply({ content: `Item with ID ${itemId} not found.` }); + return; + } + + if (!item.price) { + await interaction.editReply({ content: `Item "${item.name}" is not for sale (no price set).` }); + return; + } + + const embed = new EmbedBuilder() + .setTitle(`Item for sale: ${item.name}`) + .setDescription(item.description || "No description available.") + .addFields({ name: "Price", value: `${item.price} 🪙`, inline: true }) + .setColor("Yellow") + .setThumbnail(item.iconUrl || null) + .setImage(item.imageUrl || null); + + const buyButton = new ButtonBuilder() + .setCustomId("buy") + .setLabel("Buy") + .setStyle(ButtonStyle.Success); + + const actionRow = new ActionRowBuilder().addComponents(buyButton); + + try { + const message = await targetChannel.send({ embeds: [embed], components: [actionRow] }); + await interaction.editReply({ content: `Item posted in ${targetChannel}.` }); + + // Create a collector on the specific message + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: (i) => i.customId === "buy", + }); + + collector.on("collect", async (i) => { + await handleBuyInteraction(i, item); + }); + + } catch (error) { + console.error("Failed to send sell message:", error); + await interaction.editReply({ content: "Failed to post the item for sale." }); + } + } +}); + +async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof items.$inferSelect) { + try { + await interaction.deferReply({ ephemeral: true }); + + const userId = interaction.user.id; + const user = await userService.getUserById(userId); + + if (!user) { + await interaction.editReply({ content: "User profile not found." }); + return; + } + + if ((user.balance ?? 0n) < (item.price ?? 0n)) { + await interaction.editReply({ content: `You don't have enough money! You need ${item.price} 🪙.` }); + return; + } + + const result = await inventoryService.buyItem(userId, item.id, 1n); + + if (!result.success) { + await interaction.editReply({ content: "Transaction failed. Please try again." }); + return; + } + + await interaction.editReply({ content: `Successfully bought **${item.name}** for ${item.price} 🪙!` }); + } catch (error) { + console.error("Error processing purchase:", error); + if (interaction.deferred || interaction.replied) { + await interaction.editReply({ content: "An error occurred while processing your purchase." }); + } else { + await interaction.reply({ content: "An error occurred while processing your purchase.", ephemeral: true }); + } + } +} diff --git a/src/commands/user/profile.ts b/src/commands/user/profile.ts index 02ce5de..271d62c 100644 --- a/src/commands/user/profile.ts +++ b/src/commands/user/profile.ts @@ -1,7 +1,6 @@ import { createCommand } from "@/lib/utils"; -import { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } from "discord.js"; +import { SlashCommandBuilder, AttachmentBuilder } from "discord.js"; import { userService } from "@/modules/user/user.service"; -import { classService } from "@/modules/class/class.service"; import { generateStudentIdCard } from "@/graphics/studentID"; export const profile = createCommand({ @@ -17,10 +16,6 @@ export const profile = createCommand({ await interaction.deferReply(); const targetUser = interaction.options.getUser("user") || interaction.user; - const targetMember = await interaction.guild?.members.fetch(targetUser.id).catch(() => null); - const targetUserClass = await userService.getUserClass(targetUser.id); - const classBalance = await classService.getClassBalance(targetUserClass?.id || BigInt(0)); - const user = await userService.getOrCreateUser(targetUser.id, targetUser.username); const cardBuffer = await generateStudentIdCard({ @@ -30,7 +25,6 @@ export const profile = createCommand({ level: user!.level || 1, xp: user!.xp || 0n, au: user!.balance || 0n, - cu: classBalance || 0n, className: user!.class?.name || "D" }); diff --git a/src/graphics/studentID.ts b/src/graphics/studentID.ts index 45a381e..1b5e05a 100644 --- a/src/graphics/studentID.ts +++ b/src/graphics/studentID.ts @@ -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 { - 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= 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'); diff --git a/src/modules/inventory/inventory.service.ts b/src/modules/inventory/inventory.service.ts index ef726ae..f8a1f8b 100644 --- a/src/modules/inventory/inventory.service.ts +++ b/src/modules/inventory/inventory.service.ts @@ -120,5 +120,11 @@ export const inventoryService = { }; return tx ? await execute(tx) : await DrizzleClient.transaction(execute); - } + }, + + getItem: async (itemId: number) => { + return await DrizzleClient.query.items.findFirst({ + where: eq(items.id, itemId), + }); + }, }; diff --git a/src/scripts/test-student-id.ts b/src/scripts/test-student-id.ts index 35e70fb..6bcb339 100644 --- a/src/scripts/test-student-id.ts +++ b/src/scripts/test-student-id.ts @@ -2,7 +2,6 @@ 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'; @@ -25,9 +24,9 @@ async function main() { 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}`); + console.log(`User Balance: ${user.balance}`); + console.log(`User XP: ${user.xp}`); + console.log(`User Level: ${user.level}`); // Placeholder avatar (default discord avatar) const avatarUrl = "https://cdn.discordapp.com/embed/avatars/0.png"; @@ -42,7 +41,6 @@ async function main() { level: user.level || 1, xp: user.xp || 0n, au: user.balance || 0n, - cu: classBalance || 0n, className: 'D' }); diff --git a/test-student-id.png b/test-student-id.png new file mode 100644 index 0000000..7ea41c6 Binary files /dev/null and b/test-student-id.png differ