const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const QUEST_TYPES = { daily: { emoji: '☀️', name: 'Daily', color: 0xfbbf24 }, weekly: { emoji: '📅', name: 'Weekly', color: 0x3b82f6 }, special: { emoji: '⭐', name: 'Special', color: 0xa855f7 } }; 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') .setDescription('View and manage your quests') .addSubcommand(sub => sub.setName('view') .setDescription('View available quests') .addStringOption(opt => opt.setName('type') .setDescription('Filter by quest type') .setRequired(false) .addChoices( { name: '☀️ Daily', value: 'daily' }, { name: '📅 Weekly', value: 'weekly' }, { name: '⭐ Special', value: 'special' } ) ) ) .addSubcommand(sub => sub.setName('progress') .setDescription('View your quest progress') ) .addSubcommand(sub => sub.setName('claim') .setDescription('Claim rewards for completed quests') .addIntegerOption(opt => opt.setName('quest_id') .setDescription('The ID of the quest to claim (leave empty to claim all)') .setRequired(false) ) ), async execute(interaction, supabase) { if (!supabase) { return interaction.reply({ content: 'Database not configured.', ephemeral: true }); } const subcommand = interaction.options.getSubcommand(); switch (subcommand) { case 'view': return handleView(interaction, supabase); case 'progress': return handleProgress(interaction, supabase); case 'claim': return handleClaim(interaction, supabase); } } }; async function getUserLink(supabase, discordId) { const { data } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', discordId) .maybeSingle(); return data; } async function handleView(interaction, supabase) { await interaction.deferReply(); const guildId = interaction.guildId; const questType = interaction.options.getString('type'); const now = new Date().toISOString(); let query = supabase .from('quests') .select('*') .eq('guild_id', guildId) .eq('active', true) .or(`expires_at.is.null,expires_at.gt.${now}`) .or(`starts_at.is.null,starts_at.lte.${now}`) .order('quest_type') .order('xp_reward', { ascending: false }); if (questType) { query = query.eq('quest_type', questType); } const { data: quests, error } = await query; if (error || !quests || quests.length === 0) { const embed = new EmbedBuilder() .setColor(0xfbbf24) .setTitle('📜 Quests') .setDescription(questType ? `No active ${QUEST_TYPES[questType]?.name || questType} quests available.` : 'No active quests available right now.\n\nCheck back later or ask a server admin to add quests!') .setTimestamp(); return interaction.editReply({ embeds: [embed] }); } const link = await getUserLink(supabase, interaction.user.id); let userQuests = []; if (link) { const { data } = await supabase .from('user_quests') .select('*') .eq('user_id', link.user_id) .eq('guild_id', guildId); userQuests = data || []; } const userQuestMap = {}; for (const uq of userQuests) { userQuestMap[uq.quest_id] = uq; } const groupedQuests = {}; for (const quest of quests) { const type = quest.quest_type || 'special'; if (!groupedQuests[type]) groupedQuests[type] = []; groupedQuests[type].push(quest); } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('📜 Available Quests') .setDescription(link ? 'Complete quests to earn XP rewards!' : 'Link your account with `/verify` to track quest progress!') .setThumbnail(interaction.guild.iconURL({ size: 128 })) .setFooter({ text: `Use /quests progress to see your progress • ${quests.length} quests available` }) .setTimestamp(); for (const [type, typeQuests] of Object.entries(groupedQuests)) { const typeInfo = QUEST_TYPES[type] || { emoji: '📋', name: type }; const questList = typeQuests.slice(0, 5).map(quest => { const objInfo = OBJECTIVES[quest.objective] || { emoji: '🎯', name: quest.objective }; const userQuest = userQuestMap[quest.id]; let status = ''; if (userQuest) { if (userQuest.claimed) { status = ' ✅ Claimed'; } else if (userQuest.completed) { status = ' 🎁 Ready to claim!'; } else { const percent = Math.min(100, Math.floor((userQuest.progress / quest.target_value) * 100)); status = ` [${userQuest.progress}/${quest.target_value}] ${percent}%`; } } const expires = quest.expires_at ? ` (ends )` : ''; return `\`#${quest.id}\` **${quest.name}**${status}\n${objInfo.emoji} ${quest.target_value}x ${objInfo.name} → **${quest.xp_reward.toLocaleString()} XP**${expires}`; }).join('\n\n'); const moreQuests = typeQuests.length > 5 ? `\n\n*...and ${typeQuests.length - 5} more*` : ''; embed.addFields({ name: `${typeInfo.emoji} ${typeInfo.name} Quests`, value: questList + moreQuests, inline: false }); } await interaction.editReply({ embeds: [embed] }); } async function handleProgress(interaction, supabase) { await interaction.deferReply(); const guildId = interaction.guildId; const link = await getUserLink(supabase, interaction.user.id); if (!link) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('You need to link your account first! Use `/verify` to get started.') ] }); } const now = new Date().toISOString(); const { data: quests } = await supabase .from('quests') .select('*') .eq('guild_id', guildId) .eq('active', true) .or(`expires_at.is.null,expires_at.gt.${now}`) .or(`starts_at.is.null,starts_at.lte.${now}`); if (!quests || quests.length === 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setTitle('📊 Quest Progress') .setDescription('No active quests to track.') ] }); } const questIds = quests.map(q => q.id); const { data: userQuests } = await supabase .from('user_quests') .select('*') .eq('user_id', link.user_id) .eq('guild_id', guildId) .in('quest_id', questIds); const userQuestMap = {}; for (const uq of userQuests || []) { userQuestMap[uq.quest_id] = uq; } const inProgress = []; const completed = []; const notStarted = []; for (const quest of quests) { const uq = userQuestMap[quest.id]; if (!uq) { notStarted.push(quest); } else if (uq.claimed) { continue; } else if (uq.completed) { completed.push({ quest, userQuest: uq }); } else { inProgress.push({ quest, userQuest: uq }); } } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('📊 Your Quest Progress') .setThumbnail(interaction.user.displayAvatarURL({ size: 128 })) .setTimestamp(); if (completed.length > 0) { const completedList = completed.slice(0, 5).map(({ quest }) => { const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' }; return `${typeInfo.emoji} \`#${quest.id}\` **${quest.name}** - ${quest.xp_reward.toLocaleString()} XP 🎁`; }).join('\n'); embed.addFields({ name: '🎉 Ready to Claim!', value: completedList, inline: false }); } if (inProgress.length > 0) { const progressList = inProgress.slice(0, 5).map(({ quest, userQuest }) => { const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' }; const percent = Math.min(100, Math.floor((userQuest.progress / quest.target_value) * 100)); const progressBar = createProgressBar(percent); return `${typeInfo.emoji} \`#${quest.id}\` **${quest.name}**\n${progressBar} ${userQuest.progress}/${quest.target_value} (${percent}%)`; }).join('\n\n'); embed.addFields({ name: '🔄 In Progress', value: progressList, inline: false }); } if (notStarted.length > 0 && (completed.length + inProgress.length) < 5) { const notStartedList = notStarted.slice(0, 3).map(quest => { const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' }; const objInfo = OBJECTIVES[quest.objective] || { emoji: '🎯', name: quest.objective }; return `${typeInfo.emoji} \`#${quest.id}\` **${quest.name}** - ${quest.target_value}x ${objInfo.name}`; }).join('\n'); embed.addFields({ name: '📝 Not Started', value: notStartedList + (notStarted.length > 3 ? `\n*...and ${notStarted.length - 3} more*` : ''), inline: false }); } if (completed.length === 0 && inProgress.length === 0 && notStarted.length === 0) { embed.setDescription('You\'ve completed all available quests! Check back later for more.'); } else { const totalXP = completed.reduce((sum, { quest }) => sum + quest.xp_reward, 0); if (totalXP > 0) { embed.setDescription(`You have **${totalXP.toLocaleString()} XP** waiting to be claimed! Use \`/quests claim\``); } } embed.setFooter({ text: 'Use /quests claim to collect your rewards!' }); await interaction.editReply({ embeds: [embed] }); } async function handleClaim(interaction, supabase) { await interaction.deferReply(); const guildId = interaction.guildId; const questId = interaction.options.getInteger('quest_id'); const link = await getUserLink(supabase, interaction.user.id); if (!link) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('You need to link your account first! Use `/verify` to get started.') ] }); } let query = supabase .from('user_quests') .select('*, quests(*)') .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('completed', true) .eq('claimed', false); if (questId) { query = query.eq('quest_id', questId); } const { data: claimable, error } = await query; if (error || !claimable || claimable.length === 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setTitle('No Rewards to Claim') .setDescription(questId ? 'This quest is not completed or already claimed.' : 'You don\'t have any completed quests to claim.\n\nComplete quests to earn rewards!') ] }); } let totalXP = 0; const claimedQuests = []; const rolesGranted = []; const now = new Date().toISOString(); for (const uq of claimable) { const quest = uq.quests; if (!quest) continue; totalXP += quest.xp_reward; claimedQuests.push(quest.name); await supabase .from('user_quests') .update({ claimed: true, claimed_at: now }) .eq('id', uq.id); if (quest.role_reward) { try { await interaction.member.roles.add(quest.role_reward); rolesGranted.push(quest.role_reward); } catch (e) { console.error('Failed to add quest role reward:', e.message); } } } if (totalXP > 0) { const { data: profile } = await supabase .from('user_profiles') .select('xp, total_xp_earned') .eq('id', link.user_id) .maybeSingle(); const currentXP = profile?.xp || 0; const currentTotal = profile?.total_xp_earned || currentXP; await supabase .from('user_profiles') .update({ xp: currentXP + totalXP, total_xp_earned: currentTotal + totalXP, updated_at: now }) .eq('id', link.user_id); const { data: balance } = await supabase .from('user_balance') .select('*') .eq('user_id', link.user_id) .eq('guild_id', guildId) .maybeSingle(); if (balance) { await supabase .from('user_balance') .update({ balance: balance.balance + totalXP, total_earned: balance.total_earned + totalXP, updated_at: now }) .eq('user_id', link.user_id) .eq('guild_id', guildId); } } const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('🎉 Rewards Claimed!') .setDescription(`You claimed rewards for **${claimable.length}** quest${claimable.length > 1 ? 's' : ''}!`) .addFields( { name: '💰 XP Earned', value: `**+${totalXP.toLocaleString()} XP**`, inline: true } ) .setTimestamp(); if (claimedQuests.length <= 5) { embed.addFields({ name: '📜 Quests Completed', value: claimedQuests.map(n => `• ${n}`).join('\n'), inline: false }); } if (rolesGranted.length > 0) { embed.addFields({ name: '👑 Roles Granted', value: rolesGranted.map(r => `<@&${r}>`).join(', '), inline: false }); } await interaction.editReply({ embeds: [embed] }); } function createProgressBar(percent) { const filled = Math.floor(percent / 10); const empty = 10 - filled; return '█'.repeat(filled) + '░'.repeat(empty); }