From b2821d412cc2d9896df48677ef3d2507fa983072 Mon Sep 17 00:00:00 2001 From: Chris Ham <431647+greenham@users.noreply.github.com> Date: Sat, 16 Aug 2025 22:21:10 -0700 Subject: [PATCH] SFX -> Sfx --- src/commands/prefix/sfx.js | 69 +++++++------ src/commands/slash/sfx.js | 50 +++++----- src/commands/slash/soundboard.js | 14 ++- src/services/sfxManager.js | 163 ++++++++++++++++++------------- 4 files changed, 167 insertions(+), 129 deletions(-) diff --git a/src/commands/prefix/sfx.js b/src/commands/prefix/sfx.js index c522c47..1706637 100644 --- a/src/commands/prefix/sfx.js +++ b/src/commands/prefix/sfx.js @@ -1,24 +1,24 @@ -const axios = require('axios'); -const { chunkSubstr } = require('../../utils/helpers'); -const sfxManager = require('../../services/sfxManager'); -const voiceService = require('../../services/voiceService'); +const axios = require("axios"); +const { chunkSubstr } = require("../../utils/helpers"); +const sfxManager = require("../../services/sfxManager"); +const voiceService = require("../../services/voiceService"); module.exports = { - name: 'sfx', - description: 'Play a sound effect', + name: "sfx", + description: "Play a sound effect", /** * Smart chunking that respects markdown block boundaries - * @param {string} content - * @param {number} maxLength + * @param {string} content + * @param {number} maxLength * @returns {Array} */ smartChunkMarkdown(content, maxLength) { const chunks = []; const sections = content.split(/(\*\*[^*]+\*\*)/); // Split on headers while keeping them - - let currentChunk = ''; - + + let currentChunk = ""; + for (const section of sections) { // If adding this section would exceed the limit if (currentChunk.length + section.length > maxLength) { @@ -31,58 +31,69 @@ module.exports = { currentChunk += section; } } - + // Add the final chunk if (currentChunk.trim()) { chunks.push(currentChunk.trim()); } - + return chunks; }, - + async execute(message, args, guildConfig) { const sfxName = args[0]; // If no SFX specified, show the list if (!sfxName) { try { - const fs = require('fs'); - const path = require('path'); - const sfxReadmePath = path.join(__dirname, '..', '..', '..', 'sfx', 'README.md'); - + const fs = require("fs"); + const path = require("path"); + const sfxReadmePath = path.join( + __dirname, + "..", + "..", + "..", + "sfx", + "README.md" + ); + if (fs.existsSync(sfxReadmePath)) { - const sfxListContent = fs.readFileSync(sfxReadmePath, 'utf-8'); - + const sfxListContent = fs.readFileSync(sfxReadmePath, "utf-8"); + // Break into chunks if too long (Discord limit is 2000 characters) if (sfxListContent.length <= 2000) { await message.channel.send(sfxListContent); } else { // Smart chunking that respects markdown block boundaries const chunks = this.smartChunkMarkdown(sfxListContent, 1900); - + for (const chunk of chunks) { await message.channel.send(chunk); } } } else { // Fallback to generated list if README doesn't exist - const sfxNames = sfxManager.getSFXNames(); - const sfxList = `**Available Sound Effects (${sfxNames.length}):**\n\`\`\`\n${sfxNames.join(', ')}\n\`\`\``; + const sfxNames = sfxManager.getSfxNames(); + const sfxList = `**Available Sound Effects (${ + sfxNames.length + }):**\n\`\`\`\n${sfxNames.join(", ")}\n\`\`\``; await message.channel.send(sfxList); } } catch (error) { - console.error('Error reading SFX list:', error); - await message.reply('Could not load the SFX list.'); + console.error("Error reading SFX list:", error); + await message.reply("Could not load the SFX list."); } return; } // Check if user is in a voice channel if (!message.member.voice.channel) { - return message.reply('You need to be in a voice channel to use this command!'); + return message.reply( + "You need to be in a voice channel to use this command!" + ); } // Use the reusable SFX playing method for messages - await sfxManager.playSFXMessage(message, sfxName, guildConfig); - } -}; \ No newline at end of file + await sfxManager.playSfxMessage(message, sfxName, guildConfig); + }, +}; diff --git a/src/commands/slash/sfx.js b/src/commands/slash/sfx.js index d5f0b0c..8d9c8e0 100644 --- a/src/commands/slash/sfx.js +++ b/src/commands/slash/sfx.js @@ -1,50 +1,56 @@ -const { SlashCommandBuilder, MessageFlags } = require('discord.js'); -const sfxManager = require('../../services/sfxManager'); -const voiceService = require('../../services/voiceService'); +const { SlashCommandBuilder, MessageFlags } = require("discord.js"); +const sfxManager = require("../../services/sfxManager"); +const voiceService = require("../../services/voiceService"); module.exports = { data: new SlashCommandBuilder() - .setName('sfx') - .setDescription('Play a sound effect') - .addStringOption(option => - option.setName('sound') - .setDescription('The sound effect to play') + .setName("sfx") + .setDescription("Play a sound effect") + .addStringOption((option) => + option + .setName("sound") + .setDescription("The sound effect to play") .setRequired(true) .setAutocomplete(true) ), async execute(interaction, guildConfig) { - const sfxName = interaction.options.getString('sound'); + const sfxName = interaction.options.getString("sound"); // 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 this command!', - flags: [MessageFlags.Ephemeral] + return interaction.reply({ + content: "You need to be in a voice channel to use this command!", + flags: [MessageFlags.Ephemeral], }); } // Use the reusable SFX playing method - await sfxManager.playSFXInteraction(interaction, sfxName, guildConfig, 'slash'); + await sfxManager.playSfxInteraction( + interaction, + sfxName, + guildConfig, + "slash" + ); }, async autocomplete(interaction, guildConfig) { const focusedValue = interaction.options.getFocused().toLowerCase(); - + // Get all SFX names - const choices = sfxManager.getSFXNames(); - + const choices = sfxManager.getSfxNames(); + // Filter based on what the user has typed const filtered = choices - .filter(choice => choice.toLowerCase().includes(focusedValue)) + .filter((choice) => choice.toLowerCase().includes(focusedValue)) .slice(0, 25); // Discord limits autocomplete to 25 choices // Respond with the filtered choices await interaction.respond( - filtered.map(choice => ({ - name: choice, - value: choice + filtered.map((choice) => ({ + name: choice, + value: choice, })) ); - } -}; \ No newline at end of file + }, +}; diff --git a/src/commands/slash/soundboard.js b/src/commands/slash/soundboard.js index 6014792..b6de637 100644 --- a/src/commands/slash/soundboard.js +++ b/src/commands/slash/soundboard.js @@ -9,10 +9,9 @@ const { const fs = require("fs"); const path = require("path"); const sfxManager = require("../../services/sfxManager"); -const voiceService = require("../../services/voiceService"); // Parse categories from README.md -function getSFXCategories() { +function getSfxCategories() { try { const sfxReadmePath = path.join( __dirname, @@ -70,7 +69,7 @@ module.exports = { }); } - const categories = getSFXCategories(); + const categories = getSfxCategories(); if (!categories) { return interaction.reply({ @@ -112,8 +111,7 @@ module.exports = { const embed = new EmbedBuilder() .setTitle("🎛️ Interactive Soundboard") .setDescription("Choose a category to browse sound effects:") - .setColor(0x21c629) - .setFooter({ text: "Click a category button to browse sounds" }); + .setColor(0x21c629); await interaction.reply({ embeds: [embed], @@ -145,7 +143,7 @@ module.exports = { .toUpperCase(); } - const categories = getSFXCategories(); + const categories = getSfxCategories(); if (!categories || !categories[categoryKey]) { return interaction.reply({ @@ -155,7 +153,7 @@ module.exports = { } const allSounds = categories[categoryKey].filter((sound) => - sfxManager.hasSFX(sound) + sfxManager.hasSfx(sound) ); const soundsPerPage = 16; // 4 sounds per row × 4 rows = 16 sounds per page const totalPages = Math.ceil(allSounds.length / soundsPerPage); @@ -252,7 +250,7 @@ module.exports = { const soundName = interaction.customId.replace("soundboard_play_", ""); // Use the reusable SFX playing method - await sfxManager.playSFXInteraction( + await sfxManager.playSfxInteraction( interaction, soundName, guildConfig, diff --git a/src/services/sfxManager.js b/src/services/sfxManager.js index a71af77..fab3828 100644 --- a/src/services/sfxManager.js +++ b/src/services/sfxManager.js @@ -1,66 +1,66 @@ -const fs = require('fs'); -const path = require('path'); -const { MessageFlags } = require('discord.js'); -const voiceService = require('./voiceService'); +const fs = require("fs"); +const path = require("path"); +const { MessageFlags } = require("discord.js"); +const voiceService = require("./voiceService"); -class SFXManager { +class SfxManager { constructor() { - this.sfxPath = path.join(__dirname, '..', '..', 'sfx'); + this.sfxPath = path.join(__dirname, "..", "..", "sfx"); this.sfxList = []; this.cachedNames = []; this.searchCache = new Map(); // Cache for autocomplete searches - + // Load SFX list initially - this.loadSFXList(); - + this.loadSfxList(); + // Watch for changes - this.watchSFXDirectory(); + this.watchSfxDirectory(); } /** * Load the list of available SFX files */ - loadSFXList() { + loadSfxList() { try { if (!fs.existsSync(this.sfxPath)) { - console.log('SFX directory not found, creating...'); + console.log("SFX directory not found, creating..."); fs.mkdirSync(this.sfxPath, { recursive: true }); } const files = fs.readdirSync(this.sfxPath); this.sfxList = files - .filter(file => file.endsWith('.mp3') || file.endsWith('.wav')) - .map(file => { + .filter((file) => file.endsWith(".mp3") || file.endsWith(".wav")) + .map((file) => { const ext = path.extname(file); return { - name: file.replace(ext, ''), + name: file.replace(ext, ""), filename: file, - path: path.join(this.sfxPath, file) + path: path.join(this.sfxPath, file), }; }); - + // Cache sorted names for autocomplete this.cachedNames = this.sfxList - .map(sfx => sfx.name) + .map((sfx) => sfx.name) .sort((a, b) => a.localeCompare(b)); - + // Clear search cache when SFX list changes this.searchCache.clear(); - + console.log(`Loaded ${this.sfxList.length} sound effects`); } catch (error) { - console.error('Error loading SFX list:', error); + console.error("Error loading SFX list:", error); } } /** * Watch the SFX directory for changes */ - watchSFXDirectory() { + watchSfxDirectory() { fs.watch(this.sfxPath, (eventType, filename) => { - if (eventType === 'rename') { - console.log('SFX directory changed, reloading...'); - this.loadSFXList(); + if (eventType === "rename") { + console.log("SFX directory changed, reloading..."); + this.loadSfxList(); } }); } @@ -69,7 +69,7 @@ class SFXManager { * Get all available SFX * @returns {Array} List of SFX objects */ - getAllSFX() { + getAllSfx() { return this.sfxList; } @@ -77,59 +77,61 @@ class SFXManager { * Get SFX names for autocomplete (cached and sorted) * @returns {Array} List of SFX names */ - getSFXNames() { + getSfxNames() { return this.cachedNames; } /** * Find an SFX by name - * @param {string} name + * @param {string} name * @returns {Object|undefined} SFX object or undefined */ - findSFX(name) { - return this.sfxList.find(sfx => sfx.name.toLowerCase() === name.toLowerCase()); + findSfx(name) { + return this.sfxList.find( + (sfx) => sfx.name.toLowerCase() === name.toLowerCase() + ); } /** * Check if an SFX exists - * @param {string} name + * @param {string} name * @returns {boolean} */ - hasSFX(name) { - return this.findSFX(name) !== undefined; + hasSfx(name) { + return this.findSfx(name) !== undefined; } /** * Get the file path for an SFX - * @param {string} name + * @param {string} name * @returns {string|null} */ - getSFXPath(name) { - const sfx = this.findSFX(name); + getSfxPath(name) { + const sfx = this.findSfx(name); return sfx ? sfx.path : null; } /** * Search SFX names (for autocomplete) with caching - * @param {string} query + * @param {string} query * @returns {Array} Matching SFX names */ - searchSFX(query) { + searchSfx(query) { const lowerQuery = query.toLowerCase(); - + // Check cache first if (this.searchCache.has(lowerQuery)) { return this.searchCache.get(lowerQuery); } - + // Perform search on cached names (already sorted) const results = this.cachedNames - .filter(name => name.toLowerCase().includes(lowerQuery)) + .filter((name) => name.toLowerCase().includes(lowerQuery)) .slice(0, 25); // Discord autocomplete limit - + // Cache the result for future use this.searchCache.set(lowerQuery, results); - + return results; } @@ -141,18 +143,25 @@ class SFXManager { * @param {string} commandType - Type of command ('slash' or 'soundboard') * @returns {Promise} */ - async playSFXInteraction(interaction, sfxName, guildConfig, commandType = 'slash') { + async playSfxInteraction( + interaction, + sfxName, + guildConfig, + commandType = "slash" + ) { // Log the request - const logPrefix = commandType === 'soundboard' ? 'Soundboard' : '/sfx'; + const logPrefix = commandType === "soundboard" ? "Soundboard" : "/sfx"; console.log( - `${logPrefix} '${sfxName}' requested in ${guildConfig.internalName || interaction.guild.name}#${interaction.channel.name} from @${interaction.user.username}` + `${logPrefix} '${sfxName}' requested in ${ + guildConfig.internalName || interaction.guild.name + }#${interaction.channel.name} from @${interaction.user.username}` ); // Check if SFX exists - if (!this.hasSFX(sfxName)) { + if (!this.hasSfx(sfxName)) { await interaction.reply({ content: `❌ This sound effect does not exist!`, - flags: [MessageFlags.Ephemeral] + flags: [MessageFlags.Ephemeral], }); return; } @@ -161,14 +170,14 @@ class SFXManager { // Immediately reply with playing status await interaction.reply({ content: `🔊 Playing: **${sfxName}**`, - flags: [MessageFlags.Ephemeral] + //flags: [MessageFlags.Ephemeral], }); // Join the voice channel await voiceService.join(interaction.member.voice.channel); // Get the SFX file path and play - const sfxPath = this.getSFXPath(sfxName); + const sfxPath = this.getSfxPath(sfxName); await voiceService.play(interaction.guild.id, sfxPath, { volume: guildConfig.sfxVolume || 0.5, }); @@ -176,10 +185,13 @@ class SFXManager { // Update the interaction to show completion try { await interaction.editReply({ - content: `✅ Finished playing: **${sfxName}**` + content: `✅ Finished playing: **${sfxName}**`, }); } catch (editError) { - console.error('Error updating interaction with completion message:', editError); + console.error( + "Error updating interaction with completion message:", + editError + ); } // Leave the voice channel after playing @@ -187,18 +199,26 @@ class SFXManager { voiceService.leave(interaction.guild.id); }, 500); - console.log(`✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'`); - + console.log( + `✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'` + ); } catch (error) { - console.error(`❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`, error); - + console.error( + `❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`, + error + ); + // Update the reply with error message try { await interaction.editReply({ - content: "❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!" + content: + "❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!", }); } catch (editError) { - console.error('Error updating interaction with error message:', editError); + console.error( + "Error updating interaction with error message:", + editError + ); } } } @@ -210,33 +230,35 @@ class SFXManager { * @param {Object} guildConfig - Guild configuration * @returns {Promise} */ - async playSFXMessage(message, sfxName, guildConfig) { + async playSfxMessage(message, sfxName, guildConfig) { // Log the request console.log( - `SFX '${sfxName}' requested in ${guildConfig.internalName || message.guild.name}#${message.channel.name} from @${message.author.username}` + `SFX '${sfxName}' requested in ${ + guildConfig.internalName || message.guild.name + }#${message.channel.name} from @${message.author.username}` ); // Check if SFX exists - if (!this.hasSFX(sfxName)) { - await message.reply('❌ This sound effect does not exist!'); + if (!this.hasSfx(sfxName)) { + await message.reply("❌ This sound effect does not exist!"); return; } try { // React with speaker icon to show playing status - await message.react('🔊'); + await message.react("🔊"); // Join the voice channel await voiceService.join(message.member.voice.channel); // Get the SFX file path and play - const sfxPath = this.getSFXPath(sfxName); + const sfxPath = this.getSfxPath(sfxName); await voiceService.play(message.guild.id, sfxPath, { volume: guildConfig.sfxVolume || 0.5, }); // Add completion reaction (keep both speaker and checkmark) - await message.react('✅'); + await message.react("✅"); // Leave the voice channel after playing setTimeout(() => { @@ -244,19 +266,20 @@ class SFXManager { }, 500); console.log(`✅ Successfully played SFX '${sfxName}'`); - } catch (error) { console.error(`❌ Error playing SFX '${sfxName}':`, error); - + // Add error reaction try { - await message.react('❌'); + await message.react("❌"); } catch (reactionError) { // If reactions fail, fall back to reply - await message.reply("❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!"); + await message.reply( + "❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!" + ); } } } } -module.exports = new SFXManager(); \ No newline at end of file +module.exports = new SfxManager();