diff --git a/src/assets/graphics/lootdrop/template.png b/src/assets/graphics/lootdrop/template.png new file mode 100644 index 0000000..9d5f93d Binary files /dev/null and b/src/assets/graphics/lootdrop/template.png differ diff --git a/src/graphics/lootdrop.ts b/src/graphics/lootdrop.ts new file mode 100644 index 0000000..3aed18b --- /dev/null +++ b/src/graphics/lootdrop.ts @@ -0,0 +1,135 @@ +import { GlobalFonts, createCanvas, loadImage } from '@napi-rs/canvas'; +import path from 'path'; + +// Register Fonts (same as studentID.ts) +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'); + +export async function generateLootdropCard(amount: number, currency: string): Promise { + const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'lootdrop', 'template.png'); + const template = await loadImage(templatePath); + + const canvas = createCanvas(template.width, template.height); + const ctx = canvas.getContext('2d'); + + // Draw Template + ctx.drawImage(template, 0, 0); + + // Draw Lootdrop Text (Title-ish) + ctx.save(); + ctx.font = '48px IBMPlexSansCondensed-SemiBold'; + ctx.fillStyle = '#FFFFFF'; + ctx.textAlign = 'center'; + ctx.shadowBlur = 10; + ctx.shadowColor = 'rgba(255, 255, 255, 0.5)'; + // Center of lower half (512-1024) is roughly 768 + ctx.fillText('A STAR IS FALLING', canvas.width / 2, 660); + ctx.restore(); + + // Draw Reward Amount + ctx.save(); + ctx.font = '72px IBMPlexMono-Bold'; + ctx.fillStyle = '#DAC7A1'; + ctx.textAlign = 'center'; + //ctx.shadowBlur = 15; + //ctx.shadowColor = 'rgba(255, 215, 0, 0.8)'; + ctx.fillText(`${amount} ${currency}`, canvas.width / 2, 760); // Below title + ctx.restore(); + + // Crop the image by 64px on all sides + const croppedWidth = template.width - 128; + const croppedHeight = template.height - 128; + const croppedCanvas = createCanvas(croppedWidth, croppedHeight); + const croppedCtx = croppedCanvas.getContext('2d'); + + // Draw the original canvas onto the cropped canvas, shifted by -64 + croppedCtx.drawImage(canvas, -64, -64); + + return croppedCanvas.toBuffer('image/png'); +} + +export async function generateClaimedLootdropCard(amount: number, currency: string, username: string, avatarUrl: string): Promise { + const templatePath = path.join(process.cwd(), 'src', 'assets', 'graphics', 'lootdrop', 'template.png'); + const template = await loadImage(templatePath); + + const canvas = createCanvas(template.width, template.height); + const ctx = canvas.getContext('2d'); + + // Draw Template + ctx.drawImage(template, 0, 0); + + // Add a colored overlay to signify "claimed" + ctx.fillStyle = 'rgba(10, 10, 20, 0.85)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Draw Claimed Text (Title-ish) + ctx.save(); + ctx.font = '48px IBMPlexSansCondensed-SemiBold'; + ctx.fillStyle = '#FFFFFF'; + ctx.textAlign = 'center'; + ctx.shadowBlur = 10; + ctx.shadowColor = 'rgba(255, 255, 255, 0.5)'; + ctx.fillText('STAR CLAIMED', canvas.width / 2, 660); + ctx.restore(); + + // Draw "by username" with Avatar + ctx.save(); + ctx.font = '36px IBMPlexSansCondensed-SemiBold'; + ctx.fillStyle = '#AAAAAA'; + + // Calculate layout for centering Group (Avatar + Text) + const text = `by ${username}`; + const textMetrics = ctx.measureText(text); + const textWidth = textMetrics.width; + const avatarSize = 50; + const gap = 15; + const totalWidth = avatarSize + gap + textWidth; + + const startX = (canvas.width - totalWidth) / 2; + const baselineY = 830; + + // Draw Avatar + try { + const avatar = await loadImage(avatarUrl); + ctx.save(); + ctx.beginPath(); + // Center avatar vertically relative to text roughly (baseline - ~half cap height) + // 36px text ~ 27px cap height. Center roughly at baselineY - 14 + const avatarCenterY = baselineY - 14; + ctx.arc(startX + avatarSize / 2, avatarCenterY, avatarSize / 2, 0, Math.PI * 2); + ctx.closePath(); + ctx.clip(); + ctx.drawImage(avatar, startX, avatarCenterY - avatarSize / 2, avatarSize, avatarSize); + ctx.restore(); + } catch (e) { + // Fallback if avatar fails to load, just don't draw it (or maybe shift text?) + // For now, let's just proceed, the text will be off-center if avatar is missing but that's acceptable edge case + console.error("Failed to load avatar", e); + } + + // Draw Text + ctx.textAlign = 'left'; + ctx.fillText(text, startX + avatarSize + gap, baselineY); + ctx.restore(); + + ctx.save(); + ctx.font = '72px IBMPlexMono-Bold'; // Match Amount size + ctx.fillStyle = '#E6D2B5'; // Lighter gold/beige for better contrast + ctx.textAlign = 'center'; + ctx.shadowBlur = 10; + ctx.shadowColor = 'rgba(0, 0, 0, 0.8)'; // Dark shadow for contrast + ctx.fillText(`${amount} ${currency}`, canvas.width / 2, 760); // Same position as Unclaimed Amount + ctx.restore(); + + // Crop the image by 64px on all sides + const croppedWidth = template.width - 128; + const croppedHeight = template.height - 128; + const croppedCanvas = createCanvas(croppedWidth, croppedHeight); + const croppedCtx = croppedCanvas.getContext('2d'); + + // Draw the original canvas onto the cropped canvas, shifted by -64 + croppedCtx.drawImage(canvas, -64, -64); + + return croppedCanvas.toBuffer('image/png'); +} diff --git a/src/modules/economy/lootdrop.interaction.ts b/src/modules/economy/lootdrop.interaction.ts index 8d9859c..593d4f5 100644 --- a/src/modules/economy/lootdrop.interaction.ts +++ b/src/modules/economy/lootdrop.interaction.ts @@ -17,17 +17,19 @@ export async function handleLootdropInteraction(interaction: ButtonInteraction) content: `🎉 You successfully claimed **${result.amount} ${result.currency}**!` }); - // Update original message to show claimed state - const originalEmbed = interaction.message.embeds[0]; - if (!originalEmbed) return; - - const { embeds, components } = getLootdropClaimedMessage( - originalEmbed.title || "💰 LOOTDROP!", + const { content, files, components } = await getLootdropClaimedMessage( interaction.user.id, + interaction.user.username, + interaction.user.displayAvatarURL({ extension: "png" }), result.amount || 0, result.currency || "Coins" ); - await interaction.message.edit({ embeds, components }); + await interaction.message.edit({ + content, + embeds: [], + files, + components + }); } } diff --git a/src/modules/economy/lootdrop.service.ts b/src/modules/economy/lootdrop.service.ts index 0464ee2..3b4436e 100644 --- a/src/modules/economy/lootdrop.service.ts +++ b/src/modules/economy/lootdrop.service.ts @@ -94,10 +94,10 @@ class LootdropService { const reward = Math.floor(Math.random() * (max - min + 1)) + min; const currency = config.lootdrop.reward.currency; - const { embeds, components } = getLootdropMessage(reward, currency); + const { content, files, components } = await getLootdropMessage(reward, currency); try { - const message = await channel.send({ embeds, components }); + const message = await channel.send({ content, files, components }); // Persist to DB await DrizzleClient.insert(lootdrops).values({ diff --git a/src/modules/economy/lootdrop.view.ts b/src/modules/economy/lootdrop.view.ts index 030418c..300a902 100644 --- a/src/modules/economy/lootdrop.view.ts +++ b/src/modules/economy/lootdrop.view.ts @@ -1,31 +1,29 @@ -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js"; -import { createBaseEmbed } from "@lib/embeds"; +import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import { generateLootdropCard, generateClaimedLootdropCard } from "@/graphics/lootdrop"; -export function getLootdropMessage(reward: number, currency: string) { - const embed = createBaseEmbed( - "💰 LOOTDROP!", - `A lootdrop has appeared! Click the button below to claim **${reward} ${currency}**!`, - "#FFD700" - ); +export async function getLootdropMessage(reward: number, currency: string) { + const cardBuffer = await generateLootdropCard(reward, currency); + const attachment = new AttachmentBuilder(cardBuffer, { name: "lootdrop.png" }); const claimButton = new ButtonBuilder() .setCustomId("lootdrop_claim") .setLabel("CLAIM REWARD") - .setStyle(ButtonStyle.Success) - .setEmoji("💸"); + .setStyle(ButtonStyle.Secondary) // Changed to Secondary to fit the darker theme better? Or keep Success? Let's try Secondary with custom emoji + .setEmoji("🌠"); const row = new ActionRowBuilder() .addComponents(claimButton); - return { embeds: [embed], components: [row] }; + return { + content: "", + files: [attachment], + components: [row] + }; } -export function getLootdropClaimedMessage(originalTitle: string, userId: string, amount: number, currency: string) { - const newEmbed = createBaseEmbed( - originalTitle || "💰 LOOTDROP!", - `✅ Claimed by <@${userId}> for **${amount} ${currency}**!`, - "#00FF00" - ); +export async function getLootdropClaimedMessage(userId: string, username: string, avatarUrl: string, amount: number, currency: string) { + const cardBuffer = await generateClaimedLootdropCard(amount, currency, username, avatarUrl); + const attachment = new AttachmentBuilder(cardBuffer, { name: "lootdrop_claimed.png" }); const newRow = new ActionRowBuilder() .addComponents( @@ -37,5 +35,9 @@ export function getLootdropClaimedMessage(originalTitle: string, userId: string, .setDisabled(true) ); - return { embeds: [newEmbed], components: [newRow] }; + return { + content: ``, // Remove content as the image says it all + files: [attachment], + components: [newRow] + }; }