AeThex-Bot-Master/aethex-bot/commands/achievements.js
sirpiglr 4a39363dfd Implement a new achievement system for tracking user progress and rewards
Adds a comprehensive achievement system to the bot, including new triggers for various user actions, the ability to create, manage, and view achievements, and integration with existing XP and leveling systems. This also involves updating user statistics tracking to support achievement triggers.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 28bc7e36-c36d-4b62-b518-bcc2c649398e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/yTaZipL
Replit-Helium-Checkpoint-Created: true
2025-12-08 21:47:11 +00:00

616 lines
21 KiB
JavaScript

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 [];
}
};