import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContainerBuilder, TextDisplayBuilder, SeparatorBuilder, SeparatorSpacingSize, MessageFlags } from "discord.js"; /** * Quest entry with quest details and progress */ interface QuestEntry { progress: number | null; completedAt: Date | null; quest: { id: number; name: string; description: string | null; triggerEvent: string; requirements: any; rewards: any; }; } /** * Available quest interface */ interface AvailableQuest { id: number; name: string; description: string | null; rewards: any; requirements: any; } // Color palette for containers const COLORS = { ACTIVE: 0x3498db, // Blue - in progress AVAILABLE: 0x2ecc71, // Green - available COMPLETED: 0xf1c40f // Gold - completed }; /** * Formats quest rewards object into a human-readable string */ function formatQuestRewards(rewards: { xp?: number, balance?: number }): string { const rewardStr: string[] = []; if (rewards?.xp) rewardStr.push(`${rewards.xp} XP`); if (rewards?.balance) rewardStr.push(`${rewards.balance} πŸͺ™`); return rewardStr.join(" β€’ ") || "None"; } /** * Renders a simple progress bar */ function renderProgressBar(current: number, total: number, size: number = 10): string { const percentage = Math.min(current / total, 1); const progress = Math.round(size * percentage); const empty = size - progress; const progressText = "β–°".repeat(progress); const emptyText = "β–±".repeat(empty); return `${progressText}${emptyText} ${Math.round(percentage * 100)}%`; } /** * Creates Components v2 containers for the quest list (active quests only) */ export function getQuestListComponents(userQuests: QuestEntry[]): ContainerBuilder[] { // Filter to only show in-progress quests (not completed) const activeQuests = userQuests.filter(entry => entry.completedAt === null); const container = new ContainerBuilder() .setAccentColor(COLORS.ACTIVE) .addTextDisplayComponents( new TextDisplayBuilder().setContent("# πŸ“œ Quest Log"), new TextDisplayBuilder().setContent("-# Your active quests") ); if (activeQuests.length === 0) { container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); container.addTextDisplayComponents( new TextDisplayBuilder().setContent("*You have no active quests. Check available quests!*") ); return [container]; } activeQuests.forEach((entry) => { container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); const rewards = entry.quest.rewards as { xp?: number, balance?: number }; const rewardsText = formatQuestRewards(rewards); const requirements = entry.quest.requirements as { target?: number }; const target = requirements?.target || 1; const progress = entry.progress || 0; const progressBar = renderProgressBar(progress, target); container.addTextDisplayComponents( new TextDisplayBuilder().setContent(`**${entry.quest.name}**`), new TextDisplayBuilder().setContent(entry.quest.description || "*No description*"), new TextDisplayBuilder().setContent(`πŸ“Š ${progressBar} \`${progress}/${target}\` β€’ 🎁 ${rewardsText}`) ); }); return [container]; } /** * Creates Components v2 containers for available quests with inline accept buttons */ export function getAvailableQuestsComponents(availableQuests: AvailableQuest[]): ContainerBuilder[] { const container = new ContainerBuilder() .setAccentColor(COLORS.AVAILABLE) .addTextDisplayComponents( new TextDisplayBuilder().setContent("# πŸ—ΊοΈ Available Quests"), new TextDisplayBuilder().setContent("-# Quests you can accept") ); if (availableQuests.length === 0) { container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); container.addTextDisplayComponents( new TextDisplayBuilder().setContent("*No new quests available at the moment.*") ); return [container]; } // Limit to 10 quests (5 action rows max with 2 added for navigation) const questsToShow = availableQuests.slice(0, 10); questsToShow.forEach((quest) => { container.addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)); const rewards = quest.rewards as { xp?: number, balance?: number }; const rewardsText = formatQuestRewards(rewards); const requirements = quest.requirements as { target?: number }; const target = requirements?.target || 1; container.addTextDisplayComponents( new TextDisplayBuilder().setContent(`**${quest.name}**`), new TextDisplayBuilder().setContent(quest.description || "*No description*"), new TextDisplayBuilder().setContent(`🎯 Goal: \`${target}\` β€’ 🎁 ${rewardsText}`) ); // Add accept button inline within the container container.addActionRowComponents( new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(`quest_accept:${quest.id}`) .setLabel("Accept Quest") .setStyle(ButtonStyle.Success) .setEmoji("βœ…") ) ); }); return [container]; } /** * Returns action rows for navigation only */ export function getQuestActionRows(viewType: 'active' | 'available'): ActionRowBuilder[] { // Navigation row const navRow = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("quest_view_active") .setLabel("πŸ“œ Active") .setStyle(viewType === 'active' ? ButtonStyle.Primary : ButtonStyle.Secondary) .setDisabled(viewType === 'active'), new ButtonBuilder() .setCustomId("quest_view_available") .setLabel("πŸ—ΊοΈ Available") .setStyle(viewType === 'available' ? ButtonStyle.Primary : ButtonStyle.Secondary) .setDisabled(viewType === 'available') ); return [navRow]; } /** * Creates Components v2 celebratory message for quest completion */ export function getQuestCompletionComponents(quest: any, rewards: { xp: bigint, balance: bigint }): ContainerBuilder[] { const rewardsText = formatQuestRewards({ xp: Number(rewards.xp), balance: Number(rewards.balance) }); const container = new ContainerBuilder() .setAccentColor(COLORS.COMPLETED) .addTextDisplayComponents( new TextDisplayBuilder().setContent("# πŸŽ‰ Quest Completed!"), new TextDisplayBuilder().setContent(`Congratulations! You've completed **${quest.name}**`) ) .addSeparatorComponents(new SeparatorBuilder().setSpacing(SeparatorSpacingSize.Small)) .addTextDisplayComponents( new TextDisplayBuilder().setContent(`πŸ“ ${quest.description || "No description provided."}`), new TextDisplayBuilder().setContent(`🎁 **Rewards Earned:** ${rewardsText}`) ); return [container]; } /** * Gets MessageFlags and allowedMentions for Components v2 messages */ export function getComponentsV2MessageFlags() { return { flags: MessageFlags.IsComponentsV2, allowedMentions: { parse: [] as const } }; }