SFX -> Sfx
This commit is contained in:
@@ -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<string>}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
};
|
||||
await sfxManager.playSfxMessage(message, sfxName, guildConfig);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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<void>}
|
||||
*/
|
||||
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();
|
||||
module.exports = new SfxManager();
|
||||
|
||||
Reference in New Issue
Block a user