const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); module.exports = { data: new SlashCommandBuilder() .setName('xp-settings') .setDescription('Configure XP settings for your server') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addSubcommand(sub => sub.setName('view') .setDescription('View current XP settings')) .addSubcommand(sub => sub.setName('message-xp') .setDescription('Set XP earned per message') .addIntegerOption(opt => opt.setName('amount') .setDescription('XP per message (1-50)') .setRequired(true) .setMinValue(1) .setMaxValue(50))) .addSubcommand(sub => sub.setName('cooldown') .setDescription('Set cooldown between XP gains (seconds)') .addIntegerOption(opt => opt.setName('seconds') .setDescription('Cooldown in seconds (10-300)') .setRequired(true) .setMinValue(10) .setMaxValue(300))) .addSubcommand(sub => sub.setName('multiplier-role') .setDescription('Add/remove a role with XP multiplier') .addRoleOption(opt => opt.setName('role') .setDescription('The role to add/remove multiplier') .setRequired(true)) .addNumberOption(opt => opt.setName('multiplier') .setDescription('XP multiplier (1.1-5.0, or 0 to remove)') .setRequired(true) .setMinValue(0) .setMaxValue(5))) .addSubcommand(sub => sub.setName('bonus-channel') .setDescription('Add/remove a channel with bonus XP') .addChannelOption(opt => opt.setName('channel') .setDescription('The channel to add/remove bonus') .setRequired(true)) .addNumberOption(opt => opt.setName('multiplier') .setDescription('XP multiplier (1.1-5.0, or 0 to remove)') .setRequired(true) .setMinValue(0) .setMaxValue(5))) .addSubcommand(sub => sub.setName('toggle') .setDescription('Enable or disable XP system') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Enable or disable XP') .setRequired(true))) .addSubcommand(sub => sub.setName('level-curve') .setDescription('Set leveling difficulty curve') .addStringOption(opt => opt.setName('curve') .setDescription('Leveling curve type') .setRequired(true) .addChoices( { name: 'Easy - Faster leveling', value: 'easy' }, { name: 'Normal - Standard leveling', value: 'normal' }, { name: 'Hard - Slower leveling', value: 'hard' } ))) .addSubcommand(sub => sub.setName('reaction-xp') .setDescription('Configure XP for reactions') .addIntegerOption(opt => opt.setName('received') .setDescription('XP for receiving a reaction (0-20)') .setRequired(true) .setMinValue(0) .setMaxValue(20)) .addIntegerOption(opt => opt.setName('given') .setDescription('XP for giving a reaction (0-10)') .setRequired(true) .setMinValue(0) .setMaxValue(10))) .addSubcommand(sub => sub.setName('reaction-cooldown') .setDescription('Set cooldown between reaction XP gains') .addIntegerOption(opt => opt.setName('seconds') .setDescription('Cooldown in seconds (5-120)') .setRequired(true) .setMinValue(5) .setMaxValue(120))) .addSubcommand(sub => sub.setName('reaction-toggle') .setDescription('Enable or disable reaction XP') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Enable or disable reaction XP') .setRequired(true))) .addSubcommand(sub => sub.setName('voice-xp') .setDescription('Set XP earned per minute in voice channels') .addIntegerOption(opt => opt.setName('amount') .setDescription('XP per minute (1-20)') .setRequired(true) .setMinValue(1) .setMaxValue(20))) .addSubcommand(sub => sub.setName('voice-cooldown') .setDescription('Set cooldown between voice XP grants') .addIntegerOption(opt => opt.setName('seconds') .setDescription('Cooldown in seconds (30-300)') .setRequired(true) .setMinValue(30) .setMaxValue(300))) .addSubcommand(sub => sub.setName('voice-toggle') .setDescription('Enable or disable voice XP') .addBooleanOption(opt => opt.setName('enabled') .setDescription('Enable or disable voice XP') .setRequired(true))), async execute(interaction, client, supabase) { if (!supabase) { return interaction.reply({ content: '❌ Database not configured. XP settings require Supabase.', ephemeral: true }); } const guildId = interaction.guildId; const subcommand = interaction.options.getSubcommand(); // Get current config or create default let config = await getXpConfig(supabase, guildId); switch (subcommand) { case 'view': return handleView(interaction, config); case 'message-xp': return handleMessageXp(interaction, supabase, guildId, config); case 'cooldown': return handleCooldown(interaction, supabase, guildId, config); case 'multiplier-role': return handleMultiplierRole(interaction, supabase, guildId, config); case 'bonus-channel': return handleBonusChannel(interaction, supabase, guildId, config); case 'toggle': return handleToggle(interaction, supabase, guildId, config); case 'level-curve': return handleLevelCurve(interaction, supabase, guildId, config); case 'reaction-xp': return handleReactionXp(interaction, supabase, guildId, config); case 'reaction-cooldown': return handleReactionCooldown(interaction, supabase, guildId, config); case 'reaction-toggle': return handleReactionToggle(interaction, supabase, guildId, config); case 'voice-xp': return handleVoiceXp(interaction, supabase, guildId, config); case 'voice-cooldown': return handleVoiceCooldown(interaction, supabase, guildId, config); case 'voice-toggle': return handleVoiceToggle(interaction, supabase, guildId, config); } } }; async function getXpConfig(supabase, guildId) { try { const { data, error } = await supabase .from('xp_config') .select('*') .eq('guild_id', guildId) .maybeSingle(); if (error) throw error; if (!data) { // Return default config return { guild_id: guildId, message_xp: 5, message_cooldown: 60, multiplier_roles: [], bonus_channels: [], level_curve: 'normal', xp_enabled: true, reaction_xp_enabled: true, reaction_xp_received: 3, reaction_xp_given: 1, reaction_cooldown: 30, voice_xp_enabled: true, voice_xp: 2, voice_cooldown: 60 }; } return data; } catch (e) { console.error('Failed to get XP config:', e.message); return { guild_id: guildId, message_xp: 5, message_cooldown: 60, multiplier_roles: [], bonus_channels: [], level_curve: 'normal', xp_enabled: true, reaction_xp_enabled: true, reaction_xp_received: 3, reaction_xp_given: 1, reaction_cooldown: 30, voice_xp_enabled: true, voice_xp: 2, voice_cooldown: 60 }; } } async function saveXpConfig(supabase, guildId, config) { try { const { error } = await supabase .from('xp_config') .upsert({ guild_id: guildId, message_xp: config.message_xp, message_cooldown: config.message_cooldown, multiplier_roles: config.multiplier_roles, bonus_channels: config.bonus_channels, level_curve: config.level_curve, xp_enabled: config.xp_enabled, reaction_xp_enabled: config.reaction_xp_enabled, reaction_xp_received: config.reaction_xp_received, reaction_xp_given: config.reaction_xp_given, reaction_cooldown: config.reaction_cooldown, voice_xp_enabled: config.voice_xp_enabled, voice_xp: config.voice_xp, voice_cooldown: config.voice_cooldown, updated_at: new Date().toISOString() }); if (error) throw error; return true; } catch (e) { console.error('Failed to save XP config:', e.message); return false; } } async function handleView(interaction, config) { const multiplierRoles = config.multiplier_roles || []; const bonusChannels = config.bonus_channels || []; const rolesText = multiplierRoles.length > 0 ? multiplierRoles.map(r => `<@&${r.role_id}> → ${r.multiplier}x`).join('\n') : 'None configured'; const channelsText = bonusChannels.length > 0 ? bonusChannels.map(c => `<#${c.channel_id}> → ${c.multiplier}x`).join('\n') : 'None configured'; const curveInfo = { easy: 'Easy (50 XP per level²)', normal: 'Normal (100 XP per level²)', hard: 'Hard (200 XP per level²)' }; const reactionEnabled = config.reaction_xp_enabled !== false; const reactionReceived = config.reaction_xp_received ?? 3; const reactionGiven = config.reaction_xp_given ?? 1; const reactionCooldown = config.reaction_cooldown ?? 30; const voiceEnabled = config.voice_xp_enabled !== false; const voiceXp = config.voice_xp ?? 2; const voiceCooldown = config.voice_cooldown ?? 60; const embed = new EmbedBuilder() .setTitle('⚙️ XP Settings') .setColor(config.xp_enabled ? 0x00ff88 : 0xff4444) .addFields( { name: '📊 Status', value: config.xp_enabled ? '✅ Enabled' : '❌ Disabled', inline: true }, { name: '💬 Message XP', value: `${config.message_xp} XP`, inline: true }, { name: '⏱️ Cooldown', value: `${config.message_cooldown}s`, inline: true }, { name: '📈 Level Curve', value: curveInfo[config.level_curve] || 'Normal', inline: false }, { name: '🎭 Multiplier Roles', value: rolesText, inline: false }, { name: '📢 Bonus Channels', value: channelsText, inline: false }, { name: '😀 Reaction XP', value: reactionEnabled ? '✅ Enabled' : '❌ Disabled', inline: true }, { name: '📥 Receive Reaction', value: `${reactionReceived} XP`, inline: true }, { name: '📤 Give Reaction', value: `${reactionGiven} XP`, inline: true }, { name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true }, { name: '🎤 Voice XP', value: voiceEnabled ? '✅ Enabled' : '❌ Disabled', inline: true }, { name: '🔊 XP per Minute', value: `${voiceXp} XP`, inline: true }, { name: '⏰ Voice Cooldown', value: `${voiceCooldown}s`, inline: true } ) .setFooter({ text: 'Use /xp-settings subcommands to modify' }) .setTimestamp(); return interaction.reply({ embeds: [embed] }); } async function handleMessageXp(interaction, supabase, guildId, config) { const amount = interaction.options.getInteger('amount'); config.message_xp = amount; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Message XP set to **${amount} XP** per message.`, ephemeral: true }); } async function handleCooldown(interaction, supabase, guildId, config) { const seconds = interaction.options.getInteger('seconds'); config.message_cooldown = seconds; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ XP cooldown set to **${seconds} seconds**.`, ephemeral: true }); } async function handleMultiplierRole(interaction, supabase, guildId, config) { const role = interaction.options.getRole('role'); const multiplier = interaction.options.getNumber('multiplier'); let roles = config.multiplier_roles || []; if (multiplier === 0) { // Remove role roles = roles.filter(r => r.role_id !== role.id); config.multiplier_roles = roles; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Removed XP multiplier from ${role}.`, ephemeral: true }); } // Add or update role const existing = roles.findIndex(r => r.role_id === role.id); if (existing >= 0) { roles[existing].multiplier = multiplier; } else { roles.push({ role_id: role.id, multiplier }); } config.multiplier_roles = roles; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ ${role} now has a **${multiplier}x** XP multiplier.`, ephemeral: true }); } async function handleBonusChannel(interaction, supabase, guildId, config) { const channel = interaction.options.getChannel('channel'); const multiplier = interaction.options.getNumber('multiplier'); let channels = config.bonus_channels || []; if (multiplier === 0) { // Remove channel channels = channels.filter(c => c.channel_id !== channel.id); config.bonus_channels = channels; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Removed bonus XP from ${channel}.`, ephemeral: true }); } // Add or update channel const existing = channels.findIndex(c => c.channel_id === channel.id); if (existing >= 0) { channels[existing].multiplier = multiplier; } else { channels.push({ channel_id: channel.id, multiplier }); } config.bonus_channels = channels; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ ${channel} now has a **${multiplier}x** XP bonus.`, ephemeral: true }); } async function handleToggle(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.xp_enabled = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? '✅ XP system is now **enabled**.' : '❌ XP system is now **disabled**.', ephemeral: true }); } async function handleLevelCurve(interaction, supabase, guildId, config) { const curve = interaction.options.getString('curve'); config.level_curve = curve; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } const curveNames = { easy: 'Easy (faster leveling)', normal: 'Normal (standard leveling)', hard: 'Hard (slower leveling)' }; return interaction.reply({ content: `✅ Level curve set to **${curveNames[curve]}**.`, ephemeral: true }); } async function handleReactionXp(interaction, supabase, guildId, config) { const received = interaction.options.getInteger('received'); const given = interaction.options.getInteger('given'); config.reaction_xp_received = received; config.reaction_xp_given = given; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Reaction XP set: **${received} XP** for receiving, **${given} XP** for giving.`, ephemeral: true }); } async function handleReactionCooldown(interaction, supabase, guildId, config) { const seconds = interaction.options.getInteger('seconds'); config.reaction_cooldown = seconds; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Reaction XP cooldown set to **${seconds} seconds**.`, ephemeral: true }); } async function handleReactionToggle(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.reaction_xp_enabled = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? '✅ Reaction XP is now **enabled**.' : '❌ Reaction XP is now **disabled**.', ephemeral: true }); } async function handleVoiceXp(interaction, supabase, guildId, config) { const amount = interaction.options.getInteger('amount'); config.voice_xp = amount; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Voice XP set to **${amount} XP** per minute.`, ephemeral: true }); } async function handleVoiceCooldown(interaction, supabase, guildId, config) { const seconds = interaction.options.getInteger('seconds'); config.voice_cooldown = seconds; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: `✅ Voice XP cooldown set to **${seconds} seconds**.`, ephemeral: true }); } async function handleVoiceToggle(interaction, supabase, guildId, config) { const enabled = interaction.options.getBoolean('enabled'); config.voice_xp_enabled = enabled; const saved = await saveXpConfig(supabase, guildId, config); if (!saved) { return interaction.reply({ content: '❌ Failed to save settings. Please try again.', ephemeral: true }); } return interaction.reply({ content: enabled ? '✅ Voice XP is now **enabled**.' : '❌ Voice XP is now **disabled**.', ephemeral: true }); }