SFX -> Sfx

This commit is contained in:
Chris Ham
2025-08-16 22:21:10 -07:00
parent 46b78dd6b3
commit b2821d412c
4 changed files with 167 additions and 129 deletions

View File

@@ -1,11 +1,11 @@
const axios = require('axios'); const axios = require("axios");
const { chunkSubstr } = require('../../utils/helpers'); const { chunkSubstr } = require("../../utils/helpers");
const sfxManager = require('../../services/sfxManager'); const sfxManager = require("../../services/sfxManager");
const voiceService = require('../../services/voiceService'); const voiceService = require("../../services/voiceService");
module.exports = { module.exports = {
name: 'sfx', name: "sfx",
description: 'Play a sound effect', description: "Play a sound effect",
/** /**
* Smart chunking that respects markdown block boundaries * Smart chunking that respects markdown block boundaries
@@ -17,7 +17,7 @@ module.exports = {
const chunks = []; const chunks = [];
const sections = content.split(/(\*\*[^*]+\*\*)/); // Split on headers while keeping them const sections = content.split(/(\*\*[^*]+\*\*)/); // Split on headers while keeping them
let currentChunk = ''; let currentChunk = "";
for (const section of sections) { for (const section of sections) {
// If adding this section would exceed the limit // If adding this section would exceed the limit
@@ -46,12 +46,19 @@ module.exports = {
// If no SFX specified, show the list // If no SFX specified, show the list
if (!sfxName) { if (!sfxName) {
try { try {
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const sfxReadmePath = path.join(__dirname, '..', '..', '..', 'sfx', 'README.md'); const sfxReadmePath = path.join(
__dirname,
"..",
"..",
"..",
"sfx",
"README.md"
);
if (fs.existsSync(sfxReadmePath)) { 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) // Break into chunks if too long (Discord limit is 2000 characters)
if (sfxListContent.length <= 2000) { if (sfxListContent.length <= 2000) {
@@ -66,23 +73,27 @@ module.exports = {
} }
} else { } else {
// Fallback to generated list if README doesn't exist // Fallback to generated list if README doesn't exist
const sfxNames = sfxManager.getSFXNames(); const sfxNames = sfxManager.getSfxNames();
const sfxList = `**Available Sound Effects (${sfxNames.length}):**\n\`\`\`\n${sfxNames.join(', ')}\n\`\`\``; const sfxList = `**Available Sound Effects (${
sfxNames.length
}):**\n\`\`\`\n${sfxNames.join(", ")}\n\`\`\``;
await message.channel.send(sfxList); await message.channel.send(sfxList);
} }
} catch (error) { } catch (error) {
console.error('Error reading SFX list:', error); console.error("Error reading SFX list:", error);
await message.reply('Could not load the SFX list.'); await message.reply("Could not load the SFX list.");
} }
return; return;
} }
// Check if user is in a voice channel // Check if user is in a voice channel
if (!message.member.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 // Use the reusable SFX playing method for messages
await sfxManager.playSFXMessage(message, sfxName, guildConfig); await sfxManager.playSfxMessage(message, sfxName, guildConfig);
} },
}; };

View File

@@ -1,50 +1,56 @@
const { SlashCommandBuilder, MessageFlags } = require('discord.js'); const { SlashCommandBuilder, MessageFlags } = require("discord.js");
const sfxManager = require('../../services/sfxManager'); const sfxManager = require("../../services/sfxManager");
const voiceService = require('../../services/voiceService'); const voiceService = require("../../services/voiceService");
module.exports = { module.exports = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('sfx') .setName("sfx")
.setDescription('Play a sound effect') .setDescription("Play a sound effect")
.addStringOption(option => .addStringOption((option) =>
option.setName('sound') option
.setDescription('The sound effect to play') .setName("sound")
.setDescription("The sound effect to play")
.setRequired(true) .setRequired(true)
.setAutocomplete(true) .setAutocomplete(true)
), ),
async execute(interaction, guildConfig) { async execute(interaction, guildConfig) {
const sfxName = interaction.options.getString('sound'); const sfxName = interaction.options.getString("sound");
// Check if user is in a voice channel // Check if user is in a voice channel
if (!interaction.member.voice.channel) { if (!interaction.member.voice.channel) {
return interaction.reply({ return interaction.reply({
content: 'You need to be in a voice channel to use this command!', content: "You need to be in a voice channel to use this command!",
flags: [MessageFlags.Ephemeral] flags: [MessageFlags.Ephemeral],
}); });
} }
// Use the reusable SFX playing method // Use the reusable SFX playing method
await sfxManager.playSFXInteraction(interaction, sfxName, guildConfig, 'slash'); await sfxManager.playSfxInteraction(
interaction,
sfxName,
guildConfig,
"slash"
);
}, },
async autocomplete(interaction, guildConfig) { async autocomplete(interaction, guildConfig) {
const focusedValue = interaction.options.getFocused().toLowerCase(); const focusedValue = interaction.options.getFocused().toLowerCase();
// Get all SFX names // Get all SFX names
const choices = sfxManager.getSFXNames(); const choices = sfxManager.getSfxNames();
// Filter based on what the user has typed // Filter based on what the user has typed
const filtered = choices const filtered = choices
.filter(choice => choice.toLowerCase().includes(focusedValue)) .filter((choice) => choice.toLowerCase().includes(focusedValue))
.slice(0, 25); // Discord limits autocomplete to 25 choices .slice(0, 25); // Discord limits autocomplete to 25 choices
// Respond with the filtered choices // Respond with the filtered choices
await interaction.respond( await interaction.respond(
filtered.map(choice => ({ filtered.map((choice) => ({
name: choice, name: choice,
value: choice value: choice,
})) }))
); );
} },
}; };

View File

@@ -9,10 +9,9 @@ const {
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const sfxManager = require("../../services/sfxManager"); const sfxManager = require("../../services/sfxManager");
const voiceService = require("../../services/voiceService");
// Parse categories from README.md // Parse categories from README.md
function getSFXCategories() { function getSfxCategories() {
try { try {
const sfxReadmePath = path.join( const sfxReadmePath = path.join(
__dirname, __dirname,
@@ -70,7 +69,7 @@ module.exports = {
}); });
} }
const categories = getSFXCategories(); const categories = getSfxCategories();
if (!categories) { if (!categories) {
return interaction.reply({ return interaction.reply({
@@ -112,8 +111,7 @@ module.exports = {
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("🎛️ Interactive Soundboard") .setTitle("🎛️ Interactive Soundboard")
.setDescription("Choose a category to browse sound effects:") .setDescription("Choose a category to browse sound effects:")
.setColor(0x21c629) .setColor(0x21c629);
.setFooter({ text: "Click a category button to browse sounds" });
await interaction.reply({ await interaction.reply({
embeds: [embed], embeds: [embed],
@@ -145,7 +143,7 @@ module.exports = {
.toUpperCase(); .toUpperCase();
} }
const categories = getSFXCategories(); const categories = getSfxCategories();
if (!categories || !categories[categoryKey]) { if (!categories || !categories[categoryKey]) {
return interaction.reply({ return interaction.reply({
@@ -155,7 +153,7 @@ module.exports = {
} }
const allSounds = categories[categoryKey].filter((sound) => 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 soundsPerPage = 16; // 4 sounds per row × 4 rows = 16 sounds per page
const totalPages = Math.ceil(allSounds.length / soundsPerPage); const totalPages = Math.ceil(allSounds.length / soundsPerPage);
@@ -252,7 +250,7 @@ module.exports = {
const soundName = interaction.customId.replace("soundboard_play_", ""); const soundName = interaction.customId.replace("soundboard_play_", "");
// Use the reusable SFX playing method // Use the reusable SFX playing method
await sfxManager.playSFXInteraction( await sfxManager.playSfxInteraction(
interaction, interaction,
soundName, soundName,
guildConfig, guildConfig,

View File

@@ -1,47 +1,47 @@
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
const { MessageFlags } = require('discord.js'); const { MessageFlags } = require("discord.js");
const voiceService = require('./voiceService'); const voiceService = require("./voiceService");
class SFXManager { class SfxManager {
constructor() { constructor() {
this.sfxPath = path.join(__dirname, '..', '..', 'sfx'); this.sfxPath = path.join(__dirname, "..", "..", "sfx");
this.sfxList = []; this.sfxList = [];
this.cachedNames = []; this.cachedNames = [];
this.searchCache = new Map(); // Cache for autocomplete searches this.searchCache = new Map(); // Cache for autocomplete searches
// Load SFX list initially // Load SFX list initially
this.loadSFXList(); this.loadSfxList();
// Watch for changes // Watch for changes
this.watchSFXDirectory(); this.watchSfxDirectory();
} }
/** /**
* Load the list of available SFX files * Load the list of available SFX files
*/ */
loadSFXList() { loadSfxList() {
try { try {
if (!fs.existsSync(this.sfxPath)) { 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 }); fs.mkdirSync(this.sfxPath, { recursive: true });
} }
const files = fs.readdirSync(this.sfxPath); const files = fs.readdirSync(this.sfxPath);
this.sfxList = files this.sfxList = files
.filter(file => file.endsWith('.mp3') || file.endsWith('.wav')) .filter((file) => file.endsWith(".mp3") || file.endsWith(".wav"))
.map(file => { .map((file) => {
const ext = path.extname(file); const ext = path.extname(file);
return { return {
name: file.replace(ext, ''), name: file.replace(ext, ""),
filename: file, filename: file,
path: path.join(this.sfxPath, file) path: path.join(this.sfxPath, file),
}; };
}); });
// Cache sorted names for autocomplete // Cache sorted names for autocomplete
this.cachedNames = this.sfxList this.cachedNames = this.sfxList
.map(sfx => sfx.name) .map((sfx) => sfx.name)
.sort((a, b) => a.localeCompare(b)); .sort((a, b) => a.localeCompare(b));
// Clear search cache when SFX list changes // Clear search cache when SFX list changes
@@ -49,18 +49,18 @@ class SFXManager {
console.log(`Loaded ${this.sfxList.length} sound effects`); console.log(`Loaded ${this.sfxList.length} sound effects`);
} catch (error) { } catch (error) {
console.error('Error loading SFX list:', error); console.error("Error loading SFX list:", error);
} }
} }
/** /**
* Watch the SFX directory for changes * Watch the SFX directory for changes
*/ */
watchSFXDirectory() { watchSfxDirectory() {
fs.watch(this.sfxPath, (eventType, filename) => { fs.watch(this.sfxPath, (eventType, filename) => {
if (eventType === 'rename') { if (eventType === "rename") {
console.log('SFX directory changed, reloading...'); console.log("SFX directory changed, reloading...");
this.loadSFXList(); this.loadSfxList();
} }
}); });
} }
@@ -69,7 +69,7 @@ class SFXManager {
* Get all available SFX * Get all available SFX
* @returns {Array} List of SFX objects * @returns {Array} List of SFX objects
*/ */
getAllSFX() { getAllSfx() {
return this.sfxList; return this.sfxList;
} }
@@ -77,7 +77,7 @@ class SFXManager {
* Get SFX names for autocomplete (cached and sorted) * Get SFX names for autocomplete (cached and sorted)
* @returns {Array} List of SFX names * @returns {Array} List of SFX names
*/ */
getSFXNames() { getSfxNames() {
return this.cachedNames; return this.cachedNames;
} }
@@ -86,8 +86,10 @@ class SFXManager {
* @param {string} name * @param {string} name
* @returns {Object|undefined} SFX object or undefined * @returns {Object|undefined} SFX object or undefined
*/ */
findSFX(name) { findSfx(name) {
return this.sfxList.find(sfx => sfx.name.toLowerCase() === name.toLowerCase()); return this.sfxList.find(
(sfx) => sfx.name.toLowerCase() === name.toLowerCase()
);
} }
/** /**
@@ -95,8 +97,8 @@ class SFXManager {
* @param {string} name * @param {string} name
* @returns {boolean} * @returns {boolean}
*/ */
hasSFX(name) { hasSfx(name) {
return this.findSFX(name) !== undefined; return this.findSfx(name) !== undefined;
} }
/** /**
@@ -104,8 +106,8 @@ class SFXManager {
* @param {string} name * @param {string} name
* @returns {string|null} * @returns {string|null}
*/ */
getSFXPath(name) { getSfxPath(name) {
const sfx = this.findSFX(name); const sfx = this.findSfx(name);
return sfx ? sfx.path : null; return sfx ? sfx.path : null;
} }
@@ -114,7 +116,7 @@ class SFXManager {
* @param {string} query * @param {string} query
* @returns {Array} Matching SFX names * @returns {Array} Matching SFX names
*/ */
searchSFX(query) { searchSfx(query) {
const lowerQuery = query.toLowerCase(); const lowerQuery = query.toLowerCase();
// Check cache first // Check cache first
@@ -124,7 +126,7 @@ class SFXManager {
// Perform search on cached names (already sorted) // Perform search on cached names (already sorted)
const results = this.cachedNames const results = this.cachedNames
.filter(name => name.toLowerCase().includes(lowerQuery)) .filter((name) => name.toLowerCase().includes(lowerQuery))
.slice(0, 25); // Discord autocomplete limit .slice(0, 25); // Discord autocomplete limit
// Cache the result for future use // Cache the result for future use
@@ -141,18 +143,25 @@ class SFXManager {
* @param {string} commandType - Type of command ('slash' or 'soundboard') * @param {string} commandType - Type of command ('slash' or 'soundboard')
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async playSFXInteraction(interaction, sfxName, guildConfig, commandType = 'slash') { async playSfxInteraction(
interaction,
sfxName,
guildConfig,
commandType = "slash"
) {
// Log the request // Log the request
const logPrefix = commandType === 'soundboard' ? 'Soundboard' : '/sfx'; const logPrefix = commandType === "soundboard" ? "Soundboard" : "/sfx";
console.log( 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 // Check if SFX exists
if (!this.hasSFX(sfxName)) { if (!this.hasSfx(sfxName)) {
await interaction.reply({ await interaction.reply({
content: `❌ This sound effect does not exist!`, content: `❌ This sound effect does not exist!`,
flags: [MessageFlags.Ephemeral] flags: [MessageFlags.Ephemeral],
}); });
return; return;
} }
@@ -161,14 +170,14 @@ class SFXManager {
// Immediately reply with playing status // Immediately reply with playing status
await interaction.reply({ await interaction.reply({
content: `🔊 Playing: **${sfxName}**`, content: `🔊 Playing: **${sfxName}**`,
flags: [MessageFlags.Ephemeral] //flags: [MessageFlags.Ephemeral],
}); });
// Join the voice channel // Join the voice channel
await voiceService.join(interaction.member.voice.channel); await voiceService.join(interaction.member.voice.channel);
// Get the SFX file path and play // Get the SFX file path and play
const sfxPath = this.getSFXPath(sfxName); const sfxPath = this.getSfxPath(sfxName);
await voiceService.play(interaction.guild.id, sfxPath, { await voiceService.play(interaction.guild.id, sfxPath, {
volume: guildConfig.sfxVolume || 0.5, volume: guildConfig.sfxVolume || 0.5,
}); });
@@ -176,10 +185,13 @@ class SFXManager {
// Update the interaction to show completion // Update the interaction to show completion
try { try {
await interaction.editReply({ await interaction.editReply({
content: `✅ Finished playing: **${sfxName}**` content: `✅ Finished playing: **${sfxName}**`,
}); });
} catch (editError) { } 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 // Leave the voice channel after playing
@@ -187,18 +199,26 @@ class SFXManager {
voiceService.leave(interaction.guild.id); voiceService.leave(interaction.guild.id);
}, 500); }, 500);
console.log(`✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'`); console.log(
`✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'`
);
} catch (error) { } catch (error) {
console.error(`❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`, error); console.error(
`❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`,
error
);
// Update the reply with error message // Update the reply with error message
try { try {
await interaction.editReply({ 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) { } 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 * @param {Object} guildConfig - Guild configuration
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async playSFXMessage(message, sfxName, guildConfig) { async playSfxMessage(message, sfxName, guildConfig) {
// Log the request // Log the request
console.log( 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 // Check if SFX exists
if (!this.hasSFX(sfxName)) { if (!this.hasSfx(sfxName)) {
await message.reply('❌ This sound effect does not exist!'); await message.reply("❌ This sound effect does not exist!");
return; return;
} }
try { try {
// React with speaker icon to show playing status // React with speaker icon to show playing status
await message.react('🔊'); await message.react("🔊");
// Join the voice channel // Join the voice channel
await voiceService.join(message.member.voice.channel); await voiceService.join(message.member.voice.channel);
// Get the SFX file path and play // Get the SFX file path and play
const sfxPath = this.getSFXPath(sfxName); const sfxPath = this.getSfxPath(sfxName);
await voiceService.play(message.guild.id, sfxPath, { await voiceService.play(message.guild.id, sfxPath, {
volume: guildConfig.sfxVolume || 0.5, volume: guildConfig.sfxVolume || 0.5,
}); });
// Add completion reaction (keep both speaker and checkmark) // Add completion reaction (keep both speaker and checkmark)
await message.react('✅'); await message.react("✅");
// Leave the voice channel after playing // Leave the voice channel after playing
setTimeout(() => { setTimeout(() => {
@@ -244,19 +266,20 @@ class SFXManager {
}, 500); }, 500);
console.log(`✅ Successfully played SFX '${sfxName}'`); console.log(`✅ Successfully played SFX '${sfxName}'`);
} catch (error) { } catch (error) {
console.error(`❌ Error playing SFX '${sfxName}':`, error); console.error(`❌ Error playing SFX '${sfxName}':`, error);
// Add error reaction // Add error reaction
try { try {
await message.react('❌'); await message.react("❌");
} catch (reactionError) { } catch (reactionError) {
// If reactions fail, fall back to reply // 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(); module.exports = new SfxManager();