const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); const QUEST_TYPES = { daily: { emoji: '☀️', name: 'Daily' }, weekly: { emoji: '📅', name: 'Weekly' }, special: { emoji: '⭐', name: 'Special' } }; const OBJECTIVES = { messages: { emoji: '💬', name: 'Send Messages' }, reactions: { emoji: '😄', name: 'Add Reactions' }, voice_minutes: { emoji: '🎙️', name: 'Voice Chat (minutes)' }, commands: { emoji: '⚡', name: 'Use Commands' }, daily_claims: { emoji: '🎁', name: 'Claim Daily Rewards' }, level_ups: { emoji: '📈', name: 'Level Up' }, xp_earned: { emoji: '✨', name: 'Earn XP' } }; module.exports = { data: new SlashCommandBuilder() .setName('quests-manage') .setDescription('Manage server quests (Admin only)') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addSubcommand(sub => sub.setName('create') .setDescription('Create a new quest') .addStringOption(opt => opt.setName('name') .setDescription('Quest name') .setRequired(true) ) .addStringOption(opt => opt.setName('type') .setDescription('Quest type') .setRequired(true) .addChoices( { name: '☀️ Daily', value: 'daily' }, { name: '📅 Weekly', value: 'weekly' }, { name: '⭐ Special', value: 'special' } ) ) .addStringOption(opt => opt.setName('objective') .setDescription('What users need to do') .setRequired(true) .addChoices( { name: '💬 Send Messages', value: 'messages' }, { name: '😄 Add Reactions', value: 'reactions' }, { name: '🎙️ Voice Chat (minutes)', value: 'voice_minutes' }, { name: '⚡ Use Commands', value: 'commands' }, { name: '🎁 Claim Daily Rewards', value: 'daily_claims' }, { name: '📈 Level Up', value: 'level_ups' }, { name: '✨ Earn XP', value: 'xp_earned' } ) ) .addIntegerOption(opt => opt.setName('target') .setDescription('Amount needed to complete') .setRequired(true) .setMinValue(1) ) .addIntegerOption(opt => opt.setName('xp_reward') .setDescription('XP reward for completion') .setRequired(true) .setMinValue(1) ) .addStringOption(opt => opt.setName('description') .setDescription('Quest description') .setRequired(false) ) .addRoleOption(opt => opt.setName('role_reward') .setDescription('Role to grant on completion') .setRequired(false) ) .addBooleanOption(opt => opt.setName('repeatable') .setDescription('Can users complete this quest multiple times?') .setRequired(false) ) .addIntegerOption(opt => opt.setName('duration_hours') .setDescription('Quest duration in hours (auto-expires)') .setRequired(false) .setMinValue(1) ) ) .addSubcommand(sub => sub.setName('delete') .setDescription('Delete a quest') .addIntegerOption(opt => opt.setName('quest_id') .setDescription('The ID of the quest to delete') .setRequired(true) ) ) .addSubcommand(sub => sub.setName('edit') .setDescription('Edit an existing quest') .addIntegerOption(opt => opt.setName('quest_id') .setDescription('The ID of the quest to edit') .setRequired(true) ) .addStringOption(opt => opt.setName('name') .setDescription('New quest name') .setRequired(false) ) .addIntegerOption(opt => opt.setName('xp_reward') .setDescription('New XP reward') .setRequired(false) .setMinValue(1) ) .addIntegerOption(opt => opt.setName('target') .setDescription('New target value') .setRequired(false) .setMinValue(1) ) .addBooleanOption(opt => opt.setName('active') .setDescription('Enable or disable the quest') .setRequired(false) ) ) .addSubcommand(sub => sub.setName('list') .setDescription('List all quests') ) .addSubcommand(sub => sub.setName('reset') .setDescription('Reset quest progress for all users') .addIntegerOption(opt => opt.setName('quest_id') .setDescription('The ID of the quest to reset (leave empty to reset all)') .setRequired(false) ) .addStringOption(opt => opt.setName('type') .setDescription('Reset all quests of a type') .setRequired(false) .addChoices( { name: '☀️ Daily', value: 'daily' }, { name: '📅 Weekly', value: 'weekly' } ) ) ) .addSubcommand(sub => sub.setName('stats') .setDescription('View quest statistics') ), async execute(interaction, supabase) { if (!supabase) { return interaction.reply({ content: 'Database not configured.', ephemeral: true }); } const subcommand = interaction.options.getSubcommand(); switch (subcommand) { case 'create': return handleCreate(interaction, supabase); case 'delete': return handleDelete(interaction, supabase); case 'edit': return handleEdit(interaction, supabase); case 'list': return handleList(interaction, supabase); case 'reset': return handleReset(interaction, supabase); case 'stats': return handleStats(interaction, supabase); } } }; async function handleCreate(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; const name = interaction.options.getString('name'); const questType = interaction.options.getString('type'); const objective = interaction.options.getString('objective'); const target = interaction.options.getInteger('target'); const xpReward = interaction.options.getInteger('xp_reward'); const description = interaction.options.getString('description') || ''; const roleReward = interaction.options.getRole('role_reward'); const repeatable = interaction.options.getBoolean('repeatable') || false; const durationHours = interaction.options.getInteger('duration_hours'); let expiresAt = null; if (durationHours) { expiresAt = new Date(Date.now() + durationHours * 60 * 60 * 1000).toISOString(); } try { const { data: newQuest, error } = await supabase .from('quests') .insert({ guild_id: guildId, name: name, description: description, quest_type: questType, objective: objective, target_value: target, xp_reward: xpReward, role_reward: roleReward?.id || null, repeatable: repeatable, starts_at: new Date().toISOString(), expires_at: expiresAt, active: true }) .select() .single(); if (error) throw error; const typeInfo = QUEST_TYPES[questType] || { emoji: '📋', name: questType }; const objInfo = OBJECTIVES[objective] || { emoji: '🎯', name: objective }; const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('✅ Quest Created') .addFields( { name: 'Quest ID', value: `#${newQuest.id}`, inline: true }, { name: 'Name', value: name, inline: true }, { name: 'Type', value: `${typeInfo.emoji} ${typeInfo.name}`, inline: true }, { name: 'Objective', value: `${objInfo.emoji} ${target}x ${objInfo.name}`, inline: true }, { name: 'XP Reward', value: `${xpReward.toLocaleString()} XP`, inline: true }, { name: 'Repeatable', value: repeatable ? 'Yes' : 'No', inline: true } ); if (description) { embed.addFields({ name: 'Description', value: description, inline: false }); } if (roleReward) { embed.addFields({ name: 'Role Reward', value: `<@&${roleReward.id}>`, inline: true }); } if (expiresAt) { embed.addFields({ name: 'Expires', value: ``, inline: true }); } embed.setTimestamp(); await interaction.editReply({ embeds: [embed] }); } catch (error) { console.error('Quest create error:', error); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff0000) .setDescription('Failed to create quest.') ] }); } } async function handleDelete(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; const questId = interaction.options.getInteger('quest_id'); try { const { data: quest } = await supabase .from('quests') .select('*') .eq('id', questId) .eq('guild_id', guildId) .maybeSingle(); if (!quest) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('Quest not found.') ] }); } await supabase .from('quests') .delete() .eq('id', questId) .eq('guild_id', guildId); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0x00ff00) .setTitle('🗑️ Quest Deleted') .setDescription(`**${quest.name}** has been deleted.`) .setTimestamp() ] }); } catch (error) { console.error('Quest delete error:', error); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff0000) .setDescription('Failed to delete quest.') ] }); } } async function handleEdit(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; const questId = interaction.options.getInteger('quest_id'); const newName = interaction.options.getString('name'); const newXpReward = interaction.options.getInteger('xp_reward'); const newTarget = interaction.options.getInteger('target'); const active = interaction.options.getBoolean('active'); try { const { data: quest } = await supabase .from('quests') .select('*') .eq('id', questId) .eq('guild_id', guildId) .maybeSingle(); if (!quest) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('Quest not found.') ] }); } const updates = { updated_at: new Date().toISOString() }; const changes = []; if (newName !== null) { updates.name = newName; changes.push(`Name: ${quest.name} → ${newName}`); } if (newXpReward !== null) { updates.xp_reward = newXpReward; changes.push(`XP Reward: ${quest.xp_reward} → ${newXpReward}`); } if (newTarget !== null) { updates.target_value = newTarget; changes.push(`Target: ${quest.target_value} → ${newTarget}`); } if (active !== null) { updates.active = active; changes.push(`Active: ${quest.active} → ${active}`); } if (changes.length === 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setDescription('No changes specified.') ] }); } await supabase .from('quests') .update(updates) .eq('id', questId); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0x00ff00) .setTitle('✏️ Quest Updated') .setDescription(`**${quest.name}** has been updated.`) .addFields({ name: 'Changes', value: changes.join('\n') }) .setTimestamp() ] }); } catch (error) { console.error('Quest edit error:', error); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff0000) .setDescription('Failed to edit quest.') ] }); } } async function handleList(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; try { const { data: quests, error } = await supabase .from('quests') .select('*') .eq('guild_id', guildId) .order('quest_type') .order('created_at', { ascending: false }); if (error || !quests || quests.length === 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setTitle('📋 Server Quests') .setDescription('No quests created yet.\n\nUse `/quests-manage create` to add quests.') ] }); } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('📋 Server Quests (Admin View)') .setDescription(`Total quests: **${quests.length}**`) .setTimestamp(); const questLines = quests.map(quest => { const status = quest.active ? '✅' : '❌'; const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' }; const objInfo = OBJECTIVES[quest.objective] || { emoji: '🎯', name: quest.objective }; const expires = quest.expires_at && new Date(quest.expires_at) < new Date() ? ' ⏰' : ''; return `${status} ${typeInfo.emoji} \`#${quest.id}\` **${quest.name}** - ${objInfo.emoji} ${quest.target_value}x → ${quest.xp_reward} XP${expires}`; }); const chunks = []; let current = ''; for (const line of questLines) { if ((current + '\n' + line).length > 1000) { chunks.push(current); current = line; } else { current = current ? current + '\n' + line : line; } } if (current) chunks.push(current); for (let i = 0; i < Math.min(chunks.length, 5); i++) { embed.addFields({ name: i === 0 ? 'Quests' : '\u200b', value: chunks[i], inline: false }); } await interaction.editReply({ embeds: [embed] }); } catch (error) { console.error('Quest list error:', error); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff0000) .setDescription('Failed to list quests.') ] }); } } async function handleReset(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; const questId = interaction.options.getInteger('quest_id'); const questType = interaction.options.getString('type'); try { let deleteQuery = supabase .from('user_quests') .delete() .eq('guild_id', guildId); let description = ''; if (questId) { deleteQuery = deleteQuery.eq('quest_id', questId); description = `Progress reset for quest #${questId}`; } else if (questType) { const { data: quests } = await supabase .from('quests') .select('id') .eq('guild_id', guildId) .eq('quest_type', questType); if (quests && quests.length > 0) { deleteQuery = deleteQuery.in('quest_id', quests.map(q => q.id)); description = `Progress reset for all ${QUEST_TYPES[questType]?.name || questType} quests`; } else { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setDescription('No quests found for that type.') ] }); } } else { description = 'Progress reset for all quests'; } await deleteQuery; await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0x00ff00) .setTitle('🔄 Quest Progress Reset') .setDescription(description) .setTimestamp() ] }); } catch (error) { console.error('Quest reset error:', error); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff0000) .setDescription('Failed to reset quest progress.') ] }); } } async function handleStats(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; try { const { data: quests } = await supabase .from('quests') .select('*') .eq('guild_id', guildId); const { data: userQuests } = await supabase .from('user_quests') .select('*, quests(*)') .eq('guild_id', guildId); const totalQuests = quests?.length || 0; const activeQuests = quests?.filter(q => q.active).length || 0; const totalCompletions = userQuests?.filter(uq => uq.completed).length || 0; const totalClaimed = userQuests?.filter(uq => uq.claimed).length || 0; const uniqueParticipants = new Set(userQuests?.map(uq => uq.user_id)).size; const totalXPRewarded = userQuests ?.filter(uq => uq.claimed && uq.quests) .reduce((sum, uq) => sum + (uq.quests.xp_reward || 0), 0) || 0; const questCompletions = {}; for (const uq of userQuests || []) { if (uq.completed && uq.quests) { const name = uq.quests.name; questCompletions[name] = (questCompletions[name] || 0) + 1; } } const topQuests = Object.entries(questCompletions) .sort(([, a], [, b]) => b - a) .slice(0, 5) .map(([name, count], i) => `${i + 1}. **${name}** (${count} completions)`) .join('\n'); const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('📊 Quest Statistics') .addFields( { name: '📜 Total Quests', value: `${totalQuests} (${activeQuests} active)`, inline: true }, { name: '✅ Total Completions', value: totalCompletions.toString(), inline: true }, { name: '🎁 Total Claims', value: totalClaimed.toString(), inline: true }, { name: '👥 Participants', value: uniqueParticipants.toString(), inline: true }, { name: '💰 XP Rewarded', value: totalXPRewarded.toLocaleString(), inline: true } ) .setTimestamp(); if (topQuests) { embed.addFields({ name: '🏆 Most Completed Quests', value: topQuests, inline: false }); } await interaction.editReply({ embeds: [embed] }); } catch (error) { console.error('Quest stats error:', error); await interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff0000) .setDescription('Failed to get statistics.') ] }); } }