const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js'); const TRIGGER_TYPES = { level: { name: 'Reach Level', description: 'Triggers when user reaches specified level' }, prestige: { name: 'Reach Prestige', description: 'Triggers when user reaches prestige level' }, total_xp: { name: 'Total XP Earned', description: 'Triggers when total XP reaches value' }, messages: { name: 'Messages Sent', description: 'Triggers after sending X messages' }, reactions_given: { name: 'Reactions Given', description: 'Triggers after giving X reactions' }, reactions_received: { name: 'Reactions Received', description: 'Triggers after receiving X reactions' }, voice_minutes: { name: 'Voice Minutes', description: 'Triggers after X minutes in voice' }, daily_streak: { name: 'Daily Streak', description: 'Triggers at X day claim streak' }, commands_used: { name: 'Commands Used', description: 'Triggers after using X commands' } }; module.exports = { data: new SlashCommandBuilder() .setName('achievements') .setDescription('Manage server achievements and badges') .addSubcommand(sub => sub.setName('create') .setDescription('Create a new achievement') .addStringOption(opt => opt.setName('name') .setDescription('Achievement name') .setRequired(true) .setMaxLength(100)) .addStringOption(opt => opt.setName('trigger') .setDescription('What triggers this achievement') .setRequired(true) .addChoices( { name: 'Reach Level', value: 'level' }, { name: 'Reach Prestige', value: 'prestige' }, { name: 'Total XP Earned', value: 'total_xp' }, { name: 'Messages Sent', value: 'messages' }, { name: 'Reactions Given', value: 'reactions_given' }, { name: 'Reactions Received', value: 'reactions_received' }, { name: 'Voice Minutes', value: 'voice_minutes' }, { name: 'Daily Streak', value: 'daily_streak' }, { name: 'Commands Used', value: 'commands_used' } )) .addIntegerOption(opt => opt.setName('value') .setDescription('Trigger value (e.g., level 10, 1000 XP)') .setRequired(true) .setMinValue(1)) .addStringOption(opt => opt.setName('description') .setDescription('Achievement description') .setMaxLength(200)) .addStringOption(opt => opt.setName('icon') .setDescription('Emoji icon for the achievement') .setMaxLength(50)) .addIntegerOption(opt => opt.setName('reward_xp') .setDescription('XP reward for earning this achievement') .setMinValue(0) .setMaxValue(10000)) .addRoleOption(opt => opt.setName('reward_role') .setDescription('Role to award when achievement is earned')) .addBooleanOption(opt => opt.setName('hidden') .setDescription('Hide this achievement until earned'))) .addSubcommand(sub => sub.setName('delete') .setDescription('Delete an achievement') .addStringOption(opt => opt.setName('name') .setDescription('Achievement name to delete') .setRequired(true) .setAutocomplete(true))) .addSubcommand(sub => sub.setName('list') .setDescription('List all server achievements')) .addSubcommand(sub => sub.setName('view') .setDescription('View your earned achievements') .addUserOption(opt => opt.setName('user') .setDescription('User to view achievements for'))) .addSubcommand(sub => sub.setName('grant') .setDescription('Manually grant an achievement to a user') .addUserOption(opt => opt.setName('user') .setDescription('User to grant achievement to') .setRequired(true)) .addStringOption(opt => opt.setName('name') .setDescription('Achievement name') .setRequired(true) .setAutocomplete(true))) .addSubcommand(sub => sub.setName('revoke') .setDescription('Revoke an achievement from a user') .addUserOption(opt => opt.setName('user') .setDescription('User to revoke achievement from') .setRequired(true)) .addStringOption(opt => opt.setName('name') .setDescription('Achievement name') .setRequired(true) .setAutocomplete(true))), async autocomplete(interaction, supabase) { if (!supabase) return; const focused = interaction.options.getFocused().toLowerCase(); const guildId = interaction.guildId; try { const { data: achievements } = await supabase .from('achievements') .select('name') .eq('guild_id', guildId); const filtered = (achievements || []) .filter(a => a.name.toLowerCase().includes(focused)) .slice(0, 25) .map(a => ({ name: a.name, value: a.name })); await interaction.respond(filtered); } catch (e) { await interaction.respond([]); } }, async execute(interaction, client, supabase) { if (!supabase) { return interaction.reply({ content: 'Database not configured. Achievements require Supabase.', ephemeral: true }); } const guildId = interaction.guildId; const sub = interaction.options.getSubcommand(); switch (sub) { case 'create': return handleCreate(interaction, supabase, guildId); case 'delete': return handleDelete(interaction, supabase, guildId); case 'list': return handleList(interaction, supabase, guildId); case 'view': return handleView(interaction, supabase, guildId); case 'grant': return handleGrant(interaction, supabase, guildId, client); case 'revoke': return handleRevoke(interaction, supabase, guildId); } } }; async function handleCreate(interaction, supabase, guildId) { if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { return interaction.reply({ content: 'You need Administrator permission to create achievements.', ephemeral: true }); } const name = interaction.options.getString('name'); const trigger = interaction.options.getString('trigger'); const value = interaction.options.getInteger('value'); const description = interaction.options.getString('description') || `${TRIGGER_TYPES[trigger].name}: ${value}`; const icon = interaction.options.getString('icon') || '🏆'; const rewardXp = interaction.options.getInteger('reward_xp') || 0; const rewardRole = interaction.options.getRole('reward_role'); const hidden = interaction.options.getBoolean('hidden') || false; try { const { error } = await supabase .from('achievements') .insert({ guild_id: guildId, name, description, icon, trigger_type: trigger, trigger_value: value, reward_xp: rewardXp, reward_role_id: rewardRole?.id || null, hidden }); if (error) { if (error.code === '23505') { return interaction.reply({ content: `An achievement named "${name}" already exists.`, ephemeral: true }); } throw error; } const embed = new EmbedBuilder() .setColor(0x10b981) .setTitle('Achievement Created') .addFields( { name: 'Name', value: `${icon} ${name}`, inline: true }, { name: 'Trigger', value: `${TRIGGER_TYPES[trigger].name}: ${value.toLocaleString()}`, inline: true }, { name: 'Description', value: description }, { name: 'Rewards', value: [ rewardXp > 0 ? `+${rewardXp.toLocaleString()} XP` : null, rewardRole ? `Role: ${rewardRole}` : null, hidden ? '(Hidden achievement)' : null ].filter(Boolean).join('\n') || 'None' } ) .setTimestamp(); return interaction.reply({ embeds: [embed] }); } catch (error) { console.error('Achievement create error:', error); return interaction.reply({ content: 'Failed to create achievement.', ephemeral: true }); } } async function handleDelete(interaction, supabase, guildId) { if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { return interaction.reply({ content: 'You need Administrator permission to delete achievements.', ephemeral: true }); } const name = interaction.options.getString('name'); try { const { data, error } = await supabase .from('achievements') .delete() .eq('guild_id', guildId) .eq('name', name) .select(); if (error) throw error; if (!data || data.length === 0) { return interaction.reply({ content: `Achievement "${name}" not found.`, ephemeral: true }); } return interaction.reply({ content: `Achievement "${name}" has been deleted.`, ephemeral: true }); } catch (error) { console.error('Achievement delete error:', error); return interaction.reply({ content: 'Failed to delete achievement.', ephemeral: true }); } } async function handleList(interaction, supabase, guildId) { try { const { data: achievements, error } = await supabase .from('achievements') .select('*') .eq('guild_id', guildId) .order('trigger_type') .order('trigger_value', { ascending: true }); if (error) throw error; if (!achievements || achievements.length === 0) { return interaction.reply({ content: 'No achievements configured. Use `/achievements create` to add some!', ephemeral: true }); } const grouped = {}; for (const ach of achievements) { const type = ach.trigger_type; if (!grouped[type]) grouped[type] = []; grouped[type].push(ach); } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('Server Achievements') .setDescription(`${achievements.length} achievement(s) configured`) .setTimestamp(); for (const [type, achs] of Object.entries(grouped)) { const typeInfo = TRIGGER_TYPES[type] || { name: type }; const lines = achs.map(a => { const hiddenTag = a.hidden ? ' (Hidden)' : ''; const rewards = []; if (a.reward_xp > 0) rewards.push(`+${a.reward_xp} XP`); if (a.reward_role_id) rewards.push(`<@&${a.reward_role_id}>`); const rewardStr = rewards.length > 0 ? ` | ${rewards.join(', ')}` : ''; return `${a.icon} **${a.name}**${hiddenTag} - ${a.trigger_value.toLocaleString()}${rewardStr}`; }); embed.addFields({ name: typeInfo.name, value: lines.join('\n') || 'None', inline: false }); } return interaction.reply({ embeds: [embed] }); } catch (error) { console.error('Achievement list error:', error); return interaction.reply({ content: 'Failed to list achievements.', ephemeral: true }); } } async function handleView(interaction, supabase, guildId) { const target = interaction.options.getUser('user') || interaction.user; await interaction.deferReply(); try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', target.id) .maybeSingle(); if (!link) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex.`) ] }); } const [achievementsResult, earnedResult] = await Promise.all([ supabase.from('achievements').select('*').eq('guild_id', guildId), supabase.from('user_achievements') .select('achievement_id, earned_at') .eq('user_id', link.user_id) .eq('guild_id', guildId) ]); const allAchievements = achievementsResult.data || []; const earnedIds = new Set((earnedResult.data || []).map(e => e.achievement_id)); const earnedMap = Object.fromEntries((earnedResult.data || []).map(e => [e.achievement_id, e.earned_at])); const earned = allAchievements.filter(a => earnedIds.has(a.id)); const available = allAchievements.filter(a => !earnedIds.has(a.id) && !a.hidden); const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle(`${target.username}'s Achievements`) .setThumbnail(target.displayAvatarURL()) .setTimestamp(); if (earned.length > 0) { const earnedLines = earned.map(a => { const date = earnedMap[a.id] ? new Date(earnedMap[a.id]).toLocaleDateString() : ''; return `${a.icon} **${a.name}** - ${a.description} (${date})`; }); embed.addFields({ name: `Earned (${earned.length})`, value: earnedLines.join('\n').slice(0, 1024) }); } else { embed.addFields({ name: 'Earned', value: 'No achievements earned yet!' }); } if (available.length > 0) { const availableLines = available.slice(0, 10).map(a => { const typeInfo = TRIGGER_TYPES[a.trigger_type] || { name: a.trigger_type }; return `🔒 **${a.name}** - ${typeInfo.name}: ${a.trigger_value.toLocaleString()}`; }); if (available.length > 10) { availableLines.push(`... and ${available.length - 10} more`); } embed.addFields({ name: `Available (${available.length})`, value: availableLines.join('\n').slice(0, 1024) }); } const hiddenCount = allAchievements.filter(a => a.hidden && !earnedIds.has(a.id)).length; if (hiddenCount > 0) { embed.setFooter({ text: `${hiddenCount} hidden achievement(s) to discover` }); } return interaction.editReply({ embeds: [embed] }); } catch (error) { console.error('Achievement view error:', error); return interaction.editReply({ content: 'Failed to fetch achievements.' }); } } async function handleGrant(interaction, supabase, guildId, client) { if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { return interaction.reply({ content: 'You need Administrator permission to grant achievements.', ephemeral: true }); } const target = interaction.options.getUser('user'); const name = interaction.options.getString('name'); try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', target.id) .maybeSingle(); if (!link) { return interaction.reply({ content: `${target.tag} is not linked to AeThex.`, ephemeral: true }); } const { data: achievement } = await supabase .from('achievements') .select('*') .eq('guild_id', guildId) .eq('name', name) .maybeSingle(); if (!achievement) { return interaction.reply({ content: `Achievement "${name}" not found.`, ephemeral: true }); } const { error } = await supabase .from('user_achievements') .upsert({ user_id: link.user_id, guild_id: guildId, achievement_id: achievement.id, notified: true }, { onConflict: 'user_id,guild_id,achievement_id' }); if (error) throw error; if (achievement.reward_role_id) { const member = await interaction.guild.members.fetch(target.id).catch(() => null); if (member) { await member.roles.add(achievement.reward_role_id).catch(() => {}); } } if (achievement.reward_xp > 0) { await supabase .from('user_profiles') .update({ xp: supabase.rpc('increment_xp', { user_id: link.user_id, amount: achievement.reward_xp }) }) .eq('id', link.user_id); } return interaction.reply({ content: `${achievement.icon} Granted **${achievement.name}** to ${target}!`, ephemeral: true }); } catch (error) { console.error('Achievement grant error:', error); return interaction.reply({ content: 'Failed to grant achievement.', ephemeral: true }); } } async function handleRevoke(interaction, supabase, guildId) { if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) { return interaction.reply({ content: 'You need Administrator permission to revoke achievements.', ephemeral: true }); } const target = interaction.options.getUser('user'); const name = interaction.options.getString('name'); try { const { data: link } = await supabase .from('discord_links') .select('user_id') .eq('discord_id', target.id) .maybeSingle(); if (!link) { return interaction.reply({ content: `${target.tag} is not linked to AeThex.`, ephemeral: true }); } const { data: achievement } = await supabase .from('achievements') .select('id, name, icon, reward_role_id') .eq('guild_id', guildId) .eq('name', name) .maybeSingle(); if (!achievement) { return interaction.reply({ content: `Achievement "${name}" not found.`, ephemeral: true }); } const { data, error } = await supabase .from('user_achievements') .delete() .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('achievement_id', achievement.id) .select(); if (error) throw error; if (!data || data.length === 0) { return interaction.reply({ content: `${target.tag} doesn't have this achievement.`, ephemeral: true }); } if (achievement.reward_role_id) { const member = await interaction.guild.members.fetch(target.id).catch(() => null); if (member) { await member.roles.remove(achievement.reward_role_id).catch(() => {}); } } return interaction.reply({ content: `Revoked **${achievement.name}** from ${target}.`, ephemeral: true }); } catch (error) { console.error('Achievement revoke error:', error); return interaction.reply({ content: 'Failed to revoke achievement.', ephemeral: true }); } } module.exports.checkAchievements = async function(userId, discordMember, stats, supabase, guildId, client) { if (!supabase || !userId || !guildId) return; try { const { data: achievements } = await supabase .from('achievements') .select('*') .eq('guild_id', guildId); if (!achievements || achievements.length === 0) return; const { data: earned } = await supabase .from('user_achievements') .select('achievement_id') .eq('user_id', userId) .eq('guild_id', guildId); const earnedIds = new Set((earned || []).map(e => e.achievement_id)); const newlyEarned = []; for (const ach of achievements) { if (earnedIds.has(ach.id)) continue; let qualifies = false; const val = ach.trigger_value; switch (ach.trigger_type) { case 'level': qualifies = (stats.level || 0) >= val; break; case 'prestige': qualifies = (stats.prestige || 0) >= val; break; case 'total_xp': qualifies = (stats.totalXp || 0) >= val; break; case 'messages': qualifies = (stats.messages || 0) >= val; break; case 'reactions_given': qualifies = (stats.reactionsGiven || 0) >= val; break; case 'reactions_received': qualifies = (stats.reactionsReceived || 0) >= val; break; case 'voice_minutes': qualifies = (stats.voiceMinutes || 0) >= val; break; case 'daily_streak': qualifies = (stats.dailyStreak || 0) >= val; break; case 'commands_used': qualifies = (stats.commandsUsed || 0) >= val; break; } if (qualifies) { newlyEarned.push(ach); } } for (const ach of newlyEarned) { await supabase.from('user_achievements').insert({ user_id: userId, guild_id: guildId, achievement_id: ach.id, notified: true }).catch(() => {}); if (ach.reward_role_id && discordMember) { await discordMember.roles.add(ach.reward_role_id).catch(() => {}); } if (ach.reward_xp > 0) { const { data: profile } = await supabase .from('user_profiles') .select('xp, total_xp_earned') .eq('id', userId) .maybeSingle(); if (profile) { await supabase .from('user_profiles') .update({ xp: (profile.xp || 0) + ach.reward_xp, total_xp_earned: (profile.total_xp_earned || 0) + ach.reward_xp }) .eq('id', userId); } } if (discordMember && client) { try { await discordMember.send({ embeds: [ new EmbedBuilder() .setColor(0x10b981) .setTitle('Achievement Unlocked!') .setDescription(`${ach.icon} **${ach.name}**\n${ach.description}`) .addFields( ach.reward_xp > 0 ? { name: 'Reward', value: `+${ach.reward_xp} XP`, inline: true } : [] ) .setTimestamp() ] }).catch(() => {}); } catch (e) {} } } return newlyEarned; } catch (error) { console.error('Achievement check error:', error); return []; } };