const { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags, } = require("discord.js"); const fs = require("fs"); const path = require("path"); const sfxManager = require("../../services/sfxManager"); // Parse categories from README.md function getSfxCategories() { try { const sfxReadmePath = path.join( __dirname, "..", "..", "..", "sfx", "README.md" ); if (!fs.existsSync(sfxReadmePath)) { return null; } const content = fs.readFileSync(sfxReadmePath, "utf-8"); const categories = {}; // Parse categories and their sounds const lines = content.split("\n"); let currentCategory = null; for (const line of lines) { const headerMatch = line.match(/^\*\*([^*]+)\*\*$/); if (headerMatch) { currentCategory = headerMatch[1]; categories[currentCategory] = []; } else if (currentCategory && line.trim() && !line.startsWith("```")) { // Parse comma-separated sounds from the line const sounds = line .split(",") .map((s) => s.trim()) .filter((s) => s.length > 0); categories[currentCategory].push(...sounds); } } return categories; } catch (error) { console.error("Error parsing SFX categories:", error); return null; } } module.exports = { data: new SlashCommandBuilder() .setName("soundboard") .setDescription("Interactive soundboard with categorized buttons"), async execute(interaction, guildConfig) { // Check if user is in a voice channel if (!interaction.member.voice.channel) { return interaction.reply({ content: "You need to be in a voice channel to use the soundboard!", flags: [MessageFlags.Ephemeral], }); } const categories = getSfxCategories(); if (!categories) { return interaction.reply({ content: "Soundboard not available - SFX categories could not be loaded.", flags: [MessageFlags.Ephemeral], }); } // Create category selection buttons (4 per row for better layout) const categoryNames = Object.keys(categories); const rows = []; for (let i = 0; i < categoryNames.length; i += 4) { const row = new ActionRowBuilder(); const categoriesInRow = categoryNames.slice(i, i + 4); for (const category of categoriesInRow) { const button = new ButtonBuilder() .setCustomId( `soundboard_category_${category .toLowerCase() .replace(/\s+/g, "_") .replace(/&/g, "and")}` ) .setLabel( category.length > 80 ? category.substring(0, 77) + "..." : category ) .setStyle(ButtonStyle.Primary); row.addComponents(button); } if (row.components.length > 0) { rows.push(row); } } const embed = new EmbedBuilder() .setTitle("🎛️ Interactive Soundboard") .setDescription("Choose a category to browse sound effects:") .setColor(0x21c629); await interaction.reply({ embeds: [embed], components: rows, }); }, async handleCategorySelection(interaction, guildConfig) { const customId = interaction.customId; let categoryKey, page = 0; if (customId.includes("_page_")) { // Handle pagination: soundboard_category_general_page_1 const parts = customId .replace("soundboard_category_", "") .split("_page_"); categoryKey = parts[0] .replace(/_/g, " ") .replace(/and/g, "&") .toUpperCase(); page = parseInt(parts[1]) || 0; } else { // Handle initial category selection categoryKey = customId .replace("soundboard_category_", "") .replace(/_/g, " ") .replace(/and/g, "&") .toUpperCase(); } const categories = getSfxCategories(); if (!categories || !categories[categoryKey]) { return interaction.reply({ content: "Category not found!", flags: [MessageFlags.Ephemeral], }); } const allSounds = categories[categoryKey].filter((sound) => sfxManager.hasSfx(sound) ); const soundsPerPage = 16; // 4 sounds per row × 4 rows = 16 sounds per page const totalPages = Math.ceil(allSounds.length / soundsPerPage); const startIndex = page * soundsPerPage; const sounds = allSounds.slice(startIndex, startIndex + soundsPerPage); const rows = []; let buttonCount = 0; // Create sound buttons (4 per row, 4 rows for sounds + 1 for navigation = 16 sound buttons max) for (let i = 0; i < sounds.length && rows.length < 4; i += 4) { const row = new ActionRowBuilder(); const soundsInRow = sounds.slice(i, i + 4); for (const sound of soundsInRow) { if (buttonCount >= 16) break; // Leave room for navigation row const button = new ButtonBuilder() .setCustomId(`soundboard_play_${sound}`) .setLabel(sound.length > 80 ? sound.substring(0, 77) + "..." : sound) .setStyle(ButtonStyle.Secondary); row.addComponents(button); buttonCount++; } if (row.components.length > 0) { rows.push(row); } } // Add navigation row with back button and pagination if needed const navRow = new ActionRowBuilder(); // Add previous page button if not on first page if (page > 0) { const prevButton = new ButtonBuilder() .setCustomId( `soundboard_category_${categoryKey .toLowerCase() .replace(/\s+/g, "_")}_page_${page - 1}` ) .setLabel("« Previous") .setStyle(ButtonStyle.Secondary) .setEmoji("◀️"); navRow.addComponents(prevButton); } // Add back to categories button const backButton = new ButtonBuilder() .setCustomId("soundboard_back") .setLabel("Back to Categories") .setStyle(ButtonStyle.Primary) .setEmoji("🔙"); navRow.addComponents(backButton); // Add next page button if there are more pages if (page < totalPages - 1) { const nextButton = new ButtonBuilder() .setCustomId( `soundboard_category_${categoryKey .toLowerCase() .replace(/\s+/g, "_")}_page_${page + 1}` ) .setLabel("Next »") .setStyle(ButtonStyle.Secondary) .setEmoji("▶️"); navRow.addComponents(nextButton); } rows.push(navRow); // Show pagination info const paginationNote = totalPages > 1 ? `\n\n*Page ${page + 1} of ${totalPages} (${ allSounds.length } total sounds)*` : ""; const embed = new EmbedBuilder() .setTitle(`${categoryKey} Soundboard`) .setDescription(`Choose a sound effect to play:${paginationNote}`) .setColor(0x21c629) .setFooter({ text: "Click a sound button to play it" }); await interaction.update({ embeds: [embed], components: rows, }); }, async handleSoundPlay(interaction, guildConfig) { const soundName = interaction.customId.replace("soundboard_play_", ""); // Use the reusable SFX playing method await sfxManager.playSfxInteraction( interaction, soundName, guildConfig, "soundboard" ); }, };